070701000000000000818000000000000000000000000165F8630400008000000000000000000000000000000000000000001900000000wireplumber-0.5.0/.-.swp  b0VIM 9.1              ch  antonio                                 weatherwax                              ~antonio/obs/home/alarrosa/branches/multimedia/libs/wireplumber/wireplumber/.-                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    U3210    #"! U                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 tp           Q                            O       S                     X                            Q                            R       K                    (                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            ad     x     Q                 M           W  H            G            T  +          ]  5    
  
  
  m
  <
  ;
  	  	  	  k	  ?	  	          Q  -      q  ,        \  /        _  .        Y  0      {  F  
      V        x  w                            * chromium-115-add_BoundSessionRefreshCookieFetcher::Result.patch - drop patches:   * CVE-2023-4368: Insufficient policy enforcement in Extensions API   * CVE-2023-4367: Insufficient policy enforcement in Extensions API   * CVE-2023-4366: Use after free in Extensions   * CVE-2023-4365: Inappropriate implementation in Fullscreen   * CVE-2023-4364: Inappropriate implementation in Permission Prompts   * CVE-2023-4363: Inappropriate implementation in WebShare   * CVE-2023-4362: Heap buffer overflow in Mojom IDL   * CVE-2023-4361: Inappropriate implementation in Autofill   * CVE-2023-4360: Inappropriate implementation in Color   * CVE-2023-4359: Inappropriate implementation in App Launcher   * CVE-2023-4358: Use after free in DNS   * CVE-2023-4357: Insufficient validation of untrusted input in XML   * CVE-2023-4356: Use after free in Audio   * CVE-2023-4355: Out of bounds memory access in V8   * CVE-2023-4354: Heap buffer overflow in Skia   * CVE-2023-4353: Heap buffer overflow in ANGLE   * CVE-2023-4352: Type Confusion in V8   * CVE-2023-4351: Use after free in Network   * CVE-2023-4350: Inappropriate implementation in Fullscreen   * CVE-2023-4349: Use after free in Device Trust Connectors   * CVE-2023-2312: Use after free in Offline - fix a number of security issues (boo#1214301):     Report Critical-CH caused restart in NavigationTiming     Remove document.open sandbox inheritance,     Non-composed Mouse and Pointer enter/leave events,     FedCM bundle: Login Hint API, User Info API, and RP Context API,     Picture, Expanded Wildcards in Permissions Policy Origins,     forward cache NotRestoredReason API, Document Picture-in-   * Web APIs: AbortSignal.any(), BYOB support for Fetch, Back/     "content-visibility" animations   * New CSS features: Motion Path, and "display" and - Chromium 116.0.5845.96 * lun ago 14 2023 Andreas Stieger <andreas.stieger@gmx.de>    * CVE-2023-4431: Out of bounds memory access in Fonts   * CVE-2023-4430: Use after free in Vulkan   * CVE-2023-4429: Use after free in Loader   * CVE-2023-4428: Out of bounds memory access in CSS   * CVE-2023-4427: Out of bounds memory access in V8 - Chromium 116.0.5845.110 (boo#1214487): * mié ago 23 2023 Andreas Stieger <andreas.stieger@gmx.de>    * CVE-2023-4572: Use after free in MediaStream - Chromium 116.0.5845.140 (boo#1214758): * mié ago 30 2023 Andreas Stieger <andreas.stieger@gmx.de>    * CVE-2023-4764: Incorrect security UI in BFCache   * CVE-2023-4763: Use after free in Networks   * CVE-2023-4762: Type Confusion in V8   * CVE-2023-4761: Out of bounds memory access in FedCM - Chromium 116.0.5845.179 (boo#1215023): * mié sep 06 2023 Andreas Stieger <andreas.stieger@gmx.de>    * CVE-2023-4863: Heap buffer overflow in WebP - Chromium 116.0.5845.187 (boo#1215231): * mar sep 12 2023 Andreas Stieger <andreas.stieger@gmx.de>  - CVE-2023-4863: build with the bundled library on Leap (boo#1215231) * mié sep 13 2023 Andreas Stieger <andreas.stieger@gmx.de>    * chromium-117-emplace_back_on_vector-c++20.patch   * chromium-117-workaround_clang_bug-structured_binding.patch   * chromium-117-lp155-typename.patch   * chromium-117-string-convert.patch   * chromium-117-lp155-constructors.patch   * chromium-117-includes.patch   * chromium-117-blink-BUILD-mnemonic.patch - add patches:   * chromium-115-emplace_back_on_vector-c++20.patch   * chromium-115-workaround_clang_bug-structured_binding.patch   * chromium-116-lp155-constuctors.patch   * chromium-116-abseil-limits-include.patch   * chromium-116-blink-variant-include.patch   * chromium-116-profile-view-utils-vector-include.patch   * chromium-115-Qt-moc-version.patch   * chromium-100-InMilliseconds-constexpr.patch - drop patches: n in Interstitials  ad  /        O             <  -        g  @            K  J          o  <        f  :  
  
  
  
  A
  
  
  	  	  	  	  ]	  3	        C  B        h  :        P  &        p  G  )          H      q  2      m  '                                                       - add chromium-115-lp155-typename.patch - add chromium-115-verify_name_match-include.patch - add chromium-115-skia-include.patch - drop chromium-113-typename.patch     Various fixes from internal audits, fuzzing and other initiatives     CVE-2023-3740: Insufficient validation of untrusted input in Themes     CVE-2023-3738: Inappropriate implementation in Autofill     CVE-2023-3737: Inappropriate implementation in Notifications     CVE-2023-3736: Inappropriate implementation in Custom Tabs     CVE-2023-3735: Inappropriate implementation in Web API Permission Prompts     CVE-2023-3734: Inappropriate implementation in Picture In Picture     CVE-2023-3733: Inappropriate implementation in WebApp Installs     CVE-2023-3732: Out of bounds memory access in Mojo     CVE-2023-3730: Use after free in Tab Groups     CVE-2023-3728: Use after free in WebRTC     CVE-2023-3727: Use after free in WebRTC   * Security fixes (boo#1213462):   * Deprecate mutation events   * Deprecate the document.domain setter     auto re-authentication   * FedCM: Support credential management mediation requirements for     thread to 8 MB   * Increase the maximum size of a WebAssembly.Module() on the main   * CSS: support scroll-driven animations   * CSS: support boolean context style container queries   * CSS: accept multiple values of the display property     navigations to HTTPS, with fast fallback to HTTP.   * HTTPS: Automatically and optimistically upgrade all main-frame     types of side-channel cross-site tracking     are now partitioned in third-party contexts to prevent certain   * Security: The Storage, Service Worker, and Communication APIs - Chromium 115.0.5790.98 * mié jul 19 2023 Andreas Stieger <andreas.stieger@gmx.de>  - drop chromium-114-workaround_clang_bug-structured_binding.patch - adjust chromium-115-lp155-typename.patch   * chromium-115-add_BoundSessionRefreshCookieFetcher::Result.patch   * chromium-115-workaround_clang_bug-structured_binding.patch   * chromium-115-compiler-SkColor4f.patch   * chromium-115-emplace_back_on_vector-c++20.patch - Add build fixes on Leap:   * stability fix - Chromium 115.0.5790.102: * dom jul 23 2023 Andreas Stieger <andreas.stieger@gmx.de>    build in devel project and in Maintenance - Specify re2 build dependency in a way that makes Leap packages * vie jul 28 2023 Andreas Stieger <andreas.stieger@gmx.de>    * CVE-2023-4078: Inappropriate implementation in Extensions   * CVE-2023-4077: Insufficient data validation in Extensions   * CVE-2023-4076: Use after free in WebRTC   * CVE-2023-4075: Use after free in Cast   * CVE-2023-4074: Use after free in Blink Task Scheduling   * CVE-2023-4073: Out of bounds memory access in ANGLE   * CVE-2023-4072: Out of bounds read and write in WebGL   * CVE-2023-4071: Heap buffer overflow in Visuals   * CVE-2023-4070: Type Confusion in V8   * CVE-2023-4069: Type Confusion in V8   * CVE-2023-4068: Type Confusion in V8 - Chromium 115.0.5790.170 (boo#1213920) * jue ago 03 2023 Andreas Stieger <andreas.stieger@gmx.de>    chromium-115-dont-pass-nullptr-to-construct-re2-StringPiece.patch - Fix crash with extensions (boo#1214003) * mié ago 09 2023 Andreas Stieger <andreas.stieger@gmx.de>  - Build with bundled re2 on Leap   * chromium-116-lp155-constuctors.patch   * chromium-116-lp155-typenames.patch   * chromium-116-abseil-limits-include.patch   * chromium-116-lp155-url_load_stats-size-t.patch   * chromium-116-blink-variant-include.patch   * chromium-116-profile-view-utils-vector-include.patch - add patches:   * chromium-115-dont-pass-nullptr-to-construct-re2-StringPiece.patch   * chromium-115-skia-include.patch   * chromium-86-fix-vaapi-on-intel.patch   * chromium-115-verify_name_match-include.patch ad          X             i  @            X  W          g  <        z  y  >            g  5  !    
  
  
  W
  .
  	  	  	  a	  	      _  O          p  L  K            k  K  *              b  9  	        R          f  =        L                 * CVE-20   * CVE-2023-2465: Inappropriate implementation in CORS   * CVE-2023-2464: Inappropriate implementation in PictureInPicture   * CVE-2023-2463: Inappropriate implementation in Full Screen Mode   * CVE-2023-2462: Inappropriate implementation in Prompts   * CVE-2023-2461: Use after free in OS Inputs   * CVE-2023-2460: Insufficient validation of untrusted input in Extensions   * CVE-2023-2459: Inappropriate implementation in Prompts - Multiple security fixes (boo#1211036): - Chromium 113.0.5672.92 (boo#1211211) * mar may 09 2023 Andreas Stieger <Andreas.Stieger@gmx.de>    * Various fixes from internal audits, fuzzing and other initiatives   * CVE-2023-2726: Inappropriate implementation in WebApp Installs   * CVE-2023-2725: Use after free in Guest View   * CVE-2023-2724: Type Confusion in V8   * CVE-2023-2723: Use after free in DevTools   * CVE-2023-2722: Use after free in Autofill UI   * CVE-2023-2721: Use after free in Navigation - Chromium 113.0.5672.126 (boo#1211442): * mar may 16 2023 Andreas Stieger <Andreas.Stieger@gmx.de>  - build with llvm15 on Leap * dom may 28 2023 Andreas Stieger <andreas.stieger@gmx.de>    * chromium-113-system-zlib.patch - Added patches:   * chromium-lp151-old-drm.patch   * chromium-ffmpeg-lp152.patch   * chromium-clang-nomerge.patch   * chromium-109-clang-lp154.patch   * chromium-112-default-comparison-operators.patch - Remove un-needed patches: - Un-bundle zlib again * mar may 30 2023 Callum Farmer <gmbr3@opensuse.org>    * chromium-114-lld-argument.patch   * chromium-114-workaround_clang_bug-structured_binding.patch - Add patches   * chromium-113-workaround_clang_bug-structured_binding.patch   * chromium-113-system-zlib.patch   * chromium-103-VirtualCursor-std-layout.patch - Drop patches:   * CVE-2023-2941: Inappropriate implementation in Extensions API   * CVE-2023-2940: Inappropriate implementation in Downloads   * CVE-2023-2939: Insufficient data validation in Installer   * CVE-2023-2938: Inappropriate implementation in Picture In Picture   * CVE-2023-2937: Inappropriate implementation in Picture In Picture   * CVE-2023-2936: Type Confusion in V8   * CVE-2023-2935: Type Confusion in V8   * CVE-2023-2934: Out of bounds memory access in Mojo   * CVE-2023-2933: Use after free in PDF   * CVE-2023-2932: Use after free in PDF   * CVE-2023-2931: Use after free in PDF   * CVE-2023-2930: Use after free in Extensions   * CVE-2023-2929: Out of bounds write in Swiftshader - Security fixes:   * New Popover API   * Cookies partitioned by top level site (CHIPS)   * CSS text-wrap: balance is available - Chromium 114.0.5735.90 (boo#1211843): * dom jun 04 2023 Callum Farmer <gmbr3@opensuse.org>    * CVE-2023-3079: Type Confusion in V8 - Chromium 114.0.5735.106 (boo#1212044): * mar jun 06 2023 Andreas Stieger <andreas.stieger@gmx.de>  - Fix Leap 15.4 build - chromium-114-revert-av1enc-lp154.patch * mié jun 07 2023 Andreas Stieger <Andreas.Stieger@gmx.de>    * Various fixes from internal audits, fuzzing and other initiatives   * CVE-2023-3217: Use after free in WebXR   * CVE-2023-3216: Type Confusion in V8   * CVE-2023-3215: Use after free in WebRTC   * CVE-2023-3214: Use after free in Autofill payments - Chromium 114.0.5735.133 (boo#1212302): * mié jun 14 2023 Andreas Stieger <andreas.stieger@gmx.de>  - Install Qt5 library & prepare for Qt6 in 115 * dom jun 25 2023 Callum Farmer <gmbr3@opensuse.org>    * CVE-2023-3422: Use after free in Guest View   * CVE-2023-3421: Use after free in Media   * CVE-2023-3420: Type Confusion in V8 - Chromium 114.0.5735.198 (boo#1212755): * mar jun 27 2023 Andreas Stieger <andreas.stieger@gmx.de>    built-in copy of shim - Add chromium-115-Qt-moc-version.patch: support Qt5 & Qt6 without ad     u     Q           F            ?  >        O  !          a           o  T      
  
  
  g
  N
  *
  	  |	  F	  	        C        B        f  *        ~  c  $  #      }  |  @          \  )          f  e  0        v  u  e       * jue mar 09 20  - Bump Leap's GCC to 12 as Chromium really likes newer standards * jue mar 09 2023 Callum Farmer <gmbr3@opensuse.org>  - Revert back to GCC 11 on 15.4 as Clang 13 doesn't support GCC 12 * jue mar 09 2023 Callum Farmer <gmbr3@opensuse.org>  - Add gcc13-fix.patch in order to support GCC 13. * lun mar 20 2023 Martin Liška <mliska@suse.cz>    * CVE-2023-1534: Out of bounds read in ANGLE   * CVE-2023-1533: Use after free in WebProtect   * CVE-2023-1532: Out of bounds read in GPU Video   * CVE-2023-1531: Use after free in ANGLE   * CVE-2023-1530: Use after free in PDF   * CVE-2023-1529: Out of bounds memory access in WebHID   * CVE-2023-1528: Use after free in Passwords - Chromium 111.0.5563.110 (boo#1209598) * mié mar 22 2023 Andreas Stieger <andreas.stieger@gmx.de>    borrowed from Fedora's gcc13 patch - Update gcc13-fix.patch with few fixes required for aarch64, * jue mar 23 2023 Guillaume GARDET <guillaume.gardet@opensuse.org>    * nth-child() validation performance regression for SAP apps - Chromium 111.0.5563.147: * lun mar 27 2023 Andreas Stieger <andreas.stieger@gmx.de>    * CVE-2023-1823: Inappropriate implementation in FedCM   * CVE-2023-1822: Incorrect security UI in Navigation   * CVE-2023-1821: Inappropriate implementation in WebShare   * CVE-2023-1820: Heap buffer overflow in Browser History   * CVE-2023-1819: Out of bounds read in Accessibility   * CVE-2023-1818: Use after free in Vulkan   * CVE-2023-1817: Insufficient policy enforcement in Intents   * CVE-2023-1816: Incorrect security UI in Picture In Picture   * CVE-2023-1815: Use after free in Networking APIs   * CVE-2023-1814: Insufficient validation of untrusted input in Safe Browsing   * CVE-2023-1813: Inappropriate implementation in Extensions   * CVE-2023-1812: Out of bounds memory access in DOM Bindings   * CVE-2023-1811: Use after free in Frames   * CVE-2023-1810: Heap buffer overflow in Visuals   * Security fixes (boo#1210126):   * The recorder in devtools can now record with pierce selectors.   * The setter for document.domain is now deprecated.   * No-op fetch() handlers on service workers are skipped from now on to make navigations faster   * The algorithm to set the initial focus on <dialog> elements was updated.   * CSS now supports nesting rules. - Chromium 112.0.5615.49 * mar abr 04 2023 Andreas Stieger <andreas.stieger@gmx.de>  - Revert a breaking change with chromium-112-feed_protos.patch * vie abr 07 2023 Andreas Stieger <andreas.stieger@gmx.de>    * CVE-2023-2033: Type Confusion in V8 (boo#1210478) - Chromium 112.0.5615.121: * sáb abr 15 2023 Andreas Stieger <andreas.stieger@gmx.de>    adding chromium-112-default-comparison-operators.patch   defined outside of the class definition, a C++20 feature - Fix Leap 15.4 build failures from default comparison operators * dom abr 16 2023 Andreas Stieger <andreas.stieger@gmx.de>  - drop chromium-112-feed_protos.patch   * CVE-2023-2137: Heap buffer overflow in sqlite   * CVE-2023-2136: Integer overflow in Skia   * CVE-2023-2135: Use after free in DevTools   * CVE-2023-2134: Out of bounds memory access in Service Worker API   * CVE-2023-2133: Out of bounds memory access in Service Worker API - Chromium 112.0.5615.165 (boo#1210618): * mié abr 19 2023 Andreas Stieger <Andreas.Stieger@gmx.de>  - add chromium-113-workaround_clang_bug-structured_binding.patch - add chromium-113-typename.patch - add chromium-113-webauth-include-variant.patch - add chromium-113-webview-namespace.patch - drop no-location-leap151.patch - drop chromium-94-sql-no-assert.patch   * CVE-2023-2468: Inappropriate implementation in PictureInPicture   * CVE-2023-2467: Inappropriate implementation in Prompts   * CVE-2023-2466: Inappropriate implementation in Prompts ad     u     R             y  :            K  #        P  (  '          f  ?          [  %  
  
  
  `
  8
  
  	  	  n	  @	  	      n  B        ^  %        G      z  >      t  ]  >          j  D  3          Y  F  "  !        u  t                   * mié nov 17 2021 Steve Kowalik <steven.kowalik@suse.com>  - Ensure newer libs and LLVM is used on Leap (boo#1192310) * vie nov 19 2021 Callum Farmer <gmbr3@opensuse.org>    * gcc-enable-lto.patch: see above - Changed patches:   * chromium-96-CouponDB-include.patch   * chromium-96-DrmRenderNodePathFinder-include.patch   * chromium-96-RestrictedCookieManager-tuple.patch   * chromium-96-CommandLine-include.patch   * chromium-96-compiler.patch - Added patches:   * chromium-93-EnumTable-crash.patch - Removed build breaking patches:   * pipewire-do-not-typecheck-the-portal-session_handle.patch   * chromium-older-harfbuzz.patch   * chromium-95-system-zlib.patch   * chromium-95-BitstreamReader-namespace.patch   * chromium-95-compiler.patch - Removed old patches:   * CVE-2021-38022: Inappropriate implementation in WebAuthentication   * CVE-2021-38021: Inappropriate implementation in referrer   * CVE-2021-38020: Insufficient policy enforcement in contacts picker   * CVE-2021-38019: Insufficient policy enforcement in CORS   * CVE-2021-38018: Inappropriate implementation in navigation   * CVE-2021-38017: Insufficient policy enforcement in iframe sandbox   * CVE-2021-38016: Insufficient policy enforcement in background fetch   * CVE-2021-38015: Inappropriate implementation in input   * CVE-2021-38014: Out of bounds write in Swiftshader   * CVE-2021-38013: Heap buffer overflow in fingerprint recognition   * CVE-2021-38012: Type Confusion in V8   * CVE-2021-38011: Use after free in storage foundation   * CVE-2021-38010: Inappropriate implementation in service workers   * CVE-2021-38005: Use after free in loader   * CVE-2021-38006: Use after free in storage foundation   * CVE-2021-38009: Inappropriate implementation in cache   * CVE-2021-38008: Use after free in media   * CVE-2021-38007: Type Confusion in V8 - Chromium 96.0.4664.45 (boo#1192734):   * CVE-2021-4068: Insufficient validation of untrusted input in new tab page   * CVE-2021-4067: Use after free in window manager   * CVE-2021-4066: Integer underflow in ANGLE   * CVE-2021-4065: Use after free in autofill   * CVE-2021-4064: Use after free in screen capture   * CVE-2021-4063: Use after free in developer tools   * CVE-2021-4062: Heap buffer overflow in BFCache   * CVE-2021-4061: Type Confusion in V8   * CVE-2021-4059: Insufficient data validation in loader   * CVE-2021-4058: Heap buffer overflow in ANGLE   * CVE-2021-4057: Use after free in file API   * CVE-2021-4056: Type Confusion in loader   * CVE-2021-4055: Heap buffer overflow in extensions   * CVE-2021-4078: Type confusion in V8   * CVE-2021-4054: Incorrect security UI in autofill   * CVE-2021-4079: Out of bounds write in WebRTC   * CVE-2021-4053: Use after free in UI   * CVE-2021-4052: Use after free in web apps - Chromium 96.0.4664.93 (boo#1193519):   * Clang: issues with libstdc++   * GCC: LTO removes needed assembly symbols   * Go back to GCC - Lord of the Browsers: The Two Compilers: * jue dic 09 2021 Callum Farmer <gmbr3@opensuse.org>    * CVE-2021-4102: Use after free in V8   * CVE-2021-4101: Heap buffer overflow in Swiftshader   * CVE-2021-4100: Object lifecycle issue in ANGLE   * CVE-2021-4099: Use after free in Swiftshader   * CVE-2021-4098: Insufficient data validation in Mojo - Chromium 96.0.4664.110 (boo#1193713): * mar dic 14 2021 Andreas Stieger <andreas.stieger@gmx.de>  - No auto thread LTO: linker crash on ARM 11 is entirely broken     and extended the n-th child pseudo selector.   * CSS added trigonometric functions, additional root font units   * New developer tools in style panel for color functionality   * CSS Color Level 4   * New View Transitions API - Chromium 111.0.5563.64 * jue mar 09 2023 Andreas Stieger <andreas.stieger@gmx.de> ad  P  	     (             v  H        [  '          b  2        q  >        z  G    
  
  ;
  *
  	  	  	  	  o	  \	  =	  	  	                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    * chromium-90   * chromium-91-libyuv-aarch64.patch   * chromium-94-compiler.patch - Removed patches:   * chromium-95-system-zlib.patch   * chromium-95-quiche-include.patch   * chromium-95-libyuv-aarch64.patch   * chromium-95-compiler.patch   * chromium-95-BitstreamReader-namespace.patch - Added patches:   * CVE-2021-37995: Inappropriate implementation in WebApp Installer   * CVE-2021-37994: Inappropriate implementation in iFrame Sandbox   * CVE-2021-37996: Insufficient validation of untrusted input in Downloads   * CVE-2021-37993: Use after free in PDF Accessibility   * CVE-2021-37992: Out of bounds read in WebAudio   * CVE-2021-37991: Race in V8   * CVE-2021-37990: Inappropriate implementation in WebView   * CVE-2021-37989: Inappropriate implementation in Blink   * CVE-2021-37988: Use after free in Profiles   * CVE-2021-37987: Use after free in Network APIs   * CVE-2021-37986: Heap buffer overflow in Settings   * CVE-2021-37985: Use after free in V8   * CVE-2021-37984: Heap buffer overflow in PDFium   * CVE-2021-37983: Use after free in Dev Tools   * CVE-2021-37982: Use after free in Incognito   * CVE-2021-37981: Heap buffer overflow in Skia - Chromium 95.0.4638.54 (boo#1191844): * dom oct 24 2021 Callum Farmer <gmbr3@opensuse.org>    * CVE-2021-38003: Inappropriate implementation in V8   * CVE-2021-38002: Use after free in Web Transport   * CVE-2021-38001: Type Confusion in V8   * CVE-2021-38000: Insufficient validation of untrusted input in Intents   * CVE-2021-37999: Insufficient data validation in New Tab Page   * CVE-2021-37998: Use after free in Garbage Collection   * CVE-2021-37997: Use after free in Sign-In - Chromium 95.0.4638.69 (boo#1192184): * dom oct 31 2021 Andreas Stieger <andreas.stieger@gmx.de>  - Explicitly BuildRequire python3-six. 07070100000001000081A400000000000000000000000165F86304000000DE000000000000000000000000000000000000002000000000wireplumber-0.5.0/.editorconfig   root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[Makefile]
indent_style = tab
indent_size = 8

[*.py]
indent_style = space
indent_size = 4
  07070100000002000081A400000000000000000000000165F8630400000033000000000000000000000000000000000000001D00000000wireplumber-0.5.0/.gitignore  build/
subprojects/lua-*
subprojects/packagecache/
 07070100000003000081A400000000000000000000000165F86304000020F7000000000000000000000000000000000000002100000000wireplumber-0.5.0/.gitlab-ci.yml  stages:
  - container
  - container_coverity
  - build
  - analysis
  - pages

variables:
  FDO_UPSTREAM_REPO: 'pipewire/wireplumber'
  # change to build against a different tag or branch of pipewire
  PIPEWIRE_HEAD: 'master'

# ci-templates as of Mar 24th 2023
.templates_sha: &templates_sha dd90ac0d7a03b574eb4f18d7358083f0c97825f3

include:
  - project: 'freedesktop/ci-templates'
    ref: *templates_sha
    file: '/templates/fedora.yml'
  - project: 'freedesktop/ci-templates'
    ref: *templates_sha
    file: '/templates/ubuntu.yml'
  - project: 'freedesktop/ci-templates'
    ref: *templates_sha
    file: '/templates/alpine.yml'

.fedora:
  variables:
    # Update this tag when you want to trigger a rebuild
    FDO_DISTRIBUTION_TAG: '2023-03-24.1'
    FDO_DISTRIBUTION_VERSION: '37'
    # findutils: used by the .build script below
    # dbus-devel: required by pipewire
    # dbus-daemon: required by GDBus unit tests
    # pip, doxygen: required for documentation
    # ShellCheck, diffutils: required by the CI
    FDO_DISTRIBUTION_PACKAGES: >-
      findutils
      gcc
      gcc-c++
      git
      meson
      glib2-devel
      gobject-introspection-devel
      dbus-devel
      dbus-daemon
      python3-pip
      doxygen
      ShellCheck
      diffutils
    # install Sphinx and Breathe to generate documentation
    # also install glib2-doc (required to make documentation links to GLib work)
    # manually, to remove the 'tsflags=nodocs' flag that is enabled by default
    # in the fedora docker image
    FDO_DISTRIBUTION_EXEC: >-
      pip3 install lxml Sphinx sphinx-rtd-theme breathe ;
      dnf -y install glib2-doc --setopt='tsflags='

.ubuntu:
  variables:
    # Update this tag when you want to trigger a rebuild
    FDO_DISTRIBUTION_TAG: '2023-06-16.1'
    FDO_DISTRIBUTION_VERSION: '22.04'
    FDO_DISTRIBUTION_PACKAGES: >-
      debhelper-compat
      findutils
      git
      meson
      ninja-build
      pkg-config
      python3-pip
      dbus
      libdbus-1-dev
      libglib2.0-dev
      liblua5.3-dev
      libgirepository1.0-dev
      doxygen
      python3-lxml

.alpine:
  variables:
    # Update this tag when you want to trigger a rebuild
    FDO_DISTRIBUTION_TAG: '2023-03-24.1'
    FDO_DISTRIBUTION_VERSION: '3.15'
    FDO_DISTRIBUTION_PACKAGES: >-
      dbus
      dbus-dev
      doxygen
      elogind-dev
      findutils
      g++
      gcc
      git
      glib-dev
      gobject-introspection-dev
      lua5.4-dev
      meson
      py3-lxml
      samurai

.coverity:
  variables:
    FDO_REPO_SUFFIX: 'coverity'
    FDO_BASE_IMAGE: registry.freedesktop.org/$FDO_UPSTREAM_REPO/fedora/$FDO_DISTRIBUTION_VERSION:$FDO_DISTRIBUTION_TAG
    FDO_DISTRIBUTION_PACKAGES: >-
      curl
    FDO_DISTRIBUTION_EXEC: >-
      mkdir -p /opt ;
      cd /opt ;
      curl -o /tmp/cov-analysis-linux64.tgz https://scan.coverity.com/download/cxx/linux64
      --form project=$COVERITY_SCAN_PROJECT_NAME --form token=$COVERITY_SCAN_TOKEN ;
      tar xf /tmp/cov-analysis-linux64.tgz ;
      mv cov-analysis-linux64-* coverity ;
      rm /tmp/cov-analysis-linux64.tgz
  only:
    variables:
      - $COVERITY

.not_coverity:
  except:
    variables:
      - $COVERITY

.build:
  before_script:
    # setup the environment
    - export BUILD_ID="$CI_JOB_ID"
    - export PREFIX="$PWD/prefix-$BUILD_ID"
    - export PW_BUILD_DIR="$PWD/build-pipewire-$BUILD_ID"
    - |
      if [ -n "$FDO_CI_CONCURRENT" ]; then
         NINJA_ARGS="-j$FDO_CI_CONCURRENT $NINJA_ARGS"
         export NINJA_ARGS
      fi
    # Build pipewire
    # Fedora also ships that, but without the test plugins that we need...
    - git clone --depth=1 --branch="$PIPEWIRE_HEAD"
        https://gitlab.freedesktop.org/pipewire/pipewire.git
    - meson "$PW_BUILD_DIR" pipewire --prefix="$PREFIX"
        -Dpipewire-alsa=disabled -Dpipewire-jack=disabled
        -Dalsa=disabled -Dv4l2=disabled -Djack=disabled -Dbluez5=disabled
        -Dvulkan=disabled -Dgstreamer=disabled -Dsystemd=disabled
        -Ddocs=disabled -Dman=disabled -Dexamples=disabled -Dpw-cat=disabled
        -Dsdl2=disabled -Dsndfile=disabled -Dlibpulse=disabled -Davahi=disabled
        -Decho-cancel-webrtc=disabled -Dsession-managers=[]
        -Dvideotestsrc=enabled -Daudiotestsrc=enabled -Dtest=enabled
    - ninja $NINJA_ARGS -C "$PW_BUILD_DIR" install
    # misc environment only for wireplumber
    - export WP_BUILD_DIR="$PWD/build-wireplumber-$BUILD_ID"
    - export PKG_CONFIG_PATH="$(dirname $(find "$PREFIX" -name 'libpipewire-*.pc')):$PKG_CONFIG_PATH"
  script:
    # Build wireplumber
    - meson "$WP_BUILD_DIR" . --prefix="$PREFIX" $BUILD_OPTIONS
    - cd "$WP_BUILD_DIR"
    - ninja $NINJA_ARGS
    - ninja $NINJA_ARGS test
    - ninja $NINJA_ARGS install
  artifacts:
    name: wireplumber-$CI_COMMIT_SHA
    when: always
    paths:
      - build-*/meson-logs
      - prefix-*

container_fedora:
  extends:
    - .fedora
    - .fdo.container-build@fedora
  stage: container
  variables:
    GIT_STRATEGY: none

container_ubuntu:
  extends:
    - .ubuntu
    - .fdo.container-build@ubuntu
  stage: container
  variables:
    GIT_STRATEGY: none

container_alpine:
  extends:
    - .alpine
    - .fdo.container-build@alpine
  stage: container
  variables:
    GIT_STRATEGY: none

container_coverity:
  extends:
    - .fedora
    - .coverity
    - .fdo.container-build@fedora
  stage: container_coverity
  variables:
    GIT_STRATEGY: none

build_on_fedora_with_docs:
  extends:
    - .fedora
    - .not_coverity
    - .fdo.distribution-image@fedora
    - .build
  stage: build
  variables:
    BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=enabled -Dsystem-lua=false

build_on_fedora_no_docs:
  extends:
    - .fedora
    - .not_coverity
    - .fdo.distribution-image@fedora
    - .build
  stage: build
  variables:
    BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=false

build_on_ubuntu_with_gir:
  extends:
    - .ubuntu
    - .not_coverity
    - .fdo.distribution-image@ubuntu
    - .build
  stage: build
  variables:
    BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=true

build_on_ubuntu_no_gir:
  extends:
    - .ubuntu
    - .not_coverity
    - .fdo.distribution-image@ubuntu
    - .build
  stage: build
  variables:
    BUILD_OPTIONS: -Dintrospection=disabled -Ddoc=disabled -Dsystem-lua=true

build_on_alpine:
  extends:
    - .alpine
    - .not_coverity
    - .fdo.distribution-image@alpine
    - .build
  stage: build
  variables:
    BUILD_OPTIONS: -Dintrospection=enabled -Ddoc=disabled -Dsystem-lua=true -Delogind=disabled

build_with_coverity:
  extends:
    - .fedora
    - .coverity
    - .fdo.suffixed-image@fedora
    - .build
  stage: analysis
  script:
    - export PATH=/opt/coverity/bin:$PATH
    - meson "$WP_BUILD_DIR" . --prefix="$PREFIX"
        -Dintrospection=disabled -Ddoc=disabled
    - cov-configure --config coverity_conf.xml
        --comptype gcc --compiler cc --template
        --xml-option=append_arg@C:--ppp_translator
        --xml-option=append_arg@C:"replace/GLIB_(DEPRECATED|AVAILABLE)_ENUMERATOR_IN_\d_\d\d(_FOR\(\w+\)|)\s+=/ ="
    - cov-build --dir cov-int --config coverity_conf.xml ninja $NINJA_ARGS -C "$WP_BUILD_DIR"
    - tar caf wireplumber.tar.gz cov-int
    - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME
        --form token=$COVERITY_SCAN_TOKEN --form email=$GITLAB_USER_EMAIL
        --form file=@wireplumber.tar.gz --form version="`git describe --tags`"
        --form description="`git describe --tags` / $CI_COMMIT_TITLE / $CI_COMMIT_REF_NAME:$CI_PIPELINE_ID "
  artifacts:
    name: wireplumber-coverity-$CI_COMMIT_SHA
    when: always
    paths:
      - build-*/meson-logs
      - cov-int/build-log.txt

shellcheck:
  extends:
    - .fedora
    - .not_coverity
    - .fdo.distribution-image@fedora
  stage: analysis
  script:
    - shellcheck $(git grep -l "#\!/.*bin/.*sh")

linguas_check:
  extends:
    - .fedora
    - .not_coverity
    - .fdo.distribution-image@fedora
  stage: analysis
  script:
    - cd po
    - cat LINGUAS | sort > LINGUAS.sorted
    - ls *.po | sed s/.po//g | sort > LINGUAS.new
    - diff -u LINGUAS.sorted LINGUAS.new
    - rm -f LINGUAS.*

pages:
  extends:
    - .not_coverity
  stage: pages
  dependencies:
    - build_on_fedora_with_docs
  script:
    - mkdir public
    - cp -R prefix-*/share/doc/wireplumber/html/* public/
  artifacts:
    paths:
      - public
  only:
    - master
 07070100000004000081A400000000000000000000000165F8630400000446000000000000000000000000000000000000001A00000000wireplumber-0.5.0/LICENSE Copyright © 2019-2021 Collabora Ltd.

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
  07070100000005000081A400000000000000000000000165F86304000001AE000000000000000000000000000000000000001B00000000wireplumber-0.5.0/Makefile    WIREPLUMBER_DEBUG ?= 3

all:
	ninja -C build

install:
	ninja -C build install

uninstall:
	ninja -C build uninstall

clean:
	ninja -C build clean

run: all
	WIREPLUMBER_DEBUG=$(WIREPLUMBER_DEBUG) \
	./wp-uninstalled.sh $(DBG) wireplumber

test:
	meson test -C build

test_valgrind:
	meson test -C build --setup=valgrind

gdb:
	$(MAKE) run DBG=gdb

valgrind:
	G_SLICE=always-malloc \
	$(MAKE) run DBG="valgrind --leak-check=full"
  07070100000006000081A400000000000000000000000165F863040000C0B0000000000000000000000000000000000000001B00000000wireplumber-0.5.0/NEWS.rst    WirePlumber 0.5.0
~~~~~~~~~~~~~~~~~

Changes:

  - Bumped the minimum required version of PipeWire to 1.0.2, because we
    make use of the 'api.bluez5.internal' property of the BlueZ monitor (!613)

  - Improved the naming of Bluetooth nodes when the auto-switching loopback
    node is present (!614)

  - Updated the documentation on "settings", the Bluetooth monitor, the Access
    configuration, the file search locations and added a document on how to
    modify the configuration file (#595, !616)

Fixes:

  - Fixed checking for available routes when selecting the default node (!609)

  - Fixed an issue that was causing an infinite loop storing routes in the
    state file (!610)

  - Fixed the interpretation of boolean values in the alsa monitor rules (#586, !611)

  - Fixes a Lua crash when we have 2 smart filters, one with a target and one
    without (!612)

  - Fixed an issue where the default nodes would not be updated when the
    currently selected default node became unavailable (#588, !615)

  - Fixed an issue that would cause the Props (volume, mute, etc) of loopbacks
    and other filter nodes to not be restored at startup (#577, !617)

  - Fixed how some constants were represented in the gobject-introspection file,
    mostly by converting them from defines to enums (#540, #591)

  - Fixed an issue using WirePlumber headers in other projects due to
    redefinition of G_LOG_DOMAIN (#571)

Past releases
~~~~~~~~~~~~~

WirePlumber 0.4.90
..................

This is the first release candidate (RC1) of WirePlumber 0.5.0.

Highlights:

  - The configuration system has been changed back to load files from the
    WirePlumber configuration directories, such as ``/etc/wireplumber`` and
    ``$XDG_CONFIG_HOME/wireplumber``, unlike in the pre-releases. This was done
    because issues were observed with installations that use a different prefix
    for pipewire and wireplumber. If you had a ``wireplumber.conf`` file in
    ``/etc/pipewire`` or ``$XDG_CONFIG_HOME/pipewire``, you should move it to
    ``/etc/wireplumber`` or ``$XDG_CONFIG_HOME/wireplumber`` respectively (!601)

  - The internal base directories lookup system now also respects the
    ``XDG_CONFIG_DIRS`` and ``XDG_DATA_DIRS`` environment variables, and their
    default values as per the XDG spec, so it is possible to install
    configuration files also in places like ``/etc/xdg/wireplumber`` and
    override system-wide data paths (!601)

  - ``wpctl`` now has a ``settings`` subcommand to show, change and delete
    settings at runtime. This comes with changes in the ``WpSettings`` system to
    validate settings using a schema that is defined in the configuration file.
    The schema is also exported on a metadata object, so it is available to any
    client that wants to expose WirePlumber settings (!599, !600)

  - The ``WpConf`` API has changed to not be a singleton and support opening
    arbitrary config files. The main config file now needs to be opened prior to
    creating a ``WpCore`` and passed to the core using a property. The core uses
    that without letting the underlying ``pw_context`` open and read the default
    ``client.conf``. The core also closes the ``WpConf`` after all components
    are loaded, which means all the config loading is done early at startup.
    Finally, ``WpConf`` loads all sections lazily, keeping the underlying files
    memory mapped until it is closed and merging them on demand (!601, !606)

WirePlumber 0.4.82
..................

This is a second pre-release of WirePlumber 0.5.0, made available for testing
purposes. This is not API/ABI stable yet and there is still pending work to do
before the final 0.5.0 release, both in the codebase and the documentation.

Highlights:

  - Bluetooth auto-switching is now implemented with a virtual source node. When
    an application links to it, the actual device switches to the HSP/HFP
    profile to provide the real audio stream. This is a more robust solution
    that works with more applications and is more user-friendly than the
    previous application whitelist approach

  - Added support for dynamic log level changes via the PipeWire ``settings``
    metadata. Also added support for log level patterns in the configuration
    file

  - The "persistent" (i.e. stored) settings approach has changed to use two
    different metadata objects: ``sm-settings`` and ``persistent-sm-settings``.
    Changes in the former are applied in the current session but not stored,
    while changes in the latter are stored and restored at startup. Some work
    was also done to expose a ``wpctl`` interface to read and change these
    settings, but more is underway

  - Several WirePlumber-specific node properties that used to be called
    ``target.*`` have been renamed to ``node.*`` to match the PipeWire
    convention of ``node.dont-reconnect``. These are also now fully documented

Other changes:

  - Many documentation updates

  - Added support for SNAP container permissions

  - Fixed multiple issues related to restoring the Route parameter of devices,
    which includes volume state (#551)

  - Smart filters can now be targetted by specific streams directly when
    the ``filter.smart.targetable`` property is set (#554)

  - Ported the mechanism to override device profile priorities in the
    configuration, which is used to re-prioritize Bluetooth codecs

  - WpSettings is no longer a singleton class and there is a built-in component
    to preload an instance of it

WirePlumber 0.4.81
..................

This is a preliminary release of WirePlumber 0.5.0, which is made available
for testing purposes. Please test it and report feedback (merge requests are
also welcome ;) ). This is not API/ABI stable yet and there is still pending
work to do before the final 0.5.0 release, both in the codebase and the
documentation.

Highlights:

  - Lua scripts have been refactored to use the new event dispatcher API, which
    allows them to be split into multiple small fragments that react to
    events in a specified order. This allows scripts to be more modular and
    easier to maintain, as well as more predictable in terms of execution
    order.

  - The configuration system has been refactored to use a single SPA-JSON file,
    like PipeWire does, with support for fragments that can override options.
    This file is also now loaded using PipeWire's configuration API, which
    effectively means that the file is now loaded from the PipeWire configuration
    directories, such as ``/etc/pipewire`` and ``$XDG_CONFIG_HOME/pipewire``.

  - The configuration system now has the concept of profiles, which are groups
    of components that can be loaded together, with the ability to mark certain
    components as optional. This allows having multiple configurations that
    can be loaded using the same configuration file. Optional components also
    allow loading the same profile gracefully on different setups, where some
    components may not be available (ex, loading of the session D-Bus plugin on
    a system-wide PipeWire setup now does not fail).

  - Many configuration options are now exposed in the ``sm-settings`` metadata,
    which allows changing them at runtime. This can be leveraged in the future
    to implement configuration tools that can modify WirePlumber's behaviour
    dynamically, without restarting.

  - A new "filters" system has been implemented, which allows specifying chains
    of "filter" nodes to be dynamically linked in-between streams and devices.
    This is achieved with certain properties and metadata that can be set on
    the filter nodes themselves.

  - The default linking policy now reads some more ``target.*`` properties from
    nodes, which allows fine-tuning some aspects of their linking behaviour,
    such as whether they are allowed to be re-linked or whether an error should
    be sent to the client if they cannot be linked.

  - Some state files have been renamed and some have changed format to use JSON
    for storing complex values, such as arrays. This may cause some of the old
    state to be lost on upgrade, as there is no transition path implemented.

  - The libcamera and V4L2 monitors have a "device deduplication" logic built-in,
    which means that for each physical camera device, only one node will be
    created, either from libcamera or V4L2, depending on which one is considered
    better for the device. This is mainly to avoid having multiple nodes for
    the same camera device, which can cause confusion when looking at the list
    of available cameras in applications.

WirePlumber 0.4.17
..................

Fixes:

  - Fixed a reference counting issue in the object managers that could cause
    crashes due to memory corruption (#534)

  - Fixed an issue with filters linking to wrong targets, often with two sets
    of links (#536)

  - Fixed a crash in the endpoints policy that would show up when log messages
    were enabled at level 3 or higher

WirePlumber 0.4.16
..................

Additions:

  - Added a new "sm-objects" script that allows loading objects on demand
    via metadata entries that describe the object to load; this can be used to
    load pipewire modules, such as filters or network sources/sinks, on demand

  - Added a mechanism to override device profile priorities in the configuration,
    mainly as a way to re-prioritize Bluetooth codecs, but this also can be used
    for other devices

  - Added a mechanism in the endpoints policy to allow connecting filters
    between a certain endpoint's virtual sink and the device sink; this is
    specifically intended to allow plugging a filter-chain to act as equalizer
    on the Multimedia endpoint

  - Added wp_core_get_own_bound_id() method in WpCore

Changes:

  - PipeWire 0.3.68 is now required

  - policy-dsp now has the ability to hide hardware nodes behind the DSP sink
    to prevent hardware misuse or damage

  - JSON parsing in Lua now allows keys inside objects to be without quotes

  - Added optional argument in the Lua JSON parse() method to limit recursions,
    making it possible to partially parse a JSON object

  - It is now possible to pass ``nil`` in Lua object constructors that expect an
    optional properties object; previously, omitting the argument was the only
    way to skip the properties

  - The endpoints policy now marks the endpoint nodes as "passive" instead of
    marking their links, adjusting for the behavior change in PipeWire 0.3.68

  - Removed the "passive" property from si-standard-link, since only nodes are
    marked as passive now

Fixes:

  - Fixed the ``wpctl clear-default`` command to completely clear all the
    default nodes state instead of only the last set default

  - Reduced the amount of globals that initially match the interest in the
    object manager

  - Used an idle callback instead of pw_core_sync() in the object manager to
    expose tmp globals

WirePlumber 0.4.15
..................

Additions:

  - A new "DSP policy" module has been added; its purpose is to automatically
    load a filter-chain when a certain hardware device is present, so that
    audio always goes through this software DSP before reaching the device.
    This is mainly to support Apple M1/M2 devices, which require a software
    DSP to be always present

  - WpImplModule now supports loading module arguments directly from a SPA-JSON
    config file; this is mainly to support DSP configuration for Apple M1/M2
    and will likely be reworked for 0.5

  - Added support for automatically combining Bluetooth LE Audio device sets
    (e.g. pairs of earbuds) (!500)

  - Added command line options in ``wpctl`` to display device/node names and
    nicknames instead of descriptions

  - Added zsh completions file for ``wpctl``

  - The device profile selection policy now respects the ``device.profile``
    property if it is set on the device; this is useful to hand-pick a profile
    based on static configuration rules (alsa_monitor.rules)

Changes/Fixes:

  - Linking policy now sends an error to the client before destroying the node,
    if it determines that the node cannot be linked to any target; this fixes
    error reporting on the client side

  - Fixed a crash in suspend-node that could happen when destroying virtual
    sinks that were loaded from another process such as pw-loopback (#467)

  - Virtual machine default period size has been bumped to 1024 (#507)

  - Updated bluez5 default configuration, using ``bluez5.roles`` instead of
    ``bluez5.headset-roles`` now (!498)

  - Disabled Bluetooth autoconnect by default (!514)

  - Removed ``RestrictNamespaces`` option from the systemd services in order to
    allow libcamera to load sandboxed IPA modules (#466)

  - Fixed a JSON encoding bug with empty strings (#471)

  - Lua code can now parse strings without quotes from SPA-JSON

  - Added some missing `\since` annotations and made them show up in the
    generated gobject-introspection file, to help bindings generators

WirePlumber 0.4.14
..................

Additions:

  - Added support for managing Bluetooth-MIDI, complimenting the parts that
    were merged in PipeWire recently (!453)

  - Added a default volume configuration option for streams whose volume
    has never been saved before; that allows starting new streams at a lower
    volume than 100% by default, if desired (!480)

  - Added support for managing link errors and propagating them to the
    client(s) involved. This allows better error handling on the application
    side in case a format cannot be negotiated - useful in video streams
    (see !484, pipewire#2935)

  - snd_aloop devices are now described as being "Loopback" devices
    (pipewire#2214)

  - ALSA nodes in the pro audio profile now get increased graph priority, so
    that they are more likely to become the driver in the graph

  - Added support for disabling libcamera nodes & devices with ``node.disabled``
    and ``device.disabled``, like it works for ALSA and V4L2 (#418)

WirePlumber 0.4.13
..................

Additions:

  - Added bluetooth SCO (HSP/HFP) hardware offload support, together with an
    example script that enables this functionality on the PinePhone

  - Encoded audio (mp3, aac, etc...) can now be passed through, if this mode is
    supported by both the application and the device

  - The v4l2 monitor now also respects the ``node.disabled`` and
    ``device.disabled`` properties inside rules

  - Added "Firefox Developer Edition" to the list of apps that are allowed to
    trigger a bluetooth profile auto-switch (#381)

  - Added support in the portal access script to allow newly plugged cameras
    to be immediately visible to the portal apps

Fixes:

  - Worked around an issue that would prevent streams from properly linking
    when using effects software like EasyEffects and JamesDSP (!450)

  - Fixed destroying pavucontrol-qt monitor streams after the node that was
    being monitored is destroyed (#388)

  - Fixed a crash in the alsa.lua monitor that could happen when a disabled
    device was removed and re-added (#361)

  - Fixed a rare crash in the metadata object (#382)

  - Fixed a bug where a restored node target would override the node target
    set by the application on the node's properties (#335)

Packaging:

  - Added build options to compile wireplumber's library, daemon and tools
    independently

  - Added a build option to disable unit tests that require the dbus daemon

  - Stopped using fakesink/fakesrc in the unit tests to be able to run them
    on default pipewire installations. Compiling the spa ``test`` plugin is no
    longer necessary

  - Added pkg-config and header information in the gir file

WirePlumber 0.4.12
..................

Changes:

  - WirePlumber now maintains a stack of previously configured default nodes and
    prioritizes to one of those when the actively configured default node
    becomes unavailable, before calculating the next default using priorities
    (see !396)

  - Updated bluetooth scripts to support the name changes that happened in
    PipeWire 0.3.59 and also support the experimental Bluetooth LE functionality

  - Changed the naming of bluetooth nodes to not include the profile in it;
    this allows maintaining existing links when switching between a2dp and hfp

  - The default volume for new outputs has changed to be 40% in cubic scale
    (= -24 dB) instead of linear (= 74% cubic / -8 dB) that it was before

  - The default volume for new inputs has changed to be 100% rather than
    following the default for outputs

  - Added ``--version`` flag on the wireplumber executable (#317)

  - Added ``--limit`` flag on ``wpctl set-volume`` to limit the higher volume
    that can be set (useful when incrementing volume with a keyboard shortcut
    that calls into wpctl)

  - The properties of the alsa midi node can now be set in the config files

Fixes:

  - Fixed a crash in lua code that would happen when running in a VM (#303)

  - Fixed a crash that would happen when re-connecting to D-Bus (#305)

  - Fixed a mistake in the code that would cause device reservation not to
    work properly

  - Fixed ``wpctl clear-default`` to accept 0 as a valid setting ID

  - Fixed the logic of choosing the best profile after the active profile
    of a device becomes unavailable (#329)

  - Fixed a regression that would cause PulseAudio "corked" streams to not
    properly link and cause busy loops

  - Fixed an issue parsing spa-json objects that have a nested object as the
    value of their last property

WirePlumber 0.4.11
..................

Changes:

  - The libcamera monitor is now enabled by default, so if the libcamera source
    is enabled in PipeWire, cameras discovered with the libcamera API will be
    available out of the box. This is safe to use alongside V4L2, as long as
    the user does not try to use the same camera over different APIs at the same
    time

  - Libcamera and V4L2 nodes now get assigned a ``priority.session`` number;
    V4L2 nodes get a higher priority by default, so the default camera is going
    to be /dev/video0 over V4L2, unless changed with ``wpctl``

  - Libcamera nodes now get a user-friendly description based on their location
    (ex. built-in front camera). Additionally, V4L2 nodes now have a "(V4L2)"
    string appended to their description in order to be distinguished from
    the libcamera ones

  - 50-alsa-config.lua now has a section where you can set properties that
    will only be applied if WirePlumber is running in a virtual machine. By
    default it now sets ``api.alsa.period-size = 256`` and
    ``api.alsa.headroom = 8192`` (#162, #134)

Fixes:

  - The "enabled" properties in the config files are now "true" by default
    when they are not defined. This fixes backwards compatibility with older
    configuration files (#254)

  - Fixed device name deduplication in the alsa monitor, when device reservation
    is enabled (#241)

  - Reverted a previous fix that makes it possible again to get a glitch when
    changing default nodes while also changing the profile (GNOME Settings).
    The fix was causing other problems and the issue will be addressed
    differently in the future (#279)

  - Fixed an issue that would prevent applications from being moved to a
    recently plugged USB headset (#293)

  - Fixed an issue where wireplumber would automatically link control ports,
    if they are enabled, to audio ports, effectively breaking audio (#294)

  - The policy now always considers the profile of a device that was previously
    selected by the user, if it is available, when deciding which profile to
    activate (#179). This may break certain use cases (see !360)

  - A few documentation fixes

Tools:

  - wpctl now has a ``get-volume`` command for easier scripting of volume controls

  - wpctl now supports relative steps and percentage-based steps in ``set-volume``

  - wpctl now also prints link states

  - wpctl can now ``inspect`` metadata objects without showing critical warnings

Library:

  - A new WpDBus API was added to maintain a single D-Bus connection among
    modules that need one

  - WpCore now has a method to get the virtual machine type, if WirePlumber
    is running in a virtual machine

  - WpSpaDevice now has a ``wp_spa_device_new_managed_object_iterator()`` method

  - WpSpaJson now has a ``wp_spa_json_to_string()`` method that returns a newly
    allocated string with the correct size of the string token

  - WpLink now has a ``WP_LINK_FEATURE_ESTABLISHED`` that allows the caller to
    wait until the link is in the PAUSED or ACTIVE state. This transparently
    now enables watching links for negotiation or allocation errors and failing
    gracefully instead of keeping dead link objects around (#294)

Misc:

  - The Lua subproject was bumped to version 5.4.4

WirePlumber 0.4.10
..................

Changes:

  - Added i18n support to be able to translate some user-visible strings

  - wpctl now supports using ``@DEFAULT_{AUDIO_,VIDEO_,}{SINK,SOURCE}@`` as ID,
    almost like pactl. Additionally, it supports a ``--pid`` flag for changing
    volume and mute state by specifying a process ID, applying the state to all
    nodes of a specific client process

  - The Lua engine now supports loading Lua libraries. These can be placed
    either in the standard Lua libraries path or in the "lib" subdirectory
    of WirePlumber's "scripts" directory and can be loaded with ``require()``

  - The Lua engine's sandbox has been relaxed to allow more functionality
    in scripts (the debug & coroutine libraries and some other previously
    disabled functions)

  - Lua scripts are now wrapped in special WpPlugin objects, allowing them to
    load asynchronously and declare when they have finished their loading

  - Added a new script that provides the same functionality as
    module-fallback-sink from PipeWire, but also takes endpoints into account
    and can be customized more easily. Disabled by default for now to avoid
    conflicts

Policy:

  - Added an optional experimental feature that allows filter-like streams
    (like echo-cancel or filter-node) to match the channel layout of the
    device they connect to, on both sides of the filter; that means that if,
    for instance, a sink has 6 channels and the echo-cancel's source stream
    is linked to that sink, then the virtual sink presented by echo-cancel
    will also be configured to the same 6 channels layout. This feature needs
    to be explicitly enabled in the configuration ("filter.forward-format")

  - filter-like streams (filter-chain and such) no longer follow the default
    sink when it changes, like in PulseAudio

Fixes:

  - The suspend-node script now also suspends nodes that go into the "error"
    state, allowing them to recover from errors without having to restart
    WirePlumber

  - Fixed a crash in mixer-api when setting volume with channelVolumes (#250)

  - logind module now watches only for user state changes, avoiding errors when
    machined is not running

Misc:

  - The configuration files now have comments mentioning which options need to
    be disabled in order to run WirePlumber without D-Bus

  - The configuration files now have properties to enable/disable the monitors
    and other sections, so that it is possible to disable them by dropping in
    a file that just sets the relevant property to false

  - ``setlocale()`` is now called directly instead of relying on ``pw_init()``

  - WpSpaJson received some fixes and is now used internally to parse
    configuration files

  - More applications were added to the bluetooth auto-switch apps whitelist

WirePlumber 0.4.9
.................

Fixes:

  - restore-stream no longer crashes if properties for it are not present
    in the config (#190)

  - spa-json no longer crashes on non-x86 architectures

  - Fixed a potential crash in the bluetooth auto-switch module (#193)

  - Fixed a race condition that would cause Zoom desktop audio sharing to fail
    (#197)

  - Surround sound in some games is now exposed properly (pipewire#876)

  - Fixed a race condition that would cause the default source & sink to not
    be set at startup

  - policy-node now supports the 'target.object' key on streams and metadata

  - Multiple fixes in policy-node that make the logic in some cases behave
    more like PulseAudio (regarding nodes with the dont-reconnect property
    and regarding following the default source/sink)

  - Fixed a bug with parsing unquoted strings in spa-json

Misc:

  - The policy now supports configuring "persistent" device profiles. If a
    device is *manually* set to one of these profiles, then it will not be
    auto-switched to another profile automatically under any circumstances
    (#138, #204)

  - The device-activation module was re-written in lua

  - Brave, Edge, Vivaldi and Telegram were added in the bluetooth auto-switch
    applications list

  - ALSA nodes now use the PCM name to populate node.nick, which is useful
    at least on HDA cards using UCM, where all outputs (analog, hdmi, etc)
    are exposesd as nodes on a single profile

  - An icon name is now set on the properties of bluetooth devices

WirePlumber 0.4.8
.................

Highlights:

  - Added bluetooth profile auto-switching support. Bluetooth headsets will now
    automatically switch to the HSP/HFP profile when making a call and go back
    to the A2DP profile after the call ends (#90)

  - Added an option (enabled by default) to auto-switch to echo-cancel virtual
    device nodes when the echo-cancel module is loaded in pipewire-pulse, if
    there is no other configured default node

Fixes:

  - Fixed a regression that prevented nodes from being selected as default when
    using the pro-audio profile (#163)

  - Fixed a regression that caused encoded audio streams to stall (#178)

  - Fixed restoring bluetooth device profiles

Library:

  - A new WpSpaJson API was added as a front-end to spa-json. This is also
    exposed to Lua, so that Lua scripts can natively parse and write data in
    the spa-json format

Misc:

  - wpctl can now list the configured default sources and sinks and has a new
    command that allows clearing those configured defaults, so that wireplumber
    goes back to choosing the default nodes based on node priorities

  - The restore-stream script now has its own configuration file in
    main.lua.d/40-stream-defaults.lua and has independent options for
    restoring properties and target nodes

  - The restore-stream script now supports rule-based configuration to disable
    restoring volume properties and/or target nodes for specific streams,
    useful for applications that misbehave when we restore those (see #169)

  - policy-endpoint now assigns the "Default" role to any stream that does not
    have a role, so that it can be linked to a pre-configured endpoint

  - The route-settings-api module was dropped in favor of dealing with json
    natively in Lua, now that the API exists

WirePlumber 0.4.7
.................

Fixes:

  - Fixed a regression in 0.4.6 that caused the selection of the default audio
    sources and sinks to be delayed until some event, which effectively caused
    losing audio output in many circumstances (#148, #150, #151, #153)

  - Fixed a regression in 0.4.6 that caused the echo-cancellation pipewire
    module (and possibly others) to not work

  - A default sink or source is now not selected if there is no available route
    for it (#145)

  - Fixed an issue where some clients would wait for a bit while seeking (#146)

  - Fixed audio capture in the endpoints-based policy

  - Fixed an issue that would cause certain lua scripts to error out with older
    configuration files (#158)

WirePlumber 0.4.6
.................

Changes:

  - Fixed a lot of race condition bugs that would cause strange crashes or
    many log messages being printed when streaming clients would connect and
    disconnect very fast (#128, #78, ...)

  - Improved the logic for selecting a default target device (#74)

  - Fixed switching to headphones when the wired headphones are plugged in (#98)

  - Fixed an issue where ``udevadm trigger`` would break wireplumber (#93)

  - Fixed an issue where switching profiles of a device could kill client nodes

  - Fixed briefly switching output to a secondary device when switching device
    profiles (#85)

  - Fixed ``wpctl status`` showing default device selections when dealing with
    module-loopback virtual sinks and sources (#130)

  - WirePlumber now ignores hidden files from the config directory (#104)

  - Fixed an interoperability issue with jackdbus (pipewire#1846)

  - Fixed an issue where pulseaudio tcp clients would not have permissions to
    connect to PipeWire (pipewire#1863)

  - Fixed a crash in the journald logger with NULL debug messages (#124)

  - Enabled real-time priority for the bluetooth nodes to run in RT (#132)

  - Made the default stream volume configurable

  - Scripts are now also looked up in $XDG_CONFIG_HOME/wireplumber/scripts

  - Updated documentation on configuring WirePlumber and fixed some more
    documentation issues (#68)

  - Added support for using strings as log level selectors in WIREPLUMBER_DEBUG

WirePlumber 0.4.5
.................

Fixes:

  - Fixed a crash that could happen after a node linking error (#76)

  - Fixed a bug that would cause capture streams to link to monitor ports
    of loopback nodes instead of linking to their capture ports

  - Fixed a needless wait that would happen on applications using the pipewire
    ALSA plugin (#92)

  - Fixed an issue that would cause endless rescan loops in policy-node and
    could potentially also cause other strange behaviors in case pavucontrol
    or another monitoring utility was open while the policy was rescanning (#77)

  - Fixed the endpoints-based policy that broke in recent versions and improved
    its codebase to share more code and be more in-line with policy-node

  - The semicolon character is now escaped properly in state files (#82)

  - When a player requests encoded audio passthrough, the policy now prefers
    linking to a device that supports that instead of trying to link to the
    default device and potentially failing (#75)

  - Miscellaneous robustness fixes in policy-node

API:

  - Added WpFactory, a binding for pw_factory proxies. This allows object
    managers to query factories that are loaded in the pipewire daemon

  - The file-monitor-api plugin can now watch files for changes in addition
    to directories

WirePlumber 0.4.4
.................

Highlights:

  - Implemented linking nodes in passthrough mode, which enables encoded
    iec958 / dsd audio passthrough

  - Streams are now sent an error if it was not possible to link them to
    a target (#63)

  - When linking nodes where at least one of them has an unpositioned channel
    layout, the other one is not reconfigured to match the channel layout;
    it is instead linked with a best effort port matching logic

  - Output route switches automatically to the latest one that has become
    available (#69)

  - Policy now respects the 'node.exclusive' and 'node.passive' properties

  - Many other minor policy fixes for a smoother desktop usage experience

API:

  - Fixed an issue with the ``LocalModule()`` constructor not accepting ``nil``
    as well as the properties table properly

  - Added ``WpClient.send_error()``, ``WpSpaPod.fixate()`` and
    ``WpSpaPod.filter()`` (both in C and Lua)

Misc:

  - Bumped meson version requirement to 0.56 to be able to use
    ``meson.project_{source,build}_root()`` and ease integration with pipewire's
    build system as a subproject

  - wireplumber.service is now an alias to pipewire-session-manager.service

  - Loading the logind module no longer fails if it was not found on the system;
    there is only a message printed in the output

  - The logind module can now be compiled with elogind (#71)

  - Improvements in wp-uninstalled.sh, mostly to ease its integration with
    pipewire's build system when wireplumber is build as a subproject

  - The format of audio nodes is now selected using the same algorithm as in
    media-session

  - Fixed a nasty segfault that appeared in 0.4.3 due to a typo (#72)

  - Fixed a re-entrancy issue in the wplua runtime (#73)

WirePlumber 0.4.3
.................

Fixes:

  - Implemented logind integration to start the bluez monitor only on the
    WirePlumber instance that is running on the active seat; this fixes a bunch
    of startup warnings and the disappearance of HSP/HFP nodes after login (#54)

  - WirePlumber is now launched with GIO_USE_VFS=local to avoid strange D-Bus
    interference when the user session is restarted, which previously resulted
    in WirePlumber being terminated with SIGTERM and never recovering (#48)

  - WirePlumber now survives a restart of the D-Bus service, reconnecting to
    the bus and reclaiming the bus services that it needs (#55)

  - Implemented route-settings metadata, which fixes storing volume for
    the "System Sounds" in GNOME (#51)

  - Monitor sources can now be selected as the default source (#60)

  - Refactored some policy logic to allow linking to monitors; the policy now
    also respects "stream.capture.sink" property of streams which declares
    that the stream wants to be linked to a monitor (#66)

  - Policy now cleans up 'target.node' metadata so that streams get to follow
    the default source/sink again after the default was changed to match the
    stream's currently configured target (#65)

  - Fixed configuring virtual sources (#57)

  - Device monitors now do not crash if a SPA plugin is missing; instead, they
    print a warning to help users identify what they need to install (!214)

  - Fixed certain "proxy activation failed" warnings (#44)

  - iec958 codec configuration is now saved and restored properly (!228)

  - Fixed some logging issues with the latest version of pipewire (!227, !232)

  - Policy now respects the "node.link-group" property, which fixes issues
    with filter-chain and other virtual sources & sinks (#47)

  - Access policy now grants full permissions to flatpak "Manager" apps (#59)

Policy:

  - Added support for 'no-dsp' mode, which allows streaming audio using the
    format of the device instead of the standard float 32-bit planar format (!225)

Library:

  - WpImplMetadata is now implemented using pw_impl_metadata instead of
    using its own implementation (#52)

  - Added support for custom object property IDs in WpSpaPod (#53)

Misc:

  - Added a script to load the libcamera monitor (!231)

  - Added option to disable building unit tests (!209)

  - WirePlumber will now fail to start with a warning if pipewire-media-session
    is also running in the system (#56)

  - The bluez monitor configuration was updated to match the latest one in
    pipewire-media-session (!224)

WirePlumber 0.4.2
.................

Highlights:

  - Requires PipeWire 0.3.32 or later at runtime

  - Configuration files are now installed in $PREFIX/share/wireplumber, along
    with scripts, following the paradigm of PipeWire

  - State files are now stored in $XDG_STATE_HOME instead of $XDG_CONFIG_HOME

  - Added new ``file-monitor-api`` module, which allows Lua scripts to watch
    the filesystem for changes, using inotify

  - Added monitor for MIDI devices

  - Added a ``system-lua-version`` meson option that allows distributors to
    choose which Lua version to build against (``auto``, ``5.3`` or ``5.4``)

  - wpipc has been removed and split out to a separate project,
    https://git.automotivelinux.org/src/pipewire-ic-ipc/

Library:

  - A new ``WpImplModule`` class has been added; this allows loading a PipeWire
    module in the WirePlumber process space, keeping a handle that can be
    used to unload that module later. This is useful for loading filters,
    network sources/sinks, etc...

  - State files can now store keys that contain certain GKeyFile-reserved
    characters, such as ``[``, ``]``, ``=`` and space; this fixes storing
    stream volume state for streams using PipeWire's ALSA compatibility PCM
    plugin

  - ``WpProperties`` now uses a boxed ``WpPropertiesItem`` type in its iterators
    so that these iterators can be used with g-i bindings

  - Added API to lookup configuration and script files from multiple places
    in the filesystem

Lua:

  - A ``LocalModule`` API has been added to reflect the functionality offered
    by ``WpImplModule`` in C

  - The ``Node`` API now has a complete set of methods to reflect the methods
    of ``WpNode``

  - Added ``Port.get_direction()``

  - Added ``not-equals`` to the possible constraint verbs

  - ``Debug.dump_table`` now sorts keys before printing the table

Misc:

  - Tests no longer accidentally create files in $HOME; all transient
    files that are used for testing are now created in the build directory,
    except for sockets which are created in ``/tmp`` due to the 108-character
    limitation in socket paths

  - Tests that require optional SPA plugins are now skipped if those SPA plugins
    are not installed

  - Added a nice summary output at the end of meson configuration

  - Documented the Lua ObjectManager / Interest / Constraint APIs

  - Fixed some memory leaks

WirePlumber 0.4.1
.................

Bug fix release to go with PipeWire 0.3.31.
Please update to this version if you are using PipeWire >= 0.3.31.

Highlights:

  - WirePlumber now supports Lua 5.4. You may compile it either with Lua 5.3
    or 5.4, without any changes in behavior. The internal Lua subproject has
    also been upgraded to Lua 5.4, so any builds with ``-Dsystem-lua=false``
    will use Lua 5.4 by default

Fixes:

  - Fixed filtering of pw_metadata objects, which broke with PipeWire 0.3.31

  - Fixed a potential livelock condition in si-audio-adapter/endpoint where
    the code would wait forever for a node's ports to appear in the graph

  - Fixed granting access to camera device nodes in flatpak clients connecting
    through the camera portal

  - Fixed a lot of issues found by the coverity static analyzer

  - Fixed certain race conditions in the wpipc library

  - Fixed compilation with GCC older than v8.1

Scripts:

  - Added a policy script that matches nodes to specific devices based on the
    "media.role" of the nodes and the "device.intended-roles" of the devices

Build system:

  - Bumped GLib requirement to 2.62, as the code was already using 2.62 API

  - Added support for building WirePlumber as a PipeWire subproject

  - Doxygen version requirement has been relaxed to accept v1.8

  - The CI now also verifies that the build works on Ubuntu 20.04 LTS
    and tries multiple builds with different build options

WirePlumber 0.4.0
.................

This is the first stable release of the 0.4.x series, which is expected to be
an API & ABI stable release series to go along with PipeWire 0.3.x. It is
a fundamental goal of this series to maintain compatibility with
pipewire-media-session, making WirePlumber suitable for a desktop PulseAudio &
JACK replacement setup, while supporting other setups as well (ex. automotive)
by making use of its brand new Lua scripting engine, which allows making
customizations easily.

Highlights:

  - Re-implemented the default-routes module in lua, using the same logic
    as the one that pipewire-media-session uses. This fixes a number of issues
    related to volume controls on alsa devices.

  - Implemented a restore-stream lua script, based on the restore-stream
    module from media-session. This allows storing stream volumes and targets
    and restoring them when the stream re-connects

  - Added support for handling dont-remix streams and streams that are not
    autoconnected. Fixes ``pw-cat -p --target=0`` and the gnome-control-center
    channel test

  - Device names are now sanitized in the same way as in pipewire-media-session

  - Disabled endpoints in the default configuration. Using endpoints does
    not provide the best experience on desktop systems yet

  - Fixed a regression introduced in 0.3.96 that would not allow streams to be
    relinked on their endpoints after having been corked by the policy

Library:

  - Some API methods were changed to adhere to the programming practices
    followed elsewhere in the codebase and to be future-proof. Also added
    paddings on public structures so that from this point on, the 0.4.x series
    is going to be API & ABI stable

  - lua: added WpState and wp_metadata_set() bindings and improved
    WpObject.activate() to report errors

  - ObjectManager: added support for declaring interest on all kinds of
    properties of global objects. Previously it was only possible to declare
    interest on pipewire global properties

Misc:

  - daemon & wpexec: changed the exit codes to follow the standardized codes
    defined in sysexits.h

  - wpexec now forces the log level to be >= 1 so that lua runtime errors can be
    printed on the terminal

  - Fixed issues with gobject-introspection data that were introduced by the
    switch to doxygen

  - Fixed a build issue where wp-gtkdoc.h would not be generated in time
    for the gobject-introspection target to build

  - Added a valgrind test setup in meson, use with ``meson test --setup=valgrind``

  - Many memory leak and stability fixes

  - Updated more documentation pages

WirePlumber 0.3.96
..................

Second pre-release (RC2) of WirePlumber 0.4.0.

Highlights:

  - The policy now configures streams for channel upmixing/downmixing

  - Some issues in the policy have been fixed, related to:

    - plugging a new higher priority device while audio is playing
    - pavucontrol creating links to other stream nodes for level monitoring
    - some race condition that could happen at startup

  - Proxy object errors are now handled; this fixes memory leaks of invalid
    links and generally makes things more robust

  - The systemd service units now conflict with pipewire-media-session.service

  - Session & EndpointLink objects have been removed from the API; these were
    not in use after recent refactoring, so they have been removed in order to
    avoid carrying them in the ABI

  - The documentation system has switched to use *Doxygen* & *Sphinx*; some
    documentation has also been updated and some Lua API documentation has
    been introduced

WirePlumber 0.3.95
..................

First pre-release (RC1) of WirePlumber 0.4.0.

Highlights:

  - Lua scripting engine. All the session management logic is now scripted
    and there is also the ability to run scripts standalone with ``wpexec``
    (see tests/examples).

  - Compatibility with the latest PipeWire (0.3.26+ required). Also, most
    features and behavioral logic of pipewire-media-session 0.3.26 are
    available, making WirePlumber suitable for a desktop PulseAudio & JACK
    replacement setup.

  - Compatibility with embedded system policies, like the one on AGL, has been
    restored and is fully configurable.

  - The design of endpoints has been simplified. We now associate endpoints
    with use cases (roles) instead of physical devices. This removes the need
    for "endpoint stream" objects, allows more logic to be scripted in lua
    and makes the graph simpler. It is also possible to run without endpoints
    at all, matching the behavior of pipewire-media-session and pulseaudio.

  - Configuration is now done using a pipewire-style json .conf file plus lua
    files. Most of the options go in the lua files, while pipewire context
    properties, spa_libs and pipewire modules are configured in the json file.

  - Systemd unit files have been added and are the recommended way to run
    wireplumber. Templated unit files are also available, which allow running
    multiple instances of wireplumber with a specific configuration each.

WirePlumber 0.3.0
.................

The desktop-ready release!

Changes since 0.2.96:

  - Changed how the device endpoints & nodes are named
    to make them look better in JACK graph tools, such as qjackctl.
    JACK tools use the ':' character as a separator to distinguish the node
    name from the port name (since there are no actual nodes in JACK) and
    having ':' in our node names made the graph look strange in JACK

  - Fixed an issue with parsing wireplumber.conf that could cause
    out-of-bounds memory access

  - Fixed some pw_proxy object leaks that would show up in the log

  - Fixed more issues with unlinking the stream volume (si-convert) node
    from the ALSA sink node and suspending the both;
    This now also works with PipeWire 0.3.5 and 0.3.6, so it is possible
    to use these PipeWire versions with WirePlumber without disabling streams
    on audio sinks.

WirePlumber 0.2.96
..................

Second pre-release (RC2) of WirePlumber 0.3.0

Changes since 0.2.95:

  - Quite some work went into fixing bugs related to the ``ReserveDevice1``
    D-Bus API. It is now possible to start a JACK server before or after
    WirePlumber and WirePlumber will automatically stop using the device that
    JACK opens, while at the same time it will enable the special "JACK device"
    that allows PipeWire to interface with JACK

  - Fixed a number of issues that did not previously allow using the spa
    bluez5 device with WirePlumber. Now it is possible to at least use the
    A2DP sink (output to bluetooth speakers) without major issues

  - On the API level, ``WpCore`` was changed to allow having multiple instances
    that share the same ``pw_context``. This is useful to have multiple
    connections to PipeWire, while sharing the context infrastructure

  - ``WpCore`` also gained support for retrieving server info & properties
    and ``wpctl status`` now also prints info about the server & all clients

  - ``module-monitor`` was modified to allow loading multiple monitor instances
    with one instance of the module itself

  - Audio nodes are now configured with the sample rate that is defined
    globally in ``pipewire.conf`` with ``set-prop default.clock.rate <rate>``

  - Policy now respects the ``node.autoconnect`` property; additionally, it is
    now possible to specify endpoint ids in the ``node.target`` property of nodes
    (so endpoint ids are accepted in the ``PIPEWIRE_NODE`` environment variable,
    and in the ``path`` property of the pipewire gstreamer elements)

  - Fixed an issue where links between the si-convert audioconvert nodes and
    the actual device nodes would stay active forever; they are now declared
    as "passive" links, which allows the nodes to suspend. This requires
    changes to PipeWire that were commited after 0.3.6; when using WirePlumber
    with 0.3.5 or 0.3.6, it is recommended to disable streams on audio sinks
    by commenting out the ``streams = "audio-sink.streams"`` lines in the
    .endpoint configuration files

  - ``wireplumber.conf`` now accepts comments to be present inside blocks and
    at the end of valid configuration lines

  - Improved documentation and restructured the default configuration to be
    more readable and sensible

  - Fixed issues that prevented using WirePlumber with GLib < 2.60;
    2.58 is now the actual minimum requirement

WirePlumber 0.2.95
..................

First pre-release of WirePlumber 0.3.0.

This is the first release that targets desktop use-cases. It aims to be
fully compatible with ``pipewire-media-session``, while at the same time it
adds a couple of features that ``pipewire-media-session`` lacks, such as:

  - It makes use of session, endpoint and endpoint-stream objects
    to orchestrate the graph

  - It is configurable:

    - It supports configuration of endpoints, so that their properties
      (such as their name) can be overriden

    - It also supports declaring priorities on endpoints, so that there
      are sane defaults on the first start

    - It supports partial configuration of linking policy

    - It supports creating static node and device objects at startup,
      also driven by configuration files

  - It has the concept of session default endpoints, which can be changed
    with ``wpctl`` and are stored in XDG_CONFIG_DIR, so the user may change
    at runtime the target device of new links in a persistent way

  - It supports volume & mute controls on audio endpoints, which can be
    set with ``wpctl``

  - Last but not least, it is extensible

Also note that this release currently breaks compatibility with AGL, since
the policy management engine received a major refactoring to enable more
use-cases, and has been focusing on desktop support ever since.
Policy features specific to AGL and other embedded systems are expected
to come back in a 0.3.x point release.

WirePlumber 0.2.0
.................

As shipped in AGL Itchy Icefish 9.0.0 and Happy Halibut 8.0.5

WirePlumber 0.1.2
.................

As shipped in AGL Happy Halibut 8.0.2

WirePlumber 0.1.1
.................

As shipped in AGL Happy Halibut 8.0.1

WirePlumber 0.1.0
.................

First release of WirePlumber, as shipped in AGL Happy Halibut 8.0.0
07070100000007000081A400000000000000000000000165F8630400000544000000000000000000000000000000000000001D00000000wireplumber-0.5.0/README.rst  WirePlumber
===========

.. image:: https://gitlab.freedesktop.org/pipewire/wireplumber/badges/master/pipeline.svg
   :alt: Pipeline status

.. image:: https://scan.coverity.com/projects/21488/badge.svg
   :alt: Coverity Scan Build Status

.. image:: https://img.shields.io/badge/license-MIT-green
   :alt: License

.. image:: https://img.shields.io/badge/dynamic/json?color=informational&label=tag&query=%24%5B0%5D.name&url=https%3A%2F%2Fgitlab.freedesktop.org%2Fapi%2Fv4%2Fprojects%2F2941%2Frepository%2Ftags
   :alt: Tag

WirePlumber is a modular session / policy manager for
`PipeWire <https://pipewire.org>`_ and a GObject-based high-level library
that wraps PipeWire's API, providing convenience for writing the daemon's
modules as well as external tools for managing PipeWire.

The WirePlumber daemon implements the session & policy management service.
It follows a modular design, having plugins that implement the actual
management functionality.

The WirePlumber Library provides API that allows you to extend the WirePlumber
daemon, to write management or status tools for PipeWire
(apps that don't do actual media streaming) and to write custom session managers
for embedded devices.

Documentation
-------------

The latest version of the documentation is available online
`here <https://pipewire.pages.freedesktop.org/wireplumber/>`_
07070100000008000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001700000000wireplumber-0.5.0/docs    07070100000009000081A400000000000000000000000165F863040001AE94000000000000000000000000000000000000002300000000wireplumber-0.5.0/docs/Doxyfile.in    # Doxyfile 1.9.1

# This file describes the settings to be used by the documentation system
# doxygen (www.doxygen.org) for a project.
#
# All text after a double hash (##) is considered a comment and is placed in
# front of the TAG it is preceding.
#
# All text after a single hash (#) is considered a comment and will be ignored.
# The format is:
# TAG = value [value, ...]
# For lists, items can also be appended using:
# TAG += value [value, ...]
# Values that contain spaces should be placed between quotes (\" \").

#---------------------------------------------------------------------------
# Project related configuration options
#---------------------------------------------------------------------------

# This tag specifies the encoding used for all characters in the configuration
# file that follow. The default is UTF-8 which is also the encoding used for all
# text before the first occurrence of this tag. Doxygen uses libiconv (or the
# iconv built into libc) for the transcoding. See
# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
# The default value is: UTF-8.

DOXYFILE_ENCODING      = UTF-8

# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
# double-quotes, unless you are using Doxywizard) that should identify the
# project for which the documentation is generated. This name is used in the
# title of most generated pages and in a few other places.
# The default value is: My Project.

PROJECT_NAME           = WirePlumber

# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
# could be handy for archiving the generated documentation or if some version
# control system is used.

PROJECT_NUMBER         =

# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
# quick idea about the purpose of the project. Keep the description short.

PROJECT_BRIEF          =

# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
# in the documentation. The maximum height of the logo should not exceed 55
# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
# the logo to the output directory.

PROJECT_LOGO           =

# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
# into which the generated documentation will be written. If a relative path is
# entered, it will be relative to the location where doxygen was started. If
# left blank the current directory will be used.

OUTPUT_DIRECTORY       = @OUTPUT_DIR@

# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
# directories (in 2 levels) under the output directory of each output format and
# will distribute the generated files over these directories. Enabling this
# option can be useful when feeding doxygen a huge amount of source files, where
# putting all generated files in the same directory would otherwise causes
# performance problems for the file system.
# The default value is: NO.

CREATE_SUBDIRS         = NO

# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
# characters to appear in the names of generated files. If set to NO, non-ASCII
# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
# U+3044.
# The default value is: NO.

ALLOW_UNICODE_NAMES    = NO

# The OUTPUT_LANGUAGE tag is used to specify the language in which all
# documentation generated by doxygen is written. Doxygen will use this
# information to generate all constant output in the proper language.
# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
# Ukrainian and Vietnamese.
# The default value is: English.

OUTPUT_LANGUAGE        = English

# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
# descriptions after the members that are listed in the file and class
# documentation (similar to Javadoc). Set to NO to disable this.
# The default value is: YES.

BRIEF_MEMBER_DESC      = YES

# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
# description of a member or function before the detailed description
#
# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
# brief descriptions will be completely suppressed.
# The default value is: YES.

REPEAT_BRIEF           = YES

# This tag implements a quasi-intelligent brief description abbreviator that is
# used to form the text in various listings. Each string in this list, if found
# as the leading text of the brief description, will be stripped from the text
# and the result, after processing the whole list, is used as the annotated
# text. Otherwise, the brief description is used as-is. If left blank, the
# following values are used ($name is automatically replaced with the name of
# the entity):The $name class, The $name widget, The $name file, is, provides,
# specifies, contains, represents, a, an and the.

ABBREVIATE_BRIEF       = "The $name class" \
                         "The $name widget" \
                         "The $name file" \
                         is \
                         provides \
                         specifies \
                         contains \
                         represents \
                         a \
                         an \
                         the

# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
# doxygen will generate a detailed section even if there is only a brief
# description.
# The default value is: NO.

ALWAYS_DETAILED_SEC    = NO

# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
# inherited members of a class in the documentation of that class as if those
# members were ordinary class members. Constructors, destructors and assignment
# operators of the base classes will not be shown.
# The default value is: NO.

INLINE_INHERITED_MEMB  = NO

# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
# before files name in the file list and in the header files. If set to NO the
# shortest path that makes the file name unique will be used
# The default value is: YES.

FULL_PATH_NAMES        = YES

# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
# Stripping is only done if one of the specified strings matches the left-hand
# part of the path. The tag can be used to show relative paths in the file list.
# If left blank the directory from which doxygen is run is used as the path to
# strip.
#
# Note that you can specify absolute paths here, but also relative paths, which
# will be relative from the directory where doxygen is started.
# This tag requires that the tag FULL_PATH_NAMES is set to YES.

STRIP_FROM_PATH        =

# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
# path mentioned in the documentation of a class, which tells the reader which
# header file to include in order to use a class. If left blank only the name of
# the header file containing the class definition is used. Otherwise one should
# specify the list of include paths that are normally passed to the compiler
# using the -I flag.

STRIP_FROM_INC_PATH    =

# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
# less readable) file names. This can be useful is your file systems doesn't
# support long names like on DOS, Mac, or CD-ROM.
# The default value is: NO.

SHORT_NAMES            = NO

# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
# first line (until the first dot) of a Javadoc-style comment as the brief
# description. If set to NO, the Javadoc-style will behave just like regular Qt-
# style comments (thus requiring an explicit @brief command for a brief
# description.)
# The default value is: NO.

JAVADOC_AUTOBRIEF      = NO

# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
# such as
# /***************
# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
# Javadoc-style will behave just like regular comments and it will not be
# interpreted by doxygen.
# The default value is: NO.

JAVADOC_BANNER         = NO

# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
# line (until the first dot) of a Qt-style comment as the brief description. If
# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
# requiring an explicit \brief command for a brief description.)
# The default value is: NO.

QT_AUTOBRIEF           = NO

# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
# a brief description. This used to be the default behavior. The new default is
# to treat a multi-line C++ comment block as a detailed description. Set this
# tag to YES if you prefer the old behavior instead.
#
# Note that setting this tag to YES also means that rational rose comments are
# not recognized any more.
# The default value is: NO.

MULTILINE_CPP_IS_BRIEF = NO

# By default Python docstrings are displayed as preformatted text and doxygen's
# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the
# doxygen's special commands can be used and the contents of the docstring
# documentation blocks is shown as doxygen documentation.
# The default value is: YES.

PYTHON_DOCSTRING       = YES

# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
# documentation from any documented member that it re-implements.
# The default value is: YES.

INHERIT_DOCS           = YES

# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
# page for each member. If set to NO, the documentation of a member will be part
# of the file/class/namespace that contains it.
# The default value is: NO.

SEPARATE_MEMBER_PAGES  = NO

# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
# uses this value to replace tabs by spaces in code fragments.
# Minimum value: 1, maximum value: 16, default value: 4.

TAB_SIZE               = 8

# This tag can be used to specify a number of aliases that act as commands in
# the documentation. An alias has the form:
# name=value
# For example adding
# "sideeffect=@par Side Effects:\n"
# will allow you to put the command \sideeffect (or @sideeffect) in the
# documentation, which will result in a user-defined paragraph with heading
# "Side Effects:". You can put \n's in the value part of an alias to insert
# newlines (in the resulting output). You can put ^^ in the value part of an
# alias to insert a newline as if a physical newline was in the original file.
# When you need a literal { or } or , in the value part of an alias you have to
# escape them by means of a backslash (\), this can lead to conflicts with the
# commands \{ and \} for these it is advised to use the version @{ and @} or use
# a double escape (\\{ and \\})

ALIASES                = \
  "rst=\verbatim embed:rst:leading-asterisk" \
  "endrst=\endverbatim" \
  "gsignals=\b GObject \b Signals" \
  "gproperties=\b GObject \b Properties" \
  "gproperty{4}=<dl><dt>\1</dt><dd><p>\4</p><table><tr><td>`\2`</td><td>`\3`</td></tr></table></dd></dl>"

# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
# only. Doxygen will then generate output that is more tailored for C. For
# instance, some of the names that are used will be different. The list of all
# members will be omitted, etc.
# The default value is: NO.

OPTIMIZE_OUTPUT_FOR_C  = NO

# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
# Python sources only. Doxygen will then generate output that is more tailored
# for that language. For instance, namespaces will be presented as packages,
# qualified scopes will look different, etc.
# The default value is: NO.

OPTIMIZE_OUTPUT_JAVA   = NO

# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
# sources. Doxygen will then generate output that is tailored for Fortran.
# The default value is: NO.

OPTIMIZE_FOR_FORTRAN   = NO

# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
# sources. Doxygen will then generate output that is tailored for VHDL.
# The default value is: NO.

OPTIMIZE_OUTPUT_VHDL   = NO

# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice
# sources only. Doxygen will then generate output that is more tailored for that
# language. For instance, namespaces will be presented as modules, types will be
# separated into more groups, etc.
# The default value is: NO.

OPTIMIZE_OUTPUT_SLICE  = NO

# Doxygen selects the parser to use depending on the extension of the files it
# parses. With this tag you can assign which parser to use for a given
# extension. Doxygen has a built-in mapping, but you can override or extend it
# using this tag. The format is ext=language, where ext is a file extension, and
# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,
# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
# tries to guess whether the code is fixed or free formatted code, this is the
# default for Fortran type files). For instance to make doxygen treat .inc files
# as Fortran files (default is PHP), and .f files as C (default is Fortran),
# use: inc=Fortran f=C.
#
# Note: For files without extension you can use no_extension as a placeholder.
#
# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
# the files are not read by doxygen. When specifying no_extension you should add
# * to the FILE_PATTERNS.
#
# Note see also the list of default file extension mappings.

EXTENSION_MAPPING      =

# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
# according to the Markdown format, which allows for more readable
# documentation. See https://daringfireball.net/projects/markdown/ for details.
# The output of markdown processing is further processed by doxygen, so you can
# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
# case of backward compatibilities issues.
# The default value is: YES.

MARKDOWN_SUPPORT       = YES

# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
# to that level are automatically included in the table of contents, even if
# they do not have an id attribute.
# Note: This feature currently applies only to Markdown headings.
# Minimum value: 0, maximum value: 99, default value: 5.
# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.

TOC_INCLUDE_HEADINGS   = 5

# When enabled doxygen tries to link words that correspond to documented
# classes, or namespaces to their corresponding documentation. Such a link can
# be prevented in individual cases by putting a % sign in front of the word or
# globally by setting AUTOLINK_SUPPORT to NO.
# The default value is: YES.

AUTOLINK_SUPPORT       = YES

# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
# to include (a tag file for) the STL sources as input, then you should set this
# tag to YES in order to let doxygen match functions declarations and
# definitions whose arguments contain STL classes (e.g. func(std::string);
# versus func(std::string) {}). This also make the inheritance and collaboration
# diagrams that involve STL classes more complete and accurate.
# The default value is: NO.

BUILTIN_STL_SUPPORT    = NO

# If you use Microsoft's C++/CLI language, you should set this option to YES to
# enable parsing support.
# The default value is: NO.

CPP_CLI_SUPPORT        = NO

# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
# will parse them like normal C++ but will assume all classes use public instead
# of private inheritance when no explicit protection keyword is present.
# The default value is: NO.

SIP_SUPPORT            = NO

# For Microsoft's IDL there are propget and propput attributes to indicate
# getter and setter methods for a property. Setting this option to YES will make
# doxygen to replace the get and set methods by a property in the documentation.
# This will only work if the methods are indeed getting or setting a simple
# type. If this is not the case, or you want to show the methods anyway, you
# should set this option to NO.
# The default value is: YES.

IDL_PROPERTY_SUPPORT   = YES

# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
# tag is set to YES then doxygen will reuse the documentation of the first
# member in the group (if any) for the other members of the group. By default
# all members of a group must be documented explicitly.
# The default value is: NO.

DISTRIBUTE_GROUP_DOC   = NO

# If one adds a struct or class to a group and this option is enabled, then also
# any nested class or struct is added to the same group. By default this option
# is disabled and one has to add nested compounds explicitly via \ingroup.
# The default value is: NO.

GROUP_NESTED_COMPOUNDS = NO

# Set the SUBGROUPING tag to YES to allow class member groups of the same type
# (for instance a group of public functions) to be put as a subgroup of that
# type (e.g. under the Public Functions section). Set it to NO to prevent
# subgrouping. Alternatively, this can be done per class using the
# \nosubgrouping command.
# The default value is: YES.

SUBGROUPING            = YES

# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
# are shown inside the group in which they are included (e.g. using \ingroup)
# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
# and RTF).
#
# Note that this feature does not work in combination with
# SEPARATE_MEMBER_PAGES.
# The default value is: NO.

INLINE_GROUPED_CLASSES = NO

# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
# with only public data fields or simple typedef fields will be shown inline in
# the documentation of the scope in which they are defined (i.e. file,
# namespace, or group documentation), provided this scope is documented. If set
# to NO, structs, classes, and unions are shown on a separate page (for HTML and
# Man pages) or section (for LaTeX and RTF).
# The default value is: NO.

INLINE_SIMPLE_STRUCTS  = NO

# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
# enum is documented as struct, union, or enum with the name of the typedef. So
# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
# with name TypeT. When disabled the typedef will appear as a member of a file,
# namespace, or class. And the struct will be named TypeS. This can typically be
# useful for C code in case the coding convention dictates that all compound
# types are typedef'ed and only the typedef is referenced, never the tag name.
# The default value is: NO.

TYPEDEF_HIDES_STRUCT   = NO

# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
# cache is used to resolve symbols given their name and scope. Since this can be
# an expensive process and often the same symbol appears multiple times in the
# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
# doxygen will become slower. If the cache is too large, memory is wasted. The
# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
# symbols. At the end of a run doxygen will report the cache usage and suggest
# the optimal cache size from a speed point of view.
# Minimum value: 0, maximum value: 9, default value: 0.

LOOKUP_CACHE_SIZE      = 0

# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use
# during processing. When set to 0 doxygen will based this on the number of
# cores available in the system. You can set it explicitly to a value larger
# than 0 to get more control over the balance between CPU load and processing
# speed. At this moment only the input processing can be done using multiple
# threads. Since this is still an experimental feature the default is set to 1,
# which efficively disables parallel processing. Please report any issues you
# encounter. Generating dot graphs in parallel is controlled by the
# DOT_NUM_THREADS setting.
# Minimum value: 0, maximum value: 32, default value: 1.

NUM_PROC_THREADS       = 1

#---------------------------------------------------------------------------
# Build related configuration options
#---------------------------------------------------------------------------

# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
# documentation are documented, even if no documentation was available. Private
# class members and static file members will be hidden unless the
# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
# Note: This will also disable the warnings about undocumented members that are
# normally produced when WARNINGS is set to YES.
# The default value is: NO.

EXTRACT_ALL            = NO

# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
# be included in the documentation.
# The default value is: NO.

EXTRACT_PRIVATE        = NO

# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual
# methods of a class will be included in the documentation.
# The default value is: NO.

EXTRACT_PRIV_VIRTUAL   = NO

# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
# scope will be included in the documentation.
# The default value is: NO.

EXTRACT_PACKAGE        = NO

# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
# included in the documentation.
# The default value is: NO.

EXTRACT_STATIC         = NO

# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
# locally in source files will be included in the documentation. If set to NO,
# only classes defined in header files are included. Does not have any effect
# for Java sources.
# The default value is: YES.

EXTRACT_LOCAL_CLASSES  = NO

# This flag is only useful for Objective-C code. If set to YES, local methods,
# which are defined in the implementation section but not in the interface are
# included in the documentation. If set to NO, only methods in the interface are
# included.
# The default value is: NO.

EXTRACT_LOCAL_METHODS  = NO

# If this flag is set to YES, the members of anonymous namespaces will be
# extracted and appear in the documentation as a namespace called
# 'anonymous_namespace{file}', where file will be replaced with the base name of
# the file that contains the anonymous namespace. By default anonymous namespace
# are hidden.
# The default value is: NO.

EXTRACT_ANON_NSPACES   = NO

# If this flag is set to YES, the name of an unnamed parameter in a declaration
# will be determined by the corresponding definition. By default unnamed
# parameters remain unnamed in the output.
# The default value is: YES.

RESOLVE_UNNAMED_PARAMS = YES

# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
# undocumented members inside documented classes or files. If set to NO these
# members will be included in the various overviews, but no documentation
# section is generated. This option has no effect if EXTRACT_ALL is enabled.
# The default value is: NO.

HIDE_UNDOC_MEMBERS     = YES

# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
# undocumented classes that are normally visible in the class hierarchy. If set
# to NO, these classes will be included in the various overviews. This option
# has no effect if EXTRACT_ALL is enabled.
# The default value is: NO.

HIDE_UNDOC_CLASSES     = YES

# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
# declarations. If set to NO, these declarations will be included in the
# documentation.
# The default value is: NO.

HIDE_FRIEND_COMPOUNDS  = NO

# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
# documentation blocks found inside the body of a function. If set to NO, these
# blocks will be appended to the function's detailed documentation block.
# The default value is: NO.

HIDE_IN_BODY_DOCS      = NO

# The INTERNAL_DOCS tag determines if documentation that is typed after a
# \internal command is included. If the tag is set to NO then the documentation
# will be excluded. Set it to YES to include the internal documentation.
# The default value is: NO.

INTERNAL_DOCS          = NO

# With the correct setting of option CASE_SENSE_NAMES doxygen will better be
# able to match the capabilities of the underlying filesystem. In case the
# filesystem is case sensitive (i.e. it supports files in the same directory
# whose names only differ in casing), the option must be set to YES to properly
# deal with such files in case they appear in the input. For filesystems that
# are not case sensitive the option should be be set to NO to properly deal with
# output files written for symbols that only differ in casing, such as for two
# classes, one named CLASS and the other named Class, and to also support
# references to files without having to specify the exact matching casing. On
# Windows (including Cygwin) and MacOS, users should typically set this option
# to NO, whereas on Linux or other Unix flavors it should typically be set to
# YES.
# The default value is: system dependent.

CASE_SENSE_NAMES       = YES

# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
# their full class and namespace scopes in the documentation. If set to YES, the
# scope will be hidden.
# The default value is: NO.

HIDE_SCOPE_NAMES       = NO

# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
# append additional text to a page's title, such as Class Reference. If set to
# YES the compound reference will be hidden.
# The default value is: NO.

HIDE_COMPOUND_REFERENCE= NO

# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
# the files that are included by a file in the documentation of that file.
# The default value is: YES.

SHOW_INCLUDE_FILES     = NO

# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
# grouped member an include statement to the documentation, telling the reader
# which file to include in order to use the member.
# The default value is: NO.

SHOW_GROUPED_MEMB_INC  = NO

# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
# files with double quotes in the documentation rather than with sharp brackets.
# The default value is: NO.

FORCE_LOCAL_INCLUDES   = NO

# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
# documentation for inline members.
# The default value is: YES.

INLINE_INFO            = YES

# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
# (detailed) documentation of file and class members alphabetically by member
# name. If set to NO, the members will appear in declaration order.
# The default value is: YES.

SORT_MEMBER_DOCS       = YES

# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
# descriptions of file, namespace and class members alphabetically by member
# name. If set to NO, the members will appear in declaration order. Note that
# this will also influence the order of the classes in the class list.
# The default value is: NO.

SORT_BRIEF_DOCS        = NO

# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
# (brief and detailed) documentation of class members so that constructors and
# destructors are listed first. If set to NO the constructors will appear in the
# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
# member documentation.
# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
# detailed member documentation.
# The default value is: NO.

SORT_MEMBERS_CTORS_1ST = NO

# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
# of group names into alphabetical order. If set to NO the group names will
# appear in their defined order.
# The default value is: NO.

SORT_GROUP_NAMES       = NO

# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
# fully-qualified names, including namespaces. If set to NO, the class list will
# be sorted only by class name, not including the namespace part.
# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
# Note: This option applies only to the class list, not to the alphabetical
# list.
# The default value is: NO.

SORT_BY_SCOPE_NAME     = NO

# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
# type resolution of all parameters of a function it will reject a match between
# the prototype and the implementation of a member function even if there is
# only one candidate or it is obvious which candidate to choose by doing a
# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
# accept a match between prototype and implementation in such cases.
# The default value is: NO.

STRICT_PROTO_MATCHING  = NO

# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
# list. This list is created by putting \todo commands in the documentation.
# The default value is: YES.

GENERATE_TODOLIST      = YES

# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
# list. This list is created by putting \test commands in the documentation.
# The default value is: YES.

GENERATE_TESTLIST      = YES

# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
# list. This list is created by putting \bug commands in the documentation.
# The default value is: YES.

GENERATE_BUGLIST       = YES

# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
# the deprecated list. This list is created by putting \deprecated commands in
# the documentation.
# The default value is: YES.

GENERATE_DEPRECATEDLIST= YES

# The ENABLED_SECTIONS tag can be used to enable conditional documentation
# sections, marked by \if <section_label> ... \endif and \cond <section_label>
# ... \endcond blocks.

ENABLED_SECTIONS       =

# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
# initial value of a variable or macro / define can have for it to appear in the
# documentation. If the initializer consists of more lines than specified here
# it will be hidden. Use a value of 0 to hide initializers completely. The
# appearance of the value of individual variables and macros / defines can be
# controlled using \showinitializer or \hideinitializer command in the
# documentation regardless of this setting.
# Minimum value: 0, maximum value: 10000, default value: 30.

MAX_INITIALIZER_LINES  = 30

# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
# the bottom of the documentation of classes and structs. If set to YES, the
# list will mention the files that were used to generate the documentation.
# The default value is: YES.

SHOW_USED_FILES        = YES

# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
# will remove the Files entry from the Quick Index and from the Folder Tree View
# (if specified).
# The default value is: YES.

SHOW_FILES             = YES

# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
# page. This will remove the Namespaces entry from the Quick Index and from the
# Folder Tree View (if specified).
# The default value is: YES.

SHOW_NAMESPACES        = YES

# The FILE_VERSION_FILTER tag can be used to specify a program or script that
# doxygen should invoke to get the current version for each file (typically from
# the version control system). Doxygen will invoke the program by executing (via
# popen()) the command command input-file, where command is the value of the
# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
# by doxygen. Whatever the program writes to standard output is used as the file
# version. For an example see the documentation.

FILE_VERSION_FILTER    =

# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
# by doxygen. The layout file controls the global structure of the generated
# output files in an output format independent way. To create the layout file
# that represents doxygen's defaults, run doxygen with the -l option. You can
# optionally specify a file name after the option, if omitted DoxygenLayout.xml
# will be used as the name of the layout file.
#
# Note that if you run doxygen from a directory containing a file called
# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
# tag is left empty.

LAYOUT_FILE            =

# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
# the reference definitions. This must be a list of .bib files. The .bib
# extension is automatically appended if omitted. This requires the bibtex tool
# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
# For LaTeX the style of the bibliography can be controlled using
# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
# search path. See also \cite for info how to create references.

CITE_BIB_FILES         =

#---------------------------------------------------------------------------
# Configuration options related to warning and progress messages
#---------------------------------------------------------------------------

# The QUIET tag can be used to turn on/off the messages that are generated to
# standard output by doxygen. If QUIET is set to YES this implies that the
# messages are off.
# The default value is: NO.

QUIET                  = YES

# The WARNINGS tag can be used to turn on/off the warning messages that are
# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
# this implies that the warnings are on.
#
# Tip: Turn warnings on while writing the documentation.
# The default value is: YES.

WARNINGS               = YES

# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
# will automatically be disabled.
# The default value is: YES.

WARN_IF_UNDOCUMENTED   = YES

# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
# potential errors in the documentation, such as not documenting some parameters
# in a documented function, or documenting parameters that don't exist or using
# markup commands wrongly.
# The default value is: YES.

WARN_IF_DOC_ERROR      = YES

# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
# are documented, but have no documentation for their parameters or return
# value. If set to NO, doxygen will only warn about wrong or incomplete
# parameter documentation, but not about the absence of documentation. If
# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
# The default value is: NO.

WARN_NO_PARAMDOC       = NO

# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
# at the end of the doxygen process doxygen will return with a non-zero status.
# Possible values are: NO, YES and FAIL_ON_WARNINGS.
# The default value is: NO.

WARN_AS_ERROR          = NO

# The WARN_FORMAT tag determines the format of the warning messages that doxygen
# can produce. The string should contain the $file, $line, and $text tags, which
# will be replaced by the file and line number from which the warning originated
# and the warning text. Optionally the format may contain $version, which will
# be replaced by the version of the file (if it could be obtained via
# FILE_VERSION_FILTER)
# The default value is: $file:$line: $text.

WARN_FORMAT            = "$file:$line: $text"

# The WARN_LOGFILE tag can be used to specify a file to which warning and error
# messages should be written. If left blank the output is written to standard
# error (stderr).

WARN_LOGFILE           =

#---------------------------------------------------------------------------
# Configuration options related to the input files
#---------------------------------------------------------------------------

# The INPUT tag is used to specify the files and/or directories that contain
# documented source files. You may enter file names like myfile.cpp or
# directories like /usr/src/myproject. Separate the files or directories with
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched.

INPUT                  = @INPUT@

# This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
# documentation (see:
# https://www.gnu.org/software/libiconv/) for the list of possible encodings.
# The default value is: UTF-8.

INPUT_ENCODING         = UTF-8

# If the value of the INPUT tag contains directories, you can use the
# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
# *.h) to filter out the source-files in the directories.
#
# Note that for custom extensions or not directly supported extensions you also
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
# read by doxygen.
#
# Note the list of default checked file patterns might differ from the list of
# default file extension mappings.
#
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl,
# *.ucf, *.qsf and *.ice.

FILE_PATTERNS          =

# The RECURSIVE tag can be used to specify whether or not subdirectories should
# be searched for input files as well.
# The default value is: NO.

RECURSIVE              = NO

# The EXCLUDE tag can be used to specify files and/or directories that should be
# excluded from the INPUT source files. This way you can easily exclude a
# subdirectory from a directory tree whose root is specified with the INPUT tag.
#
# Note that relative paths are relative to the directory from which doxygen is
# run.

EXCLUDE                =

# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded
# from the input.
# The default value is: NO.

EXCLUDE_SYMLINKS       = NO

# If the value of the INPUT tag contains directories, you can use the
# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
# certain files from those directories.
#
# Note that the wildcards are matched against the file with absolute path, so to
# exclude all test directories for example use the pattern */test/*

EXCLUDE_PATTERNS       =

# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
# (namespaces, classes, functions, etc.) that should be excluded from the
# output. The symbol name can be a fully qualified name, a word, or if the
# wildcard * is used, a substring. Examples: ANamespace, AClass,
# AClass::ANamespace, ANamespace::*Test
#
# Note that the wildcards are matched against the file with absolute path, so to
# exclude all test directories use the pattern */test/*

EXCLUDE_SYMBOLS        =

# The EXAMPLE_PATH tag can be used to specify one or more files or directories
# that contain example code fragments that are included (see the \include
# command).

EXAMPLE_PATH           =

# If the value of the EXAMPLE_PATH tag contains directories, you can use the
# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
# *.h) to filter out the source-files in the directories. If left blank all
# files are included.

EXAMPLE_PATTERNS       = *

# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
# searched for input files to be used with the \include or \dontinclude commands
# irrespective of the value of the RECURSIVE tag.
# The default value is: NO.

EXAMPLE_RECURSIVE      = NO

# The IMAGE_PATH tag can be used to specify one or more files or directories
# that contain images that are to be included in the documentation (see the
# \image command).

IMAGE_PATH             =

# The INPUT_FILTER tag can be used to specify a program that doxygen should
# invoke to filter for each input file. Doxygen will invoke the filter program
# by executing (via popen()) the command:
#
# <filter> <input-file>
#
# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
# name of an input file. Doxygen will then use the output that the filter
# program writes to standard output. If FILTER_PATTERNS is specified, this tag
# will be ignored.
#
# Note that the filter must not add or remove lines; it is applied before the
# code is scanned, but not when the output code is generated. If lines are added
# or removed, the anchors will not be placed correctly.
#
# Note that for custom extensions or not directly supported extensions you also
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
# properly processed by doxygen.

INPUT_FILTER           =

# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
# basis. Doxygen will compare the file name with each pattern and apply the
# filter if there is a match. The filters are a list of the form: pattern=filter
# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
# patterns match the file name, INPUT_FILTER is applied.
#
# Note that for custom extensions or not directly supported extensions you also
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
# properly processed by doxygen.

FILTER_PATTERNS        =

# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
# INPUT_FILTER) will also be used to filter the input files that are used for
# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
# The default value is: NO.

FILTER_SOURCE_FILES    = NO

# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
# it is also possible to disable source filtering for a specific pattern using
# *.ext= (so without naming a filter).
# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.

FILTER_SOURCE_PATTERNS =

# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
# is part of the input, its contents will be placed on the main page
# (index.html). This can be useful if you have a project on for instance GitHub
# and want to reuse the introduction page also for the doxygen output.

USE_MDFILE_AS_MAINPAGE =

#---------------------------------------------------------------------------
# Configuration options related to source browsing
#---------------------------------------------------------------------------

# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
# generated. Documented entities will be cross-referenced with these sources.
#
# Note: To get rid of all source code in the generated output, make sure that
# also VERBATIM_HEADERS is set to NO.
# The default value is: NO.

SOURCE_BROWSER         = NO

# Setting the INLINE_SOURCES tag to YES will include the body of functions,
# classes and enums directly into the documentation.
# The default value is: NO.

INLINE_SOURCES         = NO

# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
# special comment blocks from generated source code fragments. Normal C, C++ and
# Fortran comments will always remain visible.
# The default value is: YES.

STRIP_CODE_COMMENTS    = YES

# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
# entity all documented functions referencing it will be listed.
# The default value is: NO.

REFERENCED_BY_RELATION = NO

# If the REFERENCES_RELATION tag is set to YES then for each documented function
# all documented entities called/used by that function will be listed.
# The default value is: NO.

REFERENCES_RELATION    = NO

# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
# to YES then the hyperlinks from functions in REFERENCES_RELATION and
# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
# link to the documentation.
# The default value is: YES.

REFERENCES_LINK_SOURCE = YES

# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
# source code will show a tooltip with additional information such as prototype,
# brief description and links to the definition and documentation. Since this
# will make the HTML file larger and loading of large files a bit slower, you
# can opt to disable this feature.
# The default value is: YES.
# This tag requires that the tag SOURCE_BROWSER is set to YES.

SOURCE_TOOLTIPS        = YES

# If the USE_HTAGS tag is set to YES then the references to source code will
# point to the HTML generated by the htags(1) tool instead of doxygen built-in
# source browser. The htags tool is part of GNU's global source tagging system
# (see https://www.gnu.org/software/global/global.html). You will need version
# 4.8.6 or higher.
#
# To use it do the following:
# - Install the latest version of global
# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file
# - Make sure the INPUT points to the root of the source tree
# - Run doxygen as normal
#
# Doxygen will invoke htags (and that will in turn invoke gtags), so these
# tools must be available from the command line (i.e. in the search path).
#
# The result: instead of the source browser generated by doxygen, the links to
# source code will now point to the output of htags.
# The default value is: NO.
# This tag requires that the tag SOURCE_BROWSER is set to YES.

USE_HTAGS              = NO

# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
# verbatim copy of the header file for each class for which an include is
# specified. Set to NO to disable this.
# See also: Section \class.
# The default value is: YES.

VERBATIM_HEADERS       = YES

#---------------------------------------------------------------------------
# Configuration options related to the alphabetical class index
#---------------------------------------------------------------------------

# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
# compounds will be generated. Enable this if the project contains a lot of
# classes, structs, unions or interfaces.
# The default value is: YES.

ALPHABETICAL_INDEX     = YES

# In case all classes in a project start with a common prefix, all classes will
# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
# can be used to specify a prefix (or a list of prefixes) that should be ignored
# while generating the index headers.
# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.

IGNORE_PREFIX          =

#---------------------------------------------------------------------------
# Configuration options related to the HTML output
#---------------------------------------------------------------------------

# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
# The default value is: YES.

GENERATE_HTML          = NO

# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it.
# The default directory is: html.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_OUTPUT            = html

# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
# generated HTML page (for example: .htm, .php, .asp).
# The default value is: .html.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_FILE_EXTENSION    = .html

# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
# each generated HTML page. If the tag is left blank doxygen will generate a
# standard header.
#
# To get valid HTML the header file that includes any scripts and style sheets
# that doxygen needs, which is dependent on the configuration options used (e.g.
# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
# default header using
# doxygen -w html new_header.html new_footer.html new_stylesheet.css
# YourConfigFile
# and then modify the file new_header.html. See also section "Doxygen usage"
# for information on how to generate the default header that doxygen normally
# uses.
# Note: The header is subject to change so you typically have to regenerate the
# default header when upgrading to a newer version of doxygen. For a description
# of the possible markers and block names see the documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_HEADER            =

# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
# generated HTML page. If the tag is left blank doxygen will generate a standard
# footer. See HTML_HEADER for more information on how to generate a default
# footer and what special commands can be used inside the footer. See also
# section "Doxygen usage" for information on how to generate the default footer
# that doxygen normally uses.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_FOOTER            =

# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
# sheet that is used by each HTML page. It can be used to fine-tune the look of
# the HTML output. If left blank doxygen will generate a default style sheet.
# See also section "Doxygen usage" for information on how to generate the style
# sheet that doxygen normally uses.
# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
# it is more robust and this tag (HTML_STYLESHEET) will in the future become
# obsolete.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_STYLESHEET        =

# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
# cascading style sheets that are included after the standard style sheets
# created by doxygen. Using this option one can overrule certain style aspects.
# This is preferred over using HTML_STYLESHEET since it does not replace the
# standard style sheet and is therefore more robust against future updates.
# Doxygen will copy the style sheet files to the output directory.
# Note: The order of the extra style sheet files is of importance (e.g. the last
# style sheet in the list overrules the setting of the previous ones in the
# list). For an example see the documentation.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_EXTRA_STYLESHEET  =

# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
# other source files which should be copied to the HTML output directory. Note
# that these files will be copied to the base HTML output directory. Use the
# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
# files will be copied as-is; there are no commands or markers available.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_EXTRA_FILES       =

# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
# will adjust the colors in the style sheet and background images according to
# this color. Hue is specified as an angle on a colorwheel, see
# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
# purple, and 360 is red again.
# Minimum value: 0, maximum value: 359, default value: 220.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_COLORSTYLE_HUE    = 220

# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
# in the HTML output. For a value of 0 the output will use grayscales only. A
# value of 255 will produce the most vivid colors.
# Minimum value: 0, maximum value: 255, default value: 100.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_COLORSTYLE_SAT    = 100

# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
# luminance component of the colors in the HTML output. Values below 100
# gradually make the output lighter, whereas values above 100 make the output
# darker. The value divided by 100 is the actual gamma applied, so 80 represents
# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
# change the gamma.
# Minimum value: 40, maximum value: 240, default value: 80.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_COLORSTYLE_GAMMA  = 80

# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
# documentation will contain a main index with vertical navigation menus that
# are dynamically created via JavaScript. If disabled, the navigation index will
# consists of multiple levels of tabs that are statically embedded in every HTML
# page. Disable this option to support browsers that do not have JavaScript,
# like the Qt help browser.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_DYNAMIC_MENUS     = YES

# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
# documentation will contain sections that can be hidden and shown after the
# page has loaded.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_DYNAMIC_SECTIONS  = NO

# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
# shown in the various tree structured indices initially; the user can expand
# and collapse entries dynamically later on. Doxygen will expand the tree to
# such a level that at most the specified number of entries are visible (unless
# a fully collapsed tree already exceeds this amount). So setting the number of
# entries 1 will produce a full collapsed tree by default. 0 is a special value
# representing an infinite number of entries and will result in a full expanded
# tree by default.
# Minimum value: 0, maximum value: 9999, default value: 100.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_INDEX_NUM_ENTRIES = 100

# If the GENERATE_DOCSET tag is set to YES, additional index files will be
# generated that can be used as input for Apple's Xcode 3 integrated development
# environment (see:
# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To
# create a documentation set, doxygen will generate a Makefile in the HTML
# output directory. Running make will produce the docset in that directory and
# running make install will install the docset in
# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
# genXcode/_index.html for more information.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.

GENERATE_DOCSET        = NO

# This tag determines the name of the docset feed. A documentation feed provides
# an umbrella under which multiple documentation sets from a single provider
# (such as a company or product suite) can be grouped.
# The default value is: Doxygen generated docs.
# This tag requires that the tag GENERATE_DOCSET is set to YES.

DOCSET_FEEDNAME        = "Doxygen generated docs"

# This tag specifies a string that should uniquely identify the documentation
# set bundle. This should be a reverse domain-name style string, e.g.
# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
# The default value is: org.doxygen.Project.
# This tag requires that the tag GENERATE_DOCSET is set to YES.

DOCSET_BUNDLE_ID       = org.doxygen.Project

# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
# the documentation publisher. This should be a reverse domain-name style
# string, e.g. com.mycompany.MyDocSet.documentation.
# The default value is: org.doxygen.Publisher.
# This tag requires that the tag GENERATE_DOCSET is set to YES.

DOCSET_PUBLISHER_ID    = org.doxygen.Publisher

# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
# The default value is: Publisher.
# This tag requires that the tag GENERATE_DOCSET is set to YES.

DOCSET_PUBLISHER_NAME  = Publisher

# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
# (see:
# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows.
#
# The HTML Help Workshop contains a compiler that can convert all HTML output
# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
# files are now used as the Windows 98 help format, and will replace the old
# Windows help format (.hlp) on all Windows platforms in the future. Compressed
# HTML files also contain an index, a table of contents, and you can search for
# words in the documentation. The HTML workshop also contains a viewer for
# compressed HTML files.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.

GENERATE_HTMLHELP      = NO

# The CHM_FILE tag can be used to specify the file name of the resulting .chm
# file. You can add a path in front of the file if the result should not be
# written to the html output directory.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.

CHM_FILE               =

# The HHC_LOCATION tag can be used to specify the location (absolute path
# including file name) of the HTML help compiler (hhc.exe). If non-empty,
# doxygen will try to run the HTML help compiler on the generated index.hhp.
# The file has to be specified with full path.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.

HHC_LOCATION           =

# The GENERATE_CHI flag controls if a separate .chi index file is generated
# (YES) or that it should be included in the main .chm file (NO).
# The default value is: NO.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.

GENERATE_CHI           = NO

# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
# and project file content.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.

CHM_INDEX_ENCODING     =

# The BINARY_TOC flag controls whether a binary table of contents is generated
# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
# enables the Previous and Next buttons.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.

BINARY_TOC             = NO

# The TOC_EXPAND flag can be set to YES to add extra items for group members to
# the table of contents of the HTML help documentation and to the tree view.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTMLHELP is set to YES.

TOC_EXPAND             = NO

# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
# (.qch) of the generated HTML documentation.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.

GENERATE_QHP           = NO

# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
# the file name of the resulting .qch file. The path specified is relative to
# the HTML output folder.
# This tag requires that the tag GENERATE_QHP is set to YES.

QCH_FILE               =

# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
# Project output. For more information please see Qt Help Project / Namespace
# (see:
# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
# The default value is: org.doxygen.Project.
# This tag requires that the tag GENERATE_QHP is set to YES.

QHP_NAMESPACE          = org.doxygen.Project

# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
# Help Project output. For more information please see Qt Help Project / Virtual
# Folders (see:
# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders).
# The default value is: doc.
# This tag requires that the tag GENERATE_QHP is set to YES.

QHP_VIRTUAL_FOLDER     = doc

# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
# filter to add. For more information please see Qt Help Project / Custom
# Filters (see:
# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
# This tag requires that the tag GENERATE_QHP is set to YES.

QHP_CUST_FILTER_NAME   =

# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
# custom filter to add. For more information please see Qt Help Project / Custom
# Filters (see:
# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
# This tag requires that the tag GENERATE_QHP is set to YES.

QHP_CUST_FILTER_ATTRS  =

# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
# project's filter section matches. Qt Help Project / Filter Attributes (see:
# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).
# This tag requires that the tag GENERATE_QHP is set to YES.

QHP_SECT_FILTER_ATTRS  =

# The QHG_LOCATION tag can be used to specify the location (absolute path
# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to
# run qhelpgenerator on the generated .qhp file.
# This tag requires that the tag GENERATE_QHP is set to YES.

QHG_LOCATION           =

# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
# generated, together with the HTML files, they form an Eclipse help plugin. To
# install this plugin and make it available under the help contents menu in
# Eclipse, the contents of the directory containing the HTML and XML files needs
# to be copied into the plugins directory of eclipse. The name of the directory
# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
# After copying Eclipse needs to be restarted before the help appears.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.

GENERATE_ECLIPSEHELP   = NO

# A unique identifier for the Eclipse help plugin. When installing the plugin
# the directory name containing the HTML and XML files should also have this
# name. Each documentation set should have its own identifier.
# The default value is: org.doxygen.Project.
# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.

ECLIPSE_DOC_ID         = org.doxygen.Project

# If you want full control over the layout of the generated HTML pages it might
# be necessary to disable the index and replace it with your own. The
# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
# of each HTML page. A value of NO enables the index and the value YES disables
# it. Since the tabs in the index contain the same information as the navigation
# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.

DISABLE_INDEX          = NO

# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
# structure should be generated to display hierarchical information. If the tag
# value is set to YES, a side panel will be generated containing a tree-like
# index structure (just like the one that is generated for HTML Help). For this
# to work a browser that supports JavaScript, DHTML, CSS and frames is required
# (i.e. any modern browser). Windows users are probably better off using the
# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
# further fine-tune the look of the index. As an example, the default style
# sheet generated by doxygen has an example that shows how to put an image at
# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
# the same information as the tab index, you could consider setting
# DISABLE_INDEX to YES when enabling this option.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.

GENERATE_TREEVIEW      = NO

# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
# doxygen will group on one line in the generated HTML documentation.
#
# Note that a value of 0 will completely suppress the enum values from appearing
# in the overview section.
# Minimum value: 0, maximum value: 20, default value: 4.
# This tag requires that the tag GENERATE_HTML is set to YES.

ENUM_VALUES_PER_LINE   = 4

# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
# to set the initial width (in pixels) of the frame in which the tree is shown.
# Minimum value: 0, maximum value: 1500, default value: 250.
# This tag requires that the tag GENERATE_HTML is set to YES.

TREEVIEW_WIDTH         = 250

# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
# external symbols imported via tag files in a separate window.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.

EXT_LINKS_IN_WINDOW    = NO

# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
# the HTML output. These images will generally look nicer at scaled resolutions.
# Possible values are: png (the default) and svg (looks nicer but requires the
# pdf2svg or inkscape tool).
# The default value is: png.
# This tag requires that the tag GENERATE_HTML is set to YES.

HTML_FORMULA_FORMAT    = png

# Use this tag to change the font size of LaTeX formulas included as images in
# the HTML documentation. When you change the font size after a successful
# doxygen run you need to manually remove any form_*.png images from the HTML
# output directory to force them to be regenerated.
# Minimum value: 8, maximum value: 50, default value: 10.
# This tag requires that the tag GENERATE_HTML is set to YES.

FORMULA_FONTSIZE       = 10

# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
# to create new LaTeX commands to be used in formulas as building blocks. See
# the section "Including formulas" for details.

FORMULA_MACROFILE      =

# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
# https://www.mathjax.org) which uses client side JavaScript for the rendering
# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
# installed or if you want to formulas look prettier in the HTML output. When
# enabled you may also need to install MathJax separately and configure the path
# to it using the MATHJAX_RELPATH option.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.

USE_MATHJAX            = NO

# When MathJax is enabled you can set the default output format to be used for
# the MathJax output. See the MathJax site (see:
# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details.
# Possible values are: HTML-CSS (which is slower, but has the best
# compatibility), NativeMML (i.e. MathML) and SVG.
# The default value is: HTML-CSS.
# This tag requires that the tag USE_MATHJAX is set to YES.

MATHJAX_FORMAT         = HTML-CSS

# When MathJax is enabled you need to specify the location relative to the HTML
# output directory using the MATHJAX_RELPATH option. The destination directory
# should contain the MathJax.js script. For instance, if the mathjax directory
# is located at the same level as the HTML output directory, then
# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
# Content Delivery Network so you can quickly see the result without installing
# MathJax. However, it is strongly recommended to install a local copy of
# MathJax from https://www.mathjax.org before deployment.
# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.
# This tag requires that the tag USE_MATHJAX is set to YES.

MATHJAX_RELPATH        =

# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
# extension names that should be enabled during MathJax rendering. For example
# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
# This tag requires that the tag USE_MATHJAX is set to YES.

MATHJAX_EXTENSIONS     =

# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
# of code that will be used on startup of the MathJax code. See the MathJax site
# (see:
# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an
# example see the documentation.
# This tag requires that the tag USE_MATHJAX is set to YES.

MATHJAX_CODEFILE       =

# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
# the HTML output. The underlying search engine uses javascript and DHTML and
# should work on any modern browser. Note that when using HTML help
# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
# there is already a search function so this one should typically be disabled.
# For large projects the javascript based search engine can be slow, then
# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
# search using the keyboard; to jump to the search box use <access key> + S
# (what the <access key> is depends on the OS and browser, but it is typically
# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
# key> to jump into the search results window, the results can be navigated
# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
# the search. The filter options can be selected when the cursor is inside the
# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
# to select a filter and <Enter> or <escape> to activate or cancel the filter
# option.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.

SEARCHENGINE           = YES

# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
# implemented using a web server instead of a web client using JavaScript. There
# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
# setting. When disabled, doxygen will generate a PHP script for searching and
# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
# and searching needs to be provided by external tools. See the section
# "External Indexing and Searching" for details.
# The default value is: NO.
# This tag requires that the tag SEARCHENGINE is set to YES.

SERVER_BASED_SEARCH    = NO

# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
# script for searching. Instead the search results are written to an XML file
# which needs to be processed by an external indexer. Doxygen will invoke an
# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
# search results.
#
# Doxygen ships with an example indexer (doxyindexer) and search engine
# (doxysearch.cgi) which are based on the open source search engine library
# Xapian (see:
# https://xapian.org/).
#
# See the section "External Indexing and Searching" for details.
# The default value is: NO.
# This tag requires that the tag SEARCHENGINE is set to YES.

EXTERNAL_SEARCH        = NO

# The SEARCHENGINE_URL should point to a search engine hosted by a web server
# which will return the search results when EXTERNAL_SEARCH is enabled.
#
# Doxygen ships with an example indexer (doxyindexer) and search engine
# (doxysearch.cgi) which are based on the open source search engine library
# Xapian (see:
# https://xapian.org/). See the section "External Indexing and Searching" for
# details.
# This tag requires that the tag SEARCHENGINE is set to YES.

SEARCHENGINE_URL       =

# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
# search data is written to a file for indexing by an external tool. With the
# SEARCHDATA_FILE tag the name of this file can be specified.
# The default file is: searchdata.xml.
# This tag requires that the tag SEARCHENGINE is set to YES.

SEARCHDATA_FILE        = searchdata.xml

# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
# projects and redirect the results back to the right project.
# This tag requires that the tag SEARCHENGINE is set to YES.

EXTERNAL_SEARCH_ID     =

# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
# projects other than the one defined by this configuration file, but that are
# all added to the same external search index. Each project needs to have a
# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
# to a relative location where the documentation can be found. The format is:
# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
# This tag requires that the tag SEARCHENGINE is set to YES.

EXTRA_SEARCH_MAPPINGS  =

#---------------------------------------------------------------------------
# Configuration options related to the LaTeX output
#---------------------------------------------------------------------------

# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
# The default value is: YES.

GENERATE_LATEX         = NO

# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it.
# The default directory is: latex.
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_OUTPUT           = latex

# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
# invoked.
#
# Note that when not enabling USE_PDFLATEX the default is latex when enabling
# USE_PDFLATEX the default is pdflatex and when in the later case latex is
# chosen this is overwritten by pdflatex. For specific output languages the
# default can have been set differently, this depends on the implementation of
# the output language.
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_CMD_NAME         =

# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
# index for LaTeX.
# Note: This tag is used in the Makefile / make.bat.
# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file
# (.tex).
# The default file is: makeindex.
# This tag requires that the tag GENERATE_LATEX is set to YES.

MAKEINDEX_CMD_NAME     = makeindex

# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to
# generate index for LaTeX. In case there is no backslash (\) as first character
# it will be automatically added in the LaTeX code.
# Note: This tag is used in the generated output file (.tex).
# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.
# The default value is: makeindex.
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_MAKEINDEX_CMD    = makeindex

# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
# documents. This may be useful for small projects and may help to save some
# trees in general.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.

COMPACT_LATEX          = NO

# The PAPER_TYPE tag can be used to set the paper type that is used by the
# printer.
# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
# 14 inches) and executive (7.25 x 10.5 inches).
# The default value is: a4.
# This tag requires that the tag GENERATE_LATEX is set to YES.

PAPER_TYPE             = a4

# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
# that should be included in the LaTeX output. The package can be specified just
# by its name or with the correct syntax as to be used with the LaTeX
# \usepackage command. To get the times font for instance you can specify :
# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
# To use the option intlimits with the amsmath package you can specify:
# EXTRA_PACKAGES=[intlimits]{amsmath}
# If left blank no extra packages will be included.
# This tag requires that the tag GENERATE_LATEX is set to YES.

EXTRA_PACKAGES         =

# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
# generated LaTeX document. The header should contain everything until the first
# chapter. If it is left blank doxygen will generate a standard header. See
# section "Doxygen usage" for information on how to let doxygen write the
# default header to a separate file.
#
# Note: Only use a user-defined header if you know what you are doing! The
# following commands have a special meaning inside the header: $title,
# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
# string, for the replacement values of the other commands the user is referred
# to HTML_HEADER.
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_HEADER           =

# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
# generated LaTeX document. The footer should contain everything after the last
# chapter. If it is left blank doxygen will generate a standard footer. See
# LATEX_HEADER for more information on how to generate a default footer and what
# special commands can be used inside the footer.
#
# Note: Only use a user-defined footer if you know what you are doing!
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_FOOTER           =

# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
# LaTeX style sheets that are included after the standard style sheets created
# by doxygen. Using this option one can overrule certain style aspects. Doxygen
# will copy the style sheet files to the output directory.
# Note: The order of the extra style sheet files is of importance (e.g. the last
# style sheet in the list overrules the setting of the previous ones in the
# list).
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_EXTRA_STYLESHEET =

# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
# other source files which should be copied to the LATEX_OUTPUT output
# directory. Note that the files will be copied as-is; there are no commands or
# markers available.
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_EXTRA_FILES      =

# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
# contain links (just like the HTML output) instead of page references. This
# makes the output suitable for online browsing using a PDF viewer.
# The default value is: YES.
# This tag requires that the tag GENERATE_LATEX is set to YES.

PDF_HYPERLINKS         = YES

# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as
# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX
# files. Set this option to YES, to get a higher quality PDF documentation.
#
# See also section LATEX_CMD_NAME for selecting the engine.
# The default value is: YES.
# This tag requires that the tag GENERATE_LATEX is set to YES.

USE_PDFLATEX           = YES

# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
# command to the generated LaTeX files. This will instruct LaTeX to keep running
# if errors occur, instead of asking the user for help. This option is also used
# when generating formulas in HTML.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_BATCHMODE        = NO

# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
# index chapters (such as File Index, Compound Index, etc.) in the output.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_HIDE_INDICES     = NO

# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
# bibliography, e.g. plainnat, or ieeetr. See
# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
# The default value is: plain.
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_BIB_STYLE        = plain

# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
# path from which the emoji images will be read. If a relative path is entered,
# it will be relative to the LATEX_OUTPUT directory. If left blank the
# LATEX_OUTPUT directory will be used.
# This tag requires that the tag GENERATE_LATEX is set to YES.

LATEX_EMOJI_DIRECTORY  =

#---------------------------------------------------------------------------
# Configuration options related to the RTF output
#---------------------------------------------------------------------------

# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
# RTF output is optimized for Word 97 and may not look too pretty with other RTF
# readers/editors.
# The default value is: NO.

GENERATE_RTF           = NO

# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it.
# The default directory is: rtf.
# This tag requires that the tag GENERATE_RTF is set to YES.

RTF_OUTPUT             = rtf

# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
# documents. This may be useful for small projects and may help to save some
# trees in general.
# The default value is: NO.
# This tag requires that the tag GENERATE_RTF is set to YES.

COMPACT_RTF            = NO

# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
# contain hyperlink fields. The RTF file will contain links (just like the HTML
# output) instead of page references. This makes the output suitable for online
# browsing using Word or some other Word compatible readers that support those
# fields.
#
# Note: WordPad (write) and others do not support links.
# The default value is: NO.
# This tag requires that the tag GENERATE_RTF is set to YES.

RTF_HYPERLINKS         = NO

# Load stylesheet definitions from file. Syntax is similar to doxygen's
# configuration file, i.e. a series of assignments. You only have to provide
# replacements, missing definitions are set to their default value.
#
# See also section "Doxygen usage" for information on how to generate the
# default style sheet that doxygen normally uses.
# This tag requires that the tag GENERATE_RTF is set to YES.

RTF_STYLESHEET_FILE    =

# Set optional variables used in the generation of an RTF document. Syntax is
# similar to doxygen's configuration file. A template extensions file can be
# generated using doxygen -e rtf extensionFile.
# This tag requires that the tag GENERATE_RTF is set to YES.

RTF_EXTENSIONS_FILE    =

#---------------------------------------------------------------------------
# Configuration options related to the man page output
#---------------------------------------------------------------------------

# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
# classes and files.
# The default value is: NO.

GENERATE_MAN           = NO

# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it. A directory man3 will be created inside the directory specified by
# MAN_OUTPUT.
# The default directory is: man.
# This tag requires that the tag GENERATE_MAN is set to YES.

MAN_OUTPUT             = man

# The MAN_EXTENSION tag determines the extension that is added to the generated
# man pages. In case the manual section does not start with a number, the number
# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
# optional.
# The default value is: .3.
# This tag requires that the tag GENERATE_MAN is set to YES.

MAN_EXTENSION          = .3

# The MAN_SUBDIR tag determines the name of the directory created within
# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
# MAN_EXTENSION with the initial . removed.
# This tag requires that the tag GENERATE_MAN is set to YES.

MAN_SUBDIR             =

# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
# will generate one additional man file for each entity documented in the real
# man page(s). These additional files only source the real man page, but without
# them the man command would be unable to find the correct page.
# The default value is: NO.
# This tag requires that the tag GENERATE_MAN is set to YES.

MAN_LINKS              = NO

#---------------------------------------------------------------------------
# Configuration options related to the XML output
#---------------------------------------------------------------------------

# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
# captures the structure of the code including all documentation.
# The default value is: NO.

GENERATE_XML           = YES

# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
# it.
# The default directory is: xml.
# This tag requires that the tag GENERATE_XML is set to YES.

XML_OUTPUT             = xml

# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
# listings (including syntax highlighting and cross-referencing information) to
# the XML output. Note that enabling this will significantly increase the size
# of the XML output.
# The default value is: YES.
# This tag requires that the tag GENERATE_XML is set to YES.

XML_PROGRAMLISTING     = NO

# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include
# namespace members in file scope as well, matching the HTML output.
# The default value is: NO.
# This tag requires that the tag GENERATE_XML is set to YES.

XML_NS_MEMB_FILE_SCOPE = NO

#---------------------------------------------------------------------------
# Configuration options related to the DOCBOOK output
#---------------------------------------------------------------------------

# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
# that can be used to generate PDF.
# The default value is: NO.

GENERATE_DOCBOOK       = NO

# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
# front of it.
# The default directory is: docbook.
# This tag requires that the tag GENERATE_DOCBOOK is set to YES.

DOCBOOK_OUTPUT         = docbook

#---------------------------------------------------------------------------
# Configuration options for the AutoGen Definitions output
#---------------------------------------------------------------------------

# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
# the structure of the code including all documentation. Note that this feature
# is still experimental and incomplete at the moment.
# The default value is: NO.

GENERATE_AUTOGEN_DEF   = NO

#---------------------------------------------------------------------------
# Configuration options related to the Perl module output
#---------------------------------------------------------------------------

# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
# file that captures the structure of the code including all documentation.
#
# Note that this feature is still experimental and incomplete at the moment.
# The default value is: NO.

GENERATE_PERLMOD       = NO

# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
# output from the Perl module output.
# The default value is: NO.
# This tag requires that the tag GENERATE_PERLMOD is set to YES.

PERLMOD_LATEX          = NO

# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
# formatted so it can be parsed by a human reader. This is useful if you want to
# understand what is going on. On the other hand, if this tag is set to NO, the
# size of the Perl module output will be much smaller and Perl will parse it
# just the same.
# The default value is: YES.
# This tag requires that the tag GENERATE_PERLMOD is set to YES.

PERLMOD_PRETTY         = YES

# The names of the make variables in the generated doxyrules.make file are
# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
# so different doxyrules.make files included by the same Makefile don't
# overwrite each other's variables.
# This tag requires that the tag GENERATE_PERLMOD is set to YES.

PERLMOD_MAKEVAR_PREFIX =

#---------------------------------------------------------------------------
# Configuration options related to the preprocessor
#---------------------------------------------------------------------------

# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
# C-preprocessor directives found in the sources and include files.
# The default value is: YES.

ENABLE_PREPROCESSING   = YES

# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
# in the source code. If set to NO, only conditional compilation will be
# performed. Macro expansion can be done in a controlled way by setting
# EXPAND_ONLY_PREDEF to YES.
# The default value is: NO.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.

MACRO_EXPANSION        = YES

# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
# the macro expansion is limited to the macros specified with the PREDEFINED and
# EXPAND_AS_DEFINED tags.
# The default value is: NO.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.

EXPAND_ONLY_PREDEF     = YES

# If the SEARCH_INCLUDES tag is set to YES, the include files in the
# INCLUDE_PATH will be searched if a #include is found.
# The default value is: YES.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.

SEARCH_INCLUDES        = YES

# The INCLUDE_PATH tag can be used to specify one or more directories that
# contain include files that are not input files but should be processed by the
# preprocessor.
# This tag requires that the tag SEARCH_INCLUDES is set to YES.

INCLUDE_PATH           =

# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
# patterns (like *.h and *.hpp) to filter out the header-files in the
# directories. If left blank, the patterns specified with FILE_PATTERNS will be
# used.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.

INCLUDE_FILE_PATTERNS  =

# The PREDEFINED tag can be used to specify one or more macro names that are
# defined before the preprocessor is started (similar to the -D option of e.g.
# gcc). The argument of the tag is a list of macros of the form: name or
# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
# is assumed. To prevent a macro definition from being undefined via #undef or
# recursively expanded use the := operator instead of the = operator.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.

PREDEFINED             = DOXYGEN_DOCUMENTATION_BUILD \
                         DOXYGEN_SHOULD_SKIP_THIS \
                         G_BEGIN_DECLS= \
                         G_END_DECLS= \
                         WP_API= \
                         WP_PRIVATE_API= \
                         WP_PLUGIN_EXPORT= \
                         WP_PADDING(...)= \
                         G_GNUC_CONST= \
                         G_GNUC_PRINTF(...)= \
                         G_GNUC_NULL_TERMINATED= \
                         G_DEFINE_TYPE_WITH_CODE(...)= \
                         G_DEFINE_ABSTRACT_TYPE(...)= \
                         G_DEFINE_TYPE(...)= \
                         G_DEFINE_BOXED_TYPE(...)= \
                         G_DEFINE_INTERFACE(...)= \
                         G_DEFINE_QUARK(...)= \
                         G_DEFINE_TYPE_WITH_PRIVATE(...)= \
                         G_DEFINE_AUTOPTR_CLEANUP_FUNC(...)= \
                         G_DECLARE_FINAL_TYPE(...)= \
                         G_DECLARE_DERIVABLE_TYPE(...)= \
                         G_DECLARE_INTERFACE(...)= \
                         GQuark=

# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
# tag can be used to specify a list of macro names that should be expanded. The
# macro definition that is found in the sources will be used. Use the PREDEFINED
# tag if you want to use a different macro definition that overrules the
# definition found in the source code.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.

EXPAND_AS_DEFINED      =

# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
# remove all references to function-like macros that are alone on a line, have
# an all uppercase name, and do not end with a semicolon. Such function macros
# are typically used for boiler-plate code, and will confuse the parser if not
# removed.
# The default value is: YES.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.

SKIP_FUNCTION_MACROS   = YES

#---------------------------------------------------------------------------
# Configuration options related to external references
#---------------------------------------------------------------------------

# The TAGFILES tag can be used to specify one or more tag files. For each tag
# file the location of the external documentation should be added. The format of
# a tag file without this location is as follows:
# TAGFILES = file1 file2 ...
# Adding location for the tag files is done as follows:
# TAGFILES = file1=loc1 "file2 = loc2" ...
# where loc1 and loc2 can be relative or absolute paths or URLs. See the
# section "Linking to external documentation" for more information about the use
# of tag files.
# Note: Each tag file must have a unique name (where the name does NOT include
# the path). If a tag file is not located in the directory in which doxygen is
# run, you must also specify the path to the tagfile here.

TAGFILES               =

# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
# tag file that is based on the input files it reads. See section "Linking to
# external documentation" for more information about the usage of tag files.

GENERATE_TAGFILE       =

# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
# the class index. If set to NO, only the inherited external classes will be
# listed.
# The default value is: NO.

ALLEXTERNALS           = NO

# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
# in the modules index. If set to NO, only the current project's groups will be
# listed.
# The default value is: YES.

EXTERNAL_GROUPS        = YES

# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
# the related pages index. If set to NO, only the current project's pages will
# be listed.
# The default value is: YES.

EXTERNAL_PAGES         = YES

#---------------------------------------------------------------------------
# Configuration options related to the dot tool
#---------------------------------------------------------------------------

# You can include diagrams made with dia in doxygen documentation. Doxygen will
# then run dia to produce the diagram and insert it in the documentation. The
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
# If left empty dia is assumed to be found in the default search path.

DIA_PATH               =

# If set to YES the inheritance and collaboration graphs will hide inheritance
# and usage relations if the target is undocumented or is not a class.
# The default value is: YES.

HIDE_UNDOC_RELATIONS   = YES

# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
# available from the path. This tool is part of Graphviz (see:
# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
# Bell Labs. The other options in this section have no effect if this option is
# set to NO
# The default value is: YES.

HAVE_DOT               = YES

# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
# to run in parallel. When set to 0 doxygen will base this on the number of
# processors available in the system. You can set it explicitly to a value
# larger than 0 to get control over the balance between CPU load and processing
# speed.
# Minimum value: 0, maximum value: 32, default value: 0.
# This tag requires that the tag HAVE_DOT is set to YES.

DOT_NUM_THREADS        = 0

# By default doxygen will tell dot to use the default font as specified with
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
# the path where dot can find it using this tag.
# This tag requires that the tag HAVE_DOT is set to YES.

DOT_FONTPATH           =

# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
# each documented class showing the direct and indirect inheritance relations.
# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.

CLASS_GRAPH            = YES

# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
# graph for each documented class showing the direct and indirect implementation
# dependencies (inheritance, containment, and class references variables) of the
# class with other documented classes.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.

COLLABORATION_GRAPH    = YES

# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
# groups, showing the direct groups dependencies.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.

GROUP_GRAPHS           = YES

# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
# collaboration diagrams in a style similar to the OMG's Unified Modeling
# Language.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.

UML_LOOK               = NO

# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
# class node. If there are many fields or methods and many nodes the graph may
# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
# number of items for each type to make the size more manageable. Set this to 0
# for no limit. Note that the threshold may be exceeded by 50% before the limit
# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
# but if the number exceeds 15, the total amount of fields shown is limited to
# 10.
# Minimum value: 0, maximum value: 100, default value: 10.
# This tag requires that the tag UML_LOOK is set to YES.

UML_LIMIT_NUM_FIELDS   = 10

# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and
# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS
# tag is set to YES, doxygen will add type and arguments for attributes and
# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen
# will not generate fields with class member information in the UML graphs. The
# class diagrams will look similar to the default class diagrams but using UML
# notation for the relationships.
# Possible values are: NO, YES and NONE.
# The default value is: NO.
# This tag requires that the tag UML_LOOK is set to YES.

DOT_UML_DETAILS        = NO

# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters
# to display on a single line. If the actual line length exceeds this threshold
# significantly it will wrapped across multiple lines. Some heuristics are apply
# to avoid ugly line breaks.
# Minimum value: 0, maximum value: 1000, default value: 17.
# This tag requires that the tag HAVE_DOT is set to YES.

DOT_WRAP_THRESHOLD     = 17

# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
# collaboration graphs will show the relations between templates and their
# instances.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.

TEMPLATE_RELATIONS     = NO

# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
# YES then doxygen will generate a graph for each documented file showing the
# direct and indirect include dependencies of the file with other documented
# files.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.

INCLUDE_GRAPH          = YES

# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
# set to YES then doxygen will generate a graph for each documented file showing
# the direct and indirect include dependencies of the file with other documented
# files.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.

INCLUDED_BY_GRAPH      = YES

# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
# dependency graph for every global function or class method.
#
# Note that enabling this option will significantly increase the time of a run.
# So in most cases it will be better to enable call graphs for selected
# functions only using the \callgraph command. Disabling a call graph can be
# accomplished by means of the command \hidecallgraph.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.

CALL_GRAPH             = NO

# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
# dependency graph for every global function or class method.
#
# Note that enabling this option will significantly increase the time of a run.
# So in most cases it will be better to enable caller graphs for selected
# functions only using the \callergraph command. Disabling a caller graph can be
# accomplished by means of the command \hidecallergraph.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.

CALLER_GRAPH           = NO

# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
# hierarchy of all classes instead of a textual one.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.

GRAPHICAL_HIERARCHY    = YES

# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
# dependencies a directory has on other directories in a graphical way. The
# dependency relations are determined by the #include relations between the
# files in the directories.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.

DIRECTORY_GRAPH        = YES

# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
# generated by dot. For an explanation of the image formats see the section
# output formats in the documentation of the dot tool (Graphviz (see:
# http://www.graphviz.org/)).
# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
# to make the SVG files visible in IE 9+ (other browsers do not have this
# requirement).
# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd,
# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo,
# gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, png:cairo,
# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
# png:gdiplus:gdiplus.
# The default value is: png.
# This tag requires that the tag HAVE_DOT is set to YES.

DOT_IMAGE_FORMAT       = png

# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
# enable generation of interactive SVG images that allow zooming and panning.
#
# Note that this requires a modern browser other than Internet Explorer. Tested
# and working are Firefox, Chrome, Safari, and Opera.
# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
# the SVG files visible. Older versions of IE do not have SVG support.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.

INTERACTIVE_SVG        = NO

# The DOT_PATH tag can be used to specify the path where the dot tool can be
# found. If left blank, it is assumed the dot tool can be found in the path.
# This tag requires that the tag HAVE_DOT is set to YES.

DOT_PATH               =

# The DOTFILE_DIRS tag can be used to specify one or more directories that
# contain dot files that are included in the documentation (see the \dotfile
# command).
# This tag requires that the tag HAVE_DOT is set to YES.

DOTFILE_DIRS           =

# The MSCFILE_DIRS tag can be used to specify one or more directories that
# contain msc files that are included in the documentation (see the \mscfile
# command).

MSCFILE_DIRS           =

# The DIAFILE_DIRS tag can be used to specify one or more directories that
# contain dia files that are included in the documentation (see the \diafile
# command).

DIAFILE_DIRS           =

# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
# path where java can find the plantuml.jar file. If left blank, it is assumed
# PlantUML is not used or called during a preprocessing step. Doxygen will
# generate a warning when it encounters a \startuml command in this case and
# will not generate output for the diagram.

PLANTUML_JAR_PATH      =

# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
# configuration file for plantuml.

PLANTUML_CFG_FILE      =

# When using plantuml, the specified paths are searched for files specified by
# the !include statement in a plantuml block.

PLANTUML_INCLUDE_PATH  =

# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
# that will be shown in the graph. If the number of nodes in a graph becomes
# larger than this value, doxygen will truncate the graph, which is visualized
# by representing a node as a red box. Note that doxygen if the number of direct
# children of the root node in a graph is already larger than
# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
# Minimum value: 0, maximum value: 10000, default value: 50.
# This tag requires that the tag HAVE_DOT is set to YES.

DOT_GRAPH_MAX_NODES    = 50

# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
# generated by dot. A depth value of 3 means that only nodes reachable from the
# root by following a path via at most 3 edges will be shown. Nodes that lay
# further from the root node will be omitted. Note that setting this option to 1
# or 2 may greatly reduce the computation time needed for large code bases. Also
# note that the size of a graph can be further restricted by
# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
# Minimum value: 0, maximum value: 1000, default value: 0.
# This tag requires that the tag HAVE_DOT is set to YES.

MAX_DOT_GRAPH_DEPTH    = 0

# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
# files in one run (i.e. multiple -o and -T options on the command line). This
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
# this, this feature is disabled by default.
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.

DOT_MULTI_TARGETS      = NO

# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
# explaining the meaning of the various boxes and arrows in the dot generated
# graphs.
# The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES.

GENERATE_LEGEND        = YES

# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate
# files that are used to generate the various graphs.
#
# Note: This setting is not only used for dot files but also for msc and
# plantuml temporary files.
# The default value is: YES.

DOT_CLEANUP            = YES
0707010000000A000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001F00000000wireplumber-0.5.0/docs/_static    0707010000000B000081A400000000000000000000000165F863040000033D000000000000000000000000000000000000002A00000000wireplumber-0.5.0/docs/_static/custom.css /* expand to full window width */
.wy-nav-content {
  max-width: none;
}

/* style function names */
.rst-content .sig-name .n {
  color: #000;
  padding-left: 0.3em;
}

/* style parenthesis in function signatures */
.rst-content .sig-paren {
  padding: 0.3em;
}

/* space between pointer symbols and variable names:
   ex: WpObject *<-padding->self */
.rst-content .sig span.p+span.n {
  padding-left: 0.3em;
}

/* space between macro names and their definitions:
   ex: FOO_TYPE_BAR<-padding->(foo_bar_get_type()) */
.rst-content .sig-name+.pre {
  padding-left: 0.5em;
}

/* hack for gobject properties, which are marked as cpp signatures */
.rst-content .sig.sig-object.cpp {
  padding-left: 0.3em;
}

div.graphviz {
  padding-top: 1em;
  padding-bottom: 1em;
}

/* space between badges */
img+img {
  padding-left: 0.5em;
}
   0707010000000C000081A400000000000000000000000165F863040000040B000000000000000000000000000000000000002200000000wireplumber-0.5.0/docs/conf.py.in # -- Project information -----------------------------------------------------

project = 'WirePlumber'
copyright = '2021-2024, Collabora'
author = 'Collabora'
release = '@VERSION@'
version = '@VERSION@'

# -- Breathe configuration ---------------------------------------------------

extensions = [
  'breathe',
  'sphinx_rtd_theme',
  'sphinx.ext.graphviz',
]

breathe_projects = {
  "WirePlumber": "@OUTDIR@/wp/xml",
}
breathe_default_project = "WirePlumber"
breathe_default_members = ('members', 'undoc-members')

breathe_domain_by_extension = {
    "h" : "c",
    "c" : "c",
}
breathe_show_define_initializer = True
breathe_show_enumvalue_initializer = True

# -- Options for HTML output -------------------------------------------------

html_theme = "sphinx_rtd_theme"

html_theme_options = {
  "display_version": True,
  "prev_next_buttons_location": "both",
  "style_external_links": True,
}

html_static_path = ['@SRCDIR@/_static']
html_css_files = ['custom.css']

graphviz_output_format = "svg"

pygments_style = "friendly"
 0707010000000D000081A400000000000000000000000165F8630400003953000000000000000000000000000000000000002900000000wireplumber-0.5.0/docs/gen-api-gtkdoc.py  #!/usr/bin/env python
#
#  Copyright 2015 The Geany contributors
#  Copyright 2021 Collabora Ltd.
#    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.

import os
import sys
import re
from lxml import etree
from optparse import OptionParser


def normalize_text(s):
    r"""
    Normalizes whitespace in text.
    >>> normalize_text("asd xxx")
    'asd xxx'
    >>> normalize_text(" asd\nxxx  ")
    'asd xxx'
    """
    return s.replace("\n", " ").strip()


CXX_NAMESPACE_RE = re.compile(r'[_a-zA-Z][_0-9a-zA-Z]*::')
def fix_definition(s):
    """
    Removes C++ name qualifications from some definitions.
    For example:
    >>> fix_definition("bool flag")
    'bool flag'
    >>> fix_definition("bool FooBar::flag")
    'bool flag'
    >>> fix_definition("void(* _GeanyObjectClass::project_open) (GKeyFile *keyfile)")
    'void(* project_open) (GKeyFile *keyfile)'
    """
    return CXX_NAMESPACE_RE.sub(r"", s)


class AtDoc(object):
    def __init__(self):
        self.retval = None
        self.since = ""
        self.annot = []

    def cb(self, type, str):
        if (type == "param"):
            words = str.split(" ", 2)
            self.annot = []
        elif (type == "return"):
            self.annot = []
        elif (type == "since"):
            self.since = str.rstrip()
        elif (type == "see"):
            return "See " + str
        elif type in ("a", "c") and str in ("NULL", "TRUE", "FALSE"):
            return "%" + str
        elif (type == "a"):
            return "@" + str
        else:
            return str

        return ""


class DoxygenProcess(object):
    def __init__(self):
        self.at = None

    # http://stackoverflow.com/questions/4624062/get-all-text-inside-a-tag-in-lxml
    @staticmethod
    def stringify_children(node):
        from lxml.etree import tostring
        from itertools import chain
        parts = ([node.text] +
                 list(chain(*([c.text, tostring(c).decode("utf-8"), c.tail] for c in node.getchildren()))) +
                 [node.tail])
        # filter removes possible Nones in texts and tails
        return "".join(filter(None, parts))

    def get_program_listing(self, xml):
        from lxml.etree import tostring
        arr = ["", "|[<!-- language=\"C\" -->"]
        for l in xml.getchildren():
            if (l.tag == "codeline"):
                # a codeline is of the form
                # <highlight class="normal">GeanyDocument<sp/>*doc<sp/>=<sp/>...;</highlight>
                # <sp/> tags must be replaced with spaces, then just use the text
                h = l.find("highlight")
                if h is not None:
                    html = tostring(h).decode("utf-8")
                    html = html.replace("<sp/>", " ")
                    arr.append("  " + tostring(etree.HTML(html), method="text").decode("utf-8"))
        arr.append("]|")
        return "\n".join(arr)

    def join_annot(self):
        s = " ".join(map(lambda x: "(%s)" % x, self.at.annot))
        return s + ": " if s else ""

    def process_element(self, xml):
        self.at = AtDoc()
        s = self.__process_element(xml)
        return s

    def get_extra(self):
        return self.join_annot()

    def get_return(self):
        return self.at.retval

    def get_since(self):
        return self.at.since

    def __process_element(self, xml):
        s = ""

        if xml.text and re.search('\S', xml.text):
            s += xml.text
        for n in xml.getchildren():
            if n.tag == "emphasis":
                s += self.at.cb("a", self.__process_element(n))
            if n.tag == "computeroutput":
                s += self.at.cb("c", self.__process_element(n))
            if n.tag == "itemizedlist":
                s += "\n" + self.__process_element(n)
            if n.tag == "listitem":
                s += " - " + self.__process_element(n)
            if n.tag == "para":
                p = self.__process_element(n)
                if re.search('\S', p):
                    s += p + "\n"
            if n.tag == "ref":
                s += n.text if n.text else ""
            if n.tag == "simplesect":
                ss = self.at.cb(n.get("kind"), self.__process_element(n))
                s += ss + "\n" if ss else ""
            if n.tag == "programlisting":
                s += self.get_program_listing(n)
            if n.tag == "xrefsect":
                s += self.__process_element(n)
            if n.tag == "xreftitle":
                s += self.__process_element(n) + ": "
            if n.tag == "xrefdescription":
                s += self.__process_element(n)
            if n.tag == "ulink":
                s += self.__process_element(n)
            if n.tag == "linebreak":
                s += "\n"
            if n.tag == "ndash":
                s += "--"
                # workaround for doxygen bug #646002
            if n.tag == "htmlonly":
                s += ""
            if n.tail:
                if re.search('\S', n.tail):
                  s += n.tail
            if n.tag.startswith("param"):
                pass  # parameters are handled separately in DoxyFunction::from_memberdef()
        return s


class DoxyMember(object):
    def __init__(self, name, brief, extra=""):
        self.name       = name
        self.brief      = brief
        self.extra      = extra


class DoxyElement(object):

    def __init__(self, name, definition, **kwargs):
        self.name       = name
        self.definition = definition
        self.brief      = kwargs.get('brief', "")
        self.detail     = kwargs.get('detail', "")
        self.members    = kwargs.get('members', [])
        self.since      = kwargs.get('since', "")
        self.extra      = kwargs.get('extra', "")
        self.retval     = kwargs.get('retval', None)

    def is_documented(self):
        return (normalize_text(self.brief) != "" or
                normalize_text(self.detail) != "" or
                normalize_text(self.since) != "")

    def add_brief(self, xml):
        proc = DoxygenProcess()
        self.brief = proc.process_element(xml)
        self.extra += proc.get_extra()

    def add_detail(self, xml):
        proc = DoxygenProcess()
        self.detail = proc.process_element(xml)
        self.extra += proc.get_extra()
        self.since = proc.get_since()

    def add_member(self, xml):
        name = xml.find("name").text
        proc = DoxygenProcess()
        brief = proc.process_element(xml.find("briefdescription"))
        # optional doxygen command output appears within <detaileddescription />
        proc.process_element(xml.find("detaileddescription"))
        self.members.append(DoxyMember(name, normalize_text(brief), proc.get_extra()))

    def add_param(self, xml):
        name = xml.find("parameternamelist").find("parametername").text
        proc = DoxygenProcess()
        brief = proc.process_element(xml.find("parameterdescription"))
        self.members.append(DoxyMember(name, normalize_text(brief), proc.get_extra()))

    def add_return(self, xml):
        proc = DoxygenProcess()
        brief = proc.process_element(xml)
        self.retval = DoxyMember("ret", normalize_text(brief), proc.get_extra())

    def to_gtkdoc(self):
        s = []
        s.append("/**")
        s.append(" * %s: %s" % (self.name, self.extra))
        for p in self.members:
            s.append(" * @%s: %s %s" % (p.name, p.extra, p.brief))
        s.append(" *")
        s.append(" * %s" % self.brief.replace("\n", "\n * "))
        s.append(" *")
        s.append(" * %s" % self.detail.replace("\n", "\n * "))
        s.append(" *")
        if self.retval:
            s.append(" * Returns: %s %s" % (self.retval.extra, self.retval.brief))
        if self.since:
            s.append(" *")
            s.append(" * Since: %s" % self.since)
        s.append(" */")
        s.append("")
        return "\n".join(s)


class DoxyTypedef(DoxyElement):
    @staticmethod
    def from_memberdef(xml):
        name = xml.find("name").text
        d = normalize_text(xml.find("definition").text)
        d += ";"
        return DoxyTypedef(name, d)


class DoxyEnum(DoxyElement):
    @staticmethod
    def from_memberdef(xml):
        name = xml.find("name").text
        d = "typedef enum {\n"
        for member in xml.findall("enumvalue"):
            v = member.find("initializer")
            d += "\t%s%s,\n" % (member.find("name").text, " "+v.text if v is not None else "")
        d += "} %s;\n" % name

        e = DoxyEnum(name, d)
        e.add_brief(xml.find("briefdescription"))
        e.add_detail(xml.find("detaileddescription"))
        for p in xml.findall("enumvalue"):
            e.add_member(p)
        return e


class DoxyStruct(DoxyElement):
    @staticmethod
    def from_compounddef(xml, typedefs=[]):
        name = xml.find("compoundname").text
        d = "struct %s {\n" % name
        memberdefs = xml.xpath(".//sectiondef[@kind='public-attrib']/memberdef")
        for p in memberdefs:
            # workaround for struct members. g-ir-scanner can't properly map struct members
            # (beginning with struct GeanyFoo) to the typedef and assigns a generic type for them
            # thus we fix that up here and enforce usage of the typedef. These are written
            # out first, before any struct definition, for this reason
            # Exception: there are no typedefs for GeanyFooPrivate so skip those. Their exact
            # type isn't needed anyway
            s = fix_definition(p.find("definition").text).lstrip()
            proc = DoxygenProcess()
            brief = proc.process_element(p.find("briefdescription"))
            private = (normalize_text(brief) == "")
            words = s.split()
            if (words[0] == "struct"):
                if not (words[1].endswith("Private") or words[1].endswith("Private*")):
                    s = " ".join(words[1:])
            d += "\t/*< %s >*/\n\t%s;\n" % ("private" if private else "public", s)

        d += "};\n"
        e = DoxyStruct(name, d)
        e.add_brief(xml.find("briefdescription"))
        e.add_detail(xml.find("detaileddescription"))
        for p in memberdefs:
            e.add_member(p)
        return e


class DoxyFunction(DoxyElement):
    @staticmethod
    def from_memberdef(xml):
        name = xml.find("name").text
        d = normalize_text(xml.find("definition").text)
        if ((xml.find("argsstring").text) is not None):
            d += " " + xml.find("argsstring").text + ";"
            d = normalize_text(d)

        e = DoxyFunction(name, d)
        e.add_brief(xml.find("briefdescription"))
        e.add_detail(xml.find("detaileddescription"))
        for p in xml.xpath(".//detaileddescription/*/parameterlist[@kind='param']/parameteritem"):
            e.add_param(p)
        x = xml.xpath(".//detaileddescription/*/simplesect[@kind='return']")
        if (len(x) > 0):
            e.add_return(x[0])
        return e


class DoxyVariable(DoxyElement):
    @staticmethod
    def from_memberdef(xml):
        name = xml.find("name").text
        d = normalize_text(xml.find("definition").text)
        d += ";"
        e = DoxyVariable(name, d)
        e.add_brief(xml.find("briefdescription"))
        t = xml.find("type")
        if t is not None:
            typestr = "".join(t.itertext()).strip()
            if typestr.startswith("const "):
                typestr = typestr[6:]
            e.type = "("+typestr+") "
        else:
            e.type = ""
        v = xml.find("initializer")
        e.value = v.text.replace("=","").replace("\n","") if v is not None else ""
        return e
    def to_gtkdoc(self):
        s = super().to_gtkdoc()
        # need this to get g-ir-scanner to recognize this as a constant
        s += "#define "+self.name+" ("+self.type+self.value+")\n"
        s += "#undef "+self.name+"\n"
        return s


def main(args):
    xml_dir = None
    outfile = None

    parser = OptionParser(usage="usage: %prog [options] XML_DIR")
    parser.add_option("-o", "--output", metavar="FILE", help="Write output to FILE",
                      action="store", dest="outfile")
    opts, args = parser.parse_args(args[1:])

    xml_dir = args[0]

    if not (os.path.exists(xml_dir)):
        sys.stderr.write("invalid xml directory\n")
        return 1

    symbols = []
    transform = etree.XSLT(etree.parse(os.path.join(xml_dir, "combine.xslt")))
    doc = etree.parse(os.path.join(xml_dir, "index.xml"))
    root = transform(doc)
    h_files = root.xpath(".//compounddef[@kind='file']/compoundname[substring(.,string-length(.)-1)='.h']/..")

    for f in h_files:
        for n0 in f.xpath(".//*/memberdef[@kind='enum']"):
            e = DoxyEnum.from_memberdef(n0)
            symbols.append(e)

    for n0 in root.xpath(".//compounddef[@kind='struct']"):
        e = DoxyStruct.from_compounddef(n0)
        symbols.append(e)

    for n0 in root.xpath(".//compounddef[@kind='group']"):
        for n1 in n0.xpath(".//*/memberdef[@kind='function']"):
            e = DoxyFunction.from_memberdef(n1)
            symbols.append(e)
        for n1 in n0.xpath(".//*/memberdef[@kind='variable']"):
            e = DoxyVariable.from_memberdef(n1)
            symbols.append(e)

    if (opts.outfile):
        try:
            outfile = open(opts.outfile, "w+")
        except OSError as err:
            sys.stderr.write("failed to open \"%s\" for writing (%s)\n" % (opts.outfile, err.strerror))
            return 1
    else:
        outfile = sys.stdout

    try:
        outfile.write("/*\n * Automatically generated file - do not edit\n */\n\n")

        for e in filter(lambda x: x.is_documented(), symbols):
            outfile.write("\n")
            outfile.write(e.to_gtkdoc())
            outfile.write("\n")

    except BrokenPipeError:
        # probably piped to head or tail
        return 0

    return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv))
 0707010000000E000081A400000000000000000000000165F863040000128F000000000000000000000000000000000000002300000000wireplumber-0.5.0/docs/meson.build    # Find dependencies
pymod = import('python')

python_doc = pymod.find_installation(
  'python3',
  modules: ['sphinx', 'sphinx_rtd_theme', 'breathe', 'sphinx.ext.graphviz'],
  required: get_option('doc')
)
summary({'Python 3 Sphinx related modules': python_doc.found()},
    bool_yn: true,
    section: 'For documentation'
)

python_gir = pymod.find_installation(
  'python3',
  modules: ['lxml'],
  required: get_option('introspection')
)
summary({'Python 3 lxml module': python_gir.found()},
    bool_yn: true,
    section: 'For introspection'
)

if get_option('doc').enabled() or get_option('introspection').enabled()
  doxygen_p = find_program('doxygen', version: '>= 1.8.0', required: true)
elif get_option('doc').auto() or get_option('introspection').auto()
  doxygen_p = find_program('doxygen', version: '>= 1.8.0', required: false)
else
  doxygen_p = disabler()
endif
summary({'Doxygen': doxygen_p.found()}, bool_yn: true, section: 'For introspection')
summary({'Doxygen': doxygen_p.found()}, bool_yn: true, section: 'For documentation')

sphinx_p = find_program('sphinx-build',
    version: '>= 2.1.0', required: get_option('doc'))
summary({'sphinx-build': sphinx_p.found()}, bool_yn: true, section: 'For documentation')
gir_p = find_program('g-ir-scanner', required: get_option('introspection'))
summary({'g-ir-scanner': gir_p.found()}, bool_yn: true, section: 'For introspection')

build_doc = python_doc.found() and doxygen_p.found() and sphinx_p.found()
build_gir = python_gir.found() and doxygen_p.found() and gir_p.found()

# Run doxygen (common for docs and g-i)

if build_doc or build_gir
  doxy_wp_conf_data = configuration_data()
  doxy_wp_conf_data.set('OUTPUT_DIR', meson.current_build_dir() / 'wp')
  doxy_wp_conf_data.set('INPUT', meson.current_source_dir() / '..' / 'lib' / 'wp')
  doxyfile_wp = configure_file(
    input: 'Doxyfile.in',
    output: 'Doxyfile-wp',
    configuration: doxy_wp_conf_data
  )

  doxyxml_wp_depfiles = [wp_lib_sources, wp_lib_headers]
  doxyxml_wp = custom_target('doxyxml_wp',
    command: [doxygen_p, doxyfile_wp],
    depend_files: doxyxml_wp_depfiles,
    output: 'wp',
    build_by_default: true,
  )
endif

# Build documentation

if build_doc
  sphinx_files = files(
    '_static'/'custom.css',
    meson.current_source_dir()/'..'/'README.rst',
    meson.current_source_dir()/'..'/'NEWS.rst',
  )
  sphinx_files += scripts_doc_files
  subdir('rst')

  sphinx_conf_data = configuration_data()
  sphinx_conf_data.set('SRCDIR', meson.current_source_dir())
  sphinx_conf_data.set('OUTDIR', meson.current_build_dir())
  sphinx_conf_data.set('VERSION', meson.project_version())
  sphinx_conf = configure_file(
    input: 'conf.py.in',
    output: 'conf.py',
    configuration: sphinx_conf_data
  )

  custom_target('doc',
    command: [sphinx_p,
      '-q',                       # quiet
      '-E',                       # rebuild from scratch
      '-j', 'auto',               # parallel build
      '-d', '@PRIVATE_DIR@',      # doctrees dir
      '-c', '@OUTDIR@',           # conf.py dir
      '@CURRENT_SOURCE_DIR@/rst', # source dir
      '@OUTPUT@',                 # output dir
    ],
    depend_files: [
      sphinx_conf, sphinx_files,
      doxyfile_wp, doxyxml_wp_depfiles,
    ],
    depends: [doxyxml_wp],
    output: 'html',
    install: true,
    install_dir: wireplumber_doc_dir,
    build_by_default: true,
  )
endif

# Build GObject introspection

if build_gir
  wp_gtkdoc_h = custom_target('wp_gtkdoc_h',
    command: [python_gir,
        '@CURRENT_SOURCE_DIR@/gen-api-gtkdoc.py',
        '-o', '@OUTPUT@',
        '@OUTDIR@/wp/xml',
    ],
    depends: [doxyxml_wp],
    depend_files: [doxyxml_wp_depfiles, 'gen-api-gtkdoc.py'],
    output: 'wp-gtkdoc.h',
    build_by_default: true,
  )

  # A dummy library dependency to force meson to make the gir target
  # depend on wp-gtkdoc.h, because generate_gir() doesn't add dependencies
  # on its sources (meson bug)
  # fixed in 0.59 by https://github.com/mesonbuild/meson/pull/8805
  dummy_c = custom_target('dummy_c',
    command: ['echo', 'int dummy(void) { return 0; }'],
    capture: true,
    output: 'dummy.c',
    build_by_default: true,
  )
  libdummy = library('dummy',
    wp_gtkdoc_h, dummy_c,
    install: false,
    soversion: '0',
    version: '0.0.0',
  )
  dummy_dep = declare_dependency(link_with: libdummy)

  gnome.generate_gir(wp_lib,
    dependencies: [wp_dep, dummy_dep],
    namespace: 'Wp',
    nsversion: wireplumber_api_version,
    export_packages: 'wireplumber-' + wireplumber_api_version,
    header: 'wp/wp.h',
    sources: [wpenums_h, wp_gtkdoc_h, wp_lib_headers],
    include_directories: [wpenums_include_dir],
    includes: ['GLib-2.0', 'GObject-2.0', 'Gio-2.0'],
    install: true,
  )
endif
 0707010000000F000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001B00000000wireplumber-0.5.0/docs/rst    07070100000010000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002200000000wireplumber-0.5.0/docs/rst/daemon 07070100000011000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000003000000000wireplumber-0.5.0/docs/rst/daemon/configuration   07070100000012000081A400000000000000000000000165F863040000040F000000000000000000000000000000000000003400000000wireplumber-0.5.0/docs/rst/daemon/configuration.rst   .. _daemon_configuration:

Configuration
=============

WirePlumber is a heavily modular daemon. By itself, it doesn't do anything
except load its configured components. The actual management logic is
implemented inside those components.

At startup, WirePlumber reads its configuration file (combined with all the
fragments it may have) and loads the components specified in the selected
profile. This configures the operation context. Then, the components take over
and drive the entirety of the daemon's operation.

The sections below describe in more detail the configuration file format and
the various options available.

.. toctree::
   :maxdepth: 1

   configuration/conf_file.rst
   configuration/components_and_profiles.rst
   configuration/features.rst
   configuration/configuration_option_types.rst
   configuration/settings.rst
   configuration/modifying_configuration.rst
   configuration/alsa.rst
   configuration/bluetooth.rst
   configuration/access.rst
   configuration/locations.rst
   configuration/multi_instance.rst
 07070100000013000081A400000000000000000000000165F86304000005E0000000000000000000000000000000000000003B00000000wireplumber-0.5.0/docs/rst/daemon/configuration/access.rst    .. _config_access:

Access configuration
====================

WirePlumber includes a "client access" policy which defines access control
rules for PipeWire clients.

Rules
-----

This policy can be configured with rules that can be used to match clients and
apply default permissions to them.

Example:

.. code-block::

   access.rules = [
     {
       matches = [
         {
           access = "flatpak"
           media.category = "Manager"
         }
       ]
       actions = {
         update-props = {
           access = "flatpak-manager"
           default_permissions = "all",
         }
       }
     }
     {
       matches = [
         {
           access = "flatpak"
         }
       ]
       actions = {
         update-props = {
           default_permissions = "rx"
         }
       }
     }
   ]

Possible permissions are any combination of:

 * ``r``: client is allowed to **read** objects, i.e. "see" them on the registry
   and list their properties
 * ``w``: client is allowed to **write** objects, i.e. call methods that modify
   their state
 * ``x``: client is allowed to **execute** methods on objects; the ``w`` flag
   must also be present to call methods that modify the object
 * ``m``: client is allowed to set **metadata** on objects
 * ``l``: nodes of this client are allowed to **link** to other nodes that the
   client can't "see" (i.e. the client doesn't have ``r`` permission on them)

The special value ``all`` is also supported and it is synonym for ``rwxm``
07070100000014000081A400000000000000000000000165F863040000431B000000000000000000000000000000000000003900000000wireplumber-0.5.0/docs/rst/daemon/configuration/alsa.rst  .. _config_alsa:

ALSA configuration
==================

One of the components of WirePlumber is the ALSA monitor. This monitor is
responsible for creating PipeWire devices and nodes for all the ALSA cards that
are available on the system. It also manages the configuration of these devices.

The ALSA monitor is enabled by default and can be disabled using the
``monitor.alsa`` :ref:`feature <config_features>` in the configuration file.

The monitor, as with all device monitors, is implemented as a SPA plugin and is
part of PipeWire. WirePlumber merely loads the plugin and lets it do its work.
The plugin then monitors UDev and creates device and node objects for all the
ALSA cards that are available on the system.

.. note::

   One thing worth remembering here is that in ALSA, a "card" represents a
   physical sound controller device, and a "device" is a logical access point
   that represents a set of inputs and/or outputs that are part of the card. In
   PipeWire, a "device" is the direct equivalent of an ALSA "card" and a "node"
   is almost equivalent (close, but not quite) of an ALSA "device".

Properties
----------

The ALSA monitor SPA plugin (``api.alsa.enum.udev``) supports properties that
can be used to configure it when it is loaded. These properties can be set in
the ``monitor.alsa.properties`` section of the WirePlumber configuration file.

Example:

.. code-block::

   monitor.alsa.properties = {
     alsa.use-acp = true
   }

.. describe:: alsa.use-acp

   A boolean that controls whether the ACP (alsa card profile) code is to be
   the default manager of the device. This will probe the device and configure
   the available profiles, ports and mixer settings. The code to do this is
   taken directly from PulseAudio and provides devices that look and feel
   exactly like the PulseAudio devices.

Rules
-----

When device and node objects are created by the ALSA monitor, they can be
configured using rules. These rules allow matching the existing properties of
these objects and updating them with new values. This is the main way of
configuring ALSA device settings.

These rules can be set in the ``monitor.alsa.rules`` section of the WirePlumber
configuration file.

Example:

.. code-block::

   monitor.alsa.rules = [
     {
       matches = [
         {
           # This matches the value of the 'device.name' property of the device.
           device.name = "~alsa_card.*"
         }
       ]
       actions = {
         update-props = {
           # Apply all the desired device settings here.
           api.alsa.use-acp = true
         }
       }
     }
     {
       matches = [
         # This matches the value of the 'node.name' property of the node.
         {
           node.name = "~alsa_output.*"
         }
       ]
       actions = {
         # Apply all the desired node specific settings here.
         update-props = {
           node.nick              = "My Node"
           priority.driver        = 100
           session.suspend-timeout-seconds = 5
         }
       }
     }
   ]

Device properties
^^^^^^^^^^^^^^^^^

The following properties can be configured on devices created by the monitor:

.. describe:: api.alsa.use-acp

   Use the ACP (alsa card profile) code to manage this device. This will probe
   the device and configure the available profiles, ports and mixer settings.
   The code to do this is taken directly from PulseAudio and provides devices
   that look and feel exactly like the PulseAudio devices.

   :Default value: ``true``
   :Type: boolean

.. describe:: api.alsa.use-ucm

   When ACP is enabled and a UCM configuration is available for a device, by
   default it is used instead of the ACP profiles. This option allows you to
   disable this and use the ACP profiles instead.

   This option does nothing if ``api.alsa.use-acp`` is set to ``false``.

   :Default value: ``true``
   :Type: boolean

.. describe:: api.alsa.soft-mixer

   Setting this option to ``true`` will disable the hardware mixer for volume
   control and mute. All volume handling will then use software volume and mute,
   leaving the hardware mixer untouched. The hardware mixer will still be used
   to mute unused audio paths in the device.

   :Type: boolean

.. describe:: api.alsa.ignore-dB

   Setting this option to ``true`` will ignore the decibel setting configured by
   the driver. Use this when the driver reports wrong settings.

   :Type: boolean

.. describe:: device.profile-set

   This option can be used to select a custom ACP profile-set name for the
   device. This can be configured in UDev rules, but it can also be specified
   here. The default is to use "default.conf".

   :Type: string

.. describe:: device.profile

   The initial active profile name. The default is to start from the "Off"
   profile and then let WirePlumber select the best profile based on its
   policy.

   :Type: string

.. describe:: api.acp.auto-profile

   Automatically select the best profile for the device. Normally this option is
   disabled because WirePlumber will manage the profile of the device.
   WirePlumber can save and load previously selected profiles. Enable this in
   custom configurations where the relevant WirePlumber components are disabled.

   :Type: boolean

.. describe:: api.acp.auto-port

   Automatically select the highest priority port that is available ("port" is a
   PulseAudio/ACP term, the equivalent of a "Route" in PipeWire). This is by
   default disabled because WirePlumber handles the task of selecting and
   restoring Routes. Enable this in custom configurations where the relevant
   WirePlumber components are disabled.

   :Type: boolean

.. describe:: api.acp.probe-rate

   Sets the samplerate used for probing the ALSA devices and collecting the
   profiles and ports.

   :Type: integer

.. describe:: api.acp.pro-channels

   Sets the number of channels to use when probing the "Pro Audio" profile.
   Normally, the maximum amount of channels will be used but with this setting
   this can be reduced, which can make it possible to use other samplerates on
   some devices.

   :Type: integer

Some of the other properties that can be configured on devices:

.. describe:: device.nick

   A short name for the device.

.. describe:: device.description

   A longer, user-friendly name of the device. This will show up in most
   user interfaces as the device's name.

.. describe:: device.disable

   Disables the device. PipeWire will remove it from the list of cards or
   devices.

   :Type: boolean

Node properties
^^^^^^^^^^^^^^^

The following properties can be configured on nodes created by the monitor:

.. describe:: priority.driver

   This configures the node driver priority. Nodes with higher priority will be
   used as a driver in the graph. Other nodes with lower priority will have to
   resample to the driver node when they are joined in the same graph. The
   default value is set based on some heuristics.

   :Type: integer

.. describe:: priority.session

   This configures the priority of the node when selecting a default node
   (default sink/source as a link target for streams). Higher priority nodes
   will be more likely candidates for becoming the default node.

   :Type: integer

   .. note::

      By default, sources have a ``priority.session`` value around 1600-2000 and
      sinks have a value around 600-1000. If you are increasing the priority of
      a sink, it is **not advised** to use a value higher than 1500, as it may
      cause a sink's monitor to be selected as the default source.

.. describe:: node.pause-on-idle

   Pause the node when nothing is linked to it anymore. This is by default false
   because some devices make a "pop" sound when they are opened/closed.
   The node will normally pause and suspend after a timeout (see below).

   :Type: boolean

.. describe:: session.suspend-timeout-seconds

   This option configures a different suspend timeout on the node. By default
   this is ``5`` seconds. For some devices (HiFi amplifiers, for example) it
   might make sense to set a higher timeout because they might require some time
   to restart after being idle.

   A value of ``0`` disables suspend for a node and will leave the ALSA device
   busy. The device can then be manually suspended with
   ``pactl suspend-sink|source``.

   :Type: integer

.. describe:: audio.format

   The sample format of the device. By default, PipeWire will use a 32 bits
   sample format but a different format can be set here.

   :Type: string (``"S16LE"``, ``"S32LE"``, ``"F32LE"``, ...)

.. describe:: audio.rate

   The sample rate of the device. By default, the ALSA device will be configured
   with the same samplerate as the global graph. If this is not supported, or a
   custom value is set here, resampling will be used to match the graph rate.

   :Type: integer

.. describe:: audio.channels

   The number of channels of the device. By default the channels and their
   position are determined by the selected device profile. You can override
   this setting here.

   :Type: integer

.. describe:: audio.position

   The position of the channels. By default the number of channels and their
   position are determined by the selected device profile. You can override
   this setting here and optionally swap or reconfigure the channel positions.

   :Type: array of strings (example: ``["FL", "FR", "LFE", "FC", "RL", "RR"]``)

.. describe:: api.alsa.use-chmap

    Use the channel map as reported by the driver. This is disabled by default
    because it is often wrong and the ACP code handles this better.

    :Type: boolean

.. describe:: api.alsa.disable-mmap

   Disable the use of mmap for the ALSA device. By default, PipeWire will access
   the memory of the device using mmap. This can be disabled and force the usage
   of the slower read and write access modes, in case the mmap support of the
   device is not working properly.

   :Type: boolean

.. describe:: channelmix.normalize

   Normalize the channel volumes when mixing & resampling, making sure that the
   original 0 dB level is preserved so that nothing sounds wildly
   quieter/louder. This is disabled by default.

   :Type: boolean

.. describe:: channelmix.mix-lfe

   Creates a "center" channel for X.0 recordings from the front stereo on X.1
   setups and pushes some low-frequency/bass from the "center" of X.1 recordings
   into the front stereo on X.0 setups. This is disabled by default.

   :Type: boolean

.. describe:: monitor.channel-volumes

   By default, the volume of the sink/source does not influence the volume on
   the monitor ports. Set this option to true to change this. PulseAudio has
   inconsistent behaviour regarding this option, it applies channel-volumes only
   when the sink/source is using software volumes.

   :Type: boolean

.. describe:: node.disable

   Disables the node. Pipewire will remove it from the list of the nodes.

   :Type: boolean

ALSA buffer properties
......................

PipeWire by default uses a timer to consume and produce samples to/from ALSA
devices. After every timeout, it queries the hardware pointers of the device and
uses this information to set a new timeout. This works well for most devices,
but there is a class of devices, so called "batch" devices, that need extra
buffering and timing tweaks to work properly. This is because batch devices only
get their hardware pointers updated after each hardware interrupt. When the
hardware interrupt frequency and the timer frequency are aligned, it is possible
for the hardware pointers to be updated just after the timer has expired,
resulting in sometimes wrong timing information being returned by the query. In
contrast, non-batch devices get pointer updates independent of the interrupt.

This means that for batch devices we need to set the interrupt at a sufficiently
high frequency, at the cost of CPU usage, while for non-batch devices we want to
set the interrupt frequency as low as possible to save CPU. For batch devices
we also need to take the extra buffering into account caused by the delayed
updates of the hardware pointers.

.. note::

   Most USB devices are batch devices and will be handled as such by PipeWire by
   default.

There are 2 tunable parameters to control the buffering and timeouts in a
device:

.. describe:: api.alsa.period-size

   This sets the device interrupt to every period-size samples for non-batch
   devices and to half of this for batch devices. For batch devices, the other
   half of the period-size is used as extra buffering to compensate for the
   delayed update. So, for batch devices, there is an additional period-size/2
   delay. It makes sense to lower the period-size for batch devices to reduce
   this delay.

   :Type: integer (samples)

.. describe:: api.alsa.headroom

   This adds extra delay between the hardware pointers and software pointers.
   In most cases this can be set to 0. For very bad devices or emulated devices
   (like in a VM) it might be necessary to increase the headroom value.

   :Type: integer (samples)

.. describe:: api.alsa.period-num

   This configures the number of periods in the hardware buffer, which controls
   its size. Note that this is multiplied by the period of the device to
   determine the size, so for batch devices, the total buffer size is
   effectively period-num * period-size/2.

   :Type: integer

In summary, this is the overview of buffering and timings:

============== ============================================ ==========================================
Property       Batch                                        Non-Batch
============== ============================================ ==========================================
IRQ Frequency  api.alsa.period-size/2                       api.alsa.period-size
Extra Delay    api.alsa.headroom + api.alsa.period-size/2   api.alsa.headroom
Buffer Size    api.alsa.period-num * api.alsa.period-size/2 api.alsa.period-num * api.alsa.period-size
============== ============================================ ==========================================

Finally, it is possible to disable the batch device tweaks with:

.. describe:: api.alsa.disable-batch

   This disables the batch device tweaks. It removes the extra delay added of
   period-size/2 if the device can support this. For batch devices it is also a
   good idea to lower the period-size (and increase the IRQ frequency) to get
   smaller batch updates and lower latency.

   :Type: boolean

ALSA extra latency properties
.............................

Extra internal delay in the DAC and ADC converters of the device itself can be
set with the ``latency.internal.*`` properties:

.. code-block::

    latency.internal.rate = 256
    latency.internal.ns = 0

You can configure a latency in samples (relative to rate with
``latency.internal.rate``) or in nanoseconds (``latency.internal.ns``).
This value will be added to the total reported latency by the node of the device.

You can use a tool like ``jack_iodelay`` to get the number of samples of
internal latency of your device.

This property is also adjustable at runtime with the ``ProcessLatency`` param.
You will need to find the id of the Node you want to change. For example:
Query the current internal latency of an ALSA node with id 58:

.. code-block:: console

    $ pw-cli e 58 ProcessLatency
    Object: size 80, type Spa:Pod:Object:Param:ProcessLatency (262156), id Spa:Enum:ParamId:ProcessLatency (16)
      Prop: key Spa:Pod:Object:Param:ProcessLatency:quantum (1), flags 00000000
        Float 0.000000
      Prop: key Spa:Pod:Object:Param:ProcessLatency:rate (2), flags 00000000
        Int 0
      Prop: key Spa:Pod:Object:Param:ProcessLatency:ns (3), flags 00000000
        Long 0

Set the internal latency to 256 samples:

.. code-block:: console

    $ pw-cli s 58 ProcessLatency '{ rate = 256 }'
    Object: size 32, type Spa:Pod:Object:Param:ProcessLatency (262156), id Spa:Enum:ParamId:ProcessLatency (16)
      Prop: key Spa:Pod:Object:Param:ProcessLatency:rate (2), flags 00000000
        Int 256
    remote 0 node 58 changed
    remote 0 port 70 changed
    remote 0 port 72 changed
    remote 0 port 74 changed
    remote 0 port 76 changed

Startup tweaks
..............

.. describe:: api.alsa.start-delay

   Some devices need some time before they can report accurate hardware pointer
   positions. In those cases, an extra start delay can be added to compensate
   for this startup delay. This sets the startup delay in samples. The default
   is 0.

   :Type: integer (samples)

IEC958 (S/PDIF) passthrough
...........................

.. describe:: iec958.codecs

   S/PDIF passthrough will only be enabled when the accepted codecs are configured
   on the ALSA device. This can be done by setting the list of supported codecs
   on this property.

   Note that it is possible to also configure this property at runtime, either
   with tools like pavucontrol or with the ``pw-cli`` tool, like this:
   ``pw-cli s <node-id> Props '{ iec958Codecs : [ PCM ] }'``

   :Type: array of strings (example: ``[ "PCM", "DTS", "AC3", "EAC3", "TrueHD", "DTS-HD" ]``)
 07070100000015000081A400000000000000000000000165F86304000033C3000000000000000000000000000000000000003E00000000wireplumber-0.5.0/docs/rst/daemon/configuration/bluetooth.rst .. _config_bluetooth:

Bluetooth configuration
=======================

Bluetooth audio and MIDI devices are managed by the BlueZ and BlueZ-MIDI
monitors, respectively.

Both monitors are enabled by default and can be disabled using the
``monitor.bluez`` and ``monitor.bluez-midi`` :ref:`features <config_features>`
in the configuration file.

As with all device monitors, both of these monitors are implemented as SPA
plugins and are part of PipeWire. WirePlumber merely loads the plugins and lets
them do their work. These plugins then monitor the BlueZ system-wide D-Bus
service and create device and node objects for all the connected Bluetooth audio
and MIDI devices.

Logind integration
------------------

The BlueZ monitors are integrated with logind to ensure that only one user at a
time can use the Bluetooth audio devices. This is because on most Linux desktop
systems, the graphical login manager (GDM, SDDM, etc.) is running as a separate
user and runs its own instance of PipeWire and Wireplumber. This means that if a
user logs in graphically, the Bluetooth audio devices will be automatically
grabbed by the PipeWire/WirePlumber instance of the graphical login manager,
and the user that logs in will not get access to them.

To overcome this, the BlueZ monitors are integrated with logind and are only
allowed to create device and node objects for Bluetooth audio devices if the
user is currently on the "active" logind session.

In some cases, however, this behavior is not desired. For example, if you
manually switch to a TTY and log in there, you may want to keep the Bluetooth
audio devices connected to the now inactive graphical session. Or you may want
to have a dedicated user that is always allowed to use the Bluetooth audio
devices, regardless of the active logind session, for example for a (possibly
headless) music player daemon.

To disable this behavior, you can set the ``monitor.bluez.seat-monitoring``
:ref:`feature <config_features>` to ``disabled``.

Example configuration :ref:`fragment <config_conf_file_fragments>` file:

.. code-block::

   wireplumber.profiles = {
     main = {
       monitor.bluez.seat-monitoring = disabled
     }
   }

.. note::

   If logind is not installed on the system, this functionality is disabled
   automatically.

Monitor Properties
------------------

The BlueZ monitor SPA plugin (``api.bluez5.enum.dbus``) supports properties that
can be used to configure it when it is loaded. These properties can be set in
the ``monitor.bluez.properties`` section of the WirePlumber configuration file.

Example:

.. code-block::

  monitor.bluez.properties = {
    bluez5.roles = [ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]
    bluez5.codecs = [ sbc sbc_xq aac ]
    bluez5.enable-sbc-xq = true
    bluez5.hfphsp-backend = "native"
  }

.. describe:: bluez5.roles

   Enabled roles.

   Currently some headsets (e.g. Sony WH-1000XM3) do not work with both
   ``hsp_ag`` and ``hfp_ag`` enabled, so by default we enable only HFP.

   Supported roles:

   - ``hsp_hs`` (HSP Headset)
   - ``hsp_ag`` (HSP Audio Gateway),
   - ``hfp_hf`` (HFP Hands-Free),
   - ``hfp_ag`` (HFP Audio Gateway)
   - ``a2dp_sink`` (A2DP Audio Sink)
   - ``a2dp_source`` (A2DP Audio Source)
   - ``bap_sink`` (LE Audio Basic Audio Profile Sink)
   - ``bap_source`` (LE Audio Basic Audio Profile Source)

   :Default value: ``[ a2dp_sink a2dp_source bap_sink bap_source hfp_hf hfp_ag ]``
   :Type: array of strings

.. describe:: bluez5.codecs

   Enabled A2DP codecs.

   Supported codecs: ``sbc``, ``sbc_xq``, ``aac``, ``ldac``, ``aptx``,
   ``aptx_hd``, ``aptx_ll``, ``aptx_ll_duplex``, ``faststream``,
   ``faststream_duplex``, ``lc3plus_h3``, ``opus_05``, ``opus_05_51``,
   ``opus_05_71``, ``opus_05_duplex``, ``opus_05_pro``, ``lc3``.

   :Default value: all available codecs
   :Type: array of strings

.. describe:: bluez5.enable-msbc

   Enable mSBC codec (wideband speech codec for HFP/HSP).

   This does not work on all headsets, so it is enabled based on the hardware
   quirks database. By explicitly setting this option you can force it to be
   enabled or disabled regardless.

   :Default value: ``true``
   :Type: boolean

.. describe:: bluez5.enable-sbc-xq

   Enable SBC-XQ codec (high quality SBC codec for A2DP).

   This does not work on all headsets, so it is enabled based on the hardware
   quirks database. By explicitly setting this option you can force it to be
   enabled or disabled regardless.

   :Default value: ``true``
   :Type: boolean

.. describe:: bluez5.enable-hw-volume

   Enable hardware volume controls.

   This does not work on all headsets, so it is enabled based on the hardware
   quirks database. By explicitly setting this option you can force it to be
   enabled or disabled regardless.

   :Default value: ``true``
   :Type: boolean

.. describe:: bluez5.hfphsp-backend

   HFP/HSP backend.

   Available values: ``any``, ``none``, ``hsphfpd``, ``ofono`` or ``native``.

   :Default value: ``native``
   :Type: string

.. describe:: bluez5.hfphsp-backend-native-modem

   Modem to use for native HFP/HSP backend ModemManager support. When enabled,
   PipeWire will forward HFP commands to the specified ModemManager device.
   This corresponds to the 'Device' property of the
   ``org.freedesktop.ModemManager1.Modem`` interface. May also be ``any`` to
   use any available modem device.

   :Default value: ``none``
   :Type: string

.. describe:: bluez5.hw-offload-sco

   HFP/HSP hardware offload SCO support.

   Using this feature requires a custom WirePlumber script that handles audio
   routing in a platform-specific way. See ``tests/examples/bt-pinephone.lua``
   for an example.

   :Default value: ``false``
   :Type: boolean

.. describe:: bluez5.default.rate

   The default audio rate for the A2DP codec configuration.

   :Default value: ``48000``
   :Type: integer

.. describe:: bluez5.default.channels

   The default number of channels for the A2DP codec configuration.

   :Default value: ``2``
   :Type: integer

.. describe:: bluez5.dummy-avrcp-player

   Register dummy AVRCP player. Some devices have wrongly functioning volume or
   playback controls if this is not enabled. Disabled by default.

   :Default value: ``false``
   :Type: boolean

.. describe:: Opus Pro Audio mode settings

   .. code-block::

      bluez5.a2dp.opus.pro.channels = 3
      bluez5.a2dp.opus.pro.coupled-streams = 1
      bluez5.a2dp.opus.pro.locations = [ FL,FR,LFE ]
      bluez5.a2dp.opus.pro.max-bitrate = 600000
      bluez5.a2dp.opus.pro.frame-dms = 50
      bluez5.a2dp.opus.pro.bidi.channels = 1
      bluez5.a2dp.opus.pro.bidi.coupled-streams = 0
      bluez5.a2dp.opus.pro.bidi.locations = [ FC ]
      bluez5.a2dp.opus.pro.bidi.max-bitrate = 160000
      bluez5.a2dp.opus.pro.bidi.frame-dms = 400

   Options for the PipeWire-specific multichannel Opus codec, which can be used
   to transport audio over Bluetooth between devices running PipeWire.

MIDI Monitor Properties
-----------------------

The BlueZ MIDI monitor SPA plugin (``api.bluez5.midi.enum``) may, in the future,
support properties that can be used to configure it when it is loaded. These
properties can be set in the ``monitor.bluez-midi.properties`` section of the
WirePlumber configuration file. At the moment of writing, there are no
properties that can be set there.

In addition, the BlueZ MIDI monitor supports a list of MIDI server node names
that can be used to create Bluetooth LE MIDI service instances. These
server node names can be set in the ``monitor.bluez-midi.servers`` section of
the WirePlumber configuration file.

Example:

.. code-block::

   monitor.bluez-midi.servers = [ "bluez_midi.server" ]

.. note::

   Typical BLE MIDI instruments have one service instance, so adding more than
   one here may confuse some clients.

Rules
-----

When device and node objects are created by the BlueZ monitor, they can be
configured using rules. These rules allow matching the existing properties of
these objects and updating them with new values. This is the main way of
configuring Bluetooth device settings.

These rules can be set in the ``monitor.bluez.rules`` section of the WirePlumber
configuration file.

Example:

.. code-block::

   monitor.bluez.rules = [
     {
       matches = [
         {
           ## This matches all bluetooth devices.
           device.name = "~bluez_card.*"
         }
       ]
       actions = {
         update-props = {
           bluez5.auto-connect = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
           bluez5.hw-volume = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
           bluez5.a2dp.ldac.quality = "auto"
           bluez5.a2dp.aac.bitratemode = 0
           bluez5.a2dp.opus.pro.application = "audio"
           bluez5.a2dp.opus.pro.bidi.application = "audio"
         }
       }
     }
     {
       matches = [
         {
           ## Matches all sources.
           node.name = "~bluez_input.*"
         }
         {
           ## Matches all sinks.
           node.name = "~bluez_output.*"
         }
       ]
       actions = {
         update-props = {
           bluez5.media-source-role = "input"

           # Common node & audio adapter properties may also be set here
           node.nick              = "My Node"
           priority.driver        = 100
           priority.session       = 100
           node.pause-on-idle     = false
           resample.quality       = 4
           channelmix.normalize   = false
           channelmix.mix-lfe     = false
           session.suspend-timeout-seconds = 5
           monitor.channel-volumes = false
         }
       }
     }
   ]

Device properties
^^^^^^^^^^^^^^^^^

The following properties can be set on device objects:

.. describe:: bluez5.auto-connect

   Auto-connect device profiles on start up or when only partial profiles have
   connected. Disabled by default if the property is not specified.

   Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
   ``hsp_ag`` and ``a2dp_source``.

   :Default value: ``[]``
   :Type: array of strings

.. describe:: bluez5.hw-volume

   Enable hardware volume controls on these profiles.

   Supported values are: ``hfp_hf``, ``hsp_hs``, ``a2dp_sink``, ``hfp_ag``,
   ``hsp_ag`` and ``a2dp_source``.

   :Default value: ``[ hfp_ag hsp_ag a2dp_source ]``
   :Type: array of strings

.. describe:: bluez5.a2dp.ldac.quality

   LDAC encoding quality.

   Available values: ``auto`` (Adaptive Bitrate, default), ``hq`` (High
   Quality, 990/909kbps), ``sq`` (Standard Quality, 660/606kbps) and ``mq``
   (Mobile use Quality, 330/303kbps).

   :Default value: ``auto``
   :Type: string

.. describe:: bluez5.a2dp.aac.bitratemode

   AAC variable bitrate mode.

   Available values: 0 (cbr, default), 1-5 (quality level).

   :Default value: ``0``
   :Type: integer

.. describe:: bluez5.a2dp.opus.pro.application

   Opus Pro Audio encoding mode.

   Available values: ``audio``, ``voip``, ``lowdelay``.

   :Default value: ``audio``
   :Type: string

.. describe:: bluez5.a2dp.opus.pro.bidi.application

   Opus Pro Audio encoding mode for bidirectional audio.

   Available values: ``audio``, ``voip``, ``lowdelay``.

   :Default value: ``audio``
   :Type: string

.. describe:: device.profile

   The profile that is activated initially when the device is connected.

   Available values: ``a2dp-sink`` (default) or ``headset-head-unit``.

   :Default value: ``a2dp-sink``
   :Type: string

Node properties
^^^^^^^^^^^^^^^

The following properties can be set on node objects:

.. describe:: bluez5.media-source-role

   Media source role, ``input`` or ``playback``. This controls how a media
   source device, such as a smartphone, is used by the system. Defaults to
   ``playback``, playing the incoming stream out to speakers. Set to ``input``
   to use the smartphone as an input for apps (like a microphone).

   :Default value: ``playback``
   :Type: string

MIDI Rules
----------

Similarly to the above rules, the BlueZ MIDI monitor also supports rules that
can be used to configure MIDI nodes when they are created.

These rules can be set in the ``monitor.bluez-midi.rules`` section of the
WirePlumber configuration file.

Example:

.. code-block::

   monitor.bluez-midi.rules = [
     {
       matches = [
         {
           node.name = "~bluez_midi.*"
         }
       ]
       actions = {
         update-props = {
           node.nick = "My Node"
           priority.driver = 100
           priority.session = 100
           node.pause-on-idle = false
           session.suspend-timeout-seconds = 5
           node.latency-offset-msec = 0
         }
       }
     }
   ]

.. note::

   It is possible to also match MIDI server nodes by testing the ``node.name``
   property against the server node names that were set in the
   ``monitor.bluez-midi.servers`` section of the WirePlumber configuration file.

MIDI-specific properties
^^^^^^^^^^^^^^^^^^^^^^^^

.. describe:: node.latency-offset-msec

   Latency adjustment to apply on the node. Larger values add a
   constant latency, but reduces timing jitter caused by Bluetooth
   transport.

   :Default value: ``0``
   :Type: integer (milliseconds)
 07070100000016000081A400000000000000000000000165F8630400001F3E000000000000000000000000000000000000004C00000000wireplumber-0.5.0/docs/rst/daemon/configuration/components_and_profiles.rst   .. _config_components_and_profiles:

Components & Profiles
=====================

WirePlumber is organized in components and profiles. Components are
functional parts that provide a specific feature, while profiles are
collections of components that are loaded together to offer a certain
overall experience.

Components
----------

Components are functional parts that provide a specific feature. They can be
described by a name, a type, a feature that they provide and a set of
dependencies, required and optional.

In the configuration file, a component is described as a SPA-JSON object,
in the ``wireplumber.components`` array section, like this:

  .. code-block::

     {
        name = <component-name>
        type = <component-type>
        arguments = { <json object> }

        # Feature that this component provides
        provides = <feature>

        # List of features that must be provided before this component is loaded
        requires = [ <features> ]

        # List of features that would offer additional functionality if provided
        # but are not strictly required
        wants = [ <features> ]
     }

Name & arguments
~~~~~~~~~~~~~~~~

The name identifies the resource that this component loads. For example,
it can be a file or a shared library. Depending on the type, the component
may also accept arguments, which are passed on to the resource when it is
loaded.

Types
~~~~~

The main types of components are:

  * **script/lua**

    A Lua script, which usually contains one or more event hooks and/or
    other custom logic. This is the main type of component as WirePlumber's
    business logic is mostly written in Lua.

  * **module**

    A WirePlumber module, which is a shared library that can be loaded
    dynamically. Modules usually provide some bundled logic to be consumed by
    scripts or some integration between WirePlumber and an external service.

  * **pw-module**

    A PipeWire module, which is also a shared library that can be loaded
    dynamically, but extends the functionality of the underlying *libpipewire*
    library. Loading PipeWire modules in the WirePlumber context can be useful
    to load custom protocol extensions or to offload some funcitonality from
    the PipeWire daemon.

  * **virtual**

    Virtual components are just load targets that can be used to pull in
    other components by defining dependencies. They do not provide any
    functionality by themselves. Note that such components do not have a "name".

  * **built-in**

    These components are functional parts that are already built into the
    WirePlumber library. They provide mostly internal support elements and checks.

Features
~~~~~~~~

A "feature" is a name that we can use to refer to what is being provided
by a component. For example, the ``monitors/alsa.lua`` script provides the
``monitor.alsa`` feature. The feature name is used to refer to the component
when defining dependencies between components and also when defining profiles.

When a component loads successfully, its feature is marked as provided,
otherwise it is not. Whether a feature is provided or not can be checked at
runtime in Lua scripts using the :func:`Core.test_feature` function and in C code
using the :c:func:`wp_core_test_feature` function.

For a list of well-known features, see :ref:`config_features`.

Dependencies
~~~~~~~~~~~~

Each component can "provide" a feature. When the component is loaded, the
feature is marked as provided. Other components can either "require"
or "want" a feature.

If a component "requires" a feature, that means that this feature **must** be
provided before this component is loaded and WirePlumber will try to load the
relevant component that provides that feature if it is not already loaded
(i.e. it will pull in the component). If that other component fails to load,
hence the feature is not provided, the component that requires it will fail
to load as well.

If a component "wants" a feature, that means that this feature would be nice
to have, in the sense that it would offer additional functionality if it
was provided, but it's not strictly needed. WirePlumber will also try to load
the relevant component that provides that feature if it is not already loaded,
meaning that it will also pull in the component. However, if that other
component fails to load, the component that wants it will still be loaded
without error.

Profiles
--------

A profile is a collection of components that are loaded together to offer
a certain overall experience.

Profiles are defined in the configuration file as a SPA-JSON object,
in the ``wireplumber.profiles`` section, like this:

  .. code-block::

     <profile> = {
       <feature name> = [ required | optional | disabled ]
       ...
     }

Each feature can be marked as *required*, *optional* or *disabled*.

  * **required**: Loading this profile will pull in the component that can
    provide this feature in and if it fails to load, the profile will fail to
    load as well.
  * **optional**: Loading this profile does not pull in the component that
    can provide this feature. If any of the required components either
    *requires* or *wants* this feature, then WirePlumber will try to load it.
    If it fails to load, the error condition depends on whether this feature was
    required or wanted by the component that pulled it in.
  * **disabled**: This feature will **not** be loaded, even if it is *wanted*
    by some component. If any required component *requires* this feature, then
    the profile will fail to load.

By default, all the features provided by all the components in the
``wireplumber.components`` section are considered to be *optional*.
That means that no component will be loaded on an empty profile, since optional
components are not pulled in automatically.

If a feature is marked as *required* in a profile, then the component that
provides that feature will be pulled in, together with all its dependencies,
both required and optional.

  .. note::

     In essence, all optional features are opt-in by default. To opt out,
     you need to mark the feature as *disabled*.

Dependency chain example
------------------------

Consider the following configuration file:

  .. code-block::

      wireplumber.components = [
        {
          name = libwireplumber-module-dbus-connection, type = module
          provides = support.dbus
        }
        {
          name = libwireplumber-module-reserve-device, type = module
          provides = support.reserve-device
          requires = [ support.dbus ]
        }
        {
          name = monitors/alsa.lua, type = script/lua
          provides = monitor.alsa
          wants = [ support.reserve-device ]
        }
      ]

      wireplumber.profiles = {
        main = {
          monitor.alsa = required
        }
      }

In this example, the ``main`` profile requires the ``monitor.alsa`` feature.
This will cause the ``monitors/alsa.lua`` script to be loaded. Now, since the
``monitors/alsa.lua`` script *wants* the ``support.reserve-device`` feature,
the ``libwireplumber-module-reserve-device`` module will also be pulled in.
And since that one *requires* the ``support.dbus`` feature, the
``libwireplumber-module-dbus-connection`` module will also be pulled in.

However, on a system without D-Bus, a user may want to opt out of the
``libwireplumber-module-dbus-connection`` module. This can be done by marking
the ``support.dbus`` feature as disabled in the profile:

  .. code-block::

     wireplumber.profiles = {
        main = {
          monitor.alsa = required
          support.dbus = disabled
        }
      }

Upon doing that, the ``libwireplumber-module-dbus-connection`` module will
not be loaded, causing the ``libwireplumber-module-reserve-device`` module
to not be loaded as well, since it requires the ``support.dbus`` feature.
The ``monitors/alsa.lua`` script will still be loaded, since it only *wants*
the ``support.reserve-device`` feature.
  07070100000017000081A400000000000000000000000165F86304000022B0000000000000000000000000000000000000003E00000000wireplumber-0.5.0/docs/rst/daemon/configuration/conf_file.rst .. _config_conf_file:

The configuration file
======================

WirePlumber's configuration file is by default ``wireplumber.conf`` and resides
in one of the WirePlumber specific
:ref:`configuration file search locations <config_locations>`.

The default configuration file can be changed on the command line by passing
the ``--config-file`` or ``-c`` option:

.. code-block:: bash

   $ wireplumber --config-file=custom.conf

.. note::

   Starting with WirePlumber 0.5, this is the only file that WirePlumber reads
   to load configuration (together with its fragments - see below). In the past,
   WirePlumber also used to read Lua configuration files that were referenced
   from ``wireplumber.conf`` and all the heavy lifting was done in Lua. This is
   no longer the case, and the Lua configuration files are no longer supported.

   Note that Lua is still the scripting language for WirePlumber, but it is only
   used for actual scripting and not for configuration.

The SPA-JSON Format
-------------------

The format of this configuration file is a variant of JSON that is also
used in PipeWire configuration files (also known as SPA-JSON). The file consists
of a global JSON object that is not explicitly typed, and a list of sections
which are essentially key-value pairs of that global JSON object. Each section
is usually a JSON object, but it can also be a JSON array.

SPA-JSON is a superset of standard JSON, so any valid JSON file is also a valid
SPA-JSON file. However, it is more permissive than standard JSON. First of all,
it allows strings to be typed without quotes (``"``), and it also allows the
character ``=`` as a separator between keys and values in addition to the
standard ``:``. This can make it look similar to INI files or other custom
configuration formats that people are familiar with, which makes it easier for
users to read and edit.

Other deviations from standard JSON include allowing comments (lines starting
with ``#`` are treated as comments) and allowing the separator characters
(``:``, ``=``, ``,``) to appear in excess or abundance. That means that you can
write ``key = value`` or ``key: value`` or ``key value`` and it will be
interpreted the same way. You may also write ``[val1, val2, val3]`` or
``[val1, val2, val3, ]`` or ``[val1 val2 val3]`` and it will be interpreted
the same way. This is allowed because the SPA-JSON parser in fact ignores all
the separator characters (the real separator is the space character).

Examples of valid SPA-JSON files:

.. code-block::

    # This is the most common syntax
    section1 = {
      string-key = value1
      number-key = 123
      boolean-key = true
    }
    section2 = [
      val1, val2, val3
    ]

.. code-block::

    # Mixed syntax
    section1 {
      "string-key" = "value1"
      number-key: 123
      boolean-key true
    }
    section2 = [
      val1, val2 val3,
    ]

.. code-block::

   # Standard JSON (albeit this comment line)
   "section1": {
     "string-key": "value1",
     "number-key": 123,
     "boolean-key": true
   }
   "section2": [
     "val1", "val2", "val3"
   ]

.. _config_conf_file_fragments:

Fragments
---------

Just like PipeWire, WirePlumber supports configuration fragments. This means
that the main configuration file can be split into multiple files, and all of
them will be loaded and merged together. This is mostly useful to allow users
to customize their configuration without having to modify the main file.

When loading the configuration file, WirePlumber will also look for
additional files in the directory that has the same name as the configuration
file suffixed with ``.d`` and will load all of them as well. For example,
loading ``wireplumber.conf`` will also load any ``.conf`` files under
``wireplumber.conf.d/``. This directory is searched in all the configuration
search locations and the fragments are loaded from *all* of them, starting
from the most system-wide locations and moving towards the most user-specific
locations, in alphanumerical order within each location (see also
:ref:`config_locations_fragments`).

When a JSON object appears in multiple files, the properties of the objects are
merged together. When a JSON array appears in multiple files, the arrays are
concatenated together. When merging objects, if specific properties appear in
many of those objects, the last one to be parsed always overwrites previous
ones, unless the value is also an object or array; if it is, then the value is
recursively merged using the same rules.

Sections
--------

WirePlumber reads the following standard sections from the configuration
file:

* *wireplumber.components*

  This section is an array that lists components that can be loaded by
  WirePlumber. For more information, see :ref:`config_components_and_profiles`.

* *wireplumber.components.rules*

  This section is an array containing rules that can be used to modify entries
  of the *wireplumber.components* array. This is useful to inject changes
  to the components list without having to modify the main configuration file.

* *wireplumber.profiles*

  This section is an object that defines profiles that can be loaded by
  WirePlumber. For more information, see :ref:`config_components_and_profiles`.

* *wireplumber.settings*

  This section is an object that defines settings that can be used to
  alter WirePlumber's behavior. For more information, see :ref:`config_settings`.

* *wireplumber.settings.schema*

  This section is an object that defines the schema for the settings that
  can be listed in *wireplumber.settings*. This is used to validate the
  settings when they are modified at runtime. For more information, see
  :ref:`config_configuration_option_types`.

In addition, there are many sections that are specific to certain components,
mostly hardware monitors, such as *monitor.alsa.properties*,
*monitor.alsa.rules*, etc. These are documented further on, in the respective
sections of this documentation that describe the configuration options of
these components.

Finally, WirePlumber also reads the following sections, which are parsed
by libpipewire to configure the PipeWire context:

* *context.properties*

  Used to define properties to configure the PipeWire context and some modules.

* *context.spa-libs*

  Used to find SPA factory names. It maps a SPA factory name regular expression
  to a library name that should contain that factory. The object property names
  are the regular expressions, and the object property values are the actual
  library names:

  .. code-block::

    <factory-name regex> = <library-name>

  For example:

  .. code-block::

    context.spa-libs = {
      api.alsa.*      = alsa/libspa-alsa
      audio.convert.* = audioconvert/libspa-audioconvert
    }

  In this example, we instruct wireplumber to lookup any *api.alsa.** factory
  in the *libspa-alsa* library, and any *audio.convert.** factory
  in the *libspa-audioconvert* library.

  .. note::

     The default configuration file already contains a list of well-known
     factory names and their corresponding libraries. You should only
     need to add entries to this section if you are using custom SPA plugins.

* *context.modules*

  Used to load PipeWire modules. This does not affect the PipeWire daemon by any
  means. It exists simply to allow loading *libpipewire* modules inside
  WirePlumber. This is usually useful to load PipeWire protocol extensions,
  so that you can export custom objects to PipeWire and other clients.

  .. note::

     PipeWire modules can also be loaded as :ref:`components <config_components_and_profiles>`,
     which may be preferrable since it allows you to load them conditionally
     based on the profile and component dependencies.

  .. admonition:: Remember

     Modules listed in *context.modules* are always loaded before attempting a
     connection to the PipeWire daemon, while modules listed in
     *wireplumber.components* are always loaded after the connection is
     established. It is important to load the PipeWire protocol-native module
     and any extensions (such as module-metadata) in the *context.modules*
     section, so that the connection can be done properly.

  Each module is described by a JSON object containing the module's *name*,
  its arguments (*args*) and a combination of *flags*, which can be ``ifexists``
  and ``nofail``.

  .. code-block::

    {
      name = <module-name>
      [ args = { <key> = <value> ... } ]
      [ flags = [ [ ifexists ] [ nofail ] ]
    }

  For example:

  .. code-block::

    context.modules = [
      { name = libpipewire-module-adapter }
      {
        name = libpipewire-module-metadata,
        flags = [ ifexists ]
      }
    ]

  The above example loads both PipeWire adapter and metadata modules. The
  metadata module will be ignored if not found because of its ``ifexists`` flag.
07070100000018000081A400000000000000000000000165F8630400001819000000000000000000000000000000000000004F00000000wireplumber-0.5.0/docs/rst/daemon/configuration/configuration_option_types.rst    .. _config_configuration_option_types:

Configuration option types
==========================

As seen in the previous sections, WirePlumber can be partly configured by
enabling or disabling features, which affect which components are getting
loaded. These components, however, can be further configured to fine-tune their
behavior. This section describes the different types of configuration options
that can be used to configure WirePlumber components.

Dynamic options ("Settings")
----------------------------

Dynamic options (also simply referred to as "settings") are configuration
options that can be changed at runtime. They are typically simple values like
booleans, integers, strings, etc. and are all located under the
``wireplumber.settings`` section in the configuration file. Their purpose is to
allow the user to change simple behavioral aspects of WirePlumber.

As the name suggests, these options are dynamic and can be changed at runtime
using ``wpctl`` or the :ref:`settings_api` API. For example, setting the
``device.routes.default-sink-volume`` setting to ``0.5`` can be done like this:

.. code-block:: bash

   $ wpctl settings device.routes.default-sink-volume 0.5

Under the hood, when WirePlumber starts, the ``metadata.sm-settings`` component
(provided by ``libwireplumber-module-settings``) reads this section from the
configuration file and populates the ``sm-settings`` metadata object, which is
exported to PipeWire. In addition, it reads the ``wireplumber.settings.schema``
section and populates the ``schema-sm-settings`` metadata object, which is used
by the API to validate the settings. Any options that are missing from
``wireplumber.settings`` are also populated in ``sm-settings`` from their
default values in the schema. Then the rest of the components read their
configuration options from this metadata object via the :ref:`settings_api` API.

Most of the components that use such dynamic options make sure to listen
to changes in the metadata object so that they can immediately adapt their
behavior. Other components, however, do not react immediately and the changes
only take effect the next time the option is needed. For instance, some options
affect created objects in a way that cannot be changed after the object has been
created, so when the option is changed it applies only to new objects and not
existing ones.

Changing the settings at runtime in the ``sm-settings`` metadata object is
a non-persistent change. The changes will be lost when WirePlumber is
restarted. However, the :ref:`settings_api` API also supports saving settings
to a state file, which will be loaded again when WirePlumber starts and
override the settings from the configuration file. This is done by using yet
another metadata object called ``persistent-sm-settings``. When a setting is
changed in the ``persistent-sm-settings`` metadata object, WirePlumber
automatically saves the change to the state file and also changes the value in
the ``sm-settings`` metadata object immediately.

To make such a persistent change using ``wpctl``, the ``--save`` option can be
used. For example, to set the ``device.routes.default-sink-volume`` setting to
``0.5`` and save it to the state file:

.. code-block:: bash

   $ wpctl settings --save device.routes.default-sink-volume 0.5

With ``wpctl``, it is also possible to restore a setting to its default value
(taken from the schema), by using the ``--reset`` option. For example, to reset
the ``device.routes.default-sink-volume`` setting, the following command can be
used:

.. code-block:: bash

   $ wpctl settings --reset device.routes.default-sink-volume

In addition, the ``--delete`` option can be used to delete a setting from the
``persistent-sm-settings`` metadata object, which will also remove it from the
state file. After deleting, the value from the ``wireplumber.settings`` section
of the configuration file will be used again. For example, to delete the
``device.routes.default-sink-volume`` setting, the following command can be
used:

.. code-block:: bash

   $ wpctl settings --delete device.routes.default-sink-volume

A list of all the available settings can be found in the :ref:`config_settings`
section.

Static options
--------------

Static options are more complex configuration structures that reside only in the
configuration file and cannot be changed at runtime. They are typically used to
configure device monitors and provide rules that match objects and perform
actions such as update their properties.

While these options could also in theory be stored in the metadata object and
be made dynamic, this is not supported because these options are both complex
and therefore hard to change on the command line, but also because they are
typically used to configure objects that are created at startup and cannot be
changed later.

Static options are located in their own top-level sections. Examples of such
sections are ``monitor.alsa.properties`` and ``monitor.alsa.rules`` that are
used to configure the ``monitor.alsa`` component. The next sections of this
documentation describe in detail all the available static options.

Component arguments
~~~~~~~~~~~~~~~~~~~

Components can also be configured statically by passing arguments to them when
they are loaded. This is done by adding an ``arguments`` key to the component
description in the ``wireplumber.components`` section (see
:ref:`config_components_and_profiles`).

The arguments are mostly meant as a way to instantiate multiple instances of the
same module or script with slightly different configuration to create a new
unique component. For example, the ``metadata.lua`` script can be instantiated
multiple times to create multiple metadata objects, each with a different name.
The name of the metadata object is passed as an argument to the script.

While many more static options could be passed as arguments, this is not
recommended because it is not possible to override the arguments by adding
:ref:`fragment<config_conf_file_fragments>` configuration files. Therefore, it
is recommended to use component-specific top-level sections, unless the option
is not meant to be changed by the user.
   07070100000019000081A400000000000000000000000165F8630400001033000000000000000000000000000000000000003D00000000wireplumber-0.5.0/docs/rst/daemon/configuration/features.rst  .. _config_features:

Well-known features
===================

This is a list of some well-known features that can be enabled or
disabled accordingly.

There are many more features actually defined in the configuration file, and it
can be confusing to go through them. This list here is meant to be a quick
reference for the most common ones that actually make sense to be toggled in
a configuration file in order to customize WirePlumber's behavior.

For more information on what features are and how they work, refer to the
previous section: :ref:`config_components_and_profiles`.

Hardware monitors
-----------------

Audio
~~~~~

.. describe:: hardware.audio

   Enables bringing up audio hardware.

   :wants: ``monitor.alsa``, ``monitor.alsa-midi``

.. describe:: monitor.alsa

   Enables the ALSA device monitor.

   :wants: ``monitor.alsa.reserve-device``

.. describe:: monitor.alsa.reserve-device

   Enables D-Bus device reservation API for ALSA devices.

   :requires: ``support.reserve-device``

.. describe:: monitor.alsa-midi

   Enables the ALSA MIDI device monitor.

Bluetooth
~~~~~~~~~

.. describe:: hardware.bluetooth

   Enables bringing up bluetooth hardware.

   :wants: ``monitor.bluez``, ``monitor.bluez-midi``

.. describe:: monitor.bluez

   Enables the BlueZ device monitor.

   :wants: ``monitor.bluez.seat-monitoring``

.. describe:: monitor.bluez.seat-monitoring

   Enables seat monitoring on the bluetooth monitor.

   When enabled, this will make sure that the bluetooth devices are only
   enabled on the active seat.

   :requires: ``support.logind``

.. describe:: monitor.bluez-midi

   Enables the BlueZ MIDI device monitor.

   :wants: ``monitor.bluez.seat-monitoring``

Video
~~~~~

.. describe:: hardware.video-capture

   Enables bringing up video capture hardware (cameras, hdmi capture devices,
   etc.)

   :wants: ``monitor.v4l2``, ``monitor.libcamera``

.. describe:: monitor.v4l2

   Enables the V4L2 device monitor.

.. describe:: monitor.libcamera

   Enables the libcamera device monitor.

Support components
------------------

.. describe:: support.dbus

   Provides a D-Bus connection to the session bus. This is needed by some other
   support features (see below) but it is generally optional. WirePlumber does
   not require a D-Bus connection to work.

   On a system where WirePlumber is configured to run system-wide (headless,
   embedded, etc), this will most likely fail to load and thus disable all the
   other support features that require it. On such systems it makes sense to
   disable this feature explicitly, to avoid the overhead of trying to connect
   to the session bus.

.. describe:: support.reserve-device

   Provides support for the
   `D-Bus device reservation API <http://git.0pointer.net/reserve.git/tree/reserve.txt>`_,
   allowing the device monitors to reserve devices for exclusive access.

   :requires: ``support.dbus``

.. describe:: support.portal-permissionstore

   Integrates with the flatpak portal permission store to give appropriate
   access permissions to flatpak applications.

   :requires: ``support.dbus``

.. describe:: support.logind

   Integrates with systemd-logind to enable specific functionality only on the
   active seat.

Policies
--------

.. describe:: policy.standard

   Enables the standard WirePlumber policy. This includes all the logic
   for enabling devices, linking streams, granting permissions to clients,
   etc, as appropriate for a desktop system.

.. describe:: policy.role-priority-system

   Enables the role priority system policy. This system creates virtual sinks
   that group streams based on their ``media.role`` property, and assigns a
   priority to each role. Depending on the priority configuration, lower
   priority roles may be corked or ducked when a higher priority role stream
   is active.

   This policy was designed for automotive and mobile systems and may not work
   as expected on desktop systems.

   Note that this policy is implemented as a superset of ``policy.standard``,
   so ``policy.standard`` should not be disabled when enabling this policy.

   :requires: ``policy.standard``
 0707010000001A000081A400000000000000000000000165F8630400001A52000000000000000000000000000000000000003E00000000wireplumber-0.5.0/docs/rst/daemon/configuration/locations.rst .. _config_locations:

Locations of files
==================

Location of configuration files
-------------------------------

WirePlumber's default locations of its configuration files are the following,
in order of priority:

 1. ``$XDG_CONFIG_HOME/wireplumber``
 2. ``$XDG_CONFIG_DIRS/wireplumber``
 3. ``$sysconfdir/wireplumber``
 4. ``$XDG_DATA_DIRS/wireplumber``
 5. ``$datadir/wireplumber``

Notes:

 * ``$syscondir`` and ``$datadir`` refer to
   `meson's directory options <https://mesonbuild.com/Builtin-options.html#directories>`_
   and are hardcoded at build time
 * ``$XDG_`` variables refer to the
   `XDG Base Directory Specification <https://specifications.freedesktop.org/basedir-spec/latest/index.html>`_

It is recommended that user specific overrides are placed in
``$XDG_CONFIG_HOME/wireplumber``, while host-specific configuration is placed in
``$XDG_CONFIG_DIRS/wireplumber`` or ``$sysconfdir/wireplumber`` and
distribution-provided configuration is placed in ``$XDG_DATA_DIRS/wireplumber``
or ``$datadir/wireplumber``.

At runtime, WirePlumber will seek out the directory with the highest priority
that contains the required configuration file. This setup allows a user or
system administrator to effortlessly override the configuration files provided
by the distribution. They can achieve this by placing a file with an identical
name in a higher priority directory.

It is also possible to override the configuration directory by setting the
``WIREPLUMBER_CONFIG_DIR`` environment variable:

.. code-block:: bash

   WIREPLUMBER_CONFIG_DIR=src/config wireplumber

``WIREPLUMBER_CONFIG_DIR`` supports listing multiple directories, using the
standard path list separator ``:``. If multiple directories are specified,
the first one has the highest priority and the last one has the lowest.

.. note::

   When the configuration directory is overriden with
   ``WIREPLUMBER_CONFIG_DIR``, the default locations are ignored and
   configuration files are *only* looked up in the directories specified by this
   variable.

.. _config_locations_fragments:

Configuration fragments
^^^^^^^^^^^^^^^^^^^^^^^

WirePlumber also supports configuration fragments. These are configuration files
that are loaded in addition to the main configuration file, allowing to
override or extend the configuration without having to copy the whole file.
See also the :ref:`config_conf_file_fragments` section for semantics.

Configuration fragments are always loaded from subdirectories of the main search
directories that have the same name as the configuration file, with the ``.d``
suffix appended. For example, if WirePlumber loads ``wireplumber.conf``, it will
also load ``wireplumber.conf.d/*.conf``. Note also that the fragment files need
to have the ``.conf`` suffix.

When WirePlumber loads a configuration file from the default locations, it will
also load all configuration fragments that are present in all of the default
locations, but following the reverse order of priority. This allows
configuration fragments that are installed in more system-wide locations to be
overriden by the system administrator or the users.

For example, assuming WirePlumber loads ``wireplumber.conf``, from any of the
search locations, it will also locate and load the following fragments, in this
order:

 1. ``$datadir/wireplumber/wireplumber.conf.d/*.conf``
 2. ``$XDG_DATA_DIRS/wireplumber/wireplumber.conf.d/*.conf``
 3. ``$sysconfdir/wireplumber/wireplumber.conf.d/*.conf``
 4. ``$XDG_CONFIG_DIRS/wireplumber/wireplumber.conf.d/*.conf``
 5. ``$XDG_CONFIG_HOME/wireplumber/wireplumber.conf.d/*.conf``

Within each search location that contains fragments, the individual fragment
files are opened in alphanumerical order. This can be important to know, because
the parsing order matters in merging. See :ref:`config_conf_file_fragments`

.. note::

   When ``WIREPLUMBER_CONFIG_DIR`` is set, the default locations are ignored and
   fragment files are *only* looked up in the directories specified by this
   variable.

Location of scripts
-------------------

WirePlumber's default locations of its data files are the following,
in order of priority:

 1. ``$XDG_DATA_HOME/wireplumber``
 2. ``$XDG_DATA_DIRS/wireplumber``
 3. ``$datadir/wireplumber``

At runtime, WirePlumber will search the directories for the highest-priority
directory to contain the needed data file.

Scripts are a specific kind of "data" files and are expected to be located
within a ``scripts`` subdirectory in the above data search locations. The "data"
directory is a somewhat more generic path that may be used for other kinds of
data files in the future.

It is also possible to override the data directory by setting the
``WIREPLUMBER_DATA_DIR`` environment variable:

.. code-block:: bash

   WIREPLUMBER_DATA_DIR=src wireplumber

As with the default data directories, script files in particular are expected
to be located within a ``scripts`` subdirectory, so in the above example the
scripts would actually reside in ``src/scripts``.

``WIREPLUMBER_DATA_DIR`` supports listing multiple directories, using the
standard path list separator ``:``. If multiple directories are specified,
the first one has the highest priority and the last one has the lowest.

.. note::

   When ``WIREPLUMBER_DATA_DIR`` is set, the default locations are ignored and
   scripts are *only* looked up in the directories specified by this variable.

Location of modules
-------------------

WirePlumber modules
^^^^^^^^^^^^^^^^^^^

WirePlumber's default location of its modules is
``$libdir/wireplumber-$api_version``, where ``$libdir`` is set at compile time
by the build system. Typically, it ends up being ``/usr/lib/wireplumber-0.5``
(or ``/usr/lib/<arch-triplet>/wireplumber-0.5`` on multiarch systems)

It is possible to override this directory at runtime by setting the
``WIREPLUMBER_MODULE_DIR`` environment variable:

.. code-block:: bash

   WIREPLUMBER_MODULE_DIR=build/modules wireplumber

``WIREPLUMBER_MODULE_DIR`` supports listing multiple directories, using the
standard path list separator ``:``. If multiple directories are specified, the
first one has the highest priority and the last one has the lowest.

.. note::

   When ``WIREPLUMBER_MODULE_DIR`` is set, the default locations are ignored and
   scripts are *only* looked up in the directories specified by this variable.

PipeWire and SPA modules
^^^^^^^^^^^^^^^^^^^^^^^^

PipeWire and SPA modules are not loaded from the same location as WirePlumber's
modules. They are loaded from the location that PipeWire loads them.

It is also possible to override these locations by using environment variables:
``SPA_PLUGIN_DIR`` and ``PIPEWIRE_MODULE_DIR``. For more details, refer to
PipeWire's documentation.
  0707010000001B000081A400000000000000000000000165F8630400003A85000000000000000000000000000000000000003900000000wireplumber-0.5.0/docs/rst/daemon/configuration/main.rst  .. _config_main:

Main configuration file
=======================

The main configuration file is by default called ``wireplumber.conf``. This can
be changed on the command line by passing the ``--config-file`` or ``-c`` option::

  wireplumber --config-file=bluetooth.conf

The ``--config-file`` option is useful to run multiple instances of wireplumber
that do separate tasks each. For more information on this subject, see the
:ref:`Multiple Instances <config_multi_instance>` section.

The format of this configuration file is the variant of JSON that is also
used in PipeWire configuration files. Note that this is subject to change
in the future.

All sections are essentially JSON objects. Lines starting with *#* are treated
as comments and ignored. The list of all possible section JSON objects are:

Common configs are present in the main configuration file(wireplumber.conf),
rest of the configs that can be grouped logically are grouped into separate
files and are placed under ``wireplumber.conf.d/``. More on this below.

* *context.properties*

  Used to define properties to configure the PipeWire context and some modules.

  Example::

    context.properties = {
      application.name = WirePlumber
      log.level = 2
    }

  This sets the daemon's name to *WirePlumber* and the log level to *2*, which
  only displays errors and warnings. See the
  :ref:`Debug Logging <daemon_logging>` section for more details.

* *context.spa-libs*

  Used to find spa factory names. It maps a spa factory name regular expression
  to a library name that should contain that factory. The object property names
  are the regular expression, and the object property values are the actual
  library name::

    <factory-name regex> = <library-name>

  Example::

    context.spa-libs = {
      api.alsa.*      = alsa/libspa-alsa
      audio.convert.* = audioconvert/libspa-audioconvert
    }

  In this example, we instruct wireplumber to only any *api.alsa.** factory name
  from the *libspa-alsa* library, and also any *audio.convert.** factory name
  from the *libspa-audioconvert* library.

* *context.modules*

  Used to load PipeWire modules. This does not affect the PipeWire daemon by any
  means. It exists simply to allow loading *libpipewire* modules in the PipeWire
  core that runs inside WirePlumber. This is usually useful to load PipeWire
  protocol extensions, so that you can export custom objects to PipeWire and
  other clients.

  Users can also pass key-value pairs if the specific module has arguments, and
  a combination of 2 flags: ``ifexists`` flag is given, the module is ignored when
  not found; if ``nofail`` is given, module initialization failures are ignored::

    {
      name = <module-name>
      [ args = { <key> = <value> ... } ]
      [ flags = [ [ ifexists ] [ nofail ] ]
    }

  Example::

    context.modules = [
      { name = libpipewire-module-adapter }
      {
        name = libpipewire-module-metadata,
        flags = [ ifexists ]
      }
    ]

  The above example loads both PipeWire adapter and metadata modules. The
  metadata module will be ignored if not found because of its ``ifexists`` flag.

* *wireplumber.components*

  Used to load WirePlumber components. Components can be either WirePlumber
  modules written in C or WirePlumber scripts written in Lua.

  Syntax::

    { name = <component-name>, type = <component-type>, deps = <dependent-setting>, flags = <flags> }

  * type:

  Valid component types include:

    * ``module``: A WirePlumber shared object module
    * ``script/lua``: A WirePlumber Lua script
      (all Lua Scripts implicitly requires libwireplumber-module-lua-scripting module)

  Example::

    wireplumber.components = [
      { name = libwireplumber-module-lua-scripting, type = module }
      { name = monitors/alsa.lua, type = script/lua }
    ]

  * deps: components can be loaded with a dependency on a wireplumber setting.
  * flags: ifexists & nofail flags are supported in this section as well.


    * `ifexists` - signals wireplumber to ignore if the module is not found.
    * `nofail` - signals wireplumber to ignore module initialization failures.

  More Examples::

    wireplumber.components = [
      # Load `libwireplumber-module-si-node` which is of type `module`.
      { name = libwireplumber-module-si-node , type = module }

      # Load `libwireplumber-module-reserve-device` module, only if the setting `alsa_monitor.alsa.reserve` is defined as true.
      { name = libwireplumber-module-reserve-device , type = module, deps = alsa_monitor.alsa.reserve }

      # Load `alsa.lua` which is of type `script/lua`.
      { name = monitors/alsa.lua, type = script/lua }

      # Load `alsa-midi.lua` Lua Script only if `alsa_monitor.alsa.midi` setting is defined as true.
      { name = monitors/alsa-midi.lua, type = script/lua, deps = alsa_monitor.alsa.midi }

      # Load `libwireplumber-module-logind` module if the setting `bluez-enable-logind` is true.
      { name = libwireplumber-module-logind , type = module, deps = bluez-enable-logind, flags = [ ifexists ] }
    ]

  .. note::

      - `name` & `type` keys are mandatory, while `deps` and `flags` keys are optional
      - All the components are loaded during the bootup and failure in finding them or any error during the loading process is a fatal error and WirePlumber will exit.


* *wireplumber.settings*

  All the Wireplumber configuration settings are now grouped under this
  section. They are moved away from Lua.

  All the default settings are distributed into different
  files(\*settings.conf) under ``wireplumber.conf.d\``

  All the settings are loaded into ``sm-settings`` metadata. Apart from the
  settings JSON files, Metadata interface can be used to change them.

  :ref:`WpSettings <settings_api>` provides APIs to its clients
  (modules, lua scripts etc) to access and track them.

  Settings can be persistent, more on this below.

  There can be two types of settings namely plain settings(called just settings
  for reasons of simplicity) and rules.

  * `Settings`

    Syntax::

      wireplumber.settings = {
        <setting1> = <value>
        <setting2> = <value>
        ..
      }

    Examples::

      wireplumber.settings = {
        alsa_monitor.alsa.reserve = true
        alsa_monitor.alsa.midi = "true"
        default-policy-duck.level = 0.3
      }

    Value can be string, int, float, boolean and can even be a JSON array.

    WpSettings exposes the `wp_settings_get_{string|int|float|boolean}()` APIs
    to access the values.

    Lua scripts, modules use these APIs to access settings.
    The client accessing the setting should know which API to use to access
    the setting accurately.

    If the Setting is a JSON array like `bt-policy-media-role.applications`
    _get_string() API need to be used and the obtained JSON element will have
    to be parsed using the :ref:`JSON APIs. <spa_json_api>`

    Persistent Behavior::

      wireplumber.settings = {
        persistent.settings = true
      }

    Persistent behavior can be enabled with the above syntax.

    When enabled, the settings will be read from conf file only once and for
    subsequent reboots they will be read from the state(cache) files, till the
    time the setting is set back to false in the .conf file.

    Settings can be changed through metadata, so when they are updated through
    metadata and if the user desires those settings to be persistent between
    reboots this persistent option can be used.

    wp_settings_register_{callback|closure} () API can be used by clients to
    keep track of the changes to settings.

    The persistent behavior is disabled by default.

  * `Rules`

    Rules are dynamic logic based settings.

    Syntax

    Simple Syntax::

      wireplumber.settings = {
        <rule-name> = [
          {
            matches = [
              {
                <pipewire property1> = <value>
                <pipewire property2> = <value>
              }
            ]
            actions = {
              update-props = {
                <pipewire property> = <value>,
                <wireplumber setting> = <value>,
              }
            }
          }
        ]
      }

    Simple Example::

      wireplumber.settings = {
        stream_default = [
          {
            matches = [
                # Matches all devices
                { application.name = "pw-play" }
            ]
            actions = {
              update-props = {
                state.restore-props = false
                state.restore-target = false
              }
            }
          }
        ]
      }

    Stream_default rule scans for pw-play app and if found it applies the two
    properties listed above.

    Advanced Syntax::

      # Nested behavior
      wireplumber.settings = {
        <rule-name> = [
          {
            matches = [
              {
                # Logical AND behavior with the JSON object
                <pipewire property1> = <value>
                <pipewire property2> = <value>
              }

              # Logical OR behavior across the JSON objects.
              {
                <pipewire property3> = <value>
              }
            ]
            actions = {
              update-props = {
                <pipewire property> = <value>,
                <wireplumber setting> = <value>,
              }
            }
          }
        ]
      }

      # Use of regular expressions
      wireplumber.settings = {
        <rule-name> = [
          {
            matches = [
              {
                # if a value starts with ``~`` it triggers regular expression evaluation
                <pipewire property1> = <~value*>
              }
            ]
            actions = {
              update-props = {
                <pipewire property> = <value>,
                <wireplumber setting> = <value>,
              }
            }
          }
        ]
      }

      # Multiple Matches with in a single rule is possible.
      wireplumber.settings = {
        <rule-name> = [
          {
            # Match 1
            matches = [
              {
                <pipewire property1> = <~value*>
              }
            ]
            actions = {
              update-props = {
                <pipewire property1> = <value>,
              }
            }


            # Match 2
            matches = [
              {
                <pipewire property2> = <~value*>
              }
            ]
            actions = {
              update-props = {
                <pipewire property2> = <value>,
              }
            }
          }
        ]
      }

    Advanced Example::

      wireplumber.settings = {

        alsa_monitor = [
          {
            matches = [
              {
                # This matches all sound cards.
                device.name = "~alsa_card.*"
              }
            ]
            actions = {
              update-props = {
                # and applies these properties.
                api.alsa.use-acp = true
              }
            }
          }
          {
            matches = [
              # Matches either input nodes or output nodes
              {
                node.name = "~alsa_input.*"
              }
              {
                node.name = "~alsa_output.*"
              }
            ]
            actions = {
              update-props = {
                node.nick              = "My Node"
                priority.driver        = 100
                session.suspend-timeout-seconds = 5
              }
            }
          }
        ]
      }

    * wp_settings_apply_rule () is WpSettings API for rules.


  * *wireplumber.virtuals*

    Virtual session items are a way of grouping different kinds of clients or
    applications(for example Music, Voice, Navigation, Gaming etc).
    The actual grouping is done based on the `media.role` of the client
    stream node.

    Virtual session items allows for that actions to be taken up at group level
    rather than at individual stream level, which can be cumbersome.

    For example imagine the following scenarios.
      * Incoming Navigation message needs to duck the volume of
        Audio playback(all the apps playing audio).
      * Incoming voice/voip call needs to stop(cork) the Audio playback.

    Virtual session items realize this functionality with ease.

    * *Defining Virtual session items*

      Example::

        virtual-items = {
          virtual-item.capture = {
            media.class = "Audio/Source"
            role = "Capture"
          }
          virtual-item.multimedia = {
            media.class = "Audio/Sink"
            role = "Multimedia"
          }
          virtual-item.navigation = {
            media.class = "Audio/Sink"
            role = "Navigation"
          }

      This example creates 3 virtual session items, with names
      ``virtual-item.capture``, ``virtual-item.multimedia`` and
      ``virtual-item.navigation`` and assigned roles ``Capture``, ``Multimedia``
      and ``Navigation`` respectively.

      First virtual item has a media class of ``Audio/Source`` used for capture
      and rest of the virtual items have ``Audio/Sink`` media class, and so are
      only used for playback.

    * *Virtual session items config*

      Example::

        Capture = {
          alias = [ "Multimedia", "Music", "Voice", "Capture" ]
          priority = 25
          action.default = "cork"
          action.capture = "mix"
          media.class = "Audio/Source"
        }
        Multimedia = {
          alias = [ "Movie" "Music" "Game" ]
          priority = 25
          action.default = "cork"
        }
        Navigation = {
          priority = 50
          action.default = "duck"
          action.Navigation = "mix"
        }


      The above example defines actions for both ``Multimedia`` and ``Navigation``
      roles. Since the Navigation role has more priority than the Multimedia
      role, when a client connects to the Navigation virtual session item, it
      will ``duck`` the volume of all Multimedia clients. If Multiple Navigation
      clients want to play audio, their audio will be mixed.

      Possible values of actions are: ``mix`` (Mixes audio),
      ``duck`` (Mixes and lowers the audio volume) or ``cork`` (Pauses audio).

    Virtual session items are not used for desktop use cases, it is more suitable
    for embedded use cases.

* *Split Configuration files*

The Main configuration file is split into multiple files. When loading the main
JSON configuration file, WirePlumber will also look for additional files in the
same directory suffixed with ``.d`` and will load all of them as well. For
example, loading ``wireplumber.conf`` will also load any files under
``wireplumber.conf.d/``. It will load all the JSON config files there. All the
configurations are logically split into files and placed in this directory.
   0707010000001C000081A400000000000000000000000165F8630400000158000000000000000000000000000000000000003C00000000wireplumber-0.5.0/docs/rst/daemon/configuration/meson.build   # you need to add here any files you add to the toc directory as well
sphinx_files += files(
  'conf_file.rst',
  'components_and_profiles.rst',
  'configuration_option_types.rst',
  'modifying_configuration.rst',
  'features.rst',
  'settings.rst',
  'alsa.rst',
  'bluetooth.rst',
  'access.rst',
  'locations.rst',
  'multi_instance.rst',
)
0707010000001D000081A400000000000000000000000165F8630400002FF5000000000000000000000000000000000000004C00000000wireplumber-0.5.0/docs/rst/daemon/configuration/modifying_configuration.rst   .. _config_modifying_configuration:

Modifying configuration
=======================

WirePlumber is a heavily modular daemon that depends on its configuration
file to operate. If you were to start WirePlumber with an empty configuration
file, it would fail to start. This is why the default configuration file is
installed in the system-wide application data directory, which prevents it from
being modified by the user.

It is technically possible, if you wish, to copy the default configuration
file in one of the other :ref:`configuration search locations <config_locations>`
and modify it. However, this is **not recommended**, as it may lead to issues
when upgrading WirePlumber.

In the :ref:`Configuration file <config_conf_file>` section, we saw that
configuration files support fragments, which allow you to override or extend the
default configuration. This is the recommended way to modify the configuration.

Working with fragments
----------------------

The easiest way to add :ref:`fragments <config_conf_file_fragments>` to
modify the default configuration is to create a directory called
``~/.config/wireplumber/wireplumber.conf.d`` and place your fragments there.

All fragment files need to have the ``.conf`` extension and must be valid
SPA-JSON files. The fragments are loaded in alphanumerical order, so you can
control the order in which they are loaded by naming them accordingly. It is
recommended to use a numeric prefix for the file names, e.g.
``10-my-fragment.conf``, ``20-my-other-fragment.conf``, etc., so that you can
easily control the order in which they are loaded.

Customizing the loaded features
-------------------------------

As seen in the :ref:`Components & Profiles <config_components_and_profiles>`
section, the list of components that are loaded can be customized by enabling or
disabling :ref:`well-known features <config_features>` in the profile that is
in use by WirePlumber.

The default profile of WirePlumber is called ``main``, so a fragment that
enables or disables a specific feature in the default configuration should look
like this:

.. code-block::

   wireplumber.profiles = {
     main = {
       some.feature.name = disabled
       some.other.feature.name = required
     }
   }

Remember that features can be ``required``, ``optional`` or ``disabled``. See
the :ref:`Components & Profiles <config_components_and_profiles>` for details.

Modifying dynamic options ("settings")
--------------------------------------

As seen in the :ref:`Configuration option types <config_configuration_option_types>`
section, WirePlumber components can be partly configured with dynamic options
(referred to as "settings"). These settings can either be modified permanently
in the configuration file, or they can be modified at runtime using the
``wpctl`` command-line tool.

To modify a setting in the configuration file, you can use a fragment like this:

.. code-block::

   wireplumber.settings = {
     some.setting.name = value
   }

For example, setting the ``device.routes.default-sink-volume`` setting to
``0.5`` can be done like this:

.. code-block::

   wireplumber.settings = {
     device.routes.default-sink-volume = 0.5
   }

.. note::

   Since the configuration file is only read at startup, this will only take
   effect after restarting WirePlumber.

If you would prefer to change the setting at runtime, you can use ``wpctl`` as
follows:

.. code-block:: bash

   $ wpctl settings device.routes.default-sink-volume 0.5
   Updated setting 'device.routes.default-sink-volume' to: 0.5

The above command changes the setting immediately, but for the current
WirePlumber instance only. If you want the setting to be applied every time
WirePlumber is started, you may also use the ``--save`` option:

.. code-block:: bash

   $ wpctl settings --save device.routes.default-sink-volume 0.5
   Updated and saved setting 'device.routes.default-sink-volume' to: 0.5

This will save the setting persistently in WirePlumber's state storage.
Even though it is not in the configuration file, this saved value will be
applied automatically when WirePlumber is started.

.. attention::

   When a setting's value is saved, it will override the value from the
   configuration file. Changing the value in the configuration file will
   have no effect until the saved value is removed. Use the ``--delete``
   switch in ``wpctl`` to remove a saved value (see below).

With ``wpctl``, it is also possible to restore a setting to its default value
(taken from the schema), by using the ``--reset`` option. For example, to reset
the ``device.routes.default-sink-volume`` setting, the following command can be
used:

.. code-block:: bash

   $ wpctl settings --reset device.routes.default-sink-volume
   Reset setting 'device.routes.default-sink-volume' successfully
   $ wpctl settings device.routes.default-sink-volume
   Value: 0.064 (Saved: 0.5)

Note that the ``--reset`` option will only reset the setting to its default
value, but it will not remove the saved value from the state file. If you want
to remove the saved value, you can use the ``--delete`` option:

.. code-block:: bash

   $ wpctl settings --delete device.routes.default-sink-volume
   Deleted setting 'device.routes.default-sink-volume' successfully
   $ wpctl settings device.routes.default-sink-volume
   Value: 0.064

A list of all the available settings can be found in the :ref:`config_settings`
section.

Modifying static options
------------------------

Static options always live in their own section of the configuration file.
Sections can be of two types: either a JSON object or a JSON array.

When dealing with a **JSON object**, you can add or modify a key-value pair by
creating a fragment like this:

.. code-block::

   wireplumber.some-section = {
     some.option = new_value
   }

This is similar to what we have seen also above for modifying profile features
and settings (because both are JSON objects).

When dealing with a **JSON array**, any values that you define in a fragment
will be appended to the array. For example, to add a new rule to the
``monitor.alsa.rules`` array, you can create a fragment like this:

.. code-block::

   monitor.alsa.rules = [
     {
       matches = [
         {
           device.name = "~alsa_card.*"
         }
       ]
       actions = {
         update-props = {
           api.alsa.use-ucm  = false
         }
       }
     }
   ]

This will add a new rule to the ``monitor.alsa.rules`` array, which will
be evaluated **after** all other rules that were parsed before. This is where
the order in which fragments are loaded actually matters.

If you don't want to append a new rule, but rather override the entire array
with a new one, you can do so by using the ``override.`` prefix on the array
name:

.. code-block::

   override.monitor.alsa.rules = [
     {
       matches = [
         {
           device.name = "~alsa_card.*"
         }
       ]
       actions = {
         update-props = {
           api.alsa.use-ucm  = false
         }
       }
     }
   ]

This will now replace the entire ``monitor.alsa.rules`` array with this new one.

.. attention::

   If you want to remove a rule from the array, you will need to override the
   whole array with a new one that does not contain the rule you want to remove.
   There is no way to remove a specific element from an array using fragments.

Another thing worth remembering here is that this behavior of appending values
to arrays also works in arrays that are nested inside other arrays or objects.
For example, consider this fragment:

.. code-block::

   monitor.bluez.properties = {
     bluez5.codecs = [ sbc_xq aac ldac ]
   }

If this is the first time that the ``bluez5.codecs`` array is being defined, it
will be created with the given values. If it already exists, the given values
will be appended to the existing array. If you want to make sure that this
fragment will override the existing array, you need to use the ``override.``
prefix on the array name:

.. code-block::

   monitor.bluez.properties = {
     override.bluez5.codecs = [ sbc_xq aac ldac ]
   }

The ``override.`` prefix may also be used in JSON object keys, to override the
entire object with a new one. For example, to override the entire
``monitor.bluez.properties`` object, you can use a fragment like this:

.. code-block::

   override.monitor.bluez.properties = {
     bluez5.codecs = [ sbc_xq aac ldac ]
   }

Here, the entire ``monitor.bluez.properties`` object will be replaced with the
new one, and all previous key-value pairs configured will be discarded. This
also means that the ``bluez5.codecs`` array will be replaced with the new one
and does not require the ``override.`` prefix.

.. note::

   Even though WirePlumber uses PipeWire's syntax for configuration files, the
   ``override.`` prefix is a WirePlumber extension and does not work in
   PipeWire.

Working with rules
------------------

Some of the static option sections in the configuration file are used to define
rules that are evaluated by WirePlumber at runtime. These rules are typically
used to match objects and perform actions on them. For example, the
``monitor.alsa.rules`` section is used to define rules that are evaluated by
the ALSA monitor to match ALSA devices and update their properties.

The syntax of these rules is the same as the syntax of
`PipeWire's rules <https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PipeWire#rules>`_.

A rule is always a JSON object with two keys: ``matches`` and ``actions``. The
``matches`` key is used to define the conditions that need to be met for the
rule to be evaluated as true, and the ``actions`` key is used to define the
actions that are performed when the rule is evaluated as true.

The ``matches`` key is always a JSON array of objects, where each object
defines a condition that needs to be met. Each condition is a list of key-value
pairs, where the key is the name of the property that is being matched, and the
value is the value that the property needs to have. Within a condition, all
the key-value pairs are combined with a logical AND, and all the conditions in
the ``matches`` array are combined with a logical OR.

The ``actions`` key is always a JSON object, where each key-value pair defines
an action that is performed when the rule is evaluated as true. The action
name is specific to the rule and is defined by the rule's documentation, but
most frequently you will see the ``update-props`` action, which is used to
update the properties of the matched object.

For example:

.. code-block::

   some.theoretical.rules = [
     {
       matches = [
         {
           object.name = "my_object"
           object.profile.name = "my_profile"
         }
         {
           object.name = "other_object"
         }
       ]
       actions = {
         update-props = {
           object.tag = "matched_by_my_rule"
         }
       }
     }
   ]

This rule is equivalent to the following expression:

.. code-block:: python

   if (properties["object.name"] == "my_object" and properties["object.profile.name"] == "my_profile") or (properties["object.name"] == "other_object"):
        properties["object.tag"] = "matched_by_my_rule"

In the ``matches`` array, it is also possible to use regular expressions to match
property values. For example, to match all nodes with a name that starts with
``my_``, you can use the following condition:

.. code-block::

   matches = [
     {
       node.name = "~my_.*"
     }
   ]

The ``~`` character signifies that the value is a regular expression. The exact
syntax of the regular expressions is the POSIX extended regex syntax, as
described in the `regex (7)` man page.

In addition to regular expressions, you may also use the ``!`` character to
negate a condition. For example, to match all nodes with a name that does not
start with ``my_``, you can use the following condition:

.. code-block::

   matches = [
     {
       node.name = "!~my_.*"
     }
   ]

The ``!`` character can be used with or without a regular expression. For
example, to match all nodes with a name that is not equal to ``my_node``,
you can use the following condition:

.. code-block::

   matches = [
     {
       node.name = "!my_node"
     }
   ]
   0707010000001E000081A400000000000000000000000165F8630400000929000000000000000000000000000000000000004300000000wireplumber-0.5.0/docs/rst/daemon/configuration/multi_instance.rst    .. _config_multi_instance:

Running multiple instances
==========================

.. warning::

   Multi-instance mode has not been extensively tested in 0.5.0, so it is
   possible that some features are not working as expected. An update on this is
   planned for a subsequent 0.5.x release.

WirePlumber has the ability to run either as a single instance daemon or as
multiple instances, meaning that there can be multiple processes, each one
doing a different task.

The most common use case for such a setup is to separate the graph orchestration
tasks from the device monitoring and object creation ones. This can be useful
for robustness and security reasons, as it allows restarting the device monitors
or running them in different security contexts without affecting the rest of the
session management functionality.

To achieve a multi-instance setup, WirePlumber can be started multiple times
with a different :ref:`profile<config_components_and_profiles>` loaded in each
instance. This can be achieved using the ``--profile`` command line option to
select the profile to load:

.. code-block:: console

  $ wireplumber --profile=custom

When no particular profile is specified, the ``main`` profile is loaded.

Systemd integration
-------------------

To make this easier to work with, a template systemd unit is provided, which is
meant to be started with the name of the profile as a template argument:

.. code-block:: console

  $ systemctl --user disable wireplumber # disable the "main" instance

  $ systemctl --user enable wireplumber@policy
  $ systemctl --user enable wireplumber@audio
  $ systemctl --user enable wireplumber@camera
  $ systemctl --user enable wireplumber@bluetooth

.. note::

   In WirePlumber 0.4, the template argument was the name of the configuration
   file to load, since profiles did not exist. In WirePlumber 0.5, the template
   argument is the name of the profile and the configuration file is always
   ``wireplumber.conf``. To change the name of the configuration file you need
   to craft custom systemd unit files and use the ``--config-file`` command line
   option as needed.

It is obviously possible to start as many instances as desired, with manually
crafted profiles, as long as it is ensured that these instances
serve a different purpose and they do not conflict with each other.
   0707010000001F000081A400000000000000000000000165F8630400000212000000000000000000000000000000000000003D00000000wireplumber-0.5.0/docs/rst/daemon/configuration/settings.rst  .. _config_settings:

Well-known Settings
===================

This section describes the settings that can be configured on WirePlumber.

Settings can be either configured statically in the configuration file
by setting them under the ``wireplumber.settings`` section, or they can be
configured dynamically at runtime by using metadata.

For more information on what "settings" are and how they work, refer to the
previous section: :ref:`config_configuration_option_types`.

.. include:: ../../../../src/scripts/lib/SETTINGS.rst
  07070100000020000081A400000000000000000000000165F8630400000DA9000000000000000000000000000000000000003100000000wireplumber-0.5.0/docs/rst/daemon/installing.rst  .. _daemon_installing:

Installing WirePlumber
======================

Dependencies
------------

In order to compile WirePlumber you will need:

* GLib >= 2.68
* PipeWire >= 1.0
* Lua 5.3 or 5.4

Lua is optional in the sense that if it is not found in the system, a bundled
version will be built and linked statically with WirePlumber. This is controlled
by the **system-lua** meson option.

For building gobject-introspection data, you will also need
`doxygen <https://www.doxygen.nl/>`_ and **g-ir-scanner**.
The latter is usually shipped together with the gobject-introspection
development files.

For building documentation, you will also need
`Sphinx <https://pypi.org/project/Sphinx/>`_,
`sphinx-rtd-theme <https://github.com/readthedocs/sphinx_rtd_theme>`_ and
`breathe <https://pypi.org/project/breathe/>`_.
It is recommended to install those using python's **pip** package manager.

Compilation
-----------

WirePlumber uses the `meson build system <https://mesonbuild.com/>`_

To configure the project, you need to first run `meson`.
The basic syntax is shown below:

.. code:: console

   meson [--prefix=/path] [...options...] [build directory] [source directory]

Assuming you want to build in a directory called 'build' inside the source
tree, you can run:

.. code:: console

   $ meson setup --prefix=/usr build
   $ meson compile -C build

Additional options
------------------

.. option:: -Dintrospection=[enabled|disabled|auto]

  Force enable or force disable building gobject-introspection data.

  The default value is **auto**, which means that g-i will be built
  if **doxygen** and **g-ir-scanner** are found and skipped otherwise.

.. option:: -Ddocs=[enabled|disabled|auto]

  Force enable or force disable building documentation.

  The default value is **auto**, which means that documentation will be built
  if **doxygen**, **sphinx** and **breathe** are found and skipped otherwise.

.. option:: -Dmodules=[true|false]

   Build the wireplumber modules.

   The default value is **true**.

.. option:: -Ddaemon=[true|false]

   Build the session manager daemon.

   The default value is **true**.

   Requires **modules** option to be **true**.

.. option:: -Dtools=[true|false]

   Enable or disable building tools.

   The default value is **true**.

   Requires **modules** option to be **true**.

.. option:: -Dsystem-lua=[enabled|disabled|auto]

   Force using lua from the system instead of the bundled one.

   The default value is **auto**, which means that system lua will be used
   if it is found, otherwise the bundled static version will be built silently.

   Use **disabled** to force using the bundled lua.

.. option:: -Dsystemd=[enabled|disabled|auto]

   Enables installing systemd units. The default is **auto**

   **enabled** and **auto** currently mean the same thing.

.. option:: -Dsystemd-system-service=[true|false]

   Enables installing systemd system service file. The default is **false**

.. option:: -Dsystemd-user-service=[true|false]

   Enables installing systemd user service file. The default is **true**

.. option:: -Dsystemd-system-unit-dir=[path]

   Directory for system systemd units.

.. option:: -Dsystemd-user-unit-dir=[path]

   Directory for user systemd units.

Installation
------------

To install, simply run the **install** target with ninja:

.. code:: console

   $ ninja -C build install

To revert the installation, there is also an **uninstall** target:

.. code:: console

   $ ninja -C build uninstall
   07070100000021000081A400000000000000000000000165F8630400002038000000000000000000000000000000000000002E00000000wireplumber-0.5.0/docs/rst/daemon/logging.rst .. _daemon_logging:

Debug Logging
=============

WirePlumber is instrumented with log messages in its entire codebase. These
messages are categorized based on two heuristics: the log topic and the log
level.

The log topic is a string that identifies which component of the code this
message is coming from. Well-known topics include:

  - **wireplumber**: messages from the wireplumber daemon
  - **wp-***: messages from libwireplumber

   - **wp-core**: messages from *WpCore*
   - **wp-proxy**: messages from *WpProxy*
   - ... and so on ...

  - **m-***: messages from wireplumber modules

   - **m-default-profile**: messages from *libwireplumber-module-default-profile*
   - **m-default-routes**: messages from *libwireplumber-module-default-routes*
   - ... and so on ...

  - **s-***: messages from scripts

   - **s-linking**: messages from the *linking/\*.lua* scripts
   - **s-default-nodes**: messages from the *default-nodes/\*.lua* scripts
   - ... and so on ...

  - **pw.***: messages from libpipewire
  - **spa.***: messages from spa plugins
  - **mod.***: messages from libpipewire modules
  - **conn.***: messages to debug the pipewire socket connection

The log level is a value that designates the importance of the message.
The levels that exist in WirePlumber are the following:

  - ``F``: *Fatal errors*. These messages represent situations where execution
    of the program cannot continue. In the extremely unlikely case that
    they appear, these messages also cause the process to be terminated.
  - ``E``: *Critical warnings* (or "errors" in the PipeWire terminology).
    These messages represent situations where something unexpected has happened
    and someone with understanding of the code should probably take a look at it.
    These situations are usually programming mistakes or omissions.
    This does not necessarily mean that the program is not functioning correctly.
    It may mean, though, that the specific part of the program that logged the
    message (the plugin, subsystem, ...) may not work optimally.
  - ``W``: *Warnings*. These messages represent situations where something has
    gone unintentionally wrong, but it was not totally unexpected. The situation
    is recovered and the program can continue. In many cases, this warning may
    mean that there is something wrong with the configuration or the environment
    and may need attention from the user.
  - ``N``: *Notices*. These are important messages that the user should notice,
    like warnings, but they do not necessarily mean a bad situation.
  - ``I``: *Informational messages*. These messages provide information about
    the internal operations of the program.
  - ``D``: *Debug messages*. These messages provide details about the
    internal operations of the program, which can be useful for debugging.
  - ``T``: *Traces*. These messages provide very verbose printouts of internal
    operations and data that affects these operations. These can be useful for
    debugging as well, but it may be best to be enabled only for the topic(s)
    that are intended to be debugged, as they can be very big in volume.

By default, WirePlumber logs only messages from levels ``F``, ``E``, ``W``
and ``N``. These messages are printed on the standard error (``stderr``) stream,
or they are logged to the systemd journal, if WirePlumber was started as a
systemd service.

The ``WIREPLUMBER_DEBUG`` environment variable can be used to change which
topics and levels are enabled. The generic syntax is:

.. code::

   WIREPLUMBER_DEBUG=[<topic pattern>:]<level>,...,

This is a comma-separated list of topics to enable, paired with a level for
each topic.

``<level>`` can be one of ``FEWNIDT`` or a numerical log level as listed below.

  0. fatal errors (``F``)
  1. critical warnings (``E``)
  2. warnings and notices (``W`` & ``N``)
  3. informational messages (``I``)
  4. debug messages (``D``)
  5. trace messages (``T``)

Each level always includes messages from the previous levels, so for instance
enabling level ``3`` (or ``I``) will also enable messages from levels ``2``
and ``1`` (``N``, ``W``, ``E`` and ``F``)

``<topic pattern>`` is an *optional* description of one or more topics.
This supports
`glob style patterns <https://developer-old.gnome.org/glib/stable/glib-Glob-style-pattern-matching.html>`_
containing ``*`` and ``?``.

If a ``<topic pattern>`` is not specified, then the given ``<level>`` is
considered to be the global log level, which applies to all topics that have
no explicit level specified.

Changing log level at runtime
-----------------------------

The debug log level can be changed at runtime using ``wpctl``:

.. code::

   wpctl set-log-level D     # enable debug logging for Wireplumber
   wpctl set-log-level -     # restore default logging for Wireplumber

   wpctl set-log-level 0 4   # enable debug logging for Pipewire daemon
   wpctl set-log-level 0 -   # restore default logging for Pipewire daemon

Equivalently, it is also possible to adjust the logging by setting
``log.level`` in the ``settings`` metadata:

.. code::

   pw-metadata -n settings <ID> log.level "D"   # WirePlumber logging

   pw-metadata -n settings 0 log.level 4        # PipeWire daemon logging

Above, ``<ID>`` should be replaced by the WirePlumber daemon client ID.

Note that PipeWire daemon log levels must be specified by numbers, not
letter codes.

Examples
--------

Show *all* messages:

.. code::

   WIREPLUMBER_DEBUG=T

Show all messages up to the *debug* level (F, E, W, N, I & D), excluding *trace*:

.. code::

   WIREPLUMBER_DEBUG=D

Show all messages up to the *notice* level (F, E, W & N),
excluding *info*, *debug* & *trace*
(this is also the default when ``WIREPLUMBER_DEBUG`` is omitted):

.. code::

   WIREPLUMBER_DEBUG=2

Show all messages from the wireplumber library (including traces), but only
up to informational messages from other topics:

.. code::

   WIREPLUMBER_DEBUG=I,wp-*:T

Show debug messages from ``wp-registry``, libpipewire and all modules, keeping
all other topics up to the *notice* level.

.. code::

   WIREPLUMBER_DEBUG=2,wp-registry:4,pw.*:4,m-*:4

Relationship with the GLib log handler & G_MESSAGES_DEBUG
---------------------------------------------------------

Older versions of WirePlumber used to use ``G_MESSAGES_DEBUG`` to control their
log output, which is the environment variable that affects GLib's default
log handler.

As of WirePlumber 0.3, ``G_MESSAGES_DEBUG`` is no longer used, since
libwireplumber replaces the default log handler.

If you are writing your own application based on libwireplumber, you can choose
if you want to replace this log handler using the flags passed to
:c:func:`wp_init`.

Relationship with the PipeWire log handler & PIPEWIRE_DEBUG
-----------------------------------------------------------

libpipewire uses the ``PIPEWIRE_DEBUG`` environment variable, with a similar syntax.
WirePlumber replaces the log handler of libpipewire with its own, rendering
``PIPEWIRE_DEBUG`` useless. Instead, you should use ``WIREPLUMBER_DEBUG``.
All the log topics that apply to libpipewire and its modules / plugins work
the same in ``WIREPLUMBER_DEBUG``.

If you are writing your own application based on libwireplumber, you can choose
if you want to replace this log handler using the flags passed to
:c:func:`wp_init`.

Mapping of PipeWire debug levels to WirePlumber
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

PipeWire supports 5 levels of debug logging. WirePlumber, on the other hand,
supports 7 levels. Some levels seem common, but the terminology and the
semantics are slightly different. The following table shows how the various
levels are mapped:

=============  ===============  ========================
Numeric Level  PipeWire         WirePlumber
=============  ===============  ========================
0              no log           ``F`` - Fatal Error
1              ``E`` - Error    ``E`` - Critical Warning
2              ``W`` - Warning  ``W`` - Warning,
                                ``N`` - Notice
3              ``I`` - Info     ``I`` - Info
4              ``D`` - Debug    ``D`` - Debug
5              ``T`` - Trace    ``T`` - Trace
=============  ===============  ========================
07070100000022000081A400000000000000000000000165F86304000000C9000000000000000000000000000000000000002E00000000wireplumber-0.5.0/docs/rst/daemon/meson.build # you need to add here any files you add to the toc directory as well
sphinx_files += files(
   'installing.rst',
   'running.rst',
   'configuration.rst',
   'logging.rst',
)

subdir('configuration')
   07070100000023000081A400000000000000000000000165F863040000129A000000000000000000000000000000000000002E00000000wireplumber-0.5.0/docs/rst/daemon/running.rst .. _daemon_running:

Running the WirePlumber daemon
==============================

Systemd
-------

WirePlumber comes with a systemd unit, ``wireplumber.service``, which should
be enabled on your user session:

.. code:: console

   $ systemctl --user --now enable wireplumber

.. note::

   On non-systemd systems, you just need to ensure that wireplumber is started
   after pipewire.

Run from the PipeWire source tree
---------------------------------

PipeWire's build system comes with an option to build WirePlumber together
with PipeWire and allows executing them together without installing either of
them.

To make this work, configure PipeWire with the
``-Dsession-managers="[ 'wireplumber' ]"`` option on the meson command line.

When compiling PipeWire, the build system will now also clone and compile
WirePlumber as a subproject.

To execute the whole stack without installing, simply execute ``make run``
after compiling.

Synopsis:

.. code:: console

   $ meson -Dsession-managers="[ 'wireplumber' ]" build
   $ ninja -C build
   $ make run

Run independently or without installing
---------------------------------------

If you wish to debug WirePlumber, it may be useful to run it separately from
PipeWire or run it directly from the source tree without installing.
To do so:

  1. Ensure that neither *WirePlumber* nor *pipewire-media-session*
     are running or started together with PipeWire

     - If any of those is started by systemd,

       - Stop the relevant systemd service, ``wireplumber.service``
         or ``pipewire-media-session.service``
       - Disable that service as well if you intend to restart PipeWire
         (so that the session manager is not restarted with it)

     - If any of those is started from pipewire.conf,

       - Kill it, in order to stop it temporarily: ``killall wireplumber``
         or ``killall pipewire-media-session``
       - Comment out with ``#`` the relevant ``{ path = "..."  args = "" }``
         line from the ``context.exec`` section in ``pipewire.conf``,
         if you intend to restart PipeWire

  2. Ensure that PipeWire is running

  3. Without stopping PipeWire, run WirePlumber.

     - if it is installed, execute ``wireplumber``
     - if it is **not** installed, execute ``make run`` in the source tree,
       or use the ``wp-uninstalled.sh`` script:

       .. code:: console

          $ ./wp-uninstalled.sh wireplumber

Replacing pipewire-media-session
--------------------------------

Older versions of PipeWire used to be distributed with an example session
manager (pipewire-media-session) that you needed to disable and replace with
WirePlumber.

.. warning::

  These instructions are only relevant to older versions of PipeWire

systemd
^^^^^^^

In most cases, ``pipewire-media-session`` is started by a systemd service unit,
``pipewire-media-session.service``.

To switch to WirePlumber, you will first need to disable that service:

.. code:: console

   $ systemctl --user --now disable pipewire-media-session

... and then, enable and use ``wireplumber.service`` in its place:

.. code:: console

   $ systemctl --user --now enable wireplumber

pipewire.conf
^^^^^^^^^^^^^

On some systems, ``pipewire-media-session`` is not started by systemd, but it
is started by pipewire itself via a configuration option in ``pipewire.conf``

To switch to wireplumber, you will need to edit
**/etc/pipewire/pipewire.conf** in an existing installation or
**src/daemon/pipewire.conf.in** in the PipeWire git tree
and change the appropriate line in the ``exec`` section:

.. code:: diff

   --- /etc/pipewire/pipewire.conf.bak
   +++ /etc/pipewire/pipewire.conf
   @@ -204,7 +204,7 @@ context.exec = [
        # but it is better to start it as a systemd service.
        # Run the session manager with -h for options.
        #
   -    #{ path = "/usr/bin/pipewire-media-session"  args = "" }
   +    { path = "wireplumber"  args = "" }
        #
        # You can optionally start the pulseaudio-server here as well
        # but it is better to start it as a systemd service.

.. code:: diff

   diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in
   index bbafa134..16ef687b 100644
   --- a/src/daemon/pipewire.conf.in
   +++ b/src/daemon/pipewire.conf.in
   @@ -220,7 +220,7 @@ context.exec = [
        # but it is better to start it as a systemd service.
        # Run the session manager with -h for options.
        #
   -    @comment@{ path = "@media_session_path@"  args = "" }
   +    { path = "wireplumber"  args = "" }
        #
        # You can optionally start the pulseaudio-server here as well
        # but it is better to start it as a systemd service.

This setup assumes that WirePlumber is *installed* on the target system.
  07070100000024000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002200000000wireplumber-0.5.0/docs/rst/design 07070100000025000081A400000000000000000000000165F8630400001A95000000000000000000000000000000000000003700000000wireplumber-0.5.0/docs/rst/design/events_and_hooks.rst    .. _design_events_and_hooks:

Events and Hooks
================

Session management is all about reacting to events and taking neccessary
actions. This is why WirePlumber's logic is all built on events and hooks.

Events
------

Events are objects that represent a change that has just happened on a PipeWire
object, or just a trigger for making a decision and potentially taking some
action.

Every event has a source, a subject and some properties, which include the
event type.

* The ``source`` is a reference to the GObject that created this event.
  Typically, this is the ``WpStandardEventSource`` plugin.
* The ``subject`` is an *optional* reference to the object that this event
  is about. For example, in a ``node-added`` event, the ``subject`` would be
  a reference to the ``WpNode`` object that was just added. Some events,
  especially those which are used only to trigger actions, do not have a
  subject.
* The ``properties`` is a dictionary that contains information about the event,
  including the event type, and also includes all the PipeWire properties of the
  ``subject``, if there is one.
* The ``event.type`` property describes the nature of the event, for example
  ``node-added`` or ``metadata-changed`` are some valid event types.

Every event also has a priority. Events with a higher priority are processed
before events with a lower priority. When two or more events have the same
priority, they are processed in a first-in-first-out manner. This logic
is defined in the *event dispatcher*.

Events are short-lived objects. They are created at the time that something is
happening and they are destroyed after they get processed. Processing an event
means executing all the hooks that are associated with it. The next section
explains what hooks are and how they are associated with events.

Hooks
-----

Hooks are objects that represent a runnable action that needs to be executed
when a certain event is processed. Every hook, therefore, consists of a
function - synchronous or asynchronous - that can be executed. Additionally,
every hook has a means to associate itself with specific events. This is
normally done by declaring *interest* to specific event properties or
combinations of them.

There are two main types of hooks: ``SimpleEventHook`` and ``AsyncEventHook``.

* ``SimpleEventHook`` contains a single, synchronous function. As soon as this
  function is executed, the hook is completed.
* ``AsyncEventHook`` contains multiple functions, combined together in a state
  machine using ``WpTransition`` underneath. The hook is completed only after
  the state machine reaches its final state and this can take any amount of time
  neccessary.

Every hook also has a name, which can be an arbitrary string of characters.
Additionally, it has two arrays of names, which declare dependencies between
this hook and others. One array is called ``before`` and the other is called
``after``. The hook names in the ``before`` array specify that this hook must
be executed *before* those other hooks. Similarly, the hook names in the
``after`` array specify that this hook must be executed *after* those other
hooks. Using this mechanism, it is possible to define the order in which
hooks will be executed, for a specific event.

Hooks are long-lived objects. They are created once, registered in the
*event dispatcher*, they are attached on events and detached after their
execution. They don't maintain any internal state, so the actions of the hook
depend solely on the event itself.

The Event Dispatcher
--------------------

The event dispatcher is a (per core) singleton object that processes all events
and also maintains a list of all the registered hooks. It has a method to
*push* events on it, which causes them to be scheduled for processing.

Scheduling of events and hooks
------------------------------

The main idea and reasoning behind this architecture is to have everything
execute in a predefined order and always wait for an action to finish before
executing the next one.

Every event has a *priority* and every hook also has an order of execution that
derives from the inter-dependencies between hooks, which are defined with
``before`` and ``after`` (see above). When an event is pushed on the dispatcher,
the dispatcher goes through all the registered hooks and checks which hooks are
configured to run on this event (their event interest matches the event).
It then makes a list of them, sorted by their order of execution, and stores it
on the event. The event is then added on the dispatcher's list of events, which
is sorted by priority.

For example::

  List of events
   |  event1 (prio 99) -> hook1, hook2, hook3
   |  event2 (prio 50) -> hook5, hook2, hook4
   v

The dispatcher has an internal ``GSource`` that is registered with
``G_PRIORITY_HIGH_IDLE`` priority. When there is at least one event in the
list of events, the source is dispatched. Every time it gets dispatched,
it takes the top-most event (the highest priority one) and executes the highest
priority hook in that event. If the hook executes synchronously, it then takes
the next hook and continues until there are no more hooks on this event;
then it goes to the next event, and so on. If the hook, however, executes
asynchronously, processing stops until the hook finishes; after finishing,
processing resumes like before.

It is important to notice here that the list of events may be modified while
events are getting processed. For example, a device is added; that's a
``device-added`` event. Then a hook is executed to set the profile. That creates
nodes, so a couple of ``node-added`` events... But there is also another hook to
set the route, which was attached on the ``device-added`` event for the device.
Suppose that we give the ``node-added`` events lower priority than the
``device-added`` events, then the ``set-route`` hook will execute right after
the ``set-profile`` and before any ``node-added`` events are processed.

Visually, with sample priorities::

  List of events
   |  "device-added" (prio 20) -> set-profile, set-route
   |  "node-added" (prio 10) -> restore-stream, create-session-item
   v

Obviously, there can also be a case where a newly added event has higher
priority than the event that was being processed before. In that case,
processing the hooks of the original event is stopped until all the hooks from
the higher priority event have been processed. For example, a capture stream
node being added may trigger the "bluetooth autoswitch" hook, which will then
change the profile of a device. Changing the profile also has to trigger setting
a new route and also handling the new device nodes, creating session items for
them... After all this is done, processing the original capture stream
``node-added`` event can continue.
   07070100000026000081A400000000000000000000000165F86304000000C6000000000000000000000000000000000000002E00000000wireplumber-0.5.0/docs/rst/design/meson.build # you need to add here any files you add to the toc directory as well
sphinx_files += files(
  'understanding_session_management.rst',
  'understanding_wireplumber.rst',
  'events_and_hooks.rst',
)
  07070100000027000081A400000000000000000000000165F86304000013F6000000000000000000000000000000000000004700000000wireplumber-0.5.0/docs/rst/design/understanding_session_management.rst    .. _design_understanding_session_management:

Understanding Session Management
================================

The PipeWire session manager is a tool that is tasked to do a lot of things.
Many people understand the term "session manager" as a tool that is responsible
for managing links between nodes, but that is only one of many tasks. To
understand the entirety of its operation, we need to discuss how PipeWire works,
first.

When PipeWire starts, it loads a set of modules that are defined in its
configuration file. These modules provide functionality to PipeWire, otherwise
it is just an empty process that does nothing. Under normal circumstances,
the modules that are loaded on PipeWire's startup contain object *factories*,
plus the native protocol module that allows inter-process communication.
Other than that, PipeWire does not really load or do anything else. This is
where session management begins.

Session management is basically about setting up PipeWire to do something
useful. This is achieved by utilizing PipeWire's exposed object factories to
create some useful objects, then work with their methods to modify and later
destroy them. Such objects include devices, nodes, ports, links and others.
This is a task that requires continuous monitoring and action taking, reacting
on a large number of different events that happen as the system is being used.

High-level areas of operation
-----------------------------

The session management logic, in WirePlumber, is divided into 6 different areas
of operation:

1. Device Enablement
^^^^^^^^^^^^^^^^^^^^

Enabling devices is a fundamental area of operation. It is achieved by using
the device monitor objects (or just "monitors"), which are typically
implemented as SPA plugins in PipeWire, but they are loaded by WirePlumber.
Their task is to discover available media devices and create objects in PipeWire
that offer a way to interact with them.

Well-known monitors include:

  - The ALSA monitor, which enables audio devices
  - The ALSA MIDI monitor, which enables MIDI devices
  - The libcamera monitor, which enables cameras
  - The Video4Linux2 (V4L2) monitor, which also enables cameras, but also
    other video capture devices through the V4L2 Linux API
  - The BlueZ monitor, which enables bluetooth audio devices

2. Device Configuration
^^^^^^^^^^^^^^^^^^^^^^^

Most devices expose complex functionality, from the computer's perspective, that
needs to be managed in order to provide a simple and smooth user experience.
For that reason, for example, audio devices are organized into *profiles* and
*routes*, which allow setting them up to serve a specific use case. These
need to be configured and managed by the session manager.

3. Client Access Control
^^^^^^^^^^^^^^^^^^^^^^^^

When client applications connect to PipeWire, they need to obtain permissions
in order to be able to access the objects exposed by PipeWire and interact
with them. In some circumstances and configurations, the session manager is also
tasked with deciding which permissions should be granted to each client.

4. Node Configuration
^^^^^^^^^^^^^^^^^^^^^

Nodes are the fundamental elements of media processing. They are typically
created either by the device monitors or by client applications. When they are
created, they are in a state where they cannot be linked. Linking them requires
some configuration, such as configuring the media format and subsequently
the number and the type of ports that should be exposed. Additionally, some
properties and metadata related to the node might need to be set according to
user preferences. All of this is taken care of by the session manager.

5. Link Management
^^^^^^^^^^^^^^^^^^

When nodes are finally ready to use, the session manager is also tasked to
decide how they should be linked together in order for media to flow though.
For instance, an audio playback stream node most likely needs to be linked to
the default audio output device node. The session manager then also needs to
create all these links and monitor all conditions that may affect them so that
dynamic re-linking is possible in case something changes
(ex. if a device disconnects). In some cases, device and node configuration
may also need to change as a result of links being created or destroyed.

6. Metadata Management
^^^^^^^^^^^^^^^^^^^^^^

While in operation, PipeWire and WirePlumber both store some additional
properties about objects and their operation in storage that lives outside
these objects. These properties are referred to as "metadata" and they are
stored in "metadata objects". This metadata can be changed externally by tools
such as `pw-metadata`, but also others.

In some circumstances, this metadata needs to interact with logic inside
the session manager. Most notably, selecting the default audio and video inputs
and outputs is done by setting metadata. The session manager then needs to
validate this information, store it and restore it on the next restart, but also
ensure that the default inputs and outputs stay valid and reasonable when
devices are plugged and unplugged dynamically.
  07070100000028000081A400000000000000000000000165F86304000011FF000000000000000000000000000000000000004000000000wireplumber-0.5.0/docs/rst/design/understanding_wireplumber.rst   .. _design_understanding_wireplumber:

Understanding WirePlumber
=========================

Knowing the fundamentals of session management, let's see here how WirePlumber
is structured.

The library
-----------

WirePlumber is built on top of the `libwireplumber` library, which provides
fundamental building blocks for expressing all the session management logic.
Libwireplumber, which is written in C and based on GObject, wraps the PipeWire
API and offers a higher level and more convenient API. While the WirePlumber
daemon implements the session management logic, the underlying library can also
be utilized outside the scope of the WirePlumber daemon. This allows for the
creation of external tools and GUIs that interact with PipeWire.

Being GObject based, the library is introspectable and can be used from any
language that supports `GObject Introspection <https://gi.readthedocs.io/en/latest/>`_.
The library is also available as a C API.

The object model
^^^^^^^^^^^^^^^^

The most fundamental code contained in the WirePlumber library is the object
model, i.e. its representation of PipeWire's objects.

PipeWire exposes several objects, such nodes and ports, via the IPC protocol
in a manner that is hard to interact with using standard object-oriented
principles, because it is asynchronous. For example, when an object is created,
its existence is announced over the protocol, but its properties are announced
later, on a secondary message. If something needs to react on this object
creation event, it typically needs to access the object's properties, so it
must wait until the properties have been sent. Doing this might sound simple,
and it is, but it becomes a tedious repetitive process to be doing this
everywhere instead of focusing on writing the actual event handling logic.

WirePlumber's library solves this by creating proxy objects that cache all the
information and updates received from PipeWire throughout each object's lifetime.
Then, it makes them available via the :ref:`WpObjectManager <obj_manager_api>`
API, which has the ability to wait until certain information (ex, the
properties) has been cached on each object before announcing it.

Session management utilities
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The library also provides a set of utilities that are useful for session
management. For example, it provides the :ref:`WpSessionItem <session_item_api>`
class that can be used to abstract a part of the graph with some logic attached
to it. It also provides the :ref:`Events & Hooks API <design_events_and_hooks>`,
which is a way to express event handling logic in a declarative way.

Misc utilities
^^^^^^^^^^^^^^

The library also provides a set of miscellaneous utilities that bridge the
PipeWire API with the GObject API, such as :ref:`WpProperties <properties_api>`,
:ref:`WpSpaPod <spa_pod_api>`, :ref:`WpSpaJson <spa_json_api>` and others.
These complement the object model and make it easier to interact with PipeWire
objects.

The daemon
----------

The WirePlumber daemon is implemented on top of the library API and its job is
to host components that implement the session management logic. By itself, it
doesn't do anything except load and activate these components. The actual logic
is implemented inside them.

Modular design ensures that it is possible to swap the implementation of
specific functionality without having to re-implement the rest of it, allowing
flexibility on target-sensitive parts, such as policy management and
making use of non-standard hardware.

There are several kinds of components, each with a different purpose. The two
main kinds are `modules` and `Lua scripts`. See also
:ref:`config_components_and_profiles`.

Modules
^^^^^^^

Modules extend the libwireplumber API for specific purposes, usually to provide
support code or to allow WirePlumber to communicate with external services (ex
D-Bus APIs). They either expose their own APIs via GObject signals (i.e. dynamic
APIs that do not require linking at compile time) or implement certain objects
through interfaces. They are written in C.

Lua scripts
^^^^^^^^^^^

Lua scripts implement most of the session management. The libwireplumber API is
made available in Lua with idiomatic bindings that make it very easy to write
session management logic.

Lua has been chosen because it is a very lightweight scripting language that is
suitable for embedding. It is also very easy to learn and use, as well as bind
it to C code. However, WirePlumber can be easily extended to support scripting
languages other than Lua. The entire Lua scripting system is implemented as a
module.
 07070100000029000081A400000000000000000000000165F8630400000383000000000000000000000000000000000000002500000000wireplumber-0.5.0/docs/rst/index.rst  .. include:: ../../README.rst

Table of Contents
=================

.. toctree::
   :maxdepth: 2
   :caption: The WirePlumber Daemon

   daemon/installing.rst
   daemon/running.rst
   daemon/configuration.rst
   daemon/logging.rst

.. toctree::
   :maxdepth: 2
   :caption: WirePlumber's Design

   design/understanding_session_management.rst
   design/understanding_wireplumber.rst
   design/events_and_hooks.rst

.. toctree::
   :maxdepth: 2
   :caption: WirePlumber's Policies

   policies/linking.rst
   policies/smart_filters.rst

.. toctree::
   :maxdepth: 2
   :caption: The WirePlumber Library

   library/c_api.rst

.. toctree::
   :maxdepth: 2
   :caption: Scripting

   scripting/lua_api.rst
   scripting/existing_scripts.rst

.. toctree::
   :maxdepth: 2
   :caption: Resources

   resources/contributing.rst
   resources/testing.rst
   resources/community.rst
   resources/releases.rst
 0707010000002A000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002300000000wireplumber-0.5.0/docs/rst/library    0707010000002B000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002900000000wireplumber-0.5.0/docs/rst/library/c_api  0707010000002C000081A400000000000000000000000165F86304000003D1000000000000000000000000000000000000002D00000000wireplumber-0.5.0/docs/rst/library/c_api.rst  .. _library_c_api:

C API Documentation
===================

.. toctree::
   :maxdepth: 1

   c_api/wp_api.rst
   c_api/core_api.rst
   c_api/object_api.rst
   c_api/obj_manager_api.rst
   c_api/obj_interest_api.rst
   c_api/iterator_api.rst
   c_api/transitions_api.rst
   c_api/wperror_api.rst
   c_api/log_api.rst
   c_api/proxy_api.rst
   c_api/pipewire_object_api.rst
   c_api/global_proxy_api.rst
   c_api/node_api.rst
   c_api/port_api.rst
   c_api/link_api.rst
   c_api/device_api.rst
   c_api/client_api.rst
   c_api/metadata_api.rst
   c_api/spa_device_api.rst
   c_api/impl_node_api.rst
   c_api/impl_module_api.rst
   c_api/properties_api.rst
   c_api/spa_type_api.rst
   c_api/spa_json_api.rst
   c_api/spa_pod_api.rst
   c_api/plugin_api.rst
   c_api/component_loader_api.rst
   c_api/settings_api.rst
   c_api/conf_api.rst
   c_api/session_item_api.rst
   c_api/si_interfaces_api.rst
   c_api/si_factory_api.rst
   c_api/state_api.rst
   c_api/base_dirs_api.rst
   0707010000002D000081A400000000000000000000000165F863040000007D000000000000000000000000000000000000003B00000000wireplumber-0.5.0/docs/rst/library/c_api/base_dirs_api.rst    .. _base_dirs_api:

Base Directories File Lookup
============================
.. doxygengroup:: wpbasedirs
   :content-only:
   0707010000002E000081A400000000000000000000000165F863040000018D000000000000000000000000000000000000003800000000wireplumber-0.5.0/docs/rst/library/c_api/client_api.rst   .. _client_api:

PipeWire Client
===============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpProxy;
      WpProxy -> WpGlobalProxy;
      WpGlobalProxy -> WpClient;
      GInterface -> WpPipewireObject;
      WpPipewireObject -> WpClient;
   }

.. doxygenstruct:: WpClient

.. doxygengroup:: wpclient
   :content-only:
   0707010000002F000081A400000000000000000000000165F8630400000141000000000000000000000000000000000000004200000000wireplumber-0.5.0/docs/rst/library/c_api/component_loader_api.rst .. _component_loader_api:

Component Loader
================
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GInterface -> WpComponentLoader;
   }

.. doxygenstruct:: WpComponentLoader

.. doxygenstruct:: _WpComponentLoaderInterface

.. doxygengroup:: wpcomponentloader
   :content-only:
   07070100000030000081A400000000000000000000000165F86304000000E9000000000000000000000000000000000000003600000000wireplumber-0.5.0/docs/rst/library/c_api/conf_api.rst .. _conf_api:

Static Configuration
====================
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpConf;
   }

.. doxygenstruct:: WpConf

.. doxygengroup:: wpconf
   :content-only:
   07070100000031000081A400000000000000000000000165F86304000000E5000000000000000000000000000000000000003600000000wireplumber-0.5.0/docs/rst/library/c_api/core_api.rst .. _core_api:

Core
====
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpCore;
   }

.. doxygenstruct:: WpCore

.. doxygengroup:: wpcore
   :content-only:
   07070100000032000081A400000000000000000000000165F863040000018D000000000000000000000000000000000000003800000000wireplumber-0.5.0/docs/rst/library/c_api/device_api.rst   .. _device_api:

PipeWire Device
===============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpProxy;
      WpProxy -> WpGlobalProxy;
      WpGlobalProxy -> WpDevice;
      GInterface -> WpPipewireObject;
      WpPipewireObject -> WpDevice;
   }

.. doxygenstruct:: WpDevice

.. doxygengroup:: wpdevice
   :content-only:
   07070100000033000081A400000000000000000000000165F8630400000174000000000000000000000000000000000000003E00000000wireplumber-0.5.0/docs/rst/library/c_api/global_proxy_api.rst .. _global_proxy_api:

PipeWire Global Object Proxy
============================
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpProxy;
      WpProxy -> WpGlobalProxy;
   }

.. doxygenstruct:: WpGlobalProxy

.. doxygenstruct:: _WpGlobalProxyClass

.. doxygengroup:: wpglobalproxy
   :content-only:
07070100000034000081A400000000000000000000000165F8630400000102000000000000000000000000000000000000003D00000000wireplumber-0.5.0/docs/rst/library/c_api/impl_module_api.rst  .. _impl_module_api:

Local Modules
=============

.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpImplModule;
   }

.. doxygenstruct:: WpImplModule
   :members:

.. doxygengroup:: wpimplmodule
   :content-only:
  07070100000035000081A400000000000000000000000165F8630400000177000000000000000000000000000000000000003B00000000wireplumber-0.5.0/docs/rst/library/c_api/impl_node_api.rst    .. _impl_node_api:

Local Nodes
===========
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpProxy;
      WpProxy -> WpImplNode;
      GInterface -> WpPipewireObject;
      WpPipewireObject -> WpImplNode;
   }

.. doxygenstruct:: WpImplNode
   :members:

.. doxygengroup:: wpimplnode
   :content-only:
 07070100000036000081A400000000000000000000000165F8630400000107000000000000000000000000000000000000003A00000000wireplumber-0.5.0/docs/rst/library/c_api/iterator_api.rst .. _iterator_api:

Iterator
========
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GBoxed -> WpIterator;
   }

.. doxygenstruct:: WpIterator

.. doxygenstruct:: _WpIteratorMethods

.. doxygengroup:: wpiterator
   :content-only:
 07070100000037000081A400000000000000000000000165F863040000017F000000000000000000000000000000000000003600000000wireplumber-0.5.0/docs/rst/library/c_api/link_api.rst .. _link_api:

PipeWire Link
=============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpProxy;
      WpProxy -> WpGlobalProxy;
      WpGlobalProxy -> WpLink;
      GInterface -> WpPipewireObject;
      WpPipewireObject -> WpLink;
   }

.. doxygenstruct:: WpLink

.. doxygengroup:: wplink
   :content-only:
 07070100000038000081A400000000000000000000000165F8630400000054000000000000000000000000000000000000003500000000wireplumber-0.5.0/docs/rst/library/c_api/log_api.rst  .. _log_api:

Debug Logging
=============
.. doxygengroup:: wplog
   :content-only:
07070100000039000081A400000000000000000000000165F8630400000337000000000000000000000000000000000000003500000000wireplumber-0.5.0/docs/rst/library/c_api/meson.build  # you need to add here any files you add to the api directory as well
sphinx_files += files(
  'base_dirs_api.rst',
  'client_api.rst',
  'component_loader_api.rst',
  'conf_api.rst',
  'core_api.rst',
  'device_api.rst',
  'global_proxy_api.rst',
  'impl_module_api.rst',
  'impl_node_api.rst',
  'iterator_api.rst',
  'link_api.rst',
  'log_api.rst',
  'metadata_api.rst',
  'node_api.rst',
  'obj_interest_api.rst',
  'obj_manager_api.rst',
  'object_api.rst',
  'pipewire_object_api.rst',
  'plugin_api.rst',
  'port_api.rst',
  'properties_api.rst',
  'proxy_api.rst',
  'session_item_api.rst',
  'settings_api.rst',
  'si_factory_api.rst',
  'si_interfaces_api.rst',
  'spa_device_api.rst',
  'spa_pod_api.rst',
  'spa_type_api.rst',
  'state_api.rst',
  'transitions_api.rst',
  'wp_api.rst',
  'wperror_api.rst',
)
 0707010000003A000081A400000000000000000000000165F86304000001D7000000000000000000000000000000000000003A00000000wireplumber-0.5.0/docs/rst/library/c_api/metadata_api.rst .. _metadata_api:

PipeWire Metadata
=================
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GBoxed -> WpMetadataItem
      GObject -> WpObject;
      WpObject -> WpProxy;
      WpProxy -> WpGlobalProxy;
      WpGlobalProxy -> WpMetadata;
      WpMetadata-> WpImplMetadata;
   }

.. doxygenstruct:: WpMetadataItem

.. doxygenstruct:: WpMetadata

.. doxygenstruct:: WpImplMetadata

.. doxygengroup:: wpmetadata
   :content-only:
 0707010000003B000081A400000000000000000000000165F863040000017F000000000000000000000000000000000000003600000000wireplumber-0.5.0/docs/rst/library/c_api/node_api.rst .. _node_api:

PipeWire Node
=============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpProxy;
      WpProxy -> WpGlobalProxy;
      WpGlobalProxy -> WpNode;
      GInterface -> WpPipewireObject;
      WpPipewireObject -> WpNode;
   }

.. doxygenstruct:: WpNode

.. doxygengroup:: wpnode
   :content-only:
 0707010000003C000081A400000000000000000000000165F8630400000104000000000000000000000000000000000000003E00000000wireplumber-0.5.0/docs/rst/library/c_api/obj_interest_api.rst .. _obj_interest_api:

Object Interest
===============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GBoxed -> WpObjectInterest;
   }

.. doxygenstruct:: WpObjectInterest

.. doxygengroup:: wpobjectinterest
   :content-only:
0707010000003D000081A400000000000000000000000165F86304000000FF000000000000000000000000000000000000003D00000000wireplumber-0.5.0/docs/rst/library/c_api/obj_manager_api.rst  .. _obj_manager_api:

Object Manager
==============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObjectManager;
   }

.. doxygenstruct:: WpObjectManager

.. doxygengroup:: wpobjectmanager
   :content-only:
 0707010000003E000081A400000000000000000000000165F86304000000FC000000000000000000000000000000000000003800000000wireplumber-0.5.0/docs/rst/library/c_api/object_api.rst   .. _object_api:

WpObject
========
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
   }

.. doxygenstruct:: WpObject

.. doxygenstruct:: _WpObjectClass

.. doxygengroup:: wpobject
   :content-only:
0707010000003F000081A400000000000000000000000165F863040000013A000000000000000000000000000000000000004100000000wireplumber-0.5.0/docs/rst/library/c_api/pipewire_object_api.rst  .. _pipewire_object_api:

PipeWire Object
===============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GInterface -> WpPipewireObject;
   }

.. doxygenstruct:: WpPipewireObject

.. doxygenstruct:: _WpPipewireObjectInterface

.. doxygengroup:: wppipewireobject
   :content-only:
  07070100000040000081A400000000000000000000000165F8630400000116000000000000000000000000000000000000003800000000wireplumber-0.5.0/docs/rst/library/c_api/plugin_api.rst   .. _plugin_api:

Plugins
=======
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpPlugin;
   }

.. doxygenstruct:: WpPlugin

.. doxygenstruct:: _WpPluginClass

.. doxygengroup:: wpplugin
   :content-only:
  07070100000041000081A400000000000000000000000165F863040000017F000000000000000000000000000000000000003600000000wireplumber-0.5.0/docs/rst/library/c_api/port_api.rst .. _port_api:

PipeWire Port
=============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpProxy;
      WpProxy -> WpGlobalProxy;
      WpGlobalProxy -> WpPort;
      GInterface -> WpPipewireObject;
      WpPipewireObject -> WpPort;
   }

.. doxygenstruct:: WpPort

.. doxygengroup:: wpport
   :content-only:
 07070100000042000081A400000000000000000000000165F8630400000102000000000000000000000000000000000000003C00000000wireplumber-0.5.0/docs/rst/library/c_api/properties_api.rst   .. _properties_api:

Properties Dictionary
=====================
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GBoxed -> WpProperties;
   }

.. doxygenstruct:: WpProperties

.. doxygengroup:: wpproperties
   :content-only:
  07070100000043000081A400000000000000000000000165F863040000011F000000000000000000000000000000000000003700000000wireplumber-0.5.0/docs/rst/library/c_api/proxy_api.rst    .. _proxy_api:

PipeWire Proxy
==============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpProxy;
   }

.. doxygenstruct:: WpProxy

.. doxygenstruct:: _WpProxyClass

.. doxygengroup:: wpproxy
   :content-only:
 07070100000044000081A400000000000000000000000165F863040000013C000000000000000000000000000000000000003E00000000wireplumber-0.5.0/docs/rst/library/c_api/session_item_api.rst .. _session_item_api:

Session Items
=============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpSessionItem;
   }

.. doxygenstruct:: WpSessionItem

.. doxygenstruct:: _WpSessionItemClass

.. doxygengroup:: wpsessionitem
   :content-only:
07070100000045000081A400000000000000000000000165F8630400000183000000000000000000000000000000000000003A00000000wireplumber-0.5.0/docs/rst/library/c_api/settings_api.rst .. _settings_api:

Settings
========
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GBoxed -> WpSettingsSpec;
      GBoxed -> WpSettingsItem;
      GObject -> WpObject;
      WpObject -> WpSettings;
   }

.. doxygenstruct:: WpSettingsSpec

.. doxygenstruct:: WpSettingsItem

.. doxygenstruct:: WpSettings

.. doxygengroup:: wpsettings
   :content-only:
 07070100000046000081A400000000000000000000000165F8630400000126000000000000000000000000000000000000003C00000000wireplumber-0.5.0/docs/rst/library/c_api/si_factory_api.rst   .. _si_factory_api:

Session Items Factory
=====================
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpSiFactory;
   }

.. doxygenstruct:: WpSiFactory

.. doxygenstruct:: _WpSiFactoryClass

.. doxygengroup:: wpsifactory
   :content-only:
  07070100000047000081A400000000000000000000000165F8630400000280000000000000000000000000000000000000003F00000000wireplumber-0.5.0/docs/rst/library/c_api/si_interfaces_api.rst    .. _si_interfaces_api:

Session Items Interfaces
========================
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GInterface -> WpSiAdapter;
      GInterface -> WpSiLinkable;
      GInterface -> WpSiLink;
      GInterface -> WpSiAcquisition;
   }

.. doxygenstruct:: WpSiAdapter

.. doxygenstruct:: _WpSiAdapterInterface

.. doxygenstruct:: WpSiLinkable

.. doxygenstruct:: _WpSiLinkableInterface

.. doxygenstruct:: WpSiLink

.. doxygenstruct:: _WpSiLinkInterface

.. doxygenstruct:: WpSiAcquisition

.. doxygenstruct:: _WpSiAcquisitionInterface

.. doxygengroup:: wpsiinterfaces
   :content-only:
07070100000048000081A400000000000000000000000165F8630400000120000000000000000000000000000000000000003C00000000wireplumber-0.5.0/docs/rst/library/c_api/spa_device_api.rst   .. _spa_device_api:

Spa Device
==========
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpObject;
      WpObject -> WpProxy;
      WpProxy -> WpSpaDevice;
   }

.. doxygenstruct:: WpSpaDevice

.. doxygengroup:: wpspadevice
   :content-only:
07070100000049000081A400000000000000000000000165F8630400000169000000000000000000000000000000000000003A00000000wireplumber-0.5.0/docs/rst/library/c_api/spa_json_api.rst .. _spa_json_api:

Spa Json
========
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GBoxed -> WpSpaJson;
      GBoxed -> WpSpaJsonBuilder;
      GBoxed -> WpSpaJsonParser;
   }

.. doxygenstruct:: WpSpaJson

.. doxygenstruct:: WpSpaJsonBuilder

.. doxygenstruct:: WpSpaJsonParser

.. doxygengroup:: wpspajson
   :content-only:
   0707010000004A000081A400000000000000000000000165F8630400000181000000000000000000000000000000000000003900000000wireplumber-0.5.0/docs/rst/library/c_api/spa_pod_api.rst  .. _spa_pod_api:

Spa Pod (Plain Old Data)
========================
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GBoxed -> WpSpaPod;
      GBoxed -> WpSpaPodBuilder;
      GBoxed -> WpSpaPodParser;
   }

.. doxygenstruct:: WpSpaPod

.. doxygenstruct:: WpSpaPodBuilder

.. doxygenstruct:: WpSpaPodParser

.. doxygengroup:: wpspapod
   :content-only:
   0707010000004B000081A400000000000000000000000165F8630400000059000000000000000000000000000000000000003A00000000wireplumber-0.5.0/docs/rst/library/c_api/spa_type_api.rst .. _spa_type_api:

Spa Type Information
====================
.. doxygengroup:: wpspatype
   0707010000004C000081A400000000000000000000000165F86304000000DF000000000000000000000000000000000000003700000000wireplumber-0.5.0/docs/rst/library/c_api/state_api.rst    .. _state_api:

State Storage
=============
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpState;
   }

.. doxygenstruct:: WpState

.. doxygengroup:: wpstate
   :content-only:
 0707010000004D000081A400000000000000000000000165F8630400000207000000000000000000000000000000000000003D00000000wireplumber-0.5.0/docs/rst/library/c_api/transitions_api.rst  .. _transitions_api:

Transitions
===========
.. graphviz::
  :align: center

   digraph inheritance {
      rankdir=LR;
      GObject -> WpTransition;
      GInterface -> GAsyncResult;
      GAsyncResult -> WpTransition;
      WpTransition -> WpFeatureActivationTransition;
   }

.. doxygenstruct:: WpTransition

.. doxygenstruct:: _WpTransitionClass

.. doxygengroup:: wptransition
   :content-only:

.. doxygenstruct:: WpFeatureActivationTransition

.. doxygengroup:: wpfeatureactivationtransition
   :content-only:
 0707010000004E000081A400000000000000000000000165F8630400000062000000000000000000000000000000000000003400000000wireplumber-0.5.0/docs/rst/library/c_api/wp_api.rst   .. _wp_api:

Library Initialization
======================
.. doxygengroup:: wp
   :content-only:
  0707010000004F000081A400000000000000000000000165F8630400000056000000000000000000000000000000000000003900000000wireplumber-0.5.0/docs/rst/library/c_api/wperror_api.rst  .. _wperror_api:

Error Codes
===========
.. doxygengroup:: wperror
   :content-only:
  07070100000050000081A400000000000000000000000165F863040000007F000000000000000000000000000000000000002F00000000wireplumber-0.5.0/docs/rst/library/meson.build    # you need to add here any files you add to the toc directory as well
sphinx_files += files(
  'c_api.rst',
)

subdir('c_api')
 07070100000051000081A400000000000000000000000165F86304000000DE000000000000000000000000000000000000002700000000wireplumber-0.5.0/docs/rst/meson.build    # you need to add here any files you add to the toc directory as well
sphinx_files += files(
  'index.rst',
)

subdir('daemon')
subdir('design')
subdir('policies')
subdir('library')
subdir('scripting')
subdir('resources')
  07070100000052000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002400000000wireplumber-0.5.0/docs/rst/policies   07070100000053000081A400000000000000000000000165F86304000019A7000000000000000000000000000000000000003000000000wireplumber-0.5.0/docs/rst/policies/linking.rst   .. _policies_linking:

Linking Policy
==============

Introduction
------------

The linking policy in WirePlumber is the logic charged to link a PipeWire stream
node with a PipeWire device node (most cases), or with another PipeWire stream
node (monitoring applications).

PipeWire stream nodes always have one of the following media classes:

- Stream/Output/Audio: For audio playback applications (Eg pw-play).
- Stream/Input/Audio: For audio capture applications (Eg pw-record).
- Stream/Input/Video: For video capture applications (Eg cheese).

And Pipewire device nodes always have one of the following media classes:

- Audio/Sink: For audio playback devices (Eg Speakers).
- Audio/Source: For audio capture devices (Eg Microphones).
- Video/Source: For video capture devices (Eg Cameras).

By default, since in most cases we want to link a stream node with a device
node, the linking policy logic when linking 2 nodes always follows the following
assignments:

.. graphviz::

   digraph nodes {
      rankdir=LR;
      APS [shape=box label=<audio playback stream<BR/>(Stream/Output/Audio)>];
      APD [shape=box label=<audio playback device<BR/>(Audio/Sink)>];
      ACS [shape=box label=<audio capture stream<BR/>(Stream/Input/Audio)>];
      ACD [shape=box label=<audio capture device<BR/>(Audio/Source)>];
      VCS [shape=box label=<video capture stream<BR/>(Stream/Input/Video)>];
      VCD [shape=box label=<video capture device<BR/>(Video/Source)>];
      APS -> APD;
      ACD -> ACS;
      VCD -> VCS;
   }


After that, once the media class of a device node has been select for a
particular stream node, and there are more than 1 device node matching such
media class, WirePlumber will select one based on a set of priorities:

First, it will check if there is a default configured device node for the
selected device media class. If there is one, and the node exists, it will link
the stream node with such configured default node. Users can easily configure
default device nodes for all the 3 different device media classes using tools
such as ``pavucontrol`` or ``wpctl``. The logic is implemented in the
``linking/find-default-target.lua`` Lua script.

If there isn't any default node configured, or there is a default node
configured but the node does not exist, WirePlumber will instead select the
best device node available. The best device node is the node with highest
session priority and available routes to the physical device. The logic is
implemented in the ``linking/find-best-target.lua`` Lua script.

If the best node could not be found because the system does not have any,
WirePlumber won't link the stream and will send a "no target node available"
error to the client.


Stream node linking properties
------------------------------

The above default linking logic behavior can be changed by setting specific
properties on the nodes.

.. note::

   These properties must be set in the **stream** nodes (not the device nodes),
   otherwise they won't have any effect.

- **target.object**:

  The name of the desired node for this stream to be linked with.
  If this property is present, WirePlumber will try to find such node, see if it
  can be linked with the stream, and if so, will use it instead of the default
  node or best node. The logic is implemented in the ``linking/find-defined-target.lua``
  Lua script. Since this property is not set by default, WirePlumber will always
  link stream nodes to the default or best device node found. This property can be
  easily set using tools such as ``pw-play`` with the ``--target`` flag.

  Note that any node name can be specified there, even if the name is not a device
  node name, but another stream node name. If this is the case, WirePlumber will
  link 2 stream nodes together. An example of this case is the monitoring nodes
  created by ``pavucontrol`` to monitor audio of all audio devices and streams.

- **node.dont-reconnect**:

  Boolean indicating whether the stream node should not be reconnected to a new
  node if its current linked node (target) was destroyed or not. By default it
  is set to ``false``, so if the property is not present in the stream node, WirePlumber
  will always try to reconnect the stream node to a new target instead of sending
  an error to the client. The logic is implemented in the ``linking/prepare-link.lua``
  Lua script.

- **node.dont-move**:

  Boolean indicating whether the stream node should not be movable or not at runtime
  using the metadata. If a stream node is not movable, it means that users cannot
  relink the stream node to a new target at runtime (using tools such as ``pavucontrol``
  or ``pw-metadata``) when the stream node is already linked to a different node. By
  default it is set to ``false``, so if the property is not present, WirePlumber will
  always move, and therefore link the stream node to a new target if it is defined and
  updated in the ``target.object`` metadata key.

- **node.dont-fallback**:

  Boolean indicating whether the stream node should not fallback to a different
  target if its defined target does not exist (the one defined with the ``target.object``
  property) or not. Therefore, if this property is set to ``true``, WirePlumber sends
  a "defined target not found" error to the client and will also destroy the stream
  node. By default it is set to ``false``, so if the property is not present in the
  stream node, WirePlumber will always fallback to the default or best target if
  the defined target was not found.

- **node.linger**:

  Boolean indicating whether the stream node should linger or not if its defined
  target was not found and the ``node.dont-fallback`` is set to true. Therefore, if
  this property is set to ``true``, the defined target was not found, and the
  ``node.dont-fallback`` is set to true, WirePlumber won't send a "defined target not found"
  error to the client, and won't destroy the stream node. This is useful if we want
  the stream to wait (without processing any data) until its defined target becomes
  available. By default it is set to ``false``, so if the property is not present in the
  stream node, WirePlumber will always destroy the node and send an error to the client
  if its target was not found and ``node.dont-fallback`` was set to true.


Linking settings
----------------

Apart from the above properties, there are also global settings for the linking
policy. See :ref:`config_settings` for more information, the linking settings
are prefixed with ``linking.``.
 07070100000054000081A400000000000000000000000165F8630400000089000000000000000000000000000000000000003000000000wireplumber-0.5.0/docs/rst/policies/meson.build   # you need to add here any files you add to the toc directory as well
sphinx_files += files(
   'linking.rst',
   'smart_filters.rst',
)
   07070100000055000081A400000000000000000000000165F8630400003720000000000000000000000000000000000000003600000000wireplumber-0.5.0/docs/rst/policies/smart_filters.rst .. _policies_smart_filters:

Smart Filters
=============

Introduction
------------

The smart filters policy allows automatically linking filters together, in a
chain, and tied to a specific target node. This is useful when we want to apply
a specific processing chain to a specific device, for example. When a stream is
about to be linked to a target node that is associated with a smart filter
chain, the policy will automatically link the stream with the first filter in
the chain, and the last filter in the chain with the target node. This is done
transparently to the client, allowing users to define a specific processing
chain for a specific device without having to create setups with virtual sinks
(or sources) that must be explicitly targetted by the clients.

Filters, in general, are nodes that are placed in the middle of the graph and
are used to modify the data that passes through them. For example, the
*echo-cancel*, the *filter-chain*, or the *loopback* nodes are filters.
Filters can be implemented either as a single node or as a pair of nodes with
opposite directions. For example, the *null-audio-sink* node can be configured
to be a single-node filter. On the other hand, the *filter-chain* is a pair of
nodes with opposite directions, where one node captures the audio from the graph
and the other node sends the modified audio back to the graph.

For the purpose of the **smart filters** policy, WirePlumber will only consider
pairs of nodes as filters, not single-node ones. More specifically, a pair of
nodes will be considered to be a filter by WirePlumber if they have the
``node.link-group`` property set to a common value. This property is always set
on pairs of nodes that are internally linked together and is a good indicator
that the nodes are implementing a filter.

That pair of nodes **must** always consist of a *stream* node and a *main* node.
The main node acts as a virtual device, where the data is sent or captured
to/from, and the stream node acts as a regular stream, where the data is sent
or received to/from the next node in the graph. This is designated by their
media class, as shown in the table below:

.. list-table::
   :widths: 30 35 35
   :header-rows: 1
   :stub-columns: 1

   * -
     - Input filter (virtual sink)
     - Output filter (virtual source)
   * - Main node
     - ``Audio/Sink`` (capture)
     - ``Audio/Source`` (playback)
   * - Stream node
     - ``Stream/Output/Audio`` (playback)
     - ``Stream/Input/Audio`` (capture)

For instance, if a smart filter is used between an application playback stream
and the default audio sink, the graph would look like this:

.. graphviz::

   digraph nodes {
      rankdir=LR;
      A [shape=box label=<application stream node<BR/>(Stream/Output/Audio)>];
      FM [shape=box label=<filter main node<BR/>(Audio/Sink)>];
      FS [shape=box label=<filter stream node<BR/>(Stream/Output/Audio)>];
      D [shape=box label=<default device node<BR/>(Audio/Sink)>];
      A -> FM;
      FS -> D;
      subgraph cluster_filter {
        style="dotted";
        FM; FS;
      }
   }

The same logic is applied if the smart filter is used between an application
capture stream and the default audio source, it is just all in the opposite
direction. This is how the graph would look like in this case:

.. graphviz::

   digraph nodes {
      rankdir=LR;
      A [shape=box label=<application stream node<BR/>(Stream/Input/Audio)>];
      FM [shape=box label=<filter main node<BR/>(Audio/Source)>];
      FS [shape=box label=<filter stream node<BR/>(Stream/Input/Audio)>];
      D [shape=box label=<default device node<BR/>(Audio/Source)>];
      D -> FS;
      FM -> A;
      subgraph cluster_filter {
        style="dotted";
        FM; FS;
      }
   }

When multiple filters have the same direction, they can also be chained together
so that the output of one filter is sent to the input of the next filter. The
next section describes how these chains can be described with properties so that
they are automatically linked by WirePlumber in any way we want.

Filter properties
-----------------

When a filter node is created, WirePlumber will check for the presence of the
following optional node properties on the **main** node:

- **filter.smart**

  Boolean indicating whether smart policy will be used for these filter nodes or
  not. This is disabled by default, therefore filter nodes will be treated as
  regular nodes, without applying any kind of extra logic. On the other hand, if
  this property is set to ``true``, automatic (smart) filter policy will be used
  when linking them. The properties below will then also apply, providing
  further instructions.

- **filter.smart.name**

  The unique name of the filter. WirePlumber will use the value of the
  ``node.link-group`` property as the filter name if this property is not set.

- **filter.smart.disabled**

  Boolean indicating whether the filter should be disabled or not. A disabled
  filter will never be used under any circumstances. If the property is not set,
  WirePlumber will consider the filter as enabled (i.e. disabled = false).

- **filter.smart.targetable**

  Boolean indicating whether the filter can be directly linked with clients that
  have it defined as a target (Eg: ``pw-play --target <filter-name>``) or not.
  This can be useful when a client wants to be linked with a filter that is in
  the middle of the chain in order to bypass the filters that are placed before
  the selected one. If the property is not set, WirePlumber will consider the
  filter not targetable by default, meaning filters will never by bypassed by
  clients, and clients will always be linked with the first filter in the chain.

- **filter.smart.target**

  A JSON object that defines the matching properties of the filter's target
  node. A filter target can never be another filter node (WirePlumber will
  ignore it), it must be a device or virtual sink (or source, depending on the
  direction of the filter). If this property is not set, WirePlumber will use
  the default sink/source as the target.

- **filter.smart.before**

  A JSON array containing the names of the filters that are supposed to be
  chained after this filter (i.e. this filter here should be chained *before*
  those). If not set, WirePlumber will link the filters by order of creation.

- **filter.smart.after**

  A JSON array containing the names of the filters that are supposed to be
  chained before this filter (i.e. this filter here should be chained *after*
  those). If not set, WirePlumber will link the filters by order of creation.

.. note::

   These properties must be set on the filter's **main** node, not the stream
   node.

As an example, we will describe here how to create 2 loopback filters in
PipeWire's configuration, with names loopback-1 and loopback-2, that will be
linked with the default audio device, and use loopback-2 filter as the last
filter in the chain.

The PipeWire configuration files for the 2 filters should be like this:

- ~/.config/pipewire/pipewire.conf.d/loopback-1.conf:

  .. code-block::
     :emphasize-lines: 8-11

     context.modules = [
         {   name = libpipewire-module-loopback
             args = {
                 node.name = loopback-1-sink
                 node.description = "Loopback 1 Sink"
                 capture.props = {
                     audio.position = [ FL FR ]
                     media.class = Audio/Sink
                     filter.smart = true
                     filter.smart.name = loopback-1
                     filter.smart.before = [ loopback-2 ]
                 }
                 playback.props = {
                     audio.position = [ FL FR ]
                     node.passive = true
                     node.dont-remix = true
                 }
             }
         }
     ]

- ~/.config/pipewire/pipewire.conf.d/loopback-2.conf:

  .. code-block::
     :emphasize-lines: 8-10

     context.modules = [
         {   name = libpipewire-module-loopback
             args = {
                 node.name = loopback-2-sink
                 node.description = "Loopback 2 Sink"
                 capture.props = {
                     audio.position = [ FL FR ]
                     media.class = Audio/Sink
                     filter.smart = true
                     filter.smart.name = loopback-2
                 }
                 playback.props = {
                     audio.position = [ FL FR ]
                     node.passive = true
                     node.dont-remix = true
                 }
             }
         }
     ]

After restarting PipeWire to apply the configuration changes, playing a test
wave audio file with paplay to the default device should result in the following
graph:

.. graphviz::

   digraph nodes {
      rankdir=LR;
      paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
      L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
      L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
      L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
      L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
      device [shape=box label=<default device node<BR/>(Audio/Sink)>];
      paplay -> L1M;
      L1S -> L2M;
      L2S -> device;
      subgraph cluster_filter1 {
        style="dotted";
        L1M; L1S;
      }
      subgraph cluster_filter2 {
        style="dotted";
        L2M; L2S;
      }
   }

Now, if we remove the ``filter.smart.before = [ loopback-2 ]`` property from the
loopback-1 filter, and add a ``filter.smart.before = [ loopback-1 ]`` property
in the loopback-2 filter configuration file, WirePlumber should link the
loopback-1 filter as the last filter in the chain, like this:

.. graphviz::

   digraph nodes {
      rankdir=LR;
      paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
      L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
      L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
      L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
      L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
      device [shape=box label=<default device node<BR/>(Audio/Sink)>];
      paplay -> L2M;
      L2S -> L1M;
      L1S -> device;
      subgraph cluster_filter1 {
        style="dotted";
        L1M; L1S;
      }
      subgraph cluster_filter2 {
        style="dotted";
        L2M; L2S;
      }
   }

In addition, the filters can have different targets. For example, we can define
the filters like this:

- ~/.config/pipewire/pipewire.conf.d/loopback-1.conf:

  .. code-block::
     :emphasize-lines: 12

     context.modules = [
         {   name = libpipewire-module-loopback
             args = {
                 node.name = loopback-1-sink
                 node.description = "Loopback 1 Sink"
                 capture.props = {
                     audio.position = [ FL FR ]
                     media.class = Audio/Sink
                     filter.smart = true
                     filter.smart.name = loopback-1
                     filter.smart.after = [ loopback-2 ]
                     filter.smart.target = { node.name = "not-default-audio-device" }
                 }
                 playback.props = {
                     audio.position = [ FL FR ]
                     node.passive = true
                     node.dont-remix = true
                 }
             }
         }
     ]

- ~/.config/pipewire/pipewire.conf.d/loopback-2.conf:

  .. code-block::

     context.modules = [
         {   name = libpipewire-module-loopback
             args = {
                 node.name = loopback-2-sink
                 node.description = "Loopback 2 Sink"
                 capture.props = {
                     audio.position = [ FL FR ]
                     media.class = Audio/Sink
                     filter.smart = true
                     filter.smart.name = loopback-2
                 }
                 playback.props = {
                     audio.position = [ FL FR ]
                     node.passive = true
                     node.dont-remix = true
                 }
             }
         }
     ]

In this case, playing a test wave audio file with paplay to the
``not-default-audio-device`` device should result in the following graph:

.. graphviz::

   digraph nodes {
      rankdir=LR;
      paplay [shape=box label=<paplay node<BR/>(Stream/Output/Audio)>];
      L1M [shape=box label=<loopback-1 main node<BR/>(Audio/Sink)>];
      L1S [shape=box label=<loopback-1 stream node<BR/>(Stream/Output/Audio)>];
      L2M [shape=box label=<loopback-2 main node<BR/>(Audio/Sink)>];
      L2S [shape=box label=<loopback-2 stream node<BR/>(Stream/Output/Audio)>];
      device [shape=box label=<not-default-audio-device node<BR/>(Audio/Sink)>];
      paplay -> L2M;
      L2S -> L1M;
      L1S -> device;
      subgraph cluster_filter1 {
        style="dotted";
        L1M; L1S;
      }
      subgraph cluster_filter2 {
        style="dotted";
        L2M; L2S;
      }
   }

In this configuration, the loopback-1 filter will only be linked if the
application stream is targeting the device node called
"not-default-audio-device".

Filters metadata
----------------

Similar to the default metadata, it is also possible to override the filter
properties using the "filters" metadata object. This allow users to change the
filters policy at runtime.

For example, assuming the id of the *loopback-1* main node is ``40``, we can
disable the filter by setting its ``filter.smart.disabled`` metadata key to
``true`` using the ``pw-metadata`` tool like this:

.. code-block:: bash

   $ pw-metadata -n filters 40 "filter.smart.disabled" true Spa:String:JSON

We can also change the target of a filter at runtime:

.. code-block:: bash

   $ pw-metadata -n filters 40 "filter.smart.target" "{ node.name = new-target-node-name }" Spa:String:JSON

Every time a key in the filters metadata changes, all filters are unlinked and
re-linked properly, following the new policy.
07070100000056000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002500000000wireplumber-0.5.0/docs/rst/resources  07070100000057000081A400000000000000000000000165F863040000036B000000000000000000000000000000000000003300000000wireplumber-0.5.0/docs/rst/resources/community.rst    .. _resources_community:

Community Resources
===================

Discussion chat room
--------------------

WirePlumber does not have its own discussion room, but we hang out in the
generic PipeWire IRC room, which is bridged to Matrix. Join us:

  - via IRC: **#pipewire** on `irc.oftc.net <https://www.oftc.net/>`_
  - via Matrix: `#pipewire:matrix.org <https://matrix.to/#/#pipewire:matrix.org>`_

Mailing list
------------

For discussions related to PipeWire **development**, there is
`the pipewire-devel mailing list <https://lists.freedesktop.org/mailman/listinfo/pipewire-devel>`_

Reporting issues
----------------

If you have found an issue with WirePlumber and wish to report it, please
`create a ticket on GitLab <https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues>`_

Please always check that your issue is not already reported before reporting it.
 07070100000058000081A400000000000000000000000165F8630400000A2C000000000000000000000000000000000000003600000000wireplumber-0.5.0/docs/rst/resources/contributing.rst .. _resources_contributing:

Contributing to WirePlumber
===========================

Coding style
------------

WirePlumber uses the `GNOME C Coding Style`_ with K&R brace placement
and 2-space indentation, similar to `GStreamer's Coding Style`_.
When in doubt, just follow the example of the existing code.

WirePlumber ships with a `editorconfig <https://editorconfig.org/>`_ file.
If your code editor / IDE supports this, it should automatically set up
the indentation settings.

.. important::

   When submitting changes for review, please ensure that the coding style
   of the changes respects the coding style of the project.

.. _GNOME C Coding Style: https://developer.gnome.org/programming-guidelines/unstable/c-coding-style.html.en
.. _GStreamer's Coding Style: https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/developing.html#what-is-the-coding-style-for-gstreamer-code

Tests
-----

See :ref:`resources_testing`

Running in gdb / valgrind / etc...
----------------------------------

The Makefile included with WirePlumber supports the ``gdb`` and ``valgrind``
targets. So, instead of ``make run`` you can do ``make gdb`` or
``make valgrind`` to do some debugging.

You can also run in any other wrapper by setting the ``DBG`` variable
on ``make run``. For example:

.. code:: console

   $ make run DBG="strace"

You may also use ``wp-uninstalled.sh`` directly as a wrapper to set up the
environment and then execute any command. For example:

.. code:: console

   $ ./wp-uninstalled.sh gdb --args wpctl status

Merge requests
--------------

In order to submit changes to the project, you should create a merge request.
To do this,

  1. fork the project on https://gitlab.freedesktop.org/pipewire/wireplumber
  2. clone the forked project on your computer
  3. make changes in a new git branch
  4. rebase your changes on top of the latest ``master`` of the main repository
  5. push that branch on the forked repository
  6. follow the link shown by ``git push`` to create the merge request
     (or alternatively, visit your forked repository on gitlab and
     create it from there)

.. important::

   While creating the merge request, it is important to enable the
   **allow commits from members who can merge to the target branch** option
   so that maintainers are able to rebase your branch, since WirePlumber uses
   a fast-forward merge policy.

For more detailed information, check out `gitlab's manual on merge requests`_

.. _gitlab's manual on merge requests: https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html
07070100000059000081A400000000000000000000000165F86304000000AF000000000000000000000000000000000000003100000000wireplumber-0.5.0/docs/rst/resources/meson.build  # you need to add here any files you add to the toc directory as well
sphinx_files += files(
   'contributing.rst',
   'testing.rst',
   'community.rst',
   'releases.rst',
)
 0707010000005A000081A400000000000000000000000165F863040000004B000000000000000000000000000000000000003200000000wireplumber-0.5.0/docs/rst/resources/releases.rst .. _resources_releases:

Releases
========

.. include:: ../../../NEWS.rst
 0707010000005B000081A400000000000000000000000165F8630400001995000000000000000000000000000000000000003100000000wireplumber-0.5.0/docs/rst/resources/testing.rst  .. _resources_testing:

Testing
=======

Automated unit tests
--------------------

WirePlumber has automated tests that you can easily run with:

.. code:: console

   $ meson test -C build

This will automatically compile all test dependencies, so you can be sure
that this always tests your latest changes.

If you wish to run a specific test instead of all of them, you can run:

.. code:: console

   $ meson test -C build test-name

When debugging a single test, you can additionally enable verbose test output
by appending ``-v`` and you can also run the test in gdb by appending ``--gdb``.

For more information on how to use ``meson test``, please refer to
`meson's manual <https://mesonbuild.com/Unit-tests.html>`_

.. important::

   When submitting changes for review, always ensure that all tests pass

Please note that many WirePlumber tests require specific SPA test plugins
to be available in your PipeWire installation. More specifically, PipeWire
needs to be configured with the following options enabled:

.. code:: console

   -Dvideotestsrc=true -Daudiotestsrc=true -Dtest=true

If these SPA plugins are not found in the system, some tests will fail.
This is expected.

WirePlumber examples
--------------------

WirePlumber ships examples in ``test/examples``.
Execute them from the top-level directory with ``wp-uninstalled.sh``:

.. code:: console

   $ ./wp-uninstalled.sh ./build/tests/examples/audiotestsrc-play


Assuming there is no other process actively using ``hw:0,0`` from alsa,
the above example should play a test tone on ``hw:0,0`` without errors.

Native API clients
------------------

pw-cat
^^^^^^

Using the default device:

.. code:: console

   $ wpctl status  # verify the default device
   $ pw-record test.wav
   $ pw-play test.wav


Using a non-default device:

.. code:: console

   $ pw-record --list-targets  # find the node id
   $ pw-record --target <node_id> test.wav
   $ pw-play --list-targets  # find the node id
   $ pw-play --target <node_id> test.wav

or

.. code:: console

   $ wpctl status  # find the capture & playback node ids
   $ pw-record --target <node_id> test.wav
   $ pw-play --target <node_id> test.wav

.. note::

   node ids can be used interchangeably when specifying targets in all use cases

video-play
^^^^^^^^^^

Using the default device:

.. code:: console

   $ cd path/to/pipewire-source-dir
   $ ./build/src/examples/video-play


Using a non-default device:

.. code:: console

   $ wpctl status  # find the device node id from the list
   $ cd path/to/pipewire-source-dir
   $ ./build/src/examples/video-play <node_id>

.. tip::

   enable videotestsrc in wireplumber's configuration to have more video
   sources available

PulseAudio compat API clients
-----------------------------

pacat
^^^^^

Using the default device:

.. code:: console

   $ wpctl status  # verify the default device
   $ parecord test.wav
   $ paplay test.wav

pavucontrol
^^^^^^^^^^^

Use the command:

.. code:: console

  $ pavucontrol

* Volume level meters should work
* Changing the volume should work

ALSA compat API clients
-----------------------

aplay / arecord
^^^^^^^^^^^^^^^

.. note::

   unless you have installed PipeWire in the default system prefix
   (``/usr``), the ALSA compat API will not work, unless you copy
   ``libasound_module_pcm_pipewire.so`` in the alsa plugins directory
   (usually ``/usr/<libdir>/alsa-lib/``) and that you add the contents of
   ``pipewire-alsa/conf/50-pipewire.conf`` in your ``~/.asoundrc``
   (or anywhere else, system-wide, where libasound can read it)

Using the default device:

.. code:: console

   $ wpctl status  # verify the default devices
   $ arecord -D pipewire -f S16_LE -r 48000 test.wav
   $ aplay -D pipewire test.wav

Using a non-default device:

.. code:: console

   $ wpctl status  # find the capture & playback node ids
   $ PIPEWIRE_NODE=<node_id> arecord -D pipewire -f S16_LE -r 48000 test.wav
   $ PIPEWIRE_NODE=<node_id> aplay -D pipewire test.wav

or

.. code:: console

   $ wpctl status  # find the capture & playback device node ids
   $ arecord -D pipewire:NODE=<node_id> -f S16_LE -r 48000 test.wav
   $ aplay -D pipewire:NODE=<node_id> test.wav


JACK compat API clients
-----------------------

qjackctl
^^^^^^^^

.. code:: console

   $ pw-jack qjackctl

* This should correctly connect.
* The "Graph" window should show the PipeWire graph.

jack_simple_client
^^^^^^^^^^^^^^^^^^

.. code:: console

   $ wpctl status  # find the target device node id
   $ wpctl inspect <node_id>  # find the node.id
   $ PIPEWIRE_NODE=<node_id> pw-jack jack_simple_client

.. note::

   The JACK layer is not controlled by the session manager, it creates its own
   links; which is why it is required to specify a node id.

Device Reservation
------------------

with PulseAudio
^^^^^^^^^^^^^^^

1. With PulseAudio running, start a pulseaudio client.

.. code:: console

   $ gst-launch-1.0 audiotestsrc ! pulsesink

2. Start PipeWire & WirePlumber

   - The device in use by PA will not be available in PW

3. Stop the PA client

   - A few seconds later, WirePlumber should assume control of the device

4. ``wpctl status`` should be able to confirm that the device is available

5. Start a PA client again

   - It should not be able to play; it will just freeze

6. Stop WirePlumber

   - The PA client should immediately start playing

with JACK
^^^^^^^^^

1. Start PipeWire & WirePlumber

   - All devices should be available

2. Start ``jackdbus``

   1. through ``qjackctl``:

      - Enable *Setup* -> *Misc* -> *Enable JACK D-Bus interface*
      - Click *Start* on the main window

   2. or manually:

      - Run ``jackdbus auto``
      - Run ``qdbus org.jackaudio.service /org/jackaudio/Controller org.jackaudio.JackControl.StartServer``

3. Wait a few seconds and run ``wpctl status`` to inspect

   - The devices taken by JACK should no longer be available
   - There should be two *JACK System* nodes (sink & source)

4. Run an audio client on PipeWire (ex ``pw-play test.wav``)

   - Notice how audio now goes through JACK

5. Stop JACK

   - through ``qjackctl``, click *Stop*
   - or manually: ``qdbus org.jackaudio.service /org/jackaudio/Controller org.jackaudio.JackControl.StopServer``

6. Wait a few seconds and run ``wpctl status`` to inspect

   - The devices that were release by JACK should again be available
   - There should be no *JACK System* nodes

.. note::

   You may also start WirePlumber *after* starting JACK. It should immediately
   go to the state described in step 3
   0707010000005C000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002500000000wireplumber-0.5.0/docs/rst/scripting  0707010000005D000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000003600000000wireplumber-0.5.0/docs/rst/scripting/existing_scripts 0707010000005E000081A400000000000000000000000165F86304000001A7000000000000000000000000000000000000003A00000000wireplumber-0.5.0/docs/rst/scripting/existing_scripts.rst .. _scripting_existing_scripts:

Existing Scripts
================

This section documents all the existing lua scripts that are shipped with
WirePlumber, including information on the hooks that they provide and
information on how to extend the logic with additional custom scripts & hooks.

.. toctree::
   :maxdepth: 1

   existing_scripts/default_nodes.rst
   existing_scripts/device.rst
   existing_scripts/linking.rst
 0707010000005F000081A400000000000000000000000165F8630400000063000000000000000000000000000000000000004800000000wireplumber-0.5.0/docs/rst/scripting/existing_scripts/default_nodes.rst   .. _existing_scripts_default_nodes:

.. include:: ../../../../src/scripts/default-nodes/README.rst
 07070100000060000081A400000000000000000000000165F8630400000055000000000000000000000000000000000000004100000000wireplumber-0.5.0/docs/rst/scripting/existing_scripts/device.rst  .. _existing_scripts_device:

.. include:: ../../../../src/scripts/device/README.rst
   07070100000061000081A400000000000000000000000165F8630400000057000000000000000000000000000000000000004200000000wireplumber-0.5.0/docs/rst/scripting/existing_scripts/linking.rst .. _existing_scripts_linking:

.. include:: ../../../../src/scripts/linking/README.rst
 07070100000062000081A400000000000000000000000165F8630400000097000000000000000000000000000000000000004200000000wireplumber-0.5.0/docs/rst/scripting/existing_scripts/meson.build # you need to add here any files you add to the toc directory as well
sphinx_files += files(
  'default_nodes.rst',
  'device.rst',
  'linking.rst',
)
 07070100000063000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002D00000000wireplumber-0.5.0/docs/rst/scripting/lua_api  07070100000064000081A400000000000000000000000165F86304000001FB000000000000000000000000000000000000003100000000wireplumber-0.5.0/docs/rst/scripting/lua_api.rst  .. _scripting_lua_api:

Lua API Documentation
=====================

.. toctree::
   :maxdepth: 1
   :caption: Contents:

   lua_api/lua_introduction.rst
   lua_api/lua_gobject.rst
   lua_api/lua_core_api.rst
   lua_api/lua_log_api.rst
   lua_api/lua_object_api.rst
   lua_api/lua_object_manager_api.rst
   lua_api/lua_object_interest_api.rst
   lua_api/lua_proxies_api.rst
   lua_api/lua_session_item_api.rst
   lua_api/lua_spa_device_api.rst
   lua_api/lua_spa_pod.rst
   lua_api/lua_local_module_api.rst
 07070100000065000081A400000000000000000000000165F86304000013A0000000000000000000000000000000000000003E00000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_core_api.rst  .. _lua_core_api:

Core
====

The :ref:`WpCore <core_api>` API is mostly transparent to lua, as the core
object is not exposed to the scripts.

For some functionality, though, the following static functions are exposed.

.. function:: Core.get_properties()

   Binds :c:func:`wp_core_get_properties`

   Returns a table with the properties of the core. These are the same
   properties that appear on WirePlumber's *client* object in the PipeWire
   global registry.

   :returns: the properties of the core
   :rtype: table

.. function:: Core.get_info()

   Returns a table with information about the core. The table contains
   the following fields:

   =========== ===========
   Field       Contains
   =========== ===========
   cookie      The value of :c:func:`wp_core_get_remote_cookie`
   name        The value of :c:func:`wp_core_get_remote_name`
   user_name   The value of :c:func:`wp_core_get_remote_user_name`
   host_name   The value of :c:func:`wp_core_get_remote_host_name`
   version     The value of :c:func:`wp_core_get_remote_version`
   properties  The value of :c:func:`wp_core_get_remote_properties`
   =========== ===========

   :returns: information about the core
   :rtype: table

.. function:: Core.idle_add(callback)

   Binds :c:func:`wp_core_idle_add_closure`

   Schedules to call *callback* the next time that the event loop will be idle

   :param function callback: the function to call; the function takes no
      arguments and must return true/false, as it is a GSourceFunc:
      return true to have the event loop call it again the next time it is idle,
      false to stop calling it and remove the associated GSource
   :returns: the GSource associated with this idle callback
   :rtype: GSource, see :func:`GSource.destroy`

.. function:: Core.timeout_add(timeout_ms, callback)

   Binds :c:func:`wp_core_timeout_add_closure`

   Schedules to call *callback* after *timeout_ms* milliseconds

   :param function callback: the function to call; the function takes no
      arguments and must return true/false, as it is a GSourceFunc:
      return true to have the event loop call it again periodically every
      *timeout_ms* milliseconds, false to stop calling it and remove the
      associated GSource
   :returns: the GSource associated with this idle callback
   :rtype: GSource, see :func:`GSource.destroy`

.. function:: GSource.destroy(self)

   For the purpose of working with :func:`Core.idle_add` and
   :func:`Core.timeout_add`, the GSource object that is returned by those
   functions contains this method.

   Call this method to destroy the source, so that the associated callback
   is not called again. This can be used to stop a repeating timer callback
   or just to abort some idle operation

   This method binds *g_source_destroy*

.. function:: Core.sync(callback)

   Binds :c:func:`wp_core_sync`

   Calls *callback* after synchronizing the transaction state with PipeWire

   :param function callback: a function to be called after syncing with PipeWire;
      the function takes one argument that will be an error string, if something
      went wrong and nil otherwise and returns nothing

.. function:: Core.quit()

   Quits the current *wpexec* process

   .. note::

      This can only be called when the script is running in *wpexec*;
      if it is running in the main WirePlumber daemon, it will print
      a warning and do nothing

.. function:: Core.require_api(..., callback)

   Ensures that the specified API plugins are loaded.

   API plugins are plugins that provide some API extensions for use in scripts.
   These plugins must always have their name end in "-api" and the names
   specified here must not have the "-api" extension.

   For instance, the "mixer-api" module provides an API to change volume/mute
   controls from scripts, via action signals. It can be used like this:

   .. code-block:: lua

      Core.require_api("mixer", function(mixer)
        -- get the volume of node 35
        local volume = mixer:call("get-volume", 35)

        -- the return value of "get-volume" is a GVariant(a{sv}),
        -- which gets translated to a Lua table
        Debug.dump_table(volume)
      end)

   See also the example in :func:`GObject.call`

   .. note::

      This can only be called when the script is running in *wpexec*;
      if it is running in the main WirePlumber daemon, it will print
      a warning and do nothing

   :param strings ...: a list of string arguments, which specify the names of
      the api plugins to load, if they are not already loaded
   :param callback: the function to call after the plugins have been loaded;
      this function takes references to the plugins as parameters

.. function:: Core.test_feature()

   Binds :c:func:`wp_core_test_feature`

   Tests if the specified feature is provided in the current WirePlumber
   configuration.

   :param string feature: the name of the feature to test
   :returns: true if the feature is provided, false otherwise
   :rtype: boolean
07070100000066000081A400000000000000000000000165F8630400002B7B000000000000000000000000000000000000003D00000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_gobject.rst  .. _lua_gobject:

GObject Integration
===================

The Lua engine that powers WirePlumber's scripts provides direct integration
with `GObject`_. Most of the objects that you will deal with in the lua scripts
are wrapping GObjects. In order to work with the scripts, you will first need
to have a basic understanding of GObject's basic concepts, such as signals and
properties.

Properties
----------

All GObjects have the ability to have `properties`_.
In C we normally use `g_object_get`_ to retrieve them and `g_object_set`_
to set them.

In WirePlumber's lua engine, these properties are exposed as object members
of the Lua object.

For example:

.. code-block:: lua

   -- read the "bound-id" GObject property from the proxy
   local proxy = function_that_returns_a_wp_proxy()
   local proxy_id = proxy["bound-id"]
   print("Bound ID: " .. proxy_id)

Writable properties can also be set in a similar fashion:

.. code-block:: lua

   -- set the "scale" GObject property to the enum value "cubic"
   local mixer = ...
   mixer["scale"] = "cubic"

Signals
-------

GObjects also have a generic mechanism to deliver events to external callbacks.
These events are called `signals`_.
To connect to a signal and handle it, you may use the *connect* method:

.. function:: GObject.connect(self, detailed_signal, callback)

   Connects the signal to a callback. When the signal is emitted by the
   underlying object, the callback will be executed.

   The signature of the callback is expected to match the signature of the
   signal, with the first parameter being the object itself.

   **Example:**

   .. code-block:: lua

      -- connects the "bound" signal from WpProxy to a callback
      local proxy = function_that_returns_a_wp_proxy()
      proxy:connect("bound", function(p, id)
        print("Proxy " .. tostring(p) .. " bound to " .. tostring(id))
      end)

   In this example, the ``p`` variable in the callback is the ``proxy`` object,
   while ``id`` is the first parameter of the *"bound"* signal, as documented
   in :c:struct:`WpProxy`

   :param detailed_signal: the signal name to listen to
                           (of the form "signal-name::detail")
   :param callback: a lua function that will be called when the signal is emitted

Signals may also be used as a way to have dynamic methods on objects. These
signals are meant to be called by external code and not handled. These signals
are called **action signals**.
You may call an action signal using the *call* method:

.. function:: GObject.call(self, action_signal, ...)

   Calls an action signal on this object.

   **Example:**

   .. code-block:: lua

      Core.require_api("default-nodes", "mixer", function(...)
        local default_nodes, mixer = ...

        -- "get-default-node" and "get-volume" are action signals of the
        -- "default-nodes-api" and "mixer-api" plugins respectively
        local id = default_nodes:call("get-default-node", "Audio/Sink")
        local volume = mixer:call("get-volume", id)

        -- the return value of "get-volume" is a GVariant(a{sv}),
        -- which gets translated to a Lua table
        Debug.dump_table(volume)
      end)

   :param action_signal: the signal name to call
   :param ...: a list of arguments that will be passed to the signal
   :returns: the return value of the action signal, if any

Type conversions
----------------

When working with GObject properties and signals, variables need to be
converted from C types to Lua types and vice versa. The following tables
list the type conversions that happen automatically:

C to Lua
^^^^^^^^

Conversion from C to lua is based on the C type.

================================ ===============================================
              C                                        Lua
================================ ===============================================
gchar, guchar, gint, guint       integer
glong, gulong, gint64, guint64   integer
gfloat, gdouble                  number
gboolean                         boolean
gchar *                          string
gpointer                         lightuserdata
WpProperties *                   table (keys: string, values: string)
enum                             string containing the nickname (short name) of
                                 the enum, or integer if the enum is not
                                 registered with GType
flags                            integer (as in C)
GVariant *                       a native type, see below
other GObject, GInterface        userdata holding reference to the object
other GBoxed                     userdata holding reference to the object
================================ ===============================================

.. _lua_gobject_lua_to_c:

Lua to C
^^^^^^^^

Conversion from Lua to C is based on the expected type in C.

============================== ==================================================
           Expecting                                  Lua
============================== ==================================================
gchar, guchar, gint, guint,    convertible to integer
glong, gulong, gint64, guint64 convertible to integer
gfloat, gdouble                convertible to number
gboolean                       convertible to boolean
gchar *                        convertible to string
gpointer                       must be lightuserdata
WpProperties *                 must be table (keys: string, values: convertible
                               to string)
enum                           must be string holding the nickname of the enum,
                               or convertible to integer
flags                          convertible to integer
GVariant *                     see below
other GObject, GInterface      must be userdata holding a compatible GObject type
other GBoxed                   must be userdata holding the same GBoxed type
============================== ==================================================

GVariant to Lua
^^^^^^^^^^^^^^^

============================= =============================================
          GVariant                                 Lua
============================= =============================================
NULL or G_VARIANT_TYPE_UNIT   nil
G_VARIANT_TYPE_INT16          integer
G_VARIANT_TYPE_INT32          integer
G_VARIANT_TYPE_INT64          integer
G_VARIANT_TYPE_UINT16         integer
G_VARIANT_TYPE_UINT32         integer
G_VARIANT_TYPE_UINT64         integer
G_VARIANT_TYPE_DOUBLE         number
G_VARIANT_TYPE_BOOLEAN        boolean
G_VARIANT_TYPE_STRING         string
G_VARIANT_TYPE_VARIANT        converted recursively
G_VARIANT_TYPE_DICTIONARY     table (keys & values converted recursively)
G_VARIANT_TYPE_ARRAY          table (children converted recursively)
============================= =============================================

Lua to GVariant
^^^^^^^^^^^^^^^

Conversion from Lua to GVariant is based on the lua type and is quite limited.

There is no way to recover an array, for instance, because there is no way
in Lua to tell if a table contains an array or a dictionary. All Lua tables
are converted to dictionaries and integer keys are converted to strings.

========= ================================
   Lua                GVariant
========= ================================
nil       G_VARIANT_TYPE_UNIT
boolean   G_VARIANT_TYPE_BOOLEAN
integer   G_VARIANT_TYPE_INT64
number    G_VARIANT_TYPE_DOUBLE
string    G_VARIANT_TYPE_STRING
table     G_VARIANT_TYPE_VARDICT (a{sv})
========= ================================

Closures
--------

When a C function is expecting a GClosure, in Lua it is possible to pass
a Lua function directly. The function is then wrapped into a custom GClosure.

When this GClosure is invalidated, the reference to the Lua function is dropped.
Similarly, when the lua engine is stopped, all the GClosures that were
created by this engine are invalidated.

Reference counting
------------------

GObject references in Lua always hold a reference to the underlying GObject.
When moving this reference around to other variables in Lua, the underlying
GObject reference is shared, but Lua reference counts the wrapper "userdata"
object.

.. code-block:: lua

   -- creating a new FooObject instance; obj holds the GObject reference
   local obj = FooObject()

   -- GObject reference is dropped and FooObject is finalized
   obj = nil

.. code-block:: lua

   -- creating a new FooObject instance; obj holds the GObject reference
   local obj = FooObject()

   function store_global(o)
     -- o is now stored in the global 'obj_global' variable
     -- the GObject ref count is still 1
     obj_global = o
   end

   -- obj userdata reference is passed to o, the GObject ref count is still 1
   store_global(obj)

   -- userdata reference dropped from obj, the GObject is still alive
   obj = nil

   -- userdata reference dropped from obj_global,
   -- the GObject ref is dropped and FooObject is finalized
   obj_global = nil

.. note::

   When assigning a variable to nil, Lua may not immediately drop
   the reference of the underlying object. This is because Lua uses a garbage
   collector and goes through all the unreferenced objects to cleanup when
   the garbage collector runs.

When a GObject that is already referenced in Lua re-appears somewhere else
through calling some API or because of a callback from C, a new reference is
added on the GObject.

.. code-block:: lua

   -- ObjectManager is created in Lua, om holds 1 ref
   local om = ObjectManager(...)
   om:connect("objects-changed", function (om)
     -- om in this scope is a local function argument that was created
     -- by the signal's closure marshaller and holds a second reference
     -- to the ObjectManager

     do_some_stuff()

     -- this second reference is dropped when the function goes out of scope
   end)

.. danger::

   Because Lua variables hold strong references to GObjects, it is dangerous
   to create closures that reference such variables, because these closures
   may create reference loops and **leak** objects

.. code-block:: lua

   local om = ObjectManager(...)

   om:connect("objects-changed", function (obj_mgr)
     -- using 'om' here instead of the local 'obj_mgr'
     -- creates a dangerous reference from the closure to 'om'
     for obj in om:iterate() do
        do_stuff(obj)
     end
   end)

   -- local userdata reference dropped, but the GClosure that was generated
   -- from the above function is still holding a reference and keeps
   -- the ObjectManager alive; the GClosure is referenced by the ObjectManager
   -- because of the signal connection, so the ObjectManager is leaked
   om = nil

.. _GObject: https://developer.gnome.org/gobject/stable/
.. _properties: https://developer.gnome.org/gobject/stable/gobject-properties.html
.. _g_object_get: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#g-object-get
.. _g_object_set: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#g-object-set
.. _signals: https://developer.gnome.org/gobject/stable/signal.html
 07070100000067000081A400000000000000000000000165F8630400000807000000000000000000000000000000000000004200000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_introduction.rst .. _lua_introduction:

Introduction
============

`Lua <https://www.lua.org/>`_ is a powerful, efficient, lightweight,
embeddable scripting language.

WirePlumber uses `Lua version 5.4 <https://www.lua.org/versions.html>`_ to
implement its engine. For older systems, Lua 5.3 is also supported.

Scripts can be ran with the ``wpexec`` tool.

Example scripts can be found in the `tests/examples` directory of the wireplumber source tree.

Lua Reference
-------------

If you are not familiar with the Lua language and its API, please refer to
the `Lua 5.4 Reference Manual <https://www.lua.org/manual/5.4/manual.html>`_

Sandbox
-------

WirePlumber's scripting engine sandboxes the lua scripts to a safe environment.
In this environment, the following rules apply:

  - Scripts are isolated from one another; global variables in one script
    are not visible from another, even though they are actually executed in
    the same ``lua_State``

  - Tables that hold API methods are not writable. While this may sound strange,
    standard Lua allows you to change standard API, for instance
    ``string.format = rogue_format`` is valid outside the sandbox.
    WirePlumber does not allow that.

  - The standard Lua API is limited to a subset of safe functions. For instance,
    functions that interact with the file system (io.*) and the process's state
    (ex.: os.exit) are **not** allowed.

    Here is a full list of Lua functions (and API tables) that are exposed:

    .. literalinclude:: ../../../../modules/module-lua-scripting/wplua/sandbox.lua
      :language: lua
      :lines: 27-30

  - Object methods are not exposed in public tables. To call an object method
    you must use the method call syntax of Lua, i.e. ``object:method(params)``

    The following, for instance, is **not** valid:

    .. code-block:: lua

       -- this will cause an exception
       local node = ...
       Node.send_command(node, "Suspend")

    The correct form is this:

    .. code-block:: lua

       local node = ...
       node:send_command("Suspend")
 07070100000068000081A400000000000000000000000165F86304000003BC000000000000000000000000000000000000004600000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_local_module_api.rst  .. _lua_local_module_api:

Local Modules
=============

The `LocalModule` object (which binds the :c:struct:`WpImplModule` C API) provides a way
to load PipeWire modules in the WirePlumber process. Instantiating the object
loads the module, and when the last reference to the returned module object is
dropped, the module is unloaded.

Constructors
~~~~~~~~~~~~

.. function:: LocalModule(name, arguments, properties)

   Loads the named module with the provided arguments and properties (either of
   which can be ``nil``).

   :param string name: the module name, such as ``"libpipewire-module-loopback"``
   :param string arguments: should be either ``nil`` or a string with the desired
        module arguments
   :param table properties: can be ``nil`` or a table that can be
        :ref:`converted <lua_gobject_lua_to_c>` to :c:struct:`WpProperties`
   :returns: a new LocalModule
   :rtype: LocalModule (:c:struct:`WpImplModule`)
   :since: 0.4.2
07070100000069000081A400000000000000000000000165F8630400000753000000000000000000000000000000000000003D00000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_log_api.rst  .. _lua_log_api:

Debug Logging
=============

.. function:: Log.warning(object, message)

   Logs a warning message, like :c:macro:`wp_warning_object`

   :param object: optional object to associate the message with; you
      may skip this and just start with the *message* as the first parameter
   :type object: GObject or GBoxed
   :param string message: the warning message to log

.. function:: Log.notice(object, message)

   Logs a notice message, like :c:macro:`wp_notice_object`

   :param object: optional object to associate the message with; you
      may skip this and just start with the *message* as the first parameter
   :type object: GObject or GBoxed
   :param string message: the normal message to log

.. function:: Log.info(object, message)

   Logs a info message, like :c:macro:`wp_info_object`

   :param object: optional object to associate the message with; you
      may skip this and just start with the *message* as the first parameter
   :type object: GObject or GBoxed
   :param string message: the info message to log

.. function:: Log.debug(object, message)

   Logs a debug message, like :c:macro:`wp_debug_object`

   :param object: optional object to associate the message with; you
      may skip this and just start with the *message* as the first parameter
   :type object: GObject or GBoxed
   :param string message: the debug message to log

.. function:: Log.trace(object, message)

   Logs a trace message, like :c:macro:`wp_trace_object`

   :param object: optional object to associate the message with; you
      may skip this and just start with the *message* as the first parameter
   :type object: GObject or GBoxed
   :param string message: the trace message to log

.. function:: Debug.dump_table(t)

   Prints a table with all its contents, recursively, to stdout
   for debugging purposes

   :param table t: any table
 0707010000006A000081A400000000000000000000000165F8630400000028000000000000000000000000000000000000004000000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_object_api.rst    .. _lua_object_api:

WpObject
========
0707010000006B000081A400000000000000000000000165F8630400001A03000000000000000000000000000000000000004900000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_object_interest_api.rst   .. _lua_object_interest_api:

Object Interest
===============

The Interest object allows you to declare interest in a specific object, or
a set of objects, and filter them. This is used in the
:ref:`ObjectManager <lua_object_manager_api>` but also in other places where
methods allow you to iterate over a set of objects or lookup a specific object.

*Interest* is a binding of :c:struct:`WpObjectInterest`, but it uses a
Lua-idiomatic way of construction instead of mapping directly the C functions,
for convenience.

Construction
~~~~~~~~~~~~

The most common use for *Interest* is in the
:ref:`ObjectManager <lua_object_manager_api>`, where you have to use the
following construction method to create it.

.. function:: Interest(decl)

   :param table decl: an interest declaration
   :returns: the interest
   :rtype: Interest (:c:struct:`WpObjectInterest`)

The interest consists of a GType and an array of constraints. It is constructed
with a table that contains all the constraints, plus the GType in the ``type``
field.

.. code-block:: lua

   local om = ObjectManager {
     Interest {
       type = "node",
       Constraint { "node.name", "matches", "alsa*", type = "pw-global" },
       Constraint { "media.class", "equals", "Audio/Sink", type = "pw-global" },
     }
   }

In the above example, the interest will match all objects of type
:c:struct:`WpNode` that contain the following 2 global properties:

  - "node.name", with a value that begins with the string "alsa"
  - "media.class", with a value that equals exactly the string "Audio/Sink"

When an object method requires an *Interest* as an argument, you may as well
directly pass the declaration table as argument instead of using the
:func:`Interest` constructor function. For instance,

.. code-block:: lua

   local fl_port = node:lookup_port {
     Constraint { "port.name", "equals", "playback_FL" }
   }

In the above example, we lookup a port in a node. The :func:`Node.lookup_port`
method takes an *Interest* as an argument, but we can pass the interest
declaration table directly. Note also that such a method does not require
declaring the ``type`` of the interest, as it has :c:struct:`WpPort` as a
hardcoded default.

The type
........

The type of an interest must be a valid GType, written as a string without the
"Wp" prefix and with the first letter optionally being lowercase. The type may
match any wireplumber object or interface.

Examples:

============= ============
 type string   parsed as
============= ============
node          WpNode
Node          WpNode
device        WpDevice
plugin        WpPlugin
siLink        WpSiLink
SiLink        WpSiLink
properties    WpProperties
============= ============

The Constraint
..............

The *Constraint* is constructed also with a table, like *Interest* itself. This
table must have the following items in this very strict order:

  - a subject string: a string that specifies the name of a property to match
  - a verb string: a string that specifies the match operation
  - an object, if the verb requires it

The verb may be any verb listed in :c:enum:`WpConstraintVerb`, using either
the nickname of the enum or the character value of it.

Allowed verbs:

============ ========= =======================================
nickname     character value
============ ========= =======================================
"equals"     "="       :c:enum:`WP_CONSTRAINT_VERB_EQUALS`
"not-equals" "!"       :c:enum:`WP_CONSTRAINT_VERB_NOT_EQUALS`
"in-list"    "c"       :c:enum:`WP_CONSTRAINT_VERB_IN_LIST`
"in-range"   "~"       :c:enum:`WP_CONSTRAINT_VERB_IN_RANGE`
"matches"    "#"       :c:enum:`WP_CONSTRAINT_VERB_MATCHES`
"is-present" "+"       :c:enum:`WP_CONSTRAINT_VERB_IS_PRESENT`
"is-absent"  "-"       :c:enum:`WP_CONSTRAINT_VERB_IS_ABSENT`
============ ========= =======================================

The values that are expected for each verb are primarily documented in
:c:func:`wp_object_interest_add_constraint`. In Lua, though, native types are
expected instead of GVariant and then they are converted according to the rules
documented in the :ref:`GObject Integration <lua_gobject>` page.

Examples of constraints:

.. code-block:: lua

   Constraint { "node.id", "equals", "42" }
   Constraint { "node.id", "equals", 42 }
   Constraint { "port.physical", "=", true }

   Constraint { "audio.channel", "not-equals", "FL" }

   Constraint { "node.name", "matches", "v4l2_input*" }
   Constraint { "format.dsp", "#", "*mono audio*" }

   -- matches either "default" or "settings" as a possible value for "metadata.name"
   Constraint { "metadata.name", "in-list", "default", "settings" }

   -- matches any priority.session between 0 and 1000, inclusive
   Constraint { "priority.session", "in-range", 0, 1000 }

   -- matches when the object has a "media.role" property
   Constraint { "media.role", "is-present" }

   -- matches when "media.role" is not present in the properties list
   Constraint { "media.role", "is-absent" }

Constraint types
................

PipeWire objects have multiple properties lists, therefore constraints also need
to have a way to specify on which property list they apply. The constraint type
may be any of the types listed in :c:enum:`WpConstraintType` and can be
specified with an additional field in the *Constraint* construction table,
called ``type``.

For instance:

.. code-block:: lua

   Constraint { "node.id", "equals", "42", type = "pw-global" }
   Constraint { "api.alsa.card.name", "matches", "*Intel*", type = "pw" }
   Constraint { "bound-id", "=", 42, type = "gobject" }

Valid types are:

========= ===============================================
type      value
========= ===============================================
pw-global :c:enum:`WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY`
pw        :c:enum:`WP_CONSTRAINT_TYPE_PW_PROPERTY`
gobject   :c:enum:`WP_CONSTRAINT_TYPE_G_PROPERTY`
========= ===============================================

Methods
~~~~~~~

.. function:: Interest.matches(self, obj)

   Binds :c:func:`wp_object_interest_matches`

   Checks if a specific object matches the interest. The object may be a GObject
   or a table convertible to :c:struct:`WpProperties`, if the interest is for
   properties.

   This is rarely useful to use directly on objects, but it may be useful to
   check if a specific table contains key-value pairs that match specific
   constraints, using "properties" as the interest type and passing a table as
   the object.

   :param self: the interest
   :param obj: an object to check for a match
   :type obj: GObject or table
   :returns: whether the object matches the interest
   :rtype: boolean
 0707010000006C000081A400000000000000000000000165F8630400000E61000000000000000000000000000000000000004800000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_object_manager_api.rst    .. _lua_object_manager_api:

Object Manager
==============

The ObjectManager (binding for :c:struct:`WpObjectManager`) provides a way to
collect a set of objects and be notified when objects that fulfill a certain
set of criteria are created or destroyed.

To start an object manager, you first need to declare interest in a certain
kind of object specifying a set of :ref:`Interests <lua_object_interest_api>`
in the constructor and then you need to activate it by calling
:func:`ObjectManager.activate`

Upon activating an ObjectManager, any pre-existing objects that match the
specified interests will immediately become available to get through
:func:`ObjectManager.iterate` and the :c:struct:`WpObjectManager` "object-added"
signal will be emitted for all of them.

Constructors
~~~~~~~~~~~~

.. function:: ObjectManager(interest_list)

   Constructs a new object manager.

   Combines :c:func:`wp_object_manager_new` and
   :c:func:`wp_object_manager_add_interest_full`

   The argument needs to be a table that contains one or more
   :ref:`Interest <lua_object_interest_api>` objects. The object manager
   will then contain all objects that match any one of the supplied interests.

   Example:

   .. code-block:: lua

      streams_om = ObjectManager {
        -- match stream nodes
        Interest {
          type = "node",
          Constraint { "media.class", "matches", "Stream/*", type = "pw-global" },
        },
        -- and device nodes that are not associated with any routes
        Interest {
          type = "node",
          Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
          Constraint { "device.routes", "equals", "0", type = "pw" },
        },
        Interest {
          type = "node",
          Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
          Constraint { "device.routes", "is-absent", type = "pw" },
        },
      }

   The above example will create an ObjectManager that matches all nodes with
   a "media.class" global property that starts with the string "Stream/"
   and additionally all those whose "media.class" starts with "Audio/" and
   they have either a "device.routes" property that equals zero or they
   don't have a "device.routes" property at all.

   :param table interest_list: a list of :ref:`interests <lua_object_interest_api>`
                               to objects
   :returns: a new object manager
   :rtype: ObjectManager (:c:struct:`WpObjectManager`)

Methods
~~~~~~~

.. function:: ObjectManager.activate(self)

   Activates the object manager.
   Binds :c:func:`wp_core_install_object_manager`.

   :param self: the object manager

.. function:: ObjectManager.get_n_objects(self)

    Binds :c:func:`wp_object_manager_get_n_objects`

   :param self: the object manager
   :returns: the number of objects managed by the object manager
   :rtype: integer

.. function:: ObjectManager.iterate(self, interest)

   Binds :c:func:`wp_object_manager_new_filtered_iterator_full`

   :param self: the object manager
   :param interest: an interest to filter objects
   :type interest: :ref:`Interest <lua_object_interest_api>` or nil or none
   :returns: all the managed objects that match the interest
   :rtype: Iterator; the iteration items are of type :ref:`GObject <lua_gobject>`

.. function:: ObjectManager.lookup(self, interest)

   Binds :c:func:`wp_object_manager_lookup`

   :param self: the object manager
   :param interest: the interest to use for the lookup
   :type interest: :ref:`Interest <lua_object_interest_api>` or nil or none
   :returns: the first managed object that matches the interest
   :rtype: :ref:`GObject <lua_gobject>`
   0707010000006D000081A400000000000000000000000165F8630400001530000000000000000000000000000000000000004100000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_proxies_api.rst   .. _lua_proxies_api:

PipeWire Proxies
================

Proxy
.....

Lua objects that bind a :ref:`WpProxy <proxy_api>` contain the following methods:

.. function:: Proxy.get_interface_type(self)

   Binds :c:func:`wp_proxy_get_interface_type`

   :param self: the proxy
   :returns: the proxy type, the proxy type version
   :rtype: string, integer

PipeWire Object
...............

Lua objects that bind a :ref:`WpPipewireObject <pipewire_object_api>`
contain the following methods:

.. function:: PipewireObject.iterate_params(self, param_name)

   Binds :c:func:`wp_pipewire_object_enum_params_sync`

   :param self: the proxy
   :param string param_name: the PipeWire param name to enumerate,
                             ex "Props", "Route"
   :returns: the available parameters
   :rtype: Iterator; the iteration items are Spa Pod objects

.. function:: PipewireObject.set_param(self, param_name, pod)

   Binds :c:func:`wp_pipewire_object_set_param`

   :param self: the proxy
   :param string param_name: The PipeWire param name to set, ex "Props", "Route"
   :param Pod pod: A Spa Pod object containing the new params

Global Proxy
............

Lua objects that bind a :ref:`WpGlobalProxy <global_proxy_api>`
contain the following methods:

.. function:: GlobalProxy.request_destroy(self)

   Binds :c:func:`wp_global_proxy_request_destroy`

   :param self: the proxy

PipeWire Node
.............

Lua objects that bind a :ref:`WpNode <node_api>` contain the following methods:

.. function:: Node.get_state(self)

   Binds :c:func:`wp_node_get_state`

   :param self: the proxy
   :returns: the current state of the node and an error message, if any
   :rtype: string (:c:enum:`WpNodeState`), string (error message)
   :since: 0.4.2

.. function:: Node.get_n_input_ports(self)

   Binds :c:func:`wp_node_get_n_input_ports`

   :param self: the proxy
   :returns: the current and max numbers of input ports on the node
   :rtype: integer (current), integer (max)
   :since: 0.4.2

.. function:: Node.get_n_output_ports(self)

   Binds :c:func:`wp_node_get_n_output_ports`

   :param self: the proxy
   :returns: the current and max numbers of output ports on the node
   :rtype: integer (current), integer (max)
   :since: 0.4.2

.. function:: Node.get_n_ports(self)

   Binds :c:func:`wp_node_get_n_ports`

   :param self: the proxy
   :returns: the number of ports on the node
   :since: 0.4.2

.. function:: Node.iterate_ports(self, interest)

   Binds :c:func:`wp_node_iterate_ports`

   :param self: the proxy
   :param interest: an interest to filter objects
   :type interest: :ref:`Interest <lua_object_interest_api>` or nil or none
   :returns: all the ports of this node that that match the interest
   :rtype: Iterator; the iteration items are of type :ref:`WpPort <port_api>`
   :since: 0.4.2

.. function:: Node.lookup_port(self, interest)

   Binds :c:func:`wp_node_lookup_port`

   :param self: the proxy
   :param interest: the interest to use for the lookup
   :type interest: :ref:`Interest <lua_object_interest_api>` or nil or none
   :returns: the first port of this node that matches the interest
   :rtype: :ref:`WpPort <port_api>`
   :since: 0.4.2

.. function:: Node.send_command(self, command)

   Binds :c:func:`wp_node_send_command`

   :param self: the proxy
   :param string command: the command to send to the node (ex "Suspend")

PipeWire Port
.............

Lua objects that bind a :ref:`WpPort <port_api>` contain the following methods:

.. function:: Port.get_direction(self)

   Binds :c:func:`wp_port_get_direction`

   :param self: the port
   :returns: the direction of the Port
   :rtype: string (:c:enum:`WpDirection`)
   :since: 0.4.2

PipeWire Client
...............

Lua objects that bind a :ref:`WpClient <client_api>`
contain the following methods:

.. function:: Client.update_permissions(self, perms)

   Binds :c:func:`wp_client_update_permissions`

   Takes a table where the keys are object identifiers and the values are
   permission strings.

   Valid object identifiers are:

   - A number, meaning the bound ID of a proxy
   - The string "any" or the string "all", which sets the default permissions
     for this client

   The permission strings have a chmod-like syntax (ex. "rwx" or "r-xm"), where:

   - "r" means permission to read the object
   - "w" means permission to write data to the object
   - "x" means permission to call methods on the object
   - "m" means permission to set metadata for the object
   - "-" is ignored and can be used to make the string more readable when
     a permission flag is omitted

   **Example:**

   .. code-block:: lua

      client:update_permissions {
        ["all"] = "r-x",
        [35] = "rwxm",
      }

   :param self: the proxy
   :param table perms: the permissions to update for this client

PipeWire Metadata
.................

Lua objects that bind a :ref:`WpMetadata <metadata_api>`
contain the following methods:

.. function:: Metadata.iterate(self, subject)

   Binds :c:func:`wp_metadata_new_iterator`

   :param self: the proxy
   :param integer subject: the subject id
   :returns: an iterator

.. function:: Metadata.find(self, subject, key)

   Binds :c:func:`wp_metadata_find`

   :param self: the proxy
   :param string subject: the subject id
   :param string key: the metadata key to find
   :returns: the value for this metadata key, the type of the value
   :rtype: string, string
0707010000006E000081A400000000000000000000000165F86304000003E0000000000000000000000000000000000000004600000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_session_item_api.rst  .. _lua_session_item_api:

Session Item
============

Lua objects that bind a :ref:`WpSessionItem <session_item_api>`
contain the following methods:

.. function:: SessionItem.get_associated_proxy(self, type)

   Binds :c:func:`wp_session_item_get_associated_proxy`

   :param self: the session item
   :param string type: the proxy type name
   :returns: the proxy object or nil

.. function:: SessionItem.reset(self)

   Binds :c:func:`wp_session_item_reset`

   :param self: the session item

.. function:: SessionItem.configure(self, properties)

   Binds :c:func:`wp_session_item_configure`

   :param self: the session item
   :param table properties: The configuration properties
   :returns: true on success, false on failure
   :rtype: boolean

.. function:: SessionItem.register(self)

   Binds :c:func:`wp_session_item_register`

   :param self: the session item

.. function:: SessionItem.remove(self)

   Binds :c:func:`wp_session_item_remove`

   :param self: the session item
0707010000006F000081A400000000000000000000000165F8630400000234000000000000000000000000000000000000004400000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_spa_device_api.rst    .. _lua_spa_device_api:

Spa Device
==========

.. function:: SpaDevice.get_managed_object(self, id)

   Binds :c:func:`wp_spa_device_get_managed_object`

   :param self: the spa device
   :param integer id: the object id
   :returns: the managed object or nil

.. function:: SpaDevice.store_managed_object(self, id, object)

   Binds :c:func:`wp_spa_device_store_managed_object`

   :param self: the spa device
   :param integer id: the object id
   :param GObject object: a GObject to store or nil to remove the existing
                          stored object
07070100000070000081A400000000000000000000000165F8630400000023000000000000000000000000000000000000003D00000000wireplumber-0.5.0/docs/rst/scripting/lua_api/lua_spa_pod.rst   .. _lua_spa_pod:

Spa Pod
=======
 07070100000071000081A400000000000000000000000165F8630400000198000000000000000000000000000000000000003900000000wireplumber-0.5.0/docs/rst/scripting/lua_api/meson.build  # you need to add here any files you add to the toc directory as well
sphinx_files += files(
  'lua_core_api.rst',
  'lua_gobject.rst',
  'lua_local_module_api.rst',
  'lua_introduction.rst',
  'lua_log_api.rst',
  'lua_object_api.rst',
  'lua_object_interest_api.rst',
  'lua_object_manager_api.rst',
  'lua_proxies_api.rst',
  'lua_session_item_api.rst',
  'lua_spa_device_api.rst',
  'lua_spa_pod.rst',
)
07070100000072000081A400000000000000000000000165F86304000000B8000000000000000000000000000000000000003100000000wireplumber-0.5.0/docs/rst/scripting/meson.build  # you need to add here any files you add to the toc directory as well
sphinx_files += files(
  'lua_api.rst',
  'existing_scripts.rst',
)

subdir('lua_api')
subdir('existing_scripts')
07070100000073000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001600000000wireplumber-0.5.0/lib 07070100000074000081A400000000000000000000000165F863040000000D000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/meson.build subdir('wp')
   07070100000075000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001900000000wireplumber-0.5.0/lib/wp  07070100000076000081A400000000000000000000000165F8630400003196000000000000000000000000000000000000002500000000wireplumber-0.5.0/lib/wp/base-dirs.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "base-dirs.h"
#include "log.h"
#include "wpversion.h"
#include "wpbuildbasedirs.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-base-dirs")

/*!
 * \defgroup wpbasedirs Base Directories File Lookup
 */

/* Returns /basedir/subdir/filename, with filename treated as a module
 * if WP_BASE_DIRS_FLAG_MODULE is set.
 * The basedir is assumed to be either an absolute path or NULL.
 * The subdir is assumed to be a path relative to basedir or NULL.
 */
static gchar *
make_path (guint flags, const gchar *basedir, const gchar *subdir,
    const gchar *filename)
{
  g_autofree gchar *full_basedir = NULL;
  g_autofree gchar *full_filename = NULL;

  /* merge subdir into basedir, if necessary */
  if (subdir) {
    full_basedir = g_canonicalize_filename (subdir, basedir);
    basedir = full_basedir;
  }

  if (flags & WP_BASE_DIRS_FLAG_MODULE) {
    g_autofree gchar *basename = g_path_get_basename (filename);
    g_autofree gchar *dirname = g_path_get_dirname (filename);
    const gchar *prefix = "";
    const gchar *suffix = "";
    if (!g_str_has_prefix (basename, "lib"))
      prefix = "lib";
    if (!g_str_has_suffix (basename, ".so"))
      suffix = ".so";
    full_filename = g_strconcat (dirname, G_DIR_SEPARATOR_S,
                                 prefix, basename, suffix, NULL);
    filename = full_filename;
  }

  return g_canonicalize_filename (filename, basedir);
}

static GPtrArray *
lookup_dirs (guint flags, gboolean is_absolute)
{
  g_autoptr(GPtrArray) dirs = g_ptr_array_new_with_free_func (g_free);
  const gchar *dir;
  const gchar *subdir =
      (flags & WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER) ? "wireplumber" : ".";

  /* Compile the list of lookup directories in priority order */
  if (is_absolute) {
    g_ptr_array_add (dirs, NULL);
  }
  else if ((flags & WP_BASE_DIRS_ENV_CONFIG) &&
      (dir = g_getenv ("WIREPLUMBER_CONFIG_DIR"))) {
    g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
    for (guint i = 0; env_dirs[i]; i++) {
      g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
    }
  }
  else if ((flags & WP_BASE_DIRS_ENV_DATA) &&
      (dir = g_getenv ("WIREPLUMBER_DATA_DIR"))) {
    g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
    for (guint i = 0; env_dirs[i]; i++) {
      g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
    }
  }
  else if ((flags & WP_BASE_DIRS_ENV_MODULE) &&
      (dir = g_getenv ("WIREPLUMBER_MODULE_DIR"))) {
    g_auto (GStrv) env_dirs = g_strsplit (dir, G_SEARCHPATH_SEPARATOR_S, 0);
    for (guint i = 0; env_dirs[i]; i++) {
      g_ptr_array_add (dirs, g_canonicalize_filename (env_dirs[i], NULL));
    }
  }
  else {
    if (flags & WP_BASE_DIRS_XDG_CONFIG_HOME) {
      dir = g_get_user_config_dir ();
      if (G_LIKELY (g_path_is_absolute (dir)))
        g_ptr_array_add (dirs, g_canonicalize_filename (subdir, dir));
    }
    if (flags & WP_BASE_DIRS_XDG_DATA_HOME) {
      dir = g_get_user_data_dir ();
      if (G_LIKELY (g_path_is_absolute (dir)))
        g_ptr_array_add (dirs, g_canonicalize_filename (subdir, dir));
    }
    if (flags & WP_BASE_DIRS_XDG_CONFIG_DIRS) {
      const gchar * const *xdg_dirs = g_get_system_config_dirs ();
      for (guint i = 0; xdg_dirs[i]; i++) {
        if (G_LIKELY (g_path_is_absolute (xdg_dirs[i])))
          g_ptr_array_add (dirs, g_canonicalize_filename (subdir, xdg_dirs[i]));
      }
    }
    if (flags & WP_BASE_DIRS_BUILD_SYSCONFDIR) {
      g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_SYSCONFDIR));
    }
    if (flags & WP_BASE_DIRS_XDG_DATA_DIRS) {
      const gchar * const *xdg_dirs = g_get_system_data_dirs ();
      for (guint i = 0; xdg_dirs[i]; i++) {
        if (G_LIKELY (g_path_is_absolute (xdg_dirs[i])))
          g_ptr_array_add (dirs, g_canonicalize_filename (subdir, xdg_dirs[i]));
      }
    }
    if (flags & WP_BASE_DIRS_BUILD_DATADIR) {
      g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_DATADIR));
    }
    if (flags & WP_BASE_DIRS_BUILD_LIBDIR) {
      subdir = (flags & WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER) ?
          "wireplumber-" WIREPLUMBER_API_VERSION : ".";
      g_ptr_array_add (dirs, g_canonicalize_filename (subdir, BUILD_LIBDIR));
    }
  }

  return g_steal_pointer (&dirs);
}

/*!
 * \brief Searches for \a filename in the hierarchy of directories specified
 * by the \a flags parameter
 *
 * Returns the highest priority file found in the hierarchy of directories
 * specified by the \a flags parameter. The \a subdir parameter is the name
 * of the subdirectory to search in, inside the specified directories. If
 * \a subdir is NULL, the base path of each directory is used.
 *
 * The \a filename parameter is the name of the file to search for. If the
 * file is found, its full path is returned. If the file is not found, NULL
 * is returned. The file is considered found if it is a regular file.
 *
 * If the \a filename is an absolute path, it is tested for existence and
 * returned as is, ignoring the lookup directories in \a flags as well as
 * the \a subdir parameter.
 *
 * \ingroup wpbasedirs
 * \param flags flags to specify the directories to look into and other
 *   options specific to the kind of file being looked up
 * \param subdir (nullable): the name of the subdirectory to search in,
 *   inside the specified directories
 * \param filename the name of the file to search for
 * \returns (transfer full) (nullable): A newly allocated string with the
 *   absolute, canonicalized file path, or NULL if the file was not found.
 * \since 0.5.0
 */
gchar *
wp_base_dirs_find_file (WpBaseDirsFlags flags, const gchar * subdir,
    const gchar * filename)
{
  gboolean is_absolute = g_path_is_absolute (filename);
  g_autoptr (GPtrArray) dir_paths = lookup_dirs (flags, is_absolute);
  gchar *ret = NULL;

  /* ignore the subdir if filename is absolute */
  if (is_absolute)
    subdir = NULL;

  for (guint i = 0; i < dir_paths->len; i++) {
    g_autofree gchar *path = make_path (flags, g_ptr_array_index (dir_paths, i),
                                        subdir, filename);
    wp_trace ("test file: %s", path);
    if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) {
      ret = g_steal_pointer (&path);
      break;
    }
  }

  wp_debug ("lookup '%s', return: %s", filename, ret);
  return ret;
}

struct conffile_iterator_item
{
  gchar *filename;
  gchar *path;
};

static void
conffile_iterator_item_clear (struct conffile_iterator_item *item)
{
  g_free (item->filename);
  g_free (item->path);
}

struct conffile_iterator_data
{
  GArray *items;
  guint idx;
};

static void
conffile_iterator_reset (WpIterator *it)
{
  struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->idx = 0;
}

static gboolean
conffile_iterator_next (WpIterator *it, GValue *item)
{
  struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);

  if (it_data->idx < it_data->items->len) {
    const gchar *path = g_array_index (it_data->items,
        struct conffile_iterator_item, it_data->idx).path;
    it_data->idx++;
    g_value_init (item, G_TYPE_STRING);
    g_value_set_string (item, path);
    return TRUE;
  }
  return FALSE;
}

static gboolean
conffile_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
    gpointer data)
{
  struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);

  for (guint i = 0; i < it_data->items->len; i++) {
    g_auto (GValue) item = G_VALUE_INIT;
    const gchar *path = g_array_index (it_data->items,
        struct conffile_iterator_item, i).path;
    g_value_init (&item, G_TYPE_STRING);
    g_value_set_string (&item, path);
    if (!func (&item, ret, data))
      return FALSE;
  }
  return TRUE;
}

static void
conffile_iterator_finalize (WpIterator *it)
{
  struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
  g_clear_pointer (&it_data->items, g_array_unref);
}

static const WpIteratorMethods conffile_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = conffile_iterator_reset,
  .next = conffile_iterator_next,
  .fold = conffile_iterator_fold,
  .finalize = conffile_iterator_finalize,
};

static gint
conffile_iterator_item_compare (const struct conffile_iterator_item *a,
    const struct conffile_iterator_item *b)
{
  return g_strcmp0 (a->filename, b->filename);
}

/*!
 * \brief Creates an iterator to iterate over all files that match \a suffix
 * within the \a subdir of the directories specified in \a flags
 *
 * The \a subdir parameter is the name of the subdirectory to search in,
 * inside the directories specified by \a flags. If \a subdir is NULL,
 * the base path of each directory is used. If \a subdir is an absolute path,
 * files are only looked up in that directory and the directories in \a flags
 * are ignored.
 *
 * The \a suffix parameter is the filename suffix to match. If \a suffix is
 * NULL, all files are matched.
 *
 * The iterator will iterate over the absolute paths of all the files
 * files found, in the order of priority of the directories, starting from
 * the lowest priority directory (e.g. /usr/share/wireplumber) and ending
 * with the highest priority directory (e.g. $XDG_CONFIG_HOME/wireplumber).
 * Files within each directory are also sorted by filename.
 *
 * \ingroup wpbasedirs
 * \param flags flags to specify the directories to look into and other
 *   options specific to the kind of file being looked up
 * \param subdir (nullable): the name of the subdirectory to search in,
 *   inside the configuration directories
 * \param suffix (nullable): The filename suffix, NULL matches all entries
 * \returns (transfer full): a new iterator iterating over strings which are
 *   absolute & canonicalized paths to the files found
 * \since 0.5.0
 */
WpIterator *
wp_base_dirs_new_files_iterator (WpBaseDirsFlags flags,
    const gchar * subdir, const gchar * suffix)
{
  g_autoptr (GArray) items =
      g_array_new (FALSE, FALSE, sizeof (struct conffile_iterator_item));
  g_autoptr (GPtrArray) dir_paths = NULL;

  g_array_set_clear_func (items, (GDestroyNotify) conffile_iterator_item_clear);

  if (subdir == NULL)
    subdir = ".";

  /* Note: this list is highest-priority first */
  dir_paths = lookup_dirs (flags, g_path_is_absolute (subdir));

  /* Run backwards through the list to get files in lowest-priority-first order */
  for (guint i = dir_paths->len; i > 0; i--) {
    g_autofree gchar *dirpath =
        g_canonicalize_filename (subdir, g_ptr_array_index (dir_paths, i - 1));
    g_autoptr (GDir) dir = g_dir_open (dirpath, 0, NULL);

    if (dir) {
      g_autoptr (GArray) dir_items = g_array_new (FALSE, FALSE,
          sizeof (struct conffile_iterator_item));

      wp_trace ("searching dir: %s", dirpath);

      /* Store all filenames with their full path in the local array */
      const gchar *filename;
      while ((filename = g_dir_read_name (dir))) {
        if (filename[0] == '.')
          continue;

        if (suffix && !g_str_has_suffix (filename, suffix))
          continue;

        /* verify the file is regular and canonicalize the path */
        g_autofree gchar *path = make_path (flags, dirpath, NULL, filename);
        if (!g_file_test (path, G_FILE_TEST_IS_REGULAR))
          continue;

        /* remove item with the same filename from the global items array,
           so that lower priority files can be shadowed */
        for (guint j = 0; j < items->len; j++) {
          struct conffile_iterator_item *item = &g_array_index (items,
              struct conffile_iterator_item, j);
          if (g_strcmp0 (item->filename, filename) == 0) {
            g_array_remove_index (items, j);
            break;
          }
        }

        /* append in the local array */
        g_array_append_val (dir_items, ((struct conffile_iterator_item) {
          .filename = g_strdup (filename),
          .path = g_steal_pointer (&path),
        }));
      }

      /* Sort files of the current dir by filename */
      g_array_sort (dir_items, (GCompareFunc) conffile_iterator_item_compare);

      /* Append the sorted files to the global array */
      g_array_append_vals (items, dir_items->data, dir_items->len);
    }
  }

  /* Construct iterator */
  WpIterator *it = wp_iterator_new (&conffile_iterator_methods,
      sizeof (struct conffile_iterator_data));
  struct conffile_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->items = g_steal_pointer (&items);
  it_data->idx = 0;
  return g_steal_pointer (&it);
}
  07070100000077000081A400000000000000000000000165F8630400000C23000000000000000000000000000000000000002500000000wireplumber-0.5.0/lib/wp/base-dirs.h  /* WirePlumber
 *
 * Copyright © 2024 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_BASE_DIRS_H__
#define __WIREPLUMBER_BASE_DIRS_H__

#include "defs.h"
#include "iterator.h"

G_BEGIN_DECLS

/*!
 * \brief Flags to specify lookup directories
 * \ingroup wpbasedirs
 *
 * These flags can be used to specify which directories to look for a file in.
 * The flags can be combined to search in multiple directories at once. Some
 * flags may also used to specify the type of the file being looked up or other
 * lookup parameters.
 *
 * Lookup is performed in the same order as the flags are listed here. Note that
 * if a WirePlumber-specific environment variable is set ($WIREPLUMBER_*_DIR)
 * and the equivalent WP_BASE_DIRS_ENV_* flag is specified, the lookup in other
 * directories is skipped, even if the file is not found in the
 * environment-specified directory.
 */
typedef enum { /*< flags >*/
  WP_BASE_DIRS_ENV_CONFIG = (1 << 0),       /*!< $WIREPLUMBER_CONFIG_DIR */
  WP_BASE_DIRS_ENV_DATA = (1 << 1),         /*!< $WIREPLUMBER_DATA_DIR */
  WP_BASE_DIRS_ENV_MODULE = (1 << 2),       /*!< $WIREPLUMBER_MODULE_DIR */

  WP_BASE_DIRS_XDG_CONFIG_HOME = (1 << 8),  /*!< XDG_CONFIG_HOME */
  WP_BASE_DIRS_XDG_DATA_HOME = (1 << 9),    /*!< XDG_DATA_HOME */

  WP_BASE_DIRS_XDG_CONFIG_DIRS = (1 << 10), /*!< XDG_CONFIG_DIRS */
  WP_BASE_DIRS_BUILD_SYSCONFDIR = (1 << 11), /*!< compile-time $sysconfdir (/etc) */

  WP_BASE_DIRS_XDG_DATA_DIRS = (1 << 12),   /*!< XDG_DATA_DIRS */
  WP_BASE_DIRS_BUILD_DATADIR = (1 << 13),   /*!< compile-time $datadir ($prefix/share) */

  WP_BASE_DIRS_BUILD_LIBDIR = (1 << 14),    /*!< compile-time $libdir ($prefix/lib) */

  /*! the file is a loadable module; prepend "lib" and append ".so" if needed */
  WP_BASE_DIRS_FLAG_MODULE = (1 << 24),

  /*! append "/wireplumber" to the location, except in the case of locations
      that are specified via WirePlumber-specific environment variables;
      in LIBDIR, append "/wireplumber-$API_version" instead */
  WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER = (1 << 25),

  WP_BASE_DIRS_CONFIGURATION =
      (WP_BASE_DIRS_ENV_CONFIG |
       WP_BASE_DIRS_XDG_CONFIG_HOME |
       WP_BASE_DIRS_XDG_CONFIG_DIRS |
       WP_BASE_DIRS_BUILD_SYSCONFDIR |
       WP_BASE_DIRS_XDG_DATA_DIRS |
       WP_BASE_DIRS_BUILD_DATADIR |
       WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),

  WP_BASE_DIRS_DATA =
      (WP_BASE_DIRS_ENV_DATA |
       WP_BASE_DIRS_XDG_DATA_HOME |
       WP_BASE_DIRS_XDG_DATA_DIRS |
       WP_BASE_DIRS_BUILD_DATADIR |
       WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),

  WP_BASE_DIRS_MODULE =
      (WP_BASE_DIRS_ENV_MODULE |
       WP_BASE_DIRS_BUILD_LIBDIR |
       WP_BASE_DIRS_FLAG_MODULE |
       WP_BASE_DIRS_FLAG_SUBDIR_WIREPLUMBER),
} WpBaseDirsFlags;

WP_API
gchar * wp_base_dirs_find_file (WpBaseDirsFlags flags,
    const gchar * subdir, const gchar * filename);

WP_API
WpIterator * wp_base_dirs_new_files_iterator (WpBaseDirsFlags flags,
    const gchar * subdir, const gchar * suffix);

G_END_DECLS

#endif
 07070100000078000081A400000000000000000000000165F863040000192B000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/client.c /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "client.h"
#include "log.h"
#include "private/pipewire-object-mixin.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-client")

/*! \defgroup wpclient WpClient */
/*!
 * \struct WpClient
 *
 * The WpClient class allows accessing the properties and methods of a PipeWire
 * client object (`struct pw_client`). A WpClient is constructed internally
 * when a new client connects to PipeWire and it is made available through the
 * WpObjectManager API.
 */

struct _WpClient
{
  WpGlobalProxy parent;
};

static void wp_client_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface);

G_DEFINE_TYPE_WITH_CODE (WpClient, wp_client, WP_TYPE_GLOBAL_PROXY,
    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
        wp_pw_object_mixin_object_interface_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
        wp_client_pw_object_mixin_priv_interface_init))

static void
wp_client_init (WpClient * self)
{
}

static void
wp_client_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case WP_PW_OBJECT_MIXIN_STEP_BIND:
  case WP_TRANSITION_STEP_ERROR:
    /* base class can handle BIND and ERROR */
    WP_OBJECT_CLASS (wp_client_parent_class)->
        activate_execute_step (object, transition, step, missing);
    break;
  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
    /* just wait, info will be emitted anyway after binding */
    break;
  default:
    g_assert_not_reached ();
  }
}

static const struct pw_client_events client_events = {
  PW_VERSION_CLIENT_EVENTS,
  .info = (HandleEventInfoFunc(client)) wp_pw_object_mixin_handle_event_info,
};

static void
wp_client_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
      client, &client_events);
}

static void
wp_client_pw_proxy_destroyed (WpProxy * proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);

  WP_PROXY_CLASS (wp_client_parent_class)->pw_proxy_destroyed (proxy);
}

static void
wp_client_class_init (WpClientClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->get_property = wp_pw_object_mixin_get_property;

  wpobject_class->get_supported_features =
      wp_pw_object_mixin_get_supported_features;
  wpobject_class->activate_get_next_step =
      wp_pw_object_mixin_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_client_activate_execute_step;

  proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Client;
  proxy_class->pw_iface_version = PW_VERSION_CLIENT;
  proxy_class->pw_proxy_created = wp_client_pw_proxy_created;
  proxy_class->pw_proxy_destroyed = wp_client_pw_proxy_destroyed;

  wp_pw_object_mixin_class_override_properties (object_class);
}

static void
wp_client_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface)
{
  wp_pw_object_mixin_priv_interface_info_init_no_params (iface, client, CLIENT);
}

/*!
 * \brief Send an error to the client
 *
 * \ingroup wpclient
 * \param self the client
 * \param id the global id to report the error on
 * \param res an errno style error code
 * \param message the error message string
 */
void
wp_client_send_error (WpClient * self, guint32 id, int res,
    const gchar * message)
{
  struct pw_client *pwp;

  g_return_if_fail (WP_IS_CLIENT (self));

  pwp = (struct pw_client *) wp_proxy_get_pw_proxy (WP_PROXY (self));
  g_return_if_fail (pwp != NULL);

  pw_client_error (pwp, id, res, message);
}

/*!
 * \brief Update client's permissions on a list of objects.
 *
 * An object id of `-1` can be used to set the default object permissions
 * for this client
 *
 * \ingroup wpclient
 * \param self the client
 * \param n_perm the number of permissions specified in the variable arguments
 * \param ... \a n_perm pairs of guint32 numbers; the first number is the
 *   object id and the second is the permissions that this client should have
 *   on this object
 */
void
wp_client_update_permissions (WpClient * self, guint n_perm, ...)
{
  va_list args;
  struct pw_permission *perm =
      g_alloca (n_perm * sizeof (struct pw_permission));

  va_start (args, n_perm);
  for (guint i = 0; i < n_perm; i++) {
    perm[i].id = va_arg (args, guint32);
    perm[i].permissions = va_arg (args, guint32);
  }
  va_end (args);

  wp_client_update_permissions_array (self, n_perm, perm);
}

/*!
 * \brief Update client's permissions on a list of objects.
 *
 * An object id of `-1` can be used to set the default object permissions
 * for this client
 *
 * \ingroup wpclient
 * \param self the client
 * \param n_perm the number of permissions specified in the \a permissions array
 * \param permissions (array length=n_perm) (element-type pw_permission): an array
 *    of permissions per object id
 */
void
wp_client_update_permissions_array (WpClient * self,
    guint n_perm, const struct pw_permission *permissions)
{
  struct pw_client *pwp;
  int client_update_permissions_result;

  g_return_if_fail (WP_IS_CLIENT (self));

  pwp = (struct pw_client *) wp_proxy_get_pw_proxy (WP_PROXY (self));
  g_return_if_fail (pwp != NULL);

  client_update_permissions_result = pw_client_update_permissions (
      pwp, n_perm, permissions);
  g_warn_if_fail (client_update_permissions_result >= 0);
}

/*!
 * \brief Updates the properties of \a self
 *
 * This requires W and X permissions on the client.
 *
 * \ingroup wpclient
 * \param self the client
 * \param updates (transfer full): updates to apply to the properties of
 *    \a self; this does not need to include properties that have not changed
 */
void
wp_client_update_properties (WpClient * self, WpProperties * updates)
{
  g_autoptr (WpProperties) upd = updates;
  struct pw_client *pwp;
  int client_update_properties_result;

  g_return_if_fail (WP_IS_CLIENT (self));
  g_return_if_fail (updates != NULL);

  pwp = (struct pw_client *) wp_proxy_get_pw_proxy (WP_PROXY (self));
  g_return_if_fail (pwp != NULL);

  client_update_properties_result = pw_client_update_properties (
      pwp, wp_properties_peek_dict (upd));

  g_warn_if_fail (client_update_properties_result >= 0);
}
 07070100000079000081A400000000000000000000000165F8630400000378000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/client.h /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_CLIENT_H__
#define __WIREPLUMBER_CLIENT_H__

#include "global-proxy.h"

G_BEGIN_DECLS

struct pw_permission;

/*!
 * \brief The WpClient GType
 * \ingroup wpclient
 */
#define WP_TYPE_CLIENT (wp_client_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpClient, wp_client, WP, CLIENT, WpGlobalProxy)

WP_API
void wp_client_send_error (WpClient * self, guint32 id, int res,
    const gchar * message);

WP_API
void wp_client_update_permissions (WpClient * self, guint n_perm, ...);

WP_API
void wp_client_update_permissions_array (WpClient * self,
    guint n_perm, const struct pw_permission *permissions);

WP_API
void wp_client_update_properties (WpClient * self, WpProperties * updates);

G_END_DECLS

#endif
0707010000007A000081A400000000000000000000000165F8630400001A12000000000000000000000000000000000000002C00000000wireplumber-0.5.0/lib/wp/component-loader.c   /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "component-loader.h"
#include "log.h"
#include "error.h"
#include "private/registry.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-comp-loader")

/*! \defgroup wpcomponentloader WpComponentLoader */
/*!
 * \struct WpComponentLoader
 *
 * An interface that provides the ability to load components.
 *
 * Components can be:
 *  - WirePlumber modules (libraries that provide WpPlugin and WpSiFactory objects)
 *  - Scripts (ex. lua scripts)
 *
 * The WirePlumber library provides built-in support for loading WirePlumber
 * modules, without a component loader. For other kinds of components,
 * a component loader is meant to be provided in by some WirePlumber module.
 * For Lua scripts specifically, a component loader is provided by the lua
 * scripting module.
 */

G_DEFINE_INTERFACE (WpComponentLoader, wp_component_loader, G_TYPE_OBJECT)

static void
wp_component_loader_default_init (WpComponentLoaderInterface * iface)
{
}

static gboolean
find_component_loader_func (gpointer cl, gpointer type)
{
  if (WP_IS_COMPONENT_LOADER (cl) &&
      (WP_COMPONENT_LOADER_GET_IFACE (cl)->supports_type (
            WP_COMPONENT_LOADER (cl), (const gchar *) type)))
    return TRUE;

  return FALSE;
}

static WpComponentLoader *
wp_component_loader_find (WpCore * core, const gchar * type)
{
  g_return_val_if_fail (WP_IS_CORE (core), NULL);
  GObject *c = wp_core_find_object (core,
      (GEqualFunc) find_component_loader_func, type);
  return c ? WP_COMPONENT_LOADER (c) : NULL;
}

static void
wp_component_loader_load (WpComponentLoader * self, WpCore * core,
    const gchar * component, const gchar * type, WpSpaJson * args,
    GCancellable * cancellable, GAsyncReadyCallback callback, gpointer data)
{
  WP_COMPONENT_LOADER_GET_IFACE (self)->load (self, core, component, type,
      args, cancellable, callback, data);
}

static GObject *
wp_component_loader_load_finish (WpComponentLoader * self, GAsyncResult * res,
    GError ** error)
{
  return WP_COMPONENT_LOADER_GET_IFACE (self)->load_finish (self, res, error);
}

static void
wp_component_loader_load_task_return (GTask * task, gpointer object)
{
  WpCore *core = g_task_get_source_object (task);
  WpRegistry *reg = wp_core_get_registry (core);
  gchar *provides = g_task_get_task_data (task);

  /* store object in the registry */
  if (object)
    wp_core_register_object (core, g_object_ref (object));

  if (provides)
    wp_registry_mark_feature_provided (reg, provides);

  g_task_return_boolean (task, TRUE);
}

static void
on_object_activated (WpObject * object, GAsyncResult * res, gpointer data)
{
  g_autoptr (GTask) task = G_TASK (data);
  g_autoptr (GError) error = NULL;

  if (!wp_object_activate_finish (object, res, &error)) {
    g_task_return_error (task, g_steal_pointer (&error));
    return;
  }

  wp_component_loader_load_task_return (task, object);
}

static void
on_component_loader_load_done (WpComponentLoader * cl, GAsyncResult * res,
    gpointer data)
{
  g_autoptr (GTask) task = G_TASK (data);
  g_autoptr (GError) error = NULL;
  g_autoptr (GObject) object = NULL;

  object = wp_component_loader_load_finish (cl, res, &error);
  if (error) {
    g_task_return_error (task, g_steal_pointer (&error));
    return;
  }

  if (object) {
    wp_trace_object (cl, "loaded object " WP_OBJECT_FORMAT, WP_OBJECT_ARGS (object));

    if (WP_IS_OBJECT (object)) {
      /* WpObject needs to be activated */
      wp_object_activate (WP_OBJECT (object), WP_OBJECT_FEATURES_ALL, NULL,
          (GAsyncReadyCallback) on_object_activated, g_steal_pointer (&task));
      return;
    }
  }

  wp_component_loader_load_task_return (task, object);
}

/*!
 * \brief Loads the specified \a component on \a self
 *
 * The \a type will determine which component loader to use. The following types
 * are built-in and will always work without a component loader:
 *  - "module" - Loads a WirePlumber module
 *  - "array" - Loads multiple components interpreting the \a args as a JSON
 *    array with component definitions, as they would appear in the
 *    configuration file. When this type is used, \a component is ignored and
 *    can be NULL
 *
 * \ingroup wpcomponentloader
 * \param self the core
 * \param component (nullable): the module name or file name
 * \param type the type of the component
 * \param args (transfer none)(nullable): additional arguments for the component,
 *   expected to be a JSON object
 * \param provides (nullable): the name of the feature that this component will
 *   provide if it loads successfully; this can be queried later with
 *   wp_core_test_feature()
 * \param cancellable (nullable): optional GCancellable
 * \param callback (scope async): the callback to call when the operation is done
 * \param data (closure): data to pass to \a callback
 */
void
wp_core_load_component (WpCore * self, const gchar * component,
    const gchar * type, WpSpaJson * args, const gchar * provides,
    GCancellable * cancellable, GAsyncReadyCallback callback, gpointer data)
{
  g_autoptr (GTask) task = NULL;
  g_autoptr (WpComponentLoader) cl = NULL;

  task = g_task_new (self, cancellable, callback, data);
  g_task_set_source_tag (task, wp_core_load_component);

  if (provides)
    g_task_set_task_data (task, g_strdup (provides), g_free);

  /* find a component loader for that type and load the component */
  cl = wp_component_loader_find (self, type);
  if (!cl) {
    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "No component loader was found for components of type '%s'", type);
    return;
  }

  wp_debug_object (self, "load '%s', type '%s', loader " WP_OBJECT_FORMAT,
      component, type, WP_OBJECT_ARGS (cl));

  wp_component_loader_load (cl, self, component, type, args, cancellable,
      (GAsyncReadyCallback) on_component_loader_load_done, g_object_ref (task));
}

/*!
 * \brief Finishes the operation started by wp_core_load_component().
 * This is meant to be called in the callback that was passed to that method.
 *
 * \ingroup wpcomponentloader
 * \param self the component loader object
 * \param res the async result
 * \param error (out) (optional): the operation's error, if it occurred
 * \returns TRUE if the requested component was loaded, FALSE otherwise
 */
gboolean
wp_core_load_component_finish (WpCore * self, GAsyncResult * res,
    GError ** error)
{
  g_return_val_if_fail (
    g_async_result_is_tagged (res, wp_core_load_component), FALSE);

  return g_task_propagate_boolean (G_TASK (res), error);
}
  0707010000007B000081A400000000000000000000000165F863040000057C000000000000000000000000000000000000002C00000000wireplumber-0.5.0/lib/wp/component-loader.h   /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_COMPONENT_LOADER_H__
#define __WIREPLUMBER_COMPONENT_LOADER_H__

#include "plugin.h"
#include "spa-json.h"

G_BEGIN_DECLS

/*!
 * \brief The WpComponentLoader GType
 * \ingroup wpcomponentloader
 */
#define WP_TYPE_COMPONENT_LOADER (wp_component_loader_get_type ())
WP_API
G_DECLARE_INTERFACE (WpComponentLoader, wp_component_loader,
                     WP, COMPONENT_LOADER, GObject)

struct _WpComponentLoaderInterface
{
  GTypeInterface interface;

  gboolean (*supports_type) (WpComponentLoader * self, const gchar * type);

  void (*load) (WpComponentLoader * self, WpCore * core,
      const gchar * component, const gchar * type, WpSpaJson * args,
      GCancellable * cancellable, GAsyncReadyCallback callback, gpointer data);

  GObject * (*load_finish) (WpComponentLoader * self, GAsyncResult * res,
      GError ** error);

  /*< private >*/
  WP_PADDING(5)
};

WP_API
void wp_core_load_component (WpCore * self, const gchar * component,
    const gchar * type, WpSpaJson * args, const gchar * provides,
    GCancellable * cancellable, GAsyncReadyCallback callback, gpointer data);

WP_API
gboolean wp_core_load_component_finish (WpCore * self, GAsyncResult * res,
    GError ** error);

G_END_DECLS

#endif
0707010000007C000081A400000000000000000000000165F8630400003BF7000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/conf.c   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "conf.h"
#include "log.h"
#include "json-utils.h"
#include "base-dirs.h"
#include "error.h"

#include <pipewire/pipewire.h>
#include <spa/utils/result.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")

#define OVERRIDE_SECTION_PREFIX "override."

/*! \defgroup wpconf WpConf */
/*!
 * \struct WpConf
 *
 * WpConf allows accessing the different sections of the wireplumber
 * configuration.
 */

typedef struct _WpConfSection WpConfSection;
struct _WpConfSection
{
  gchar *name;
  WpSpaJson *value;
  gchar *location;
};

static void
wp_conf_section_clear (WpConfSection * section)
{
  g_free (section->name);
  g_clear_pointer (&section->value, wp_spa_json_unref);
  g_free (section->location);
}
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpConfSection, wp_conf_section_clear)

struct _WpConf
{
  GObject parent;

  /* Props */
  gchar *name;

  /* Private */
  GArray *conf_sections; /* element-type: WpConfSection */
  GPtrArray *files; /* element-type: GMappedFile* */
};

enum {
  PROP_0,
  PROP_NAME,
};

G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)

static void
wp_conf_init (WpConf * self)
{
  self->conf_sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));
  g_array_set_clear_func (self->conf_sections, (GDestroyNotify) wp_conf_section_clear);
  self->files = g_ptr_array_new_with_free_func ((GDestroyNotify) g_mapped_file_unref);
}

static void
wp_conf_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpConf *self = WP_CONF (object);

  switch (property_id) {
  case PROP_NAME:
    self->name = g_value_dup_string (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_conf_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpConf *self = WP_CONF (object);

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, self->name);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_conf_finalize (GObject * object)
{
  WpConf *self = WP_CONF (object);

  wp_conf_close (self);
  g_clear_pointer (&self->conf_sections, g_array_unref);
  g_clear_pointer (&self->files, g_ptr_array_unref);
  g_clear_pointer (&self->name, g_free);

  G_OBJECT_CLASS (wp_conf_parent_class)->finalize (object);
}

static void
wp_conf_class_init (WpConfClass * klass)
{
  GObjectClass * object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = wp_conf_finalize;
  object_class->set_property = wp_conf_set_property;
  object_class->get_property = wp_conf_get_property;

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name", "The name of the configuration file",
          NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Creates a new WpConf object
 *
 * This does not open the files, it only creates the object. For most use cases,
 * you should use wp_conf_new_open() instead.
 *
 * \ingroup wpconf
 * \param name the name of the configuration file
 * \param properties (transfer full) (nullable): unused, reserved for future use
 * \returns (transfer full): a new WpConf object
 */
WpConf *
wp_conf_new (const gchar * name, WpProperties * properties)
{
  g_return_val_if_fail (name, NULL);
  g_clear_pointer (&properties, wp_properties_unref);
  return g_object_new (WP_TYPE_CONF, "name", name, NULL);
}

/*!
 * \brief Creates a new WpConf object and opens the configuration file and its
 *   fragments, keeping them mapped in memory for further access.
 *
 * \ingroup wpconf
 * \param name the name of the configuration file
 * \param properties (transfer full) (nullable): unused, reserved for future use
 * \param error (out) (nullable): return location for a GError, or NULL
 * \returns (transfer full) (nullable): a new WpConf object, or NULL
 *   if an error occurred
 */
WpConf *
wp_conf_new_open (const gchar * name, WpProperties * properties, GError ** error)
{
  g_return_val_if_fail (name, NULL);

  g_autoptr (WpConf) self = wp_conf_new (name, properties);
  if (!wp_conf_open (self, error))
    return NULL;
  return g_steal_pointer (&self);
}

static gboolean
detect_old_conf_format (WpConf * self, GMappedFile *file)
{
  const gchar *data = g_mapped_file_get_contents (file);
  gsize size = g_mapped_file_get_length (file);

  /* wireplumber 0.4 used to have components of type = config/lua */
  return g_strrstr_len (data, size, "config/lua") ? TRUE : FALSE;
}

static gboolean
open_and_load_sections (WpConf * self, const gchar *path, GError ** error)
{
  g_autoptr (GMappedFile) file = g_mapped_file_new (path, FALSE, error);
  if (!file)
    return FALSE;

  /* test if the file is a relic from 0.4 */
  if (detect_old_conf_format (self, file)) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "The configuration file at '%s' is likely an old WirePlumber 0.4 config "
        "and is not supported anymore. Try removing it.", path);
    return FALSE;
  }

  g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_stringn (
      g_mapped_file_get_contents (file), g_mapped_file_get_length (file));
  g_autoptr (WpSpaJsonParser) parser = wp_spa_json_parser_new_undefined (json);
  g_autoptr (GArray) sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));

  g_array_set_clear_func (sections, (GDestroyNotify) wp_conf_section_clear);

  while (TRUE) {
    g_auto (WpConfSection) section = { 0, };
    g_autoptr (WpSpaJson) tmp = NULL;

    /* parse the section name */
    tmp = wp_spa_json_parser_get_json (parser);
    if (!tmp)
      break;

    if (wp_spa_json_is_container (tmp) ||
        wp_spa_json_is_int (tmp) ||
        wp_spa_json_is_float (tmp) ||
        wp_spa_json_is_boolean (tmp) ||
        wp_spa_json_is_null (tmp))
    {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
          "invalid section name (not a string): %.*s",
          (int) wp_spa_json_get_size (tmp), wp_spa_json_get_data (tmp));
      return FALSE;
    }

    section.name = wp_spa_json_parse_string (tmp);
    g_clear_pointer (&tmp, wp_spa_json_unref);

    /* parse the section contents */
    tmp = wp_spa_json_parser_get_json (parser);
    if (!tmp) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
          "section '%s' has no value", section.name);
      return FALSE;
    }

    section.value = g_steal_pointer (&tmp);
    section.location = g_strdup (path);
    g_array_append_val (sections, section);
    memset (&section, 0, sizeof (section));
  }

  /* store the mapped file and the sections; note that the stored WpSpaJson
     still point to the data in the GMappedFile, so this is why we keep the
     GMappedFile alive */
  g_ptr_array_add (self->files, g_steal_pointer (&file));
  g_array_append_vals (self->conf_sections, sections->data, sections->len);
  g_array_set_clear_func (sections, NULL);

  return TRUE;
}

/*!
 * \brief Opens the configuration file and its fragments and keeps them
 *   mapped in memory for further access.
 *
 * \ingroup wpconf
 * \param self the configuration
 * \param error (out)(nullable): return location for a GError, or NULL
 * \returns TRUE on success, FALSE on error
 */
gboolean
wp_conf_open (WpConf * self, GError ** error)
{
  g_return_val_if_fail (WP_IS_CONF (self), FALSE);

  g_autofree gchar *path = NULL;
  g_autoptr (WpIterator) iterator = NULL;
  g_auto (GValue) value = G_VALUE_INIT;

  /* open the main file */
  path = wp_base_dirs_find_file (WP_BASE_DIRS_CONFIGURATION, NULL, self->name);
  if (path) {
    wp_info_object (self, "opening main file: %s", path);
    if (!open_and_load_sections (self, path, error))
      return FALSE;
  }
  g_clear_pointer (&path, g_free);

  /* open the .conf.d/ fragments */
  path = g_strdup_printf ("%s.d", self->name);
  iterator = wp_base_dirs_new_files_iterator (WP_BASE_DIRS_CONFIGURATION, path,
      ".conf");

  for (; wp_iterator_next (iterator, &value); g_value_unset (&value)) {
    const gchar *filename = g_value_get_string (&value);

    wp_info_object (self, "opening fragment file: %s", filename);

    g_autoptr (GError) e = NULL;
    if (!open_and_load_sections (self, filename, &e)) {
      wp_warning_object (self, "failed to open '%s': %s", filename, e->message);
      continue;
    }
  }

  if (self->files->len == 0) {
    g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
        "Could not locate configuration file '%s'", self->name);
    return FALSE;
  }

  return TRUE;
}

/*!
 * \brief Closes the configuration file and its fragments
 *
 * \ingroup wpconf
 * \param self the configuration
 */
void
wp_conf_close (WpConf * self)
{
  g_return_if_fail (WP_IS_CONF (self));

  g_array_set_size (self->conf_sections, 0);
  g_ptr_array_set_size (self->files, 0);
}

/*!
 * \brief Tests if the configuration files are open
 *
 * \ingroup wpconf
 * \param self the configuration
 * \returns TRUE if the configuration files are open, FALSE otherwise
 */
gboolean
wp_conf_is_open (WpConf * self)
{
  g_return_val_if_fail (WP_IS_CONF (self), FALSE);
  return self->files->len > 0;
}

/*!
 * \brief Gets the name of the configuration file
 *
 * \ingroup wpconf
 * \param self the configuration
 * \returns the name of the configuration file
 */
const gchar *
wp_conf_get_name (WpConf * self)
{
  g_return_val_if_fail (WP_IS_CONF (self), NULL);
  return self->name;
}

static WpSpaJson *
ensure_merged_section (WpConf * self, const gchar *section)
{
  g_autoptr (WpSpaJson) merged = NULL;
  WpConfSection *merged_section = NULL;

  /* check if the section is already merged */
  for (guint i = 0; i < self->conf_sections->len; i++) {
    WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
    if (g_str_equal (s->name, section)) {
      if (!s->location) {
        wp_debug_object (self, "section %s is already merged", section);
        return wp_spa_json_ref (s->value);
      }
    }
  }

  /* Iterate over the sections and merge them */
  for (guint i = 0; i < self->conf_sections->len; i++) {
    WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
    const gchar *s_name = s->name;

    /* skip the "override." prefix and take a note */
    gboolean override = g_str_has_prefix (s_name, OVERRIDE_SECTION_PREFIX);
    if (override)
      s_name += strlen (OVERRIDE_SECTION_PREFIX);

    if (g_str_equal (s_name, section)) {
      /* Merge sections if a previous value exists and
         the 'override.' prefix is not present */
      if (!override && merged) {
        g_autoptr (WpSpaJson) new_merged =
            wp_json_utils_merge_containers (merged, s->value);
        if (!merged) {
          wp_warning_object (self,
              "skipping merge of '%s' from '%s' as JSON containers are not compatible",
              section, s->location);
          continue;
        }

        g_clear_pointer (&merged, wp_spa_json_unref);
        merged = g_steal_pointer (&new_merged);
        merged_section = NULL;
      }
      /* Otherwise always replace */
      else {
        g_clear_pointer (&merged, wp_spa_json_unref);
        merged = wp_spa_json_ref (s->value);
        merged_section = s;
      }
    }
  }

  /* cache the result */
  if (merged_section) {
    /* if the merged json came from a single location, just clear
       the location from that WpConfSection to mark it as the result */
    wp_info_object (self, "section '%s' is used as-is from '%s'", section,
        merged_section->location);
    g_clear_pointer (&merged_section->location, g_free);
  } else if (merged) {
    /* if the merged json came from multiple locations, create a new
       WpConfSection to store it */
    WpConfSection s = { g_strdup (section), wp_spa_json_ref (merged), NULL };
    g_array_append_val (self->conf_sections, s);
    wp_info_object (self, "section '%s' is merged from multiple locations",
        section);
  } else {
    wp_info_object (self, "section '%s' is not defined", section);
  }

  return g_steal_pointer (&merged);
}

/*!
 * This method will get the JSON value of a specific section from the
 * configuration. If the same section is defined in multiple locations, the
 * sections with the same name will be either merged in case of arrays and
 * objects, or overridden in case of boolean, int, double and strings.
 *
 * \ingroup wpconf
 * \param self the configuration
 * \param section the section name
 * \returns (transfer full) (nullable): the JSON value of the section or NULL
 *   if the section does not exist
 */
WpSpaJson *
wp_conf_get_section (WpConf *self, const gchar *section)
{
  g_return_val_if_fail (WP_IS_CONF (self), NULL);
  g_return_val_if_fail (section, NULL);

  return ensure_merged_section (self, section);
}

/*!
 * \brief Updates the given properties with the values of a specific section
 * from the configuration.
 *
 * \ingroup wpconf
 * \param self the configuration
 * \param section the section name
 * \param props the properties to update
 * \returns the number of properties updated
 */
gint
wp_conf_section_update_props (WpConf *self, const gchar *section,
    WpProperties *props)
{
  g_autoptr (WpSpaJson) json = NULL;

  g_return_val_if_fail (WP_IS_CONF (self), -1);
  g_return_val_if_fail (section, -1);
  g_return_val_if_fail (props, -1);

  json = wp_conf_get_section (self, section);
  if (!json)
    return 0;
  return wp_properties_update_from_json (props, json);
}

#include "private/parse-conf-section.c"

/*!
 * \brief Parses standard pw_context sections from \a conf
 *
 * \ingroup wpconf
 * \param self the configuration
 * \param context the associated pw_context
 */
void
wp_conf_parse_pw_context_sections (WpConf * self, struct pw_context * context)
{
  gint res;
  WpProperties *conf_wp;
  struct pw_properties *conf_pw;

  g_return_if_fail (WP_IS_CONF (self));
  g_return_if_fail (context);

  /* convert needed sections into a pipewire-style conf dictionary */
  conf_wp = wp_properties_new ("config.path", "wpconf", NULL);
  {
    g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.spa-libs");
    if (j) {
      g_autofree gchar *js = wp_spa_json_parse_string (j);
      wp_properties_set (conf_wp, "context.spa-libs", js);
    }
  }
  {
    g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.modules");
    if (j) {
      g_autofree gchar *js = wp_spa_json_parse_string (j);
      wp_properties_set (conf_wp, "context.modules", js);
    }
  }
  conf_pw = wp_properties_unref_and_take_pw_properties (conf_wp);

  /* parse sections */
  if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.spa-libs")) < 0)
    goto error;
  wp_info_object (self, "parsed %d context.spa-libs items", res);

  if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.modules")) < 0)
    goto error;
  if (res > 0)
    wp_info_object (self, "parsed %d context.modules items", res);
  else
    wp_warning_object (self, "no modules loaded from context.modules");

out:
  pw_properties_free (conf_pw);
  return;

error:
  wp_critical_object (self, "failed to parse pw_context sections: %s",
      spa_strerror (res));
  goto out;
}
 0707010000007D000081A400000000000000000000000165F8630400000488000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/conf.h   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_CONF_H__
#define __WIREPLUMBER_CONF_H__

#include "spa-json.h"
#include "properties.h"

G_BEGIN_DECLS

struct pw_context;

/*!
 * \brief The WpConf GType
 * \ingroup wpconf
 */
#define WP_TYPE_CONF (wp_conf_get_type ())

WP_API
G_DECLARE_FINAL_TYPE (WpConf, wp_conf, WP, CONF, GObject)

WP_API
WpConf * wp_conf_new (const gchar * name, WpProperties * properties);

WP_API
WpConf * wp_conf_new_open (const gchar * name, WpProperties * properties,
    GError ** error);

WP_API
gboolean wp_conf_open (WpConf * self, GError ** error);

WP_API
void wp_conf_close (WpConf * self);

WP_API
gboolean wp_conf_is_open (WpConf * self);

WP_API
const gchar * wp_conf_get_name (WpConf * self);

WP_API
WpSpaJson * wp_conf_get_section (WpConf *self, const gchar *section);

WP_API
gint wp_conf_section_update_props (WpConf * self, const gchar * section,
    WpProperties * props);

WP_API
void wp_conf_parse_pw_context_sections (WpConf * self,
    struct pw_context * context);

G_END_DECLS

#endif
0707010000007E000081A400000000000000000000000165F8630400009EA0000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/core.c   /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "core.h"
#include "wp.h"
#include "private/registry.h"
#include "private/internal-comp-loader.h"

#include <pipewire/pipewire.h>

#include <spa/utils/result.h>
#include <spa/debug/types.h>
#include <spa/support/cpu.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-core")

/*
 * Integration between the PipeWire main loop and GMainLoop
 */

#define WP_LOOP_SOURCE(x) ((WpLoopSource *) x)

typedef struct _WpLoopSource WpLoopSource;
struct _WpLoopSource
{
  GSource parent;
  struct pw_loop *loop;
};

static gboolean
wp_loop_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
{
  int result;

  wp_trace_boxed (G_TYPE_SOURCE, s, "entering pw main loop");

  pw_loop_enter (WP_LOOP_SOURCE(s)->loop);
  result = pw_loop_iterate (WP_LOOP_SOURCE(s)->loop, 0);
  pw_loop_leave (WP_LOOP_SOURCE(s)->loop);

  wp_trace_boxed (G_TYPE_SOURCE, s, "leaving pw main loop");

  if (G_UNLIKELY (result < 0))
    wp_warning_boxed (G_TYPE_SOURCE, s,
        "pw_loop_iterate failed: %s", spa_strerror (result));

  return G_SOURCE_CONTINUE;
}

static void
wp_loop_source_finalize (GSource * s)
{
  pw_loop_destroy (WP_LOOP_SOURCE(s)->loop);
}

static GSourceFuncs source_funcs = {
  NULL,
  NULL,
  wp_loop_source_dispatch,
  wp_loop_source_finalize
};

static GSource *
wp_loop_source_new (void)
{
  GSource *s = g_source_new (&source_funcs, sizeof (WpLoopSource));
  WP_LOOP_SOURCE(s)->loop = pw_loop_new (NULL);

  g_source_add_unix_fd (s,
      pw_loop_get_fd (WP_LOOP_SOURCE(s)->loop),
      G_IO_IN | G_IO_ERR | G_IO_HUP);

  return (GSource *) s;
}

/*! \defgroup wpcore WpCore */
/*!
 * \struct WpCore
 *
 * The core is the central object around which everything operates. It is
 * essential to create a WpCore before using any other WirePlumber API.
 *
 * The core object has the following responsibilities:
 *  * it initializes the PipeWire library
 *  * it creates a `pw_context` and allows connecting to the PipeWire server,
 *    creating a local `pw_core`
 *  * it glues the PipeWire library's event loop system with GMainLoop
 *  * it maintains a list of registered objects, which other classes use
 *    to keep objects loaded permanently into memory
 *  * it watches the PipeWire registry and keeps track of remote and local
 *    objects that appear in the registry, making them accessible through
 *    the WpObjectManager API.
 *
 * The core is also responsible for loading components, which are defined in
 * the main configuration file. Components are loaded when
 * WP_CORE_FEATURE_COMPONENTS is activated.
 *
 * \b Configuration
 *
 * The main configuration file needs to be created and opened before the core
 * is created, using the WpConf API. It is then passed to the core as an
 * argument in the constructor.
 *
 * If a configuration file is not provided, the core will let the underlying
 * `pw_context` load its own configuration, based on the rules that apply to
 * all pipewire clients (e.g. it respects the `PIPEWIRE_CONFIG_NAME` environment
 * variable and loads "client.conf" as a last resort).
 *
 * If a configuration file is provided, the core does not let the underlying
 * `pw_context` load any configuration and instead uses the provided WpConf
 * object.
 *
 * \gproperties
 *
 * \gproperty{g-main-context, GMainContext *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   A GMainContext to attach to}
 *
 * \gproperty{properties, WpProperties *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The pipewire properties of the pw_core}
 *
 * \gproperty{pw-context, gpointer (struct pw_context *), G_PARAM_READABLE,
 *   The pipewire context}
 *
 * \gproperty{pw-core, gpointer (struct pw_core *), G_PARAM_READABLE,
 *   The pipewire core}
 *
 * \gproperty{conf, WpConf *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The main configuration file}
 *
 * \gsignals
 *
 * \par connected
 * \parblock
 * \code
 * void
 * connected_callback (WpCore * self,
 *                     gpointer user_data)
 * \endcode
 * Emitted when the core is successfully connected to the PipeWire server
 * \endparblock
 *
 * \par disconnected
 * \parblock
 * \code
 * void
 * disconnected_callback (WpCore * self,
 *                        gpointer user_data)
 * \endcode
 * Emitted when the core is disconnected from the PipeWire server
 * \endparblock
 */
struct _WpCore
{
  WpObject parent;

  /* main loop integration */
  GMainContext *g_main_context;

  /* extra properties */
  WpProperties *properties;

  /* pipewire main objects */
  struct pw_context *pw_context;
  struct pw_core *pw_core;
  struct pw_core_info *info;

  /* pipewire main listeners */
  struct spa_hook core_listener;
  struct spa_hook proxy_core_listener;

  /* the main configuration file */
  WpConf *conf;

  WpRegistry registry;
  GHashTable *async_tasks; // <int seq, GTask*>
};

enum {
  PROP_0,
  PROP_G_MAIN_CONTEXT,
  PROP_PROPERTIES,
  PROP_PW_CONTEXT,
  PROP_PW_CORE,
  PROP_CONF,
};

enum {
  SIGNAL_CONNECTED,
  SIGNAL_DISCONNECTED,
  NUM_SIGNALS
};

static guint32 signals[NUM_SIGNALS];

G_DEFINE_TYPE (WpCore, wp_core, WP_TYPE_OBJECT)

static void
core_info (void *data, const struct pw_core_info * info)
{
  WpCore *self = WP_CORE (data);
  gboolean new_connection = (self->info == NULL);

  self->info = pw_core_info_update (self->info, info);

  wp_info_object (self, "connected to server: %s, cookie: %u",
      self->info->name, self->info->cookie);

  if (new_connection) {
    g_signal_emit (self, signals[SIGNAL_CONNECTED], 0);
    wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_CONNECTED, 0);
  }
}

static void
core_done (void *data, uint32_t id, int seq)
{
  WpCore *self = WP_CORE (data);
  g_autoptr (GTask) task = NULL;

  g_hash_table_steal_extended (self->async_tasks, GINT_TO_POINTER (seq), NULL,
      (gpointer *) &task);
  wp_debug_object (self, "done, seq 0x%x, task " WP_OBJECT_FORMAT,
      seq, WP_OBJECT_ARGS (task));

  if (task)
    g_task_return_boolean (task, TRUE);
}

static gboolean
core_disconnect_async (WpCore * self)
{
  wp_core_disconnect (self);
  return G_SOURCE_REMOVE;
}

static void
core_error (void *data, uint32_t id, int seq, int res, const char *message)
{
  WpCore *self = WP_CORE (data);

  /* protocol socket disconnected; schedule disconnecting our core */
  if (id == 0 && res == -EPIPE) {
    wp_core_idle_add_closure (self, NULL, g_cclosure_new_object (
            G_CALLBACK (core_disconnect_async), G_OBJECT (self)));
  }
}

static const struct pw_core_events core_events = {
  PW_VERSION_CORE_EVENTS,
  .info = core_info,
  .done = core_done,
  .error = core_error,
};

static gboolean
async_tasks_finish (gpointer key, gpointer value, gpointer user_data)
{
  GTask *task = G_TASK (value);
  g_return_val_if_fail (task, FALSE);

  g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_INVARIANT, "core disconnected");
  return TRUE;
}

static void
proxy_core_destroy (void *data)
{
  WpCore *self = WP_CORE (data);
  g_hash_table_foreach_remove (self->async_tasks, async_tasks_finish, NULL);
  g_clear_pointer (&self->info, pw_core_info_free);
  spa_hook_remove(&self->core_listener);
  spa_hook_remove(&self->proxy_core_listener);
  self->pw_core = NULL;
  wp_debug_object (self, "emit disconnected");
  g_signal_emit (self, signals[SIGNAL_DISCONNECTED], 0);
  wp_object_update_features (WP_OBJECT (self), 0, WP_CORE_FEATURE_CONNECTED);
}

static const struct pw_proxy_events proxy_core_events = {
  PW_VERSION_PROXY_EVENTS,
  .destroy = proxy_core_destroy,
};

static void
wp_core_init (WpCore * self)
{
  wp_registry_init (&self->registry);
  self->async_tasks = g_hash_table_new_full (g_direct_hash, g_direct_equal,
      NULL, g_object_unref);

  wp_core_register_object (self,
      g_object_new (WP_TYPE_INTERNAL_COMP_LOADER, NULL));
}

static void
wp_core_constructed (GObject *object)
{
  WpCore *self = WP_CORE (object);
  g_autoptr (GSource) source = NULL;

  /* loop */
  source = wp_loop_source_new ();
  g_source_attach (source, self->g_main_context);

  /* context */
  if (!self->pw_context) {
    struct pw_properties *p = NULL;
    const gchar *str = NULL;

    /* use our own configuration file, if specified */
    if (self->conf) {
      wp_info_object (self, "using configuration file: %s",
          wp_conf_get_name (self->conf));

      /* ensure we have our very own properties set,
         since we are going to modify it */
      self->properties = self->properties ?
          wp_properties_ensure_unique_owner (self->properties) :
          wp_properties_new_empty ();

      /* load context.properties */
      wp_conf_section_update_props (self->conf, "context.properties",
          self->properties);

      /* disable loading of a configuration file in pw_context */
      wp_properties_set (self->properties, PW_KEY_CONFIG_NAME, "null");
    }

    /* properties are fully stored in the pw_context, no need to keep a copy */
    p = self->properties ?
        wp_properties_unref_and_take_pw_properties (self->properties) : NULL;
    self->properties = NULL;

    self->pw_context = pw_context_new (WP_LOOP_SOURCE(source)->loop, p,
        sizeof (grefcount));
    g_return_if_fail (self->pw_context);

    /* use the same config option as pipewire to set the log level */
    p = (struct pw_properties *) pw_context_get_properties (self->pw_context);
    if (!g_getenv("WIREPLUMBER_DEBUG") &&
        (str = pw_properties_get(p, "log.level")) != NULL) {
      if (!wp_log_set_level (str))
        wp_warning ("ignoring invalid log.level in config file: %s", str);
    }

    /* parse pw_context specific configuration sections */
    if (self->conf)
      wp_conf_parse_pw_context_sections (self->conf, self->pw_context);

    /* Init refcount */
    grefcount *rc = pw_context_get_user_data (self->pw_context);
    g_return_if_fail (rc);
    g_ref_count_init (rc);
  } else {
    /* Increase refcount */
    grefcount *rc = pw_context_get_user_data (self->pw_context);
    g_return_if_fail (rc);
    g_ref_count_inc (rc);
  }

  G_OBJECT_CLASS (wp_core_parent_class)->constructed (object);
}

static void
wp_core_dispose (GObject * obj)
{
  WpCore *self = WP_CORE (obj);

  wp_registry_clear (&self->registry);
  wp_object_update_features (WP_OBJECT (self), 0, WP_CORE_FEATURE_COMPONENTS);

  G_OBJECT_CLASS (wp_core_parent_class)->dispose (obj);
}

static void
wp_core_finalize (GObject * obj)
{
  WpCore *self = WP_CORE (obj);
  grefcount *rc = pw_context_get_user_data (self->pw_context);
  g_return_if_fail (rc);

  wp_core_disconnect (self);

  /* Clear pw-context if refcount reaches 0 */
  if (g_ref_count_dec (rc))
    g_clear_pointer (&self->pw_context, pw_context_destroy);

  g_clear_pointer (&self->properties, wp_properties_unref);
  g_clear_pointer (&self->g_main_context, g_main_context_unref);
  g_clear_pointer (&self->async_tasks, g_hash_table_unref);
  g_clear_object (&self->conf);

  wp_debug_object (self, "WpCore destroyed");

  G_OBJECT_CLASS (wp_core_parent_class)->finalize (obj);
}

static void
wp_core_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpCore *self = WP_CORE (object);

  switch (property_id) {
  case PROP_G_MAIN_CONTEXT:
    g_value_set_boxed (value, self->g_main_context);
    break;
  case PROP_PROPERTIES:
    g_value_take_boxed (value, wp_core_get_properties (self));
    break;
  case PROP_PW_CONTEXT:
    g_value_set_pointer (value, self->pw_context);
    break;
  case PROP_PW_CORE:
    g_value_set_pointer (value, self->pw_core);
    break;
  case PROP_CONF:
    g_value_set_object (value, self->conf);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_core_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpCore *self = WP_CORE (object);

  switch (property_id) {
  case PROP_G_MAIN_CONTEXT:
    self->g_main_context = g_value_dup_boxed (value);
    break;
  case PROP_PROPERTIES:
    self->properties = g_value_dup_boxed (value);
    break;
  case PROP_PW_CONTEXT:
    self->pw_context = g_value_get_pointer (value);
    break;
  case PROP_CONF:
    self->conf = g_value_dup_object (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static WpObjectFeatures
wp_core_get_supported_features (WpObject * self)
{
  return WP_CORE_FEATURE_CONNECTED |
      WP_CORE_FEATURE_COMPONENTS;
}

enum {
  STEP_CONNECT = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_LOAD_COMPONENTS,
};

static guint
wp_core_activate_get_next_step (WpObject * self,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
    case WP_TRANSITION_STEP_NONE:
      if (missing & WP_CORE_FEATURE_CONNECTED)
        return STEP_CONNECT;
      G_GNUC_FALLTHROUGH;

    case STEP_CONNECT:
      if (missing & WP_CORE_FEATURE_COMPONENTS)
        return STEP_LOAD_COMPONENTS;
      G_GNUC_FALLTHROUGH;

    case STEP_LOAD_COMPONENTS:
      return WP_TRANSITION_STEP_NONE;

    default:
      return WP_TRANSITION_STEP_ERROR;
  }
}

static void
on_components_loaded (WpCore * self, GAsyncResult *res,
    WpTransition * transition)
{
  g_autoptr (GError) error = NULL;

  if (!wp_core_load_component_finish (self, res, &error)) {
    wp_transition_return_error (transition, g_error_new (
        WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "failed to load components: %s", error->message));
    return;
  }

  if (self->conf) {
    wp_info_object (self, "done loading components, closing conf file...");
    wp_conf_close (self->conf);
  }

  wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_COMPONENTS, 0);
}

static void
wp_core_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  WpCore *self = WP_CORE (object);

  switch (step) {
    case STEP_CONNECT: {
      wp_info_object (self, "connecting to pipewire...");

      if (!wp_core_connect (self)) {
        wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
            WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_SERVICE_UNAVAILABLE,
            "Failed to connect to PipeWire"));
      }
      break;
    }

    case STEP_LOAD_COMPONENTS: {
      g_autoptr (WpProperties) props = wp_core_get_properties (self);

      if (spa_atob (wp_properties_get (props, "wireplumber.export-core"))) {
        /* do not load any components on the export core */
        wp_object_update_features (WP_OBJECT (self), WP_CORE_FEATURE_COMPONENTS, 0);
        return;
      }
      else {
        const gchar *profile = wp_properties_get (props, "wireplumber.profile");

        wp_info_object (self,
            "parsing & loading components for profile [%s]...", profile);

        /* Load components that are defined in the configuration section */
        wp_core_load_component (self, profile, "profile", NULL, NULL, NULL,
            (GAsyncReadyCallback) on_components_loaded, transition);
      }
      break;
    }

    case WP_TRANSITION_STEP_ERROR:
      break;
    default:
      g_assert_not_reached ();
  }
}

static void
wp_core_deactivate (WpObject * self, WpObjectFeatures features)
{
  if (features & WP_CORE_FEATURE_CONNECTED)
    wp_core_disconnect (WP_CORE (self));

  /* WP_CORE_FEATURE_COMPONENTS cannot be manually deactivated */
}

static void
wp_core_class_init (WpCoreClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;

  object_class->constructed = wp_core_constructed;
  object_class->dispose = wp_core_dispose;
  object_class->finalize = wp_core_finalize;
  object_class->get_property = wp_core_get_property;
  object_class->set_property = wp_core_set_property;

  wpobject_class->get_supported_features = wp_core_get_supported_features;
  wpobject_class->activate_get_next_step = wp_core_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_core_activate_execute_step;
  wpobject_class->deactivate = wp_core_deactivate;

  g_object_class_install_property (object_class, PROP_G_MAIN_CONTEXT,
      g_param_spec_boxed ("g-main-context", "g-main-context",
          "A GMainContext to attach to", G_TYPE_MAIN_CONTEXT,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PROPERTIES,
      g_param_spec_boxed ("properties", "properties", "Extra properties",
          WP_TYPE_PROPERTIES,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PW_CONTEXT,
      g_param_spec_pointer ("pw-context", "pw-context", "The pipewire context",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PW_CORE,
      g_param_spec_pointer ("pw-core", "pw-core", "The pipewire core",
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_CONF,
      g_param_spec_object ("conf", "conf", "The main configuration file",
          WP_TYPE_CONF,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  signals[SIGNAL_CONNECTED] = g_signal_new ("connected",
      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
      G_TYPE_NONE, 0);

  signals[SIGNAL_DISCONNECTED] = g_signal_new ("disconnected",
      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
      G_TYPE_NONE, 0);
}

/*!
 * \brief Creates a new core object
 *
 * \ingroup wpcore
 * \param context (transfer none) (nullable): the GMainContext to use for events
 * \param conf (transfer full) (nullable): the main configuration file
 * \param properties (transfer full) (nullable): additional properties, which
 *   are also passed to pw_context_new() and pw_context_connect()
 * \returns (transfer full): a new WpCore
 */
WpCore *
wp_core_new (GMainContext * context, WpConf * conf, WpProperties * properties)
{
  g_autoptr (WpConf) c = conf;
  g_autoptr (WpProperties) props = properties;

  return g_object_new (WP_TYPE_CORE,
      "g-main-context", context,
      "conf", conf,
      "properties", properties,
      "pw-context", NULL,
      NULL);
}

/*!
 * \brief Clones a core with the same context as \a self
 *
 * \ingroup wpcore
 * \param self the core
 * \returns (transfer full): the clone WpCore
 */
WpCore *
wp_core_clone (WpCore * self)
{
  return g_object_new (WP_TYPE_CORE,
      "core", self,
      "g-main-context", self->g_main_context,
      "conf", self->conf,
      "properties", self->properties,
      "pw-context", self->pw_context,
      NULL);
}

static gboolean
find_export_core (gconstpointer a, gconstpointer b)
{
  gpointer obj = (gpointer) a;
  if (WP_IS_CORE ((gpointer) obj)) {
    g_autoptr (WpProperties) props = wp_core_get_properties (WP_CORE (obj));
    if (spa_atob (wp_properties_get (props, "wireplumber.export-core")))
      return TRUE;
  }
  return FALSE;
}

/*!
 * \brief Returns the special WpCore that is used to maintain a secondary
 * connection to PipeWire, for exporting objects
 *
 * The export core is enabled by loading the built-in "export-core" component.
 *
 * \ingroup wpcore
 * \param self the core
 * \returns (transfer full): the export WpCore
 */
WpCore *
wp_core_get_export_core (WpCore * self)
{
  g_return_val_if_fail (WP_IS_CORE (self), NULL);

  return wp_core_find_object (self, find_export_core, NULL);
}

/*!
 * \brief Gets the main configuration file of the core
 *
 * \ingroup wpcore
 * \param self the core
 * \returns (transfer full) (nullable): the main configuration file
 */
WpConf *
wp_core_get_conf (WpCore * self)
{
  g_return_val_if_fail (WP_IS_CORE (self), NULL);
  return self->conf ? g_object_ref (self->conf) : NULL;
}

/*!
 * \brief Gets the GMainContext of the core
 *
 * \ingroup wpcore
 * \param self the core
 * \returns (transfer none) (nullable): the GMainContext that is in
 *   use by this core for events
 */
GMainContext *
wp_core_get_g_main_context (WpCore * self)
{
  g_return_val_if_fail (WP_IS_CORE (self), NULL);
  return self->g_main_context;
}

/*!
 * \brief Gets the internal PipeWire context of the core
 *
 * \ingroup wpcore
 * \param self the core
 * \returns (transfer none): the internal pw_context object
 */
struct pw_context *
wp_core_get_pw_context (WpCore * self)
{
  g_return_val_if_fail (WP_IS_CORE (self), NULL);
  return self->pw_context;
}

/*!
 * \brief Gets the internal PipeWire core of the core
 *
 * \ingroup wpcore
 * \param self the core
 * \returns (transfer none) (nullable): the internal pw_core object,
 *   or NULL if the core is not connected to PipeWire
 */
struct pw_core *
wp_core_get_pw_core (WpCore * self)
{
  g_return_val_if_fail (WP_IS_CORE (self), NULL);
  return self->pw_core;
}

/*!
 * \brief Gets the virtual machine type of the core
 *
 * \ingroup wpcore
 * \param self the core
 * \returns (transfer full) (nullable): a comma separated string with all the
 *   virtual machine types that this core matches, or NULL if the core is not
 *   running in a virtual machine.
 *
 * \since 0.4.11
 */
gchar *
wp_core_get_vm_type (WpCore *self)
{
  const struct spa_support *support;
  uint32_t n_support;
  struct spa_cpu *spa_cpu;
  uint32_t vm_type;
  gchar *res;
  gboolean first = TRUE;

  struct vm_type_info {
    uint32_t type;
    const gchar *name;
  };

  static struct vm_type_info vm_types[] = {
    {SPA_CPU_VM_OTHER, "other"},
    {SPA_CPU_VM_KVM, "kvm"},
    {SPA_CPU_VM_QEMU, "qemu"},
    {SPA_CPU_VM_BOCHS, "bochs"},
    {SPA_CPU_VM_XEN, "zen"},
    {SPA_CPU_VM_UML, "uml"},
    {SPA_CPU_VM_VMWARE, "vmware"},
    {SPA_CPU_VM_ORACLE, "oracle"},
    {SPA_CPU_VM_MICROSOFT, "microsoft"},
    {SPA_CPU_VM_ZVM, "zvm"},
    {SPA_CPU_VM_PARALLELS, "parallels"},
    {SPA_CPU_VM_BHYVE, "bhyve"},
    {SPA_CPU_VM_QNX, "qnx"},
    {SPA_CPU_VM_ACRN, "acrn"},
    {SPA_CPU_VM_POWERVM, "powervm"},
    {0, NULL},
  };

  g_return_val_if_fail (WP_IS_CORE (self), NULL);
  g_return_val_if_fail (self->pw_context, NULL);

  /* Get spa_cpu */
  support = pw_context_get_support (self->pw_context, &n_support);
  spa_cpu = spa_support_find (support, n_support, SPA_TYPE_INTERFACE_CPU);
  g_return_val_if_fail (spa_cpu, NULL);

  /* Just return NULL if CPU is not a VM */
  vm_type = spa_cpu_get_vm_type (spa_cpu);
  if (vm_type == SPA_CPU_VM_NONE)
    return NULL;

  /* Otherwise return a string with all matching VM types */
  res = g_strdup ("");
  for (guint i = 0; vm_types[i].name; i++) {
    if (vm_type & vm_types[i].type) {
      gchar *tmp = g_strdup_printf ("%s%s%s", res, first ? "": ",",
          vm_types[i].name);
      g_free (res);
      res = tmp;
      first = FALSE;
    }
  }

  return res;
}

/*!
 * \brief Connects this core to the PipeWire server.
 *
 * When connection succeeds, the WpCore \c "connected" signal is emitted.
 *
 * \ingroup wpcore
 * \param self the core
 * \returns TRUE if the core is effectively connected or FALSE if
 *   connection failed
 */
gboolean
wp_core_connect (WpCore *self)
{
  struct pw_properties *p = NULL;

  g_return_val_if_fail (WP_IS_CORE (self), FALSE);

  /* Don't do anything if core is already connected */
  if (self->pw_core)
    return TRUE;

  /* Connect */
  p = self->properties ? wp_properties_to_pw_properties (self->properties) : NULL;
  self->pw_core = pw_context_connect (self->pw_context, p, 0);
  if (!self->pw_core)
    return FALSE;

  /* Add the core listeners */
  pw_core_add_listener (self->pw_core, &self->core_listener, &core_events, self);
  pw_proxy_add_listener((struct pw_proxy*)self->pw_core,
      &self->proxy_core_listener, &proxy_core_events, self);

  /* Add the registry listener */
  wp_registry_attach (&self->registry, self->pw_core);

  return TRUE;
}

/*!
 * \brief Disconnects this core from the PipeWire server.
 *
 * This also effectively destroys all WpCore objects that were created through
 * the registry, destroys the pw_core and finally emits the WpCore
 * \c "disconnected" signal.
 *
 * \ingroup wpcore
 * \param self the core
 */
void
wp_core_disconnect (WpCore *self)
{
  wp_registry_detach (&self->registry);

  /* pw_core_disconnect destroys the core proxy
    and we continue in proxy_core_destroy() */
  if (self->pw_core)
    pw_core_disconnect (self->pw_core);
}

/*!
 * \brief Checks if the core is connected to PipeWire
 *
 * \ingroup wpcore
 * \param self the core
 * \returns TRUE if the core is connected to PipeWire, FALSE otherwise
 */
gboolean
wp_core_is_connected (WpCore * self)
{
  g_return_val_if_fail (WP_IS_CORE (self), FALSE);
  return self->pw_core && self->info;
}

/*!
 * \brief Gets the bound id of the client object that is created as a result
 * of this core being connected to the PipeWire daemon
 *
 * \ingroup wpcore
 * \since 0.4.16
 * \param self the core
 * \returns the bound id of this client
 */
guint32
wp_core_get_own_bound_id (WpCore * self)
{
  struct pw_client *client;

  g_return_val_if_fail (wp_core_is_connected (self), SPA_ID_INVALID);

  client = pw_core_get_client (self->pw_core);
  return pw_proxy_get_bound_id ((struct pw_proxy *) client);
}

/*!
 * \brief Gets the cookie of the core's connected PipeWire instance
 *
 * \ingroup wpcore
 * \param self the core
 * \returns The cookie of the PipeWire instance that \a self is connected to.
 *     The cookie is a unique random number for identifying an instance of
 *     PipeWire
 */
guint32
wp_core_get_remote_cookie (WpCore * self)
{
  g_return_val_if_fail (wp_core_is_connected (self), 0);

  return self->info->cookie;
}

/*!
 * \brief Gets the name of the core's connected PipeWire instance
 * \ingroup wpcore
 * \param self the core
 * \returns The name of the PipeWire instance that \a self is connected to
 */
const gchar *
wp_core_get_remote_name (WpCore * self)
{
  g_return_val_if_fail (wp_core_is_connected (self), NULL);

  return self->info->name;
}

/*!
 * \brief Gets the user name of the core's connected PipeWire instance
 * \ingroup wpcore
 * \param self the core
 * \returns The name of the user that started the PipeWire instance that
 *     \a self is connected to
 */
const gchar *
wp_core_get_remote_user_name (WpCore * self)
{
  g_return_val_if_fail (wp_core_is_connected (self), NULL);

  return self->info->user_name;
}

/*!
 * \brief Gets the host name of the core's connected PipeWire instance
 * \ingroup wpcore
 * \param self the core
 * \returns The name of the host where the PipeWire instance that
 *     \a self is connected to is running on
 */
const gchar *
wp_core_get_remote_host_name (WpCore * self)
{
  g_return_val_if_fail (wp_core_is_connected (self), NULL);

  return self->info->host_name;
}

/*!
 * \brief Gets the version of the core's connected PipeWire instance
 * \ingroup wpcore
 * \param self the core
 * \returns The version of the PipeWire instance that \a self is connected to
 */
const gchar *
wp_core_get_remote_version (WpCore * self)
{
  g_return_val_if_fail (wp_core_is_connected (self), NULL);

  return self->info->version;
}

/*!
 * \brief Gets the properties of the core's connected PipeWire instance
 * \ingroup wpcore
 * \param self the core
 * \returns (transfer full): the properties of the PipeWire instance that
 *     \a self is connected to
 */
WpProperties *
wp_core_get_remote_properties (WpCore * self)
{
  g_return_val_if_fail (wp_core_is_connected (self), NULL);

  return wp_properties_new_wrap_dict (self->info->props);
}

/*!
 * \brief Gets the properties of the core
 * \ingroup wpcore
 * \param self the core
 * \returns (transfer full): the properties of \a self
 */
WpProperties *
wp_core_get_properties (WpCore * self)
{
  g_return_val_if_fail (WP_IS_CORE (self), NULL);

  /* pw_core has all the properties of the pw_context,
     plus our updates, passed in pw_context_connect() */
  if (self->pw_core)
    return wp_properties_new_wrap (pw_core_get_properties (self->pw_core));

  /* if there is no connection yet, return the properties of the context */
  else if (!self->properties)
    return wp_properties_new_wrap (pw_context_get_properties (self->pw_context));

  /* ... plus any further updates that we got from wp_core_update_properties() */
  else {
    /* we need to copy here in order to augment with the updates */
    WpProperties *ret =
        wp_properties_new_copy (pw_context_get_properties (self->pw_context));
    wp_properties_update (ret, self->properties);
    return ret;
  }
}

/*!
 * \brief Updates the properties of \a self on the connection, making them
 * appear on the client object that represents this connection.
 *
 * If \a self is not connected yet, these properties are stored and passed to
 * pw_context_connect() when connecting.
 *
 * \ingroup wpcore
 * \param self the core
 * \param updates (transfer full): updates to apply to the properties of
 *    \a self; this does not need to include properties that have not changed
 */
void
wp_core_update_properties (WpCore * self, WpProperties * updates)
{
  g_autoptr (WpProperties) upd = updates;

  g_return_if_fail (WP_IS_CORE (self));
  g_return_if_fail (updates != NULL);

  /* store updates locally so that
    - they persist after disconnection
    - we can pass them to pw_context_connect */
  if (!self->properties)
    self->properties = wp_properties_new_empty ();
  wp_properties_update (self->properties, upd);

  if (self->pw_core)
    pw_core_update_properties (self->pw_core, wp_properties_peek_dict (upd));
}

/*!
 * \brief Adds an idle callback to be called in the same GMainContext as the
 * one used by this core.
 *
 * This is essentially the same as g_idle_add_full(), but it adds the created
 * GSource on the GMainContext used by this core instead of the default context.
 *
 * \ingroup wpcore
 * \param self the core
 * \param source (out) (optional): the source
 * \param function (scope notified): the function to call
 * \param data (closure): data to pass to \a function
 * \param destroy (nullable): a function to destroy \a data
 */
void
wp_core_idle_add (WpCore * self, GSource **source, GSourceFunc function,
    gpointer data, GDestroyNotify destroy)
{
  g_autoptr (GSource) s = NULL;

  g_return_if_fail (WP_IS_CORE (self));

  s = g_idle_source_new ();
  g_source_set_callback (s, function, data, destroy);
  g_source_attach (s, self->g_main_context);

  if (source)
    *source = g_source_ref (s);
}

/*!
 * \brief Adds an idle callback to be called in the same GMainContext as
 * the one used by this core.
 *
 * This is the same as wp_core_idle_add(), but it allows you to specify a
 * GClosure instead of a C callback.
 *
 * \ingroup wpcore
 * \param self the core
 * \param source (out) (optional): the source
 * \param closure the closure to invoke
 */
void
wp_core_idle_add_closure (WpCore * self, GSource **source, GClosure * closure)
{
  g_autoptr (GSource) s = NULL;

  g_return_if_fail (WP_IS_CORE (self));
  g_return_if_fail (closure != NULL);

  s = g_idle_source_new ();
  g_source_set_closure (s, closure);
  g_source_attach (s, self->g_main_context);

  if (source)
    *source = g_source_ref (s);
}

/*!
 * \brief Adds a timeout callback to be called at regular intervals in the same
 * GMainContext as the one used by this core.
 *
 * The function is called repeatedly until it returns FALSE, at which point
 * the timeout is automatically destroyed and the function will not be called
 * again. The first call to the function will be at the end of the first
 * interval.

 * This is essentially the same as g_timeout_add_full(), but it adds
 * the created GSource on the GMainContext used by this core instead of the
 * default context.
 *
 * \ingroup wpcore
 * \param self the core
 * \param source (out) (optional): the source
 * \param timeout_ms the timeout in milliseconds
 * \param function (scope notified): the function to call
 * \param data (closure): data to pass to \a function
 * \param destroy (nullable): a function to destroy \a data
 */
void
wp_core_timeout_add (WpCore * self, GSource **source, guint timeout_ms,
    GSourceFunc function, gpointer data, GDestroyNotify destroy)
{
  g_autoptr (GSource) s = NULL;

  g_return_if_fail (WP_IS_CORE (self));

  s = g_timeout_source_new (timeout_ms);
  g_source_set_callback (s, function, data, destroy);
  g_source_attach (s, self->g_main_context);

  if (source)
    *source = g_source_ref (s);
}

/*!
 * \brief Adds a timeout callback to be called at regular intervals in the same
 * GMainContext as the one used by this core.
 *
 * This is the same as wp_core_timeout_add(), but it allows you to specify a
 * GClosure instead of a C callback.
 *
 * \ingroup wpcore
 * \param self the core
 * \param source (out) (optional): the source
 * \param timeout_ms the timeout in milliseconds
 * \param closure the closure to invoke
 */
void
wp_core_timeout_add_closure (WpCore * self, GSource **source, guint timeout_ms,
    GClosure * closure)
{
  g_autoptr (GSource) s = NULL;

  g_return_if_fail (WP_IS_CORE (self));
  g_return_if_fail (closure != NULL);

  s = g_timeout_source_new (timeout_ms);
  g_source_set_closure (s, closure);
  g_source_attach (s, self->g_main_context);

  if (source)
    *source = g_source_ref (s);
}

/*!
 * \brief Asks the PipeWire server to call the \a callback via an event.
 *
 * Since methods are handled in-order and events are delivered
 * in-order, this can be used as a barrier to ensure all previous
 * methods and the resulting events have been handled.
 *
 * In both success and error cases, \a callback is always called.
 * Use wp_core_sync_finish() from within the \a callback to determine whether
 * the operation completed successfully or if an error occurred.
 *
 * \ingroup wpcore
 * \param self the core
 * \param cancellable (nullable): a GCancellable to cancel the operation
 * \param callback (scope async): a function to call when the operation is done
 * \param user_data (closure): data to pass to \a callback
 * \returns TRUE if the sync operation was started, FALSE if an error
 *   occurred before returning from this function
 */
gboolean
wp_core_sync (WpCore * self, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer user_data)
{
  return wp_core_sync_closure (self, cancellable,
      g_cclosure_new (G_CALLBACK (callback), user_data, NULL));
}

static void
invoke_closure (GObject * obj, GAsyncResult * res, gpointer data)
{
  GClosure *closure = data;
  GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT };
  g_value_init (&values[0], G_TYPE_OBJECT);
  g_value_init (&values[1], G_TYPE_OBJECT);
  g_value_set_object (&values[0], obj);
  g_value_set_object (&values[1], res);
  g_closure_invoke (closure, NULL, 2, values, NULL);
  g_value_unset (&values[0]);
  g_value_unset (&values[1]);
  g_closure_unref (closure);
}

/*!
 * \brief Asks the PipeWire server to invoke the \a closure via an event.
 *
 * Since methods are handled in-order and events are delivered
 * in-order, this can be used as a barrier to ensure all previous
 * methods and the resulting events have been handled.
 *
 * In both success and error cases, \a closure is always invoked.
 * Use wp_core_sync_finish() from within the \a closure to determine whether
 * the operation completed successfully or if an error occurred.
 *
 * \ingroup wpcore
 * \since 0.4.6
 * \param self the core
 * \param cancellable (nullable): a GCancellable to cancel the operation
 * \param closure (transfer floating): a closure to invoke when the operation
 *    is done
 * \returns TRUE if the sync operation was started, FALSE if an error
 *   occurred before returning from this function
 */
gboolean
wp_core_sync_closure (WpCore * self, GCancellable * cancellable,
    GClosure * closure)
{
  g_autoptr (GTask) task = NULL;
  int seq;

  g_return_val_if_fail (WP_IS_CORE (self), FALSE);
  g_return_val_if_fail (closure, FALSE);

  closure = g_closure_ref (closure);
  g_closure_sink (closure);
  if (G_CLOSURE_NEEDS_MARSHAL (closure))
    g_closure_set_marshal (closure, g_cclosure_marshal_VOID__OBJECT);

  task = g_task_new (self, cancellable, invoke_closure, closure);

  if (G_UNLIKELY (!self->pw_core)) {
    g_warn_if_reached ();
    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_INVARIANT, "No pipewire core");
    return FALSE;
  }

  seq = pw_core_sync (self->pw_core, 0, 0);
  if (G_UNLIKELY (seq < 0)) {
    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_OPERATION_FAILED, "pw_core_sync failed: %s",
        g_strerror (-seq));
    return FALSE;
  }

  wp_debug_object (self, "sync, seq 0x%x, task " WP_OBJECT_FORMAT,
      seq, WP_OBJECT_ARGS (task));

  g_hash_table_insert (self->async_tasks, GINT_TO_POINTER (seq),
      g_steal_pointer (&task));
  return TRUE;
}

/*!
 * \brief This function is meant to be called from within the callback of
 * wp_core_sync() in order to determine the success or failure of the operation.
 *
 * \ingroup wpcore
 * \param self the core
 * \param res a GAsyncResult
 * \param error (out) (optional): the error that occurred, if any
 * \returns TRUE if the operation succeeded, FALSE otherwise
 */
gboolean
wp_core_sync_finish (WpCore * self, GAsyncResult * res, GError ** error)
{
  g_return_val_if_fail (WP_IS_CORE (self), FALSE);
  g_return_val_if_fail (g_task_is_valid (res, self), FALSE);

  return g_task_propagate_boolean (G_TASK (res), error);
}


/*!
 * \brief Finds a registered object
 *
 * \param self the core
 * \param func (scope call): a function that takes the object being searched
 *   as the first argument and \a data as the second. it should return TRUE if
 *   the object is found or FALSE otherwise
 * \param data the second argument to \a func
 * \returns (transfer full) (type GObject*) (nullable): the registered object
 *   or NULL if not found
 */
gpointer
wp_core_find_object (WpCore * self, GEqualFunc func, gconstpointer data)
{
  GObject *object;
  guint i;

  g_return_val_if_fail (WP_IS_CORE (self), NULL);

  /* prevent bad things when called from within wp_registry_clear() */
  if (G_UNLIKELY (!self->registry.objects))
    return NULL;

  for (i = 0; i < self->registry.objects->len; i++) {
    object = g_ptr_array_index (self->registry.objects, i);
    if (func (object, data))
      return g_object_ref (object);
  }

  return NULL;
}

/*!
 * \brief Registers \a obj with the core, making it appear on WpObjectManager
 * instances as well.
 *
 * The core will also maintain a ref to that object until it
 * is removed.
 *
 * \ingroup wpcore
 * \param self the core
 * \param obj (transfer full) (type GObject*): the object to register
 */
void
wp_core_register_object (WpCore * self, gpointer obj)
{
  g_return_if_fail (WP_IS_CORE (self));
  g_return_if_fail (G_IS_OBJECT (obj));

  /* prevent bad things when called from within wp_registry_clear() */
  if (G_UNLIKELY (!self->registry.objects)) {
    g_object_unref (obj);
    return;
  }

  /* ensure the registered object is associated with this core */
  if (WP_IS_OBJECT (obj)) {
    g_autoptr (WpCore) obj_core = wp_object_get_core (WP_OBJECT (obj));
    g_return_if_fail (obj_core == self);
  }

  g_ptr_array_add (self->registry.objects, obj);

  /* notify object managers */
  wp_registry_notify_add_object (&self->registry, obj);
}

/*!
 * \brief Detaches and unrefs the specified object from this core.
 *
 * \ingroup wpcore
 * \param self the core
 * \param obj (transfer none) (type GObject*): a pointer to the object to remove
 */
void
wp_core_remove_object (WpCore * self, gpointer obj)
{
  g_return_if_fail (WP_IS_CORE (self));
  g_return_if_fail (G_IS_OBJECT (obj));

  /* prevent bad things when called from within wp_registry_clear() */
  if (G_UNLIKELY (!self->registry.objects))
    return;

  /* notify object managers */
  wp_registry_notify_rm_object (&self->registry, obj);

  g_ptr_array_remove_fast (self->registry.objects, obj);
}

/*!
 * \brief Test if a global feature is provided
 *
 * \ingroup wpcore
 * \param self the core
 * \param feature the feature name
 * \returns TRUE if the feature is provided, FALSE otherwise
 */
gboolean
wp_core_test_feature (WpCore * self, const gchar * feature)
{
  return g_ptr_array_find_with_equal_func (self->registry.features, feature,
      g_str_equal, NULL);
}

WpRegistry *
wp_core_get_registry (WpCore * self)
{
  return &self->registry;
}

WpCore *
wp_registry_get_core (WpRegistry * self)
{
  return SPA_CONTAINER_OF (self, WpCore, registry);
}
0707010000007F000081A400000000000000000000000165F8630400000DA7000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/core.h   /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_CORE_H__
#define __WIREPLUMBER_CORE_H__

#include "object.h"
#include "properties.h"
#include "spa-json.h"
#include "conf.h"

G_BEGIN_DECLS

struct pw_context;
struct pw_core;
typedef struct _WpObjectManager WpObjectManager;

/*!
 * \brief Flags to be used as WpObjectFeatures on WpCore
 * \ingroup wpcore
 */
typedef enum { /*< flags >*/
  /*! connects to pipewire */
  WP_CORE_FEATURE_CONNECTED = (1 << 0),
  /*! loads components defined in the configuration */
  WP_CORE_FEATURE_COMPONENTS = (1 << 1),
} WpCoreFeatures;

/*!
 * \brief The WpCore GType
 * \ingroup wpcore
 */
#define WP_TYPE_CORE (wp_core_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpCore, wp_core, WP, CORE, WpObject)

/* Basic */

WP_API
WpCore * wp_core_new (GMainContext * context, WpConf * conf,
    WpProperties * properties);

WP_API
WpCore * wp_core_clone (WpCore * self);

WP_API
WpCore * wp_core_get_export_core (WpCore * self);

WP_API
WpConf * wp_core_get_conf (WpCore * self);

WP_API
GMainContext * wp_core_get_g_main_context (WpCore * self);

WP_API
struct pw_context * wp_core_get_pw_context (WpCore * self);

WP_API
struct pw_core * wp_core_get_pw_core (WpCore * self);

WP_API
gchar *wp_core_get_vm_type (WpCore *self);

/* Connection */

WP_API
gboolean wp_core_connect (WpCore *self);

WP_API
void wp_core_disconnect (WpCore *self);

WP_API
gboolean wp_core_is_connected (WpCore * self);

/* Properties */

WP_API
guint32 wp_core_get_own_bound_id (WpCore * self);

WP_API
guint32 wp_core_get_remote_cookie (WpCore * self);

WP_API
const gchar * wp_core_get_remote_name (WpCore * self);

WP_API
const gchar * wp_core_get_remote_user_name (WpCore * self);

WP_API
const gchar * wp_core_get_remote_host_name (WpCore * self);

WP_API
const gchar * wp_core_get_remote_version (WpCore * self);

WP_API
WpProperties * wp_core_get_remote_properties (WpCore * self);

WP_API
WpProperties * wp_core_get_properties (WpCore * self);

WP_API
void wp_core_update_properties (WpCore * self, WpProperties * updates);

/* Callback */

WP_API
void wp_core_idle_add (WpCore * self, GSource **source, GSourceFunc function,
    gpointer data, GDestroyNotify destroy);

WP_API
void wp_core_idle_add_closure (WpCore * self, GSource **source,
    GClosure * closure);

WP_API
void wp_core_timeout_add (WpCore * self, GSource **source, guint timeout_ms,
    GSourceFunc function, gpointer data, GDestroyNotify destroy);

WP_API
void wp_core_timeout_add_closure (WpCore * self, GSource **source,
    guint timeout_ms, GClosure * closure);

WP_API
gboolean wp_core_sync (WpCore * self, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer user_data);

WP_API
gboolean wp_core_sync_closure (WpCore * self, GCancellable * cancellable,
    GClosure * closure);

WP_API
gboolean wp_core_sync_finish (WpCore * self, GAsyncResult * res,
    GError ** error);

/* Object Registry */

WP_API
gpointer wp_core_find_object (WpCore * self, GEqualFunc func,
    gconstpointer data);

WP_API
void wp_core_register_object (WpCore * self, gpointer obj);

WP_API
void wp_core_remove_object (WpCore * self, gpointer obj);

/* Object Manager */

WP_API
void wp_core_install_object_manager (WpCore * self, WpObjectManager * om);

/* Global Features */

WP_API
gboolean wp_core_test_feature (WpCore * self, const gchar * feature);

G_END_DECLS

#endif
 07070100000080000081A400000000000000000000000165F8630400000335000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/defs.h   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_DEFS_H__
#define __WIREPLUMBER_DEFS_H__

#if defined(__GNUC__)
# define WP_PLUGIN_EXPORT __attribute__ ((visibility ("default")))
# define WP_API_EXPORT extern __attribute__ ((visibility ("default")))
#else
# define WP_PLUGIN_EXPORT
# define WP_API_EXPORT extern
#endif

#define WP_API_IMPORT extern

#ifndef WP_API
# ifdef BUILDING_WP
#  define WP_API WP_API_EXPORT
# else
#  define WP_API WP_API_IMPORT
# endif
#endif

#ifndef WP_PRIVATE_API
# ifdef BUILDING_WP
#  define WP_PRIVATE_API
# else
#  define WP_PRIVATE_API __attribute__ ((deprecated ("Private API")))
# endif
#endif

#define WP_PADDING(n) gpointer _wp_padding[n];

#endif
   07070100000081000081A400000000000000000000000165F8630400005416000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/device.c /* WirePlumber
 *
 * Copyright © 2019-2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "device.h"
#include "node.h"
#include "core.h"
#include "log.h"
#include "error.h"
#include "private/pipewire-object-mixin.h"

#include <pipewire/impl.h>
#include <spa/debug/types.h>
#include <spa/monitor/device.h>
#include <spa/utils/result.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-device")

/*! \defgroup wpdevice WpDevice */
/*!
 * \struct WpDevice
 *
 * The WpDevice class allows accessing the properties and methods of a
 * PipeWire device object (`struct pw_device`).
 *
 * A WpDevice is constructed internally when a new device appears on the
 * PipeWire registry and it is made available through the WpObjectManager API.
 * Alternatively, a WpDevice can also be constructed using
 * wp_device_new_from_factory(), which creates a new device object
 * on the remote PipeWire server by calling into a factory.
 *
 */

struct _WpDevice
{
  WpGlobalProxy parent;
};

static void wp_device_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface);

G_DEFINE_TYPE_WITH_CODE (WpDevice, wp_device, WP_TYPE_GLOBAL_PROXY,
    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
        wp_pw_object_mixin_object_interface_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
        wp_device_pw_object_mixin_priv_interface_init));

static void
wp_device_init (WpDevice * self)
{
}

static void
wp_device_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case WP_PW_OBJECT_MIXIN_STEP_BIND:
  case WP_TRANSITION_STEP_ERROR:
    /* base class can handle BIND and ERROR */
    WP_OBJECT_CLASS (wp_device_parent_class)->
        activate_execute_step (object, transition, step, missing);
    break;
  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
    /* just wait, info will be emitted anyway after binding */
    break;
  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
    wp_pw_object_mixin_cache_params (object, missing);
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
wp_device_deactivate (WpObject * object, WpObjectFeatures features)
{
  wp_pw_object_mixin_deactivate (object, features);
  WP_OBJECT_CLASS (wp_device_parent_class)->deactivate (object, features);
}

static const struct pw_device_events device_events = {
  PW_VERSION_DEVICE_EVENTS,
  .info = (HandleEventInfoFunc(device)) wp_pw_object_mixin_handle_event_info,
  .param = wp_pw_object_mixin_handle_event_param,
};

static void
wp_device_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
      device, &device_events);
}

static void
wp_device_pw_proxy_destroyed (WpProxy * proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);

  WP_PROXY_CLASS (wp_device_parent_class)->pw_proxy_destroyed (proxy);
}

static void
wp_device_class_init (WpDeviceClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->get_property = wp_pw_object_mixin_get_property;

  wpobject_class->get_supported_features =
      wp_pw_object_mixin_get_supported_features;
  wpobject_class->activate_get_next_step =
      wp_pw_object_mixin_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_device_activate_execute_step;
  wpobject_class->deactivate = wp_device_deactivate;

  proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Device;
  proxy_class->pw_iface_version = PW_VERSION_DEVICE;
  proxy_class->pw_proxy_created = wp_device_pw_proxy_created;
  proxy_class->pw_proxy_destroyed = wp_device_pw_proxy_destroyed;

  wp_pw_object_mixin_class_override_properties (object_class);
}

static gint
wp_device_enum_params (gpointer instance, guint32 id,
    guint32 start, guint32 num, WpSpaPod *filter)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  return pw_device_enum_params (d->iface, 0, id, start, num,
      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
}

static gint
wp_device_set_param (gpointer instance, guint32 id, guint32 flags,
    WpSpaPod * param)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  g_autoptr (WpSpaPod) p = param;
  return pw_device_set_param (d->iface, id, flags,
      wp_spa_pod_get_spa_pod (p));
}

static void
wp_device_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface)
{
  wp_pw_object_mixin_priv_interface_info_init (iface, device, DEVICE);
  iface->enum_params = wp_device_enum_params;
  iface->set_param = wp_device_set_param;
}

/*!
 * \brief Constructs a device on the PipeWire server by asking the remote
 * factory \a factory_name to create it.
 *
 * Because of the nature of the PipeWire protocol, this operation completes
 * asynchronously at some point in the future. In order to find out when
 * this is done, you should call wp_object_activate(), requesting at least
 * %WP_PROXY_FEATURE_BOUND. When this feature is ready, the device is ready for
 * use on the server. If the device cannot be created, this activation operation
 * will fail.
 *
 * \ingroup wpdevice
 * \param core the wireplumber core
 * \param factory_name the pipewire factory name to construct the device
 * \param properties (nullable) (transfer full): the properties to pass to the
 *   factory
 * \returns (nullable) (transfer full): the new device or %NULL if the core
 *   is not connected and therefore the device cannot be created
 */

WpDevice *
wp_device_new_from_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties)
{
  g_autoptr (WpProperties) props = properties;
  return g_object_new (WP_TYPE_DEVICE,
      "core", core,
      "factory-name", factory_name,
      "global-properties", props,
      NULL);
}

/*! \defgroup wpspadevice WpSpaDevice */

struct _WpSpaDevice
{
  WpProxy parent;
  struct spa_handle *handle;
  struct spa_device *device;
  struct spa_hook listener;
  WpProperties *properties;
  GPtrArray *managed_objs;
};

enum {
  PROP_0,
  PROP_SPA_DEVICE_HANDLE,
  PROP_PROPERTIES,
};

enum
{
  SIGNAL_CREATE_OBJECT,
  SIGNAL_OBJECT_REMOVED,
  SPA_DEVICE_LAST_SIGNAL,
};

static guint spa_device_signals[SPA_DEVICE_LAST_SIGNAL] = { 0 };

G_DEFINE_TYPE (WpSpaDevice, wp_spa_device, WP_TYPE_PROXY)

static void
object_unref_safe (gpointer object)
{
  if (object)
    g_object_unref (object);
}

static void
wp_spa_device_init (WpSpaDevice * self)
{
  self->properties = wp_properties_new_empty ();
  self->managed_objs = g_ptr_array_new_with_free_func (object_unref_safe);
}

static void
wp_spa_device_constructed (GObject *object)
{
  WpSpaDevice *self = WP_SPA_DEVICE (object);
  gint res;

  g_return_if_fail (self->handle);

  /* Get the handle interface */
  res = spa_handle_get_interface (self->handle, SPA_TYPE_INTERFACE_Device,
      (gpointer *) &self->device);
  if (res < 0) {
    wp_warning_object (self,
        "Could not get device interface from SPA handle: %s",
        spa_strerror (res));
    return;
  }

  G_OBJECT_CLASS (wp_spa_device_parent_class)->constructed (object);
}

static void
wp_spa_device_finalize (GObject * object)
{
  WpSpaDevice *self = WP_SPA_DEVICE (object);

  self->device = NULL;
  g_clear_pointer (&self->handle, pw_unload_spa_handle);
  g_clear_pointer (&self->properties, wp_properties_unref);
  g_clear_pointer (&self->managed_objs, g_ptr_array_unref);

  G_OBJECT_CLASS (wp_spa_device_parent_class)->finalize (object);
}

static void
wp_spa_device_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpSpaDevice *self = WP_SPA_DEVICE (object);

  switch (property_id) {
  case PROP_SPA_DEVICE_HANDLE:
    self->handle = g_value_get_pointer (value);
    break;
  case PROP_PROPERTIES: {
    WpProperties *p = g_value_get_boxed (value);
    if (p)
      wp_properties_update (self->properties, p);
    break;
  }
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_spa_device_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpSpaDevice *self = WP_SPA_DEVICE (object);

  switch (property_id) {
  case PROP_SPA_DEVICE_HANDLE:
    g_value_set_pointer (value, self->handle);
    break;
  case PROP_PROPERTIES:
    g_value_take_boxed (value, wp_properties_ref (self->properties));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
spa_device_event_info (void *data, const struct spa_device_info *info)
{
  WpSpaDevice *self = WP_SPA_DEVICE (data);

  /*
   * This is emited syncrhonously at the time we add the listener and
   * before object_info is emited. It gives us additional properties
   * about the device, like the "api.alsa.card.*" ones that are not
   * set by the monitor
   */
  if (info->change_mask & SPA_DEVICE_CHANGE_MASK_PROPS)
    wp_properties_update_from_dict (self->properties, info->props);
}

static void
spa_device_event_event (void *data, const struct spa_event *event)
{
  WpSpaDevice *self = WP_SPA_DEVICE (data);
  g_autoptr (WpSpaPod) pod =
      wp_spa_pod_new_wrap_const ((const struct spa_pod *) event);
  guint32 id = -1;
  const gchar *type = NULL;
  g_autoptr (WpSpaPod) props = NULL;
  g_autoptr (GObject) child = NULL;

  wp_trace_boxed (WP_TYPE_SPA_POD, pod, "device event");

  if (wp_spa_pod_get_object (pod, &type,
          "Object", "i", &id,
          "Props", "?P", &props,
          NULL))
    child = wp_spa_device_get_managed_object (self, id);

  if (child && !g_strcmp0 (type, "ObjectConfig") &&
      WP_IS_PIPEWIRE_OBJECT (child) && props) {
    wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (child), "Props", 0,
        g_steal_pointer (&props));
  }
}

static void
spa_device_event_object_info (void *data, uint32_t id,
    const struct spa_device_object_info *info)
{
  WpSpaDevice *self = WP_SPA_DEVICE (data);

  if (info) {
    const gchar *type;
    g_autoptr (WpProperties) props = NULL;

    type = spa_debug_type_short_name (info->type);
    props = wp_properties_new_wrap_dict (info->props);

    g_signal_emit (self, spa_device_signals[SIGNAL_CREATE_OBJECT], 0,
        id, type, info->factory_name, props);
  }
  else {
    g_signal_emit (self, spa_device_signals[SIGNAL_OBJECT_REMOVED], 0, id);
    wp_spa_device_store_managed_object (self, id, NULL);
  }
}

static const struct spa_device_events spa_device_events = {
  SPA_VERSION_DEVICE_EVENTS,
  .info = spa_device_event_info,
  .event = spa_device_event_event,
  .object_info = spa_device_event_object_info,
};

static WpObjectFeatures
wp_spa_device_get_supported_features (WpObject * object)
{
  return WP_PROXY_FEATURE_BOUND | WP_SPA_DEVICE_FEATURE_ENABLED;
}

enum {
  STEP_EXPORT = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_ADD_DEVICE_LISTENER,
};

static guint
wp_spa_device_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  if (missing & WP_PROXY_FEATURE_BOUND)
    return STEP_EXPORT;
  else if (missing & WP_SPA_DEVICE_FEATURE_ENABLED)
    return STEP_ADD_DEVICE_LISTENER;
  else
    return WP_TRANSITION_STEP_NONE;
}

static void
wp_spa_device_activate_execute_step (WpObject * object,
      WpFeatureActivationTransition * transition, guint step,
      WpObjectFeatures missing)
{
  WpSpaDevice *self = WP_SPA_DEVICE (object);

  switch (step) {
  case STEP_EXPORT: {
    g_autoptr (WpCore) core = wp_object_get_core (object);
    struct pw_core *pw_core = wp_core_get_pw_core (core);
    g_return_if_fail (pw_core);

    wp_proxy_set_pw_proxy (WP_PROXY (self),
        pw_core_export (pw_core, SPA_TYPE_INTERFACE_Device,
            wp_properties_peek_dict (self->properties),
            self->device, 0));
    break;
  }
  case STEP_ADD_DEVICE_LISTENER: {
    gint res = spa_device_add_listener (self->device, &self->listener,
        &spa_device_events, self);
    if (res < 0)
      wp_transition_return_error (WP_TRANSITION (transition),
          g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
              "failed to activate device: %s", spa_strerror (res)));
    else
      wp_object_update_features (object, WP_SPA_DEVICE_FEATURE_ENABLED, 0);
    break;
  }
  case WP_TRANSITION_STEP_ERROR:
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
wp_spa_device_deactivate (WpObject * object, WpObjectFeatures features)
{
  WP_OBJECT_CLASS (wp_spa_device_parent_class)->deactivate (object, features);

  if (features & WP_SPA_DEVICE_FEATURE_ENABLED) {
    WpSpaDevice *self = WP_SPA_DEVICE (object);
    spa_hook_remove (&self->listener);
    g_ptr_array_set_size (self->managed_objs, 0);
    wp_object_update_features (object, 0, WP_SPA_DEVICE_FEATURE_ENABLED);
  }
}

/*!
 * \struct WpSpaDevice
 *
 * A WpSpaDevice allows running a `spa_device` object locally,
 * loading the implementation from a SPA factory. This is useful to run device
 * monitors inside the session manager and have control over creating the
 * actual nodes that the `spa_device` requests to create.
 *
 * To enable the spa device, call wp_object_activate() requesting
 * WP_SPA_DEVICE_FEATURE_ENABLED.
 *
 * For actual devices (not device monitors) it also possible and desirable
 * to export the device to PipeWire, which can be done by requesting
 * WP_PROXY_FEATURE_BOUND from wp_object_activate(). When exporting, the
 * export should be done before enabling the device, by requesting both
 * features at the same time.
 *
 * \gproperties
 *
 * \gproperty{properties, WpProperties *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   Properties of the spa device}
 *
 * \gproperty{spa-device-handle, gpointer, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The spa device handle}
 *
 * \gsignals
 *
 * \par create-object
 * \parblock
 * \code
 * void
 * create_object_callback (WpSpaDevice * self,
 *                         guint id,
 *                         gchar * type,
 *                         gchar * factory,
 *                         WpProperties * properties,
 *                         gpointer user_data)
 * \endcode
 *
 * This signal is emitted when the device is creating a managed object
 * The handler is expected to actually construct the object using the requested
 * SPA factory and with the given properties. The handler should then store the
 * object with wp_spa_device_store_managed_object. The WpSpaDevice will later
 * unref the reference stored by this function when the managed object is to be
 * destroyed.
 *
 * Parameters:
 * - `id` - the id of the managed object
 * - `type` - the SPA type that the managed object should have
 * - `factory` - the name of the SPA factory to use to construct the managed object
 * - `properties` - additional properties that the managed object should have
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par object-removed
 * \parblock
 * \code
 * void
 * object_removed_callback (WpSpaDevice * self,
 *                          guint id,
 *                          gpointer user_data)
 * \endcode
 *
 * This signal is emitted when the device has deleted a managed object.
 * The handler may optionally release additional resources associated with this
 * object.
 *
 * It is not necessary to call wp_spa_device_store_managed_object() to remove
 * the managed object, as this is done internally after this signal is fired.
 *
 * Parameters:
 * - `id` - the id of the managed object
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 */
static void
wp_spa_device_class_init (WpSpaDeviceClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;

  object_class->constructed = wp_spa_device_constructed;
  object_class->finalize = wp_spa_device_finalize;
  object_class->set_property = wp_spa_device_set_property;
  object_class->get_property = wp_spa_device_get_property;

  wpobject_class->get_supported_features = wp_spa_device_get_supported_features;
  wpobject_class->activate_get_next_step = wp_spa_device_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_spa_device_activate_execute_step;
  wpobject_class->deactivate = wp_spa_device_deactivate;

  g_object_class_install_property (object_class, PROP_SPA_DEVICE_HANDLE,
      g_param_spec_pointer ("spa-device-handle", "spa-device-handle",
          "The spa device handle",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PROPERTIES,
      g_param_spec_boxed ("properties", "properties",
          "Properties of the device", WP_TYPE_PROPERTIES,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  spa_device_signals[SIGNAL_CREATE_OBJECT] = g_signal_new (
      "create-object", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      0, NULL, NULL, NULL, G_TYPE_NONE, 4, G_TYPE_UINT, G_TYPE_STRING,
      G_TYPE_STRING, WP_TYPE_PROPERTIES);

  spa_device_signals[SIGNAL_OBJECT_REMOVED] = g_signal_new (
      "object-removed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT);
}

/*!
 * \brief Constructs an SPA Device object from an existing device handle.
 *
 * \ingroup wpspadevice
 * \param core the wireplumber core
 * \param spa_device_handle the spa device handle
 * \param properties (nullable) (transfer full): additional properties of the device
 * \returns (transfer full): A new WpSpaDevice
 */
WpSpaDevice *
wp_spa_device_new_wrap (WpCore * core, gpointer spa_device_handle,
    WpProperties * properties)
{
  g_autoptr (WpProperties) props = properties;
  return g_object_new (WP_TYPE_SPA_DEVICE,
      "core", core,
      "spa-device-handle", spa_device_handle,
      "properties", props,
      NULL);
}

/*!
 * \brief Constructs a `SPA_TYPE_INTERFACE_Device` by loading the given SPA
 * \a factory_name.
 *
 * To export this device to the PipeWire server, you need to call
 * wp_object_activate() requesting WP_PROXY_FEATURE_BOUND and
 * wait for the operation to complete.
 *
 * \ingroup wpspadevice
 * \param core the wireplumber core
 * \param factory_name the name of the SPA factory
 * \param properties (nullable) (transfer full): properties to be passed to device
 *    constructor
 * \returns (nullable) (transfer full): A new WpSpaDevice wrapping the
 *   device that was constructed by the factory, or NULL if the factory
 *   does not exist or was unable to construct the device
 */
WpSpaDevice *
wp_spa_device_new_from_spa_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties)
{
  g_autoptr (WpProperties) props = properties;
  struct pw_context *pw_context = wp_core_get_pw_context (core);
  struct spa_handle *handle = NULL;

  g_return_val_if_fail (pw_context != NULL, NULL);

  /* Load the monitor handle */
  handle = pw_context_load_spa_handle (pw_context, factory_name,
      props ? wp_properties_peek_dict (props) : NULL);
  if (!handle) {
    wp_notice ("SPA handle '%s' could not be loaded; is it installed?",
        factory_name);
    return NULL;
  }

  return wp_spa_device_new_wrap (core, handle, g_steal_pointer (&props));
}

/*!
 * \brief Gets the properties of this device.
 *
 * \ingroup wpspadevice
 * \param self the spa device
 * \returns (transfer full): the device properties
 */
WpProperties *
wp_spa_device_get_properties (WpSpaDevice * self)
{
  g_return_val_if_fail (WP_IS_SPA_DEVICE (self), NULL);
  return wp_properties_ref (self->properties);
}

/*!
 * \brief Iterates through all the objects managed by this device.
 *
 * \ingroup wpspadevice
 * \param self the spa device
 * \returns (transfer full): a WpIterator that iterates over all the objects
 *   managed by this device
 * \since 0.4.11
 */
WpIterator *
wp_spa_device_new_managed_object_iterator (WpSpaDevice * self)
{
  g_return_val_if_fail (WP_IS_SPA_DEVICE (self), NULL);
  return wp_iterator_new_ptr_array (g_ptr_array_ref (self->managed_objs),
      G_TYPE_OBJECT);
}

/*!
 * \brief Gets one of the objects managed by this device.
 *
 * \ingroup wpspadevice
 * \param self the spa device
 * \param id the (device-internal) id of the object to get
 * \returns (transfer full): the managed object associated with \a id
 */
GObject *
wp_spa_device_get_managed_object (WpSpaDevice * self, guint id)
{
  g_return_val_if_fail (WP_IS_SPA_DEVICE (self), NULL);

  GObject *ret = (id < self->managed_objs->len) ?
      g_ptr_array_index (self->managed_objs, id) : NULL;
  return ret ? g_object_ref (ret) : ret;
}

/*!
 * \brief Stores or removes a managed object into/from a device.
 *
 * \ingroup wpspadevice
 * \param self the spa device
 * \param id the (device-internal) id of the object
 * \param object (transfer full) (nullable): the object to store or NULL to remove
 *   the managed object associated with \a id
 */
void
wp_spa_device_store_managed_object (WpSpaDevice * self, guint id,
    GObject * object)
{
  g_return_if_fail (WP_IS_SPA_DEVICE (self));

  if (id >= self->managed_objs->len)
    g_ptr_array_set_size (self->managed_objs, id + 1);

  /* replace the item at @em id; g_ptr_array_insert is tempting to use here
     instead, but it's wrong because it will not remove the previous item */
  gpointer *ptr = &g_ptr_array_index (self->managed_objs, id);
  if (*ptr)
    g_object_unref (*ptr);
  *ptr = object;
}
  07070100000082000081A400000000000000000000000165F8630400000687000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/device.h /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_DEVICE_H__
#define __WIREPLUMBER_DEVICE_H__

#include "global-proxy.h"

G_BEGIN_DECLS

/* WpDevice */

/*!
 * \brief The WpDevice GType
 * \ingroup wpdevice
 */
#define WP_TYPE_DEVICE (wp_device_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpDevice, wp_device, WP, DEVICE, WpGlobalProxy)

WP_API
WpDevice * wp_device_new_from_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties);

/* WpSpaDevice */

/*!
 * \brief Flags to be used as WpObjectFeatures for WpSpaDevice
 * \ingroup wpspadevice
 */
typedef enum { /*< flags >*/
  /*! enables a spa device */
  WP_SPA_DEVICE_FEATURE_ENABLED = (WP_PROXY_FEATURE_CUSTOM_START << 0),
} WpSpaDeviceFeatures;

/*!
 * \brief The WpSpaDevice GType
 * \ingroup wpspadevice
 */
#define WP_TYPE_SPA_DEVICE (wp_spa_device_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpSpaDevice, wp_spa_device, WP, SPA_DEVICE, WpProxy)

WP_API
WpSpaDevice * wp_spa_device_new_wrap (WpCore * core,
    gpointer spa_device_handle, WpProperties * properties);

WP_API
WpSpaDevice * wp_spa_device_new_from_spa_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties);

WP_API
WpProperties * wp_spa_device_get_properties (WpSpaDevice * self);

WP_API
WpIterator * wp_spa_device_new_managed_object_iterator (WpSpaDevice * self);

WP_API
GObject * wp_spa_device_get_managed_object (WpSpaDevice * self, guint id);

WP_API
void wp_spa_device_store_managed_object (WpSpaDevice * self, guint id,
    GObject * object);

G_END_DECLS

#endif
 07070100000083000081A400000000000000000000000165F8630400000110000000000000000000000000000000000000002100000000wireplumber-0.5.0/lib/wp/error.c  /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "error.h"

/*! \defgroup wperror Errors */

G_DEFINE_QUARK (wireplumber-library, wp_domain_library);
07070100000084000081A400000000000000000000000165F863040000040F000000000000000000000000000000000000002100000000wireplumber-0.5.0/lib/wp/error.h  /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WP_ERROR_H__
#define __WP_ERROR_H__

#include <glib.h>
#include "defs.h"

G_BEGIN_DECLS

/*!
 * \brief A GError domain for errors that occurred within the context of the
 * WirePlumber library.
 * \ingroup wperror
 */
#define WP_DOMAIN_LIBRARY (wp_domain_library_quark ())
WP_API
GQuark wp_domain_library_quark (void);

/*!
 * \brief Error codes that can appear in a GError when the error domain
 * is WP_DOMAIN_LIBRARY
 * \ingroup wperror
 */
typedef enum {
  /*! an invariant check failed; this most likely
   *  indicates a programming error */
  WP_LIBRARY_ERROR_INVARIANT,
  /*! an unexpected/invalid argument was given */
  WP_LIBRARY_ERROR_INVALID_ARGUMENT,
  /*! an operation failed */
  WP_LIBRARY_ERROR_OPERATION_FAILED,
  /*! a required external service was unavailable */
  WP_LIBRARY_ERROR_SERVICE_UNAVAILABLE,
} WpLibraryErrorEnum;

G_END_DECLS

#endif
 07070100000085000081A400000000000000000000000165F8630400002597000000000000000000000000000000000000002C00000000wireplumber-0.5.0/lib/wp/event-dispatcher.c   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "event-dispatcher.h"
#include "log.h"

#include <spa/support/plugin.h>
#include <spa/support/system.h>
#include <pipewire/pipewire.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-dispatcher")

typedef struct _EventData EventData;
struct _EventData
{
  WpEvent *event;
  WpIterator *hooks_iter;
  WpEventHook *current_hook_in_async;
  gint64 seq;
};

static inline EventData *
event_data_new (WpEvent * event)
{
  static gint64 seqn = 0;
  EventData *event_data = g_new0 (EventData, 1);
  event_data->event = wp_event_ref (event);
  event_data->hooks_iter = wp_event_new_hooks_iterator (event);
  event_data->seq = seqn++;
  return event_data;
}

static void
event_data_free (EventData * self)
{
  g_clear_pointer (&self->event, wp_event_unref);
  g_clear_pointer (&self->hooks_iter, wp_iterator_unref);
  g_clear_object (&self->current_hook_in_async);
  g_free (self);
}

struct _WpEventDispatcher
{
  GObject parent;

  GWeakRef core;
  GPtrArray *hooks; /* registered hooks */
  GSource *source;  /* the event loop source */
  GList *events;    /* the events stack */
  struct spa_system *system;
  int eventfd;
};

G_DEFINE_TYPE (WpEventDispatcher, wp_event_dispatcher, G_TYPE_OBJECT)

#define WP_EVENT_SOURCE_DISPATCHER(x) \
    WP_EVENT_DISPATCHER (((WpEventSource *) x)->dispatcher)

typedef struct _WpEventSource WpEventSource;
struct _WpEventSource
{
  GSource parent;
  WpEventDispatcher *dispatcher;
};

static gboolean
wp_event_source_check (GSource * s)
{
  WpEventDispatcher *d = WP_EVENT_SOURCE_DISPATCHER (s);
  return d && d->events &&
      !((EventData *) g_list_first (d->events)->data)->current_hook_in_async;
}

static void
on_event_hook_done (WpEventHook * hook, GAsyncResult * res, EventData * data)
{
  g_autoptr (GError) error = NULL;
  g_autoptr (WpEventDispatcher) dispatcher =
      wp_event_hook_get_dispatcher (hook);

  g_assert (data->current_hook_in_async == hook);

  if (!wp_event_hook_finish (hook, res, &error) && error &&
      error->domain != G_IO_ERROR && error->code != G_IO_ERROR_CANCELLED)
    wp_notice_object (hook, "failed: %s", error->message);

  g_clear_object (&data->current_hook_in_async);
  spa_system_eventfd_write (dispatcher->system, dispatcher->eventfd, 1);
}

static gboolean
wp_event_source_dispatch (GSource * s, GSourceFunc callback, gpointer user_data)
{
  WpEventDispatcher *d = WP_EVENT_SOURCE_DISPATCHER (s);
  uint64_t count;

  /* clear the eventfd */
  spa_system_eventfd_read (d->system, d->eventfd, &count);

  /* get the highest priority event */
  GList *levent = g_list_first (d->events);
  while (levent) {
    EventData *event_data = (EventData *) (levent->data);
    WpEvent *event = event_data->event;
    GCancellable *cancellable = wp_event_get_cancellable (event);
    g_auto (GValue) value = G_VALUE_INIT;
    gboolean has_next = FALSE;

    /* event hook is still in progress, we will continue later */
    if (event_data->current_hook_in_async)
      return G_SOURCE_CONTINUE;

    /* check if the event was cancelled */
    if (g_cancellable_is_cancelled (cancellable)) {
      wp_debug_object (d, "event(%p) cancelled remove it", event);
      has_next = FALSE;
    } else {
      /* get the highest priority hook */
      has_next = wp_iterator_next (event_data->hooks_iter, &value);
    }

    if (has_next) {
      WpEventHook *hook = g_value_get_object (&value);
      const gchar *name = wp_event_hook_get_name (hook);

      event_data->current_hook_in_async = g_object_ref (hook);

      wp_trace_object(d, "dispatching event (%s) running hook <%p>(%s)",
          wp_event_get_name(event), hook, name);

      /* execute the hook, possibly async */
      wp_event_hook_run (hook, event, cancellable,
          (GAsyncReadyCallback) on_event_hook_done, event_data);
    } else {
      /* clear the event after all hooks are done */
      d->events = g_list_delete_link (d->events, g_steal_pointer (&levent));
      g_clear_pointer (&event_data, event_data_free);
    }

    /* get the next event */
    levent = g_list_first (d->events);
  }

  return G_SOURCE_CONTINUE;
}

static GSourceFuncs source_funcs = {
  NULL,
  wp_event_source_check,
  wp_event_source_dispatch,
  NULL
};

static void
wp_event_dispatcher_init (WpEventDispatcher * self)
{
  g_weak_ref_init (&self->core, NULL);
  self->hooks = g_ptr_array_new_with_free_func (g_object_unref);

  self->source = g_source_new (&source_funcs, sizeof (WpEventSource));
  ((WpEventSource *) self->source)->dispatcher = self;

  /* this is higher than normal "idle" operations but lower than the default
     priority, which is used for events from the PipeWire socket and timers */
  g_source_set_priority (self->source, G_PRIORITY_HIGH_IDLE);
}

static void
wp_event_dispatcher_finalize (GObject * object)
{
  WpEventDispatcher *self = WP_EVENT_DISPATCHER (object);

  g_list_free_full (g_steal_pointer (&self->events),
      (GDestroyNotify) event_data_free);

  ((WpEventSource *) self->source)->dispatcher = NULL;
  g_source_destroy (self->source);
  g_clear_pointer (&self->source, g_source_unref);

  close (self->eventfd);

  g_clear_pointer (&self->hooks, g_ptr_array_unref);
  g_weak_ref_clear (&self->core);

  G_OBJECT_CLASS (wp_event_dispatcher_parent_class)->finalize (object);
}

static void
wp_event_dispatcher_class_init (WpEventDispatcherClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->finalize = wp_event_dispatcher_finalize;
}

/*!
 * \brief Returns the event dispatcher instance that is associated with the
 * given core.
 *
 * This method will also create the instance and register it with the core,
 * if it had not been created before.
 *
 * \ingroup wpeventdispatcher
 * \param core the core
 * \return (transfer full): the event dispatcher instance
 */
WpEventDispatcher *
wp_event_dispatcher_get_instance (WpCore * core)
{
  WpEventDispatcher *dispatcher = wp_core_find_object (core,
      (GEqualFunc) WP_IS_EVENT_DISPATCHER, NULL);

  if (G_UNLIKELY (!dispatcher)) {
    dispatcher = g_object_new (WP_TYPE_EVENT_DISPATCHER, NULL);
    g_weak_ref_set (&dispatcher->core, core);

    struct pw_context *context = wp_core_get_pw_context (core);
    uint32_t n_support;
    const struct spa_support *support =
        pw_context_get_support (context, &n_support);
    dispatcher->system =
        spa_support_find (support, n_support, SPA_TYPE_INTERFACE_System);

    dispatcher->eventfd = spa_system_eventfd_create (dispatcher->system, 0);
    g_source_add_unix_fd (dispatcher->source, dispatcher->eventfd, G_IO_IN);

    g_source_attach (dispatcher->source, wp_core_get_g_main_context (core));
    wp_core_register_object (core, g_object_ref (dispatcher));

    wp_info_object (dispatcher, "event-dispatcher inited");
  }

  return dispatcher;
}

static gint
event_cmp_func (const EventData *a, const EventData *b)
{
  gint c = wp_event_get_priority (b->event) - wp_event_get_priority (a->event);
  return (c != 0) ? c : (gint)(a->seq - b->seq);
}

/*!
 * \brief Pushes a new event onto the event stack for dispatching only if there
 * are any hooks are available for it.
 * \ingroup wpeventdispatcher
 *
 * \param self the dispatcher
 * \param event (transfer full): the new event
 */
void
wp_event_dispatcher_push_event (WpEventDispatcher * self, WpEvent * event)
{
  g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
  g_return_if_fail (event != NULL);

  if (wp_event_collect_hooks (event, self)) {
    EventData *event_data = event_data_new (event);

    self->events = g_list_insert_sorted (self->events, event_data,
        (GCompareFunc) event_cmp_func);
    wp_debug_object (self, "pushed event (%s)", wp_event_get_name (event));

    /* wakeup the GSource */
    spa_system_eventfd_write (self->system, self->eventfd, 1);
  }

  wp_event_unref (event);
}

/*!
 * \brief Registers an event hook
 * \ingroup wpeventdispatcher
 *
 * \param self the event dispatcher
 * \param hook (transfer none): the hook to register
 */
void
wp_event_dispatcher_register_hook (WpEventDispatcher * self,
    WpEventHook * hook)
{
  g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
  g_return_if_fail (WP_IS_EVENT_HOOK (hook));

  g_autoptr (WpEventDispatcher) already_registered_dispatcher =
      wp_event_hook_get_dispatcher (hook);
  g_return_if_fail (already_registered_dispatcher == NULL);

  wp_event_hook_set_dispatcher (hook, self);
  g_ptr_array_add (self->hooks, g_object_ref (hook));
}

/*!
 * \brief Unregisters an event hook
 * \ingroup wpeventdispatcher
 *
 * \param self the event dispatcher
 * \param hook (transfer none): the hook to unregister
 */
void
wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
    WpEventHook * hook)
{
  g_return_if_fail (WP_IS_EVENT_DISPATCHER (self));
  g_return_if_fail (WP_IS_EVENT_HOOK (hook));

  g_autoptr (WpEventDispatcher) already_registered_dispatcher =
      wp_event_hook_get_dispatcher (hook);
  g_return_if_fail (already_registered_dispatcher == self);

  wp_event_hook_set_dispatcher (hook, NULL);
  g_ptr_array_remove_fast (self->hooks, hook);
}

/*!
 * \brief Returns an iterator to iterate over all the registered hooks
 * \ingroup wpeventdispatcher
 *
 * \param self the event dispatcher
 * \return (transfer full): a new iterator
 */
WpIterator *
wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self)
{
  GPtrArray *items =
      g_ptr_array_copy (self->hooks, (GCopyFunc) g_object_ref, NULL);
  return wp_iterator_new_ptr_array (items, WP_TYPE_EVENT_HOOK);
}
 07070100000086000081A400000000000000000000000165F86304000004AF000000000000000000000000000000000000002C00000000wireplumber-0.5.0/lib/wp/event-dispatcher.h   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_EVENT_DISPATCHER_H__
#define __WIREPLUMBER_EVENT_DISPATCHER_H__

#include "core.h"
#include "event.h"
#include "event-hook.h"

G_BEGIN_DECLS

/*! \defgroup wpeventdispatcher WpEventDispatcher */
/*!
 * \struct WpEventDispatcher
 *
 * The event dispatcher holds all the events and hooks and dispatches them. It orchestras the show on event stack.
 */
#define WP_TYPE_EVENT_DISPATCHER (wp_event_dispatcher_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpEventDispatcher, wp_event_dispatcher,
                      WP, EVENT_DISPATCHER, GObject)

WP_API
WpEventDispatcher * wp_event_dispatcher_get_instance (WpCore * core);

WP_API
void wp_event_dispatcher_push_event (WpEventDispatcher * self, WpEvent * event);

WP_API
void wp_event_dispatcher_register_hook (WpEventDispatcher * self,
    WpEventHook * hook);

WP_API
void wp_event_dispatcher_unregister_hook (WpEventDispatcher * self,
    WpEventHook * hook);

WP_API
WpIterator * wp_event_dispatcher_new_hooks_iterator (WpEventDispatcher * self);

G_END_DECLS

#endif
 07070100000087000081A400000000000000000000000165F8630400005915000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/event-hook.c /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "event-hook.h"
#include "event-dispatcher.h"
#include "transition.h"
#include "log.h"
#include "wpenums.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-hook")

typedef struct _WpEventHookPrivate WpEventHookPrivate;
struct _WpEventHookPrivate
{
  GWeakRef dispatcher;
  gchar *name;
  gchar **before;
  gchar **after;
};

enum {
  PROP_0,
  PROP_NAME,
  PROP_RUNS_BEFORE_HOOKS,
  PROP_RUNS_AFTER_HOOKS,
  PROP_DISPATCHER,
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpEventHook, wp_event_hook, G_TYPE_OBJECT)

static void
wp_event_hook_init (WpEventHook * self)
{
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  g_weak_ref_init (&priv->dispatcher, NULL);
}

static void
wp_event_hook_finalize (GObject * object)
{
  WpEventHook *self = WP_EVENT_HOOK (object);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);

  g_weak_ref_clear (&priv->dispatcher);
  g_strfreev (priv->before);
  g_strfreev (priv->after);
  g_free (priv->name);

  G_OBJECT_CLASS (wp_event_hook_parent_class)->finalize (object);
}

static void
wp_event_hook_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpEventHook *self = WP_EVENT_HOOK (object);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);

  switch (property_id) {
  case PROP_NAME:
    priv->name = g_value_dup_string (value);
    break;
  case PROP_RUNS_BEFORE_HOOKS:
    priv->before = g_value_dup_boxed (value);
    break;
  case PROP_RUNS_AFTER_HOOKS:
    priv->after = g_value_dup_boxed (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_event_hook_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpEventHook *self = WP_EVENT_HOOK (object);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, priv->name);
    break;
  case PROP_RUNS_BEFORE_HOOKS:
    g_value_set_boxed (value, priv->before);
    break;
  case PROP_RUNS_AFTER_HOOKS:
    g_value_set_boxed (value, priv->after);
    break;
  case PROP_DISPATCHER:
    g_value_take_object (value, wp_event_hook_get_dispatcher (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_event_hook_class_init (WpEventHookClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->finalize = wp_event_hook_finalize;
  object_class->set_property = wp_event_hook_set_property;
  object_class->get_property = wp_event_hook_get_property;

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name", "The hook name", "",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_RUNS_BEFORE_HOOKS,
      g_param_spec_boxed ("runs-before-hooks", "runs-before-hooks",
          "runs-before-hooks", G_TYPE_STRV,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_RUNS_AFTER_HOOKS,
      g_param_spec_boxed ("runs-after-hooks", "runs-after-hooks",
          "runs-after-hooks", G_TYPE_STRV,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_DISPATCHER,
      g_param_spec_object ("dispatcher", "dispatcher",
          "The associated event dispatcher", WP_TYPE_EVENT_DISPATCHER,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Returns the name of the hook
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \return the event hook name
 */
const gchar *
wp_event_hook_get_name (WpEventHook * self)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), 0);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  return priv->name;
}

/*!
 * \brief Returns the names of the hooks that should run after this hook,
 *    or in other words, this hook should run before them
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \return (array zero-terminated=1)(element-type utf8)(transfer none):
 *    a NULL-terminated array of hook names
 */
const gchar * const *
wp_event_hook_get_runs_before_hooks (WpEventHook * self)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  return (const gchar * const *) priv->before;
}

/*!
 * \brief Returns the names of the hooks that should run before this hook,
 *    or in other words, this hook should run after them
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \return (array zero-terminated=1)(element-type utf8)(transfer none):
 *    a NULL-terminated array of hook names
 */
const gchar * const *
wp_event_hook_get_runs_after_hooks (WpEventHook * self)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  return (const gchar * const *) priv->after;
}

/*!
 * \brief Returns the associated event dispatcher
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \return (transfer full)(nullable): the event dispatcher on which this hook
 *   is registered, or NULL if the hook is not registered
 */
WpEventDispatcher *
wp_event_hook_get_dispatcher (WpEventHook * self)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  return g_weak_ref_get (&priv->dispatcher);
}

void
wp_event_hook_set_dispatcher (WpEventHook * self, WpEventDispatcher * dispatcher)
{
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  wp_trace_object (dispatcher, "hook (%s) registered", priv->name);
  g_weak_ref_set (&priv->dispatcher, dispatcher);
}

/*!
 * \brief Checks if the hook should be executed for a given event
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \param event the event
 * \return TRUE if the hook should be executed for the given event,
 *    FALSE otherwise
 */
gboolean
wp_event_hook_runs_for_event (WpEventHook * self, WpEvent * event)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), FALSE);
  g_return_val_if_fail (WP_EVENT_HOOK_GET_CLASS (self)->runs_for_event, FALSE);
  return WP_EVENT_HOOK_GET_CLASS (self)->runs_for_event (self, event);
}

/*!
 * \brief Runs the hook on the given event
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \param event the event that triggered the hook
 * \param cancellable (nullable): a GCancellable to cancel the async operation
 * \param callback (scope async): a callback to fire after execution of the hook
 *    has completed
 * \param callback_data (closure): data for the callback
 */
void
wp_event_hook_run (WpEventHook * self,
    WpEvent * event, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data)
{
  g_return_if_fail (WP_IS_EVENT_HOOK (self));
  g_return_if_fail (WP_EVENT_HOOK_GET_CLASS (self)->run);
  WP_EVENT_HOOK_GET_CLASS (self)->run (self, event, cancellable, callback,
      callback_data);
}

/*!
 * \brief Finishes the async operation that was started by wp_event_hook_run()
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \param res the async operation result
 * \param error (out) (optional): the error of the operation, if any
 * \returns FALSE if there was an error, TRUE otherwise
 */
gboolean
wp_event_hook_finish (WpEventHook * self, GAsyncResult * res, GError ** error)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), FALSE);
  g_return_val_if_fail (WP_EVENT_HOOK_GET_CLASS (self)->finish, FALSE);
  return WP_EVENT_HOOK_GET_CLASS (self)->finish (self, res, error);
}



/*!
 * \struct WpInterestEventHook
 *
 * An event hook that declares interest in specific events. This subclass
 * implements the WpEventHook.runs_for_event() vmethod and returns TRUE from
 * that method if the given event has properties that match one of the declared
 * interests.
 */

typedef struct _WpInterestEventHookPrivate WpInterestEventHookPrivate;
struct _WpInterestEventHookPrivate
{
  /* element-type: WpObjectInterest* */
  GPtrArray *interests;
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpInterestEventHook,
                                     wp_interest_event_hook, WP_TYPE_EVENT_HOOK)

static void
wp_interest_event_hook_init (WpInterestEventHook * self)
{
  WpInterestEventHookPrivate *priv =
      wp_interest_event_hook_get_instance_private (self);
  priv->interests = g_ptr_array_new_with_free_func (
      (GDestroyNotify) wp_object_interest_unref);
}

static void
wp_interest_event_hook_finalize (GObject * object)
{
  WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (object);
  WpInterestEventHookPrivate *priv =
      wp_interest_event_hook_get_instance_private (self);

  g_clear_pointer (&priv->interests, g_ptr_array_unref);

  G_OBJECT_CLASS (wp_interest_event_hook_parent_class)->finalize (object);
}

static gboolean
wp_interest_event_hook_runs_for_event (WpEventHook * hook, WpEvent * event)
{
  WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
  WpInterestEventHookPrivate *priv =
      wp_interest_event_hook_get_instance_private (self);
  g_autoptr (WpProperties) properties = wp_event_get_properties (event);
  g_autoptr (GObject) subject = wp_event_get_subject (event);
  GType gtype = subject ? G_OBJECT_TYPE (subject) : WP_TYPE_EVENT;
  guint i;
  WpObjectInterest *interest = NULL;
  WpInterestMatch match;

  const unsigned int MATCH_ALL_PROPS = (WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
                               WP_INTEREST_MATCH_PW_PROPERTIES |
                               WP_INTEREST_MATCH_G_PROPERTIES);

  for (i = 0; i < priv->interests->len; i++) {
    interest = g_ptr_array_index (priv->interests, i);
    match = wp_object_interest_matches_full (interest,
        WP_INTEREST_MATCH_FLAGS_CHECK_ALL,
        gtype, subject, properties, properties);

    /* the interest may have a GType that matches the GType of the subject
       or it may have WP_TYPE_EVENT as its GType, in which case it will
       match any type of subject */
    if (match == WP_INTEREST_MATCH_ALL)
      return TRUE;
    else if (subject && (match & MATCH_ALL_PROPS) == MATCH_ALL_PROPS) {
      match = wp_object_interest_matches_full (interest, 0,
          WP_TYPE_EVENT, NULL, NULL, NULL);
      if (match & WP_INTEREST_MATCH_GTYPE)
        return TRUE;
    }
  }
  return FALSE;
}

static void
wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpEventHookClass *hook_class = (WpEventHookClass *) klass;

  object_class->finalize = wp_interest_event_hook_finalize;
  hook_class->runs_for_event = wp_interest_event_hook_runs_for_event;
}

/*!
 * \brief Equivalent to:
 * \code
 * WpObjectInterest *i = wp_object_interest_new (WP_TYPE_EVENT, ...);
 * wp_interest_event_hook_add_interest_full (self, i);
 * \endcode
 *
 * The constraints specified in the variable arguments must follow the rules
 * documented in wp_object_interest_new().
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \param ... a list of constraints, terminated by NULL
 */
void
wp_interest_event_hook_add_interest (WpInterestEventHook * self, ...)
{
  WpObjectInterest *interest;
  va_list args;

  g_return_if_fail (WP_IS_INTEREST_EVENT_HOOK (self));

  va_start (args, self);
  interest = wp_object_interest_new_valist (WP_TYPE_EVENT, &args);
  wp_interest_event_hook_add_interest_full (self, interest);
  va_end (args);
}

/*!
 * \brief Declares interest on events. The interest must be constructed to
 * match WP_TYPE_EVENT objects and it is going to be matched against
 * the WpEvent's properties.
 *
 * \param self the event hook
 * \param interest (transfer full): the event interest
 */
void
wp_interest_event_hook_add_interest_full (WpInterestEventHook * self,
    WpObjectInterest * interest)
{
  g_autoptr (GError) error = NULL;

  g_return_if_fail (WP_IS_INTEREST_EVENT_HOOK (self));

  if (G_UNLIKELY (!wp_object_interest_validate (interest, &error))) {
    wp_critical_object (self, "interest validation failed: %s",
        error->message);
    wp_object_interest_unref (interest);
    return;
  }

  WpInterestEventHookPrivate *priv =
      wp_interest_event_hook_get_instance_private (self);
  g_ptr_array_add (priv->interests, interest);
}



/*!
 * \struct WpSimpleEventHook
 *
 * An event hook that runs a GClosure, synchronously.
 */

struct _WpSimpleEventHook
{
  WpInterestEventHook parent;
  GClosure *closure;
};

enum {
  PROP_SIMPLE_0,
  PROP_SIMPLE_CLOSURE,
};

G_DEFINE_TYPE (WpSimpleEventHook, wp_simple_event_hook,
               WP_TYPE_INTEREST_EVENT_HOOK)

static void
wp_simple_event_hook_init (WpSimpleEventHook * self)
{
}

static void
wp_simple_event_hook_finalize (GObject * object)
{
  WpSimpleEventHook *self = WP_SIMPLE_EVENT_HOOK (object);

  g_clear_pointer (&self->closure, g_closure_unref);

  G_OBJECT_CLASS (wp_simple_event_hook_parent_class)->finalize (object);
}

static void
wp_simple_event_hook_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpSimpleEventHook *self = WP_SIMPLE_EVENT_HOOK (object);

  switch (property_id) {
  case PROP_SIMPLE_CLOSURE:
    self->closure = g_value_dup_boxed (value);
    g_closure_sink (self->closure);
    if (G_CLOSURE_NEEDS_MARSHAL (self->closure))
      g_closure_set_marshal (self->closure, g_cclosure_marshal_generic);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_simple_event_hook_run (WpEventHook * hook,
    WpEvent * event, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data)
{
  WpSimpleEventHook *self = WP_SIMPLE_EVENT_HOOK (hook);

  GValue values[2] = { G_VALUE_INIT };
  g_value_init (&values[0], WP_TYPE_EVENT);
  g_value_set_boxed (&values[0], event);
  g_closure_invoke (self->closure, NULL, 1, values, NULL);
  g_value_unset (&values[0]);

  callback ((GObject *) self, NULL, callback_data);
}

static gboolean
wp_simple_event_hook_finish (WpEventHook * hook, GAsyncResult * res,
    GError ** error)
{
  return TRUE;
}

static void
wp_simple_event_hook_class_init (WpSimpleEventHookClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpEventHookClass *hook_class = (WpEventHookClass *) klass;

  object_class->finalize = wp_simple_event_hook_finalize;
  object_class->set_property = wp_simple_event_hook_set_property;
  hook_class->run = wp_simple_event_hook_run;
  hook_class->finish = wp_simple_event_hook_finish;

  g_object_class_install_property (object_class, PROP_SIMPLE_CLOSURE,
      g_param_spec_boxed ("closure", "closure", "The closure", G_TYPE_CLOSURE,
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Constructs a new simple event hook
 *
 * \param name the name of the hook
 * \param before (array zero-terminated=1)(element-type utf8)(transfer none)(nullable):
 *    an array of hook names that should run after this hook
 * \param after (array zero-terminated=1)(element-type utf8)(transfer none)(nullable):
 *    an array of hook names that should run before this hook
 * \param closure the closure to invoke when the hook is executed; the closure
 *   should accept two parameters: the event dispatcher and the event, returning
 *   nothing
 * \return a new simple event hook
 */
WpEventHook *
wp_simple_event_hook_new (const gchar *name,
    const gchar * before[], const gchar * after[], GClosure * closure)
{
  g_return_val_if_fail (closure != NULL, NULL);

  return g_object_new (WP_TYPE_SIMPLE_EVENT_HOOK,
      "name", name,
      "runs-before-hooks", before,
      "runs-after-hooks", after,
      "closure", closure,
      NULL);
}


/* WpAsyncEventHookTransition */

#define WP_TYPE_ASYNC_EVENT_HOOK_TRANSITION \
    (wp_async_event_hook_transition_get_type ())
G_DECLARE_FINAL_TYPE (WpAsyncEventHookTransition,
                      wp_async_event_hook_transition,
                      WP, ASYNC_EVENT_HOOK_TRANSITION, WpTransition)


/*!
 * \struct WpAsyncEventHook
 *
 * An event hook that runs a WpTransition, implemented with closures.
 */

struct _WpAsyncEventHook
{
  WpInterestEventHook parent;
  GClosure *get_next_step;
  GClosure *execute_step;
};

enum {
  PROP_ASYNC_0,
  PROP_ASYNC_GET_NEXT_STEP,
  PROP_ASYNC_EXECUTE_STEP,
};

G_DEFINE_TYPE (WpAsyncEventHook, wp_async_event_hook,
               WP_TYPE_INTEREST_EVENT_HOOK)

static void
wp_async_event_hook_init (WpAsyncEventHook * self)
{
}

static void
wp_async_event_hook_finalize (GObject * object)
{
  WpAsyncEventHook *self = WP_ASYNC_EVENT_HOOK (object);

  g_clear_pointer (&self->get_next_step, g_closure_unref);
  g_clear_pointer (&self->execute_step, g_closure_unref);

  G_OBJECT_CLASS (wp_async_event_hook_parent_class)->finalize (object);
}

static void
wp_async_event_hook_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpAsyncEventHook *self = WP_ASYNC_EVENT_HOOK (object);

  switch (property_id) {
  case PROP_ASYNC_GET_NEXT_STEP:
    self->get_next_step = g_value_dup_boxed (value);
    g_closure_sink (self->get_next_step);
    if (G_CLOSURE_NEEDS_MARSHAL (self->get_next_step))
      g_closure_set_marshal (self->get_next_step, g_cclosure_marshal_generic);
    break;
  case PROP_ASYNC_EXECUTE_STEP:
    self->execute_step = g_value_dup_boxed (value);
    g_closure_sink (self->execute_step);
    if (G_CLOSURE_NEEDS_MARSHAL (self->execute_step))
      g_closure_set_marshal (self->execute_step, g_cclosure_marshal_VOID__UINT);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_async_event_hook_run (WpEventHook * hook,
    WpEvent * event, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data)
{
  WpTransition *transition = wp_transition_new (
      WP_TYPE_ASYNC_EVENT_HOOK_TRANSITION,
      hook, cancellable, callback, callback_data);
  wp_transition_set_data (transition, wp_event_ref (event),
      (GDestroyNotify) wp_event_unref);
  wp_transition_set_source_tag (transition, wp_async_event_hook_run);
  wp_transition_advance (transition);
}

static gboolean
wp_async_event_hook_finish (WpEventHook * hook, GAsyncResult * res,
    GError ** error)
{
  g_return_val_if_fail (g_async_result_is_tagged (res, wp_async_event_hook_run),
      FALSE);
  return wp_transition_finish (res, error);
}

static void
wp_async_event_hook_class_init (WpAsyncEventHookClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpEventHookClass *hook_class = (WpEventHookClass *) klass;

  object_class->finalize = wp_async_event_hook_finalize;
  object_class->set_property = wp_async_event_hook_set_property;
  hook_class->run = wp_async_event_hook_run;
  hook_class->finish = wp_async_event_hook_finish;

  g_object_class_install_property (object_class, PROP_ASYNC_GET_NEXT_STEP,
      g_param_spec_boxed ("get-next-step", "get-next-step",
          "The get-next-step closure", G_TYPE_CLOSURE,
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_ASYNC_EXECUTE_STEP,
      g_param_spec_boxed ("execute-step", "execute-step",
          "The execute-step closure", G_TYPE_CLOSURE,
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Constructs a new async event hook
 *
 * \param name the name of the hook
 * \param before (array zero-terminated=1)(element-type utf8)(transfer none)(nullable):
 *    an array of hook names that should run after this hook
 * \param after (array zero-terminated=1)(element-type utf8)(transfer none)(nullable):
 *    an array of hook names that should run before this hook
 * \param get_next_step the closure to invoke to get the next step
 * \param execute_step the closure to invoke to execute the step
 * \return a new async event hook
 */
WpEventHook *
wp_async_event_hook_new (const gchar *name,
    const gchar * before[], const gchar * after[],
    GClosure * get_next_step, GClosure * execute_step)
{
  g_return_val_if_fail (get_next_step != NULL, NULL);
  g_return_val_if_fail (execute_step != NULL, NULL);

  return g_object_new (WP_TYPE_ASYNC_EVENT_HOOK,
      "name", name,
      "runs-before-hooks", before,
      "runs-after-hooks", after,
      "get-next-step", get_next_step,
      "execute-step", execute_step,
      NULL);
}


/* WpAsyncEventHookTransition - implementation */

struct _WpAsyncEventHookTransition
{
  WpTransition parent;
};

G_DEFINE_TYPE (WpAsyncEventHookTransition,
               wp_async_event_hook_transition,
               WP_TYPE_TRANSITION)

static void
wp_async_event_hook_transition_init (
    WpAsyncEventHookTransition * transition)
{
}

static guint
wp_async_event_hook_transition_get_next_step (
    WpTransition * transition, guint step)
{
  WpAsyncEventHook *hook =
      WP_ASYNC_EVENT_HOOK (wp_transition_get_source_object (transition));
  guint next_step = WP_TRANSITION_STEP_ERROR;
  GValue ret = G_VALUE_INIT;
  GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT };

  g_value_init (&ret, G_TYPE_UINT);
  g_value_init (&values[0], G_TYPE_OBJECT);
  g_value_init (&values[1], G_TYPE_UINT);
  g_value_set_object (&values[0], transition);
  g_value_set_uint (&values[1], step);
  g_closure_invoke (hook->get_next_step, &ret, 2, values, NULL);
  g_value_unset (&values[0]);
  g_value_unset (&values[1]);
  next_step = g_value_get_uint (&ret);
  g_value_unset (&ret);
  return next_step;
}

static void
wp_async_event_hook_transition_execute_step (
    WpTransition * transition, guint step)
{
  WpAsyncEventHook *hook =
      WP_ASYNC_EVENT_HOOK (wp_transition_get_source_object (transition));
  GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT };

  g_value_init (&values[0], G_TYPE_OBJECT);
  g_value_init (&values[1], G_TYPE_UINT);
  g_value_set_object (&values[0], transition);
  g_value_set_uint (&values[1], step);
  g_closure_invoke (hook->execute_step, NULL, 2, values, NULL);
  g_value_unset (&values[0]);
  g_value_unset (&values[1]);
}

static void
wp_async_event_hook_transition_class_init (
    WpAsyncEventHookTransitionClass * klass)
{
  WpTransitionClass *transition_class = (WpTransitionClass *) klass;

  transition_class->get_next_step =
      wp_async_event_hook_transition_get_next_step;
  transition_class->execute_step =
      wp_async_event_hook_transition_execute_step;
}
   07070100000088000081A400000000000000000000000165F8630400000D6D000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/event-hook.h /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_EVENT_HOOK_H__
#define __WIREPLUMBER_EVENT_HOOK_H__

#include "properties.h"
#include "object-interest.h"

G_BEGIN_DECLS

typedef struct _WpEvent WpEvent;
typedef struct _WpEventDispatcher WpEventDispatcher;

/*! \defgroup wpeventhook WpEventHook */
/*!
 * \struct WpEventHook
 *
 * The event hook is a structure that describes some executable action
 * that an event dispatcher will run when a matching event has been received.
 */
#define WP_TYPE_EVENT_HOOK (wp_event_hook_get_type ())
WP_API
G_DECLARE_DERIVABLE_TYPE (WpEventHook, wp_event_hook, WP, EVENT_HOOK, GObject)

struct _WpEventHookClass
{
  GObjectClass parent_class;

  gboolean (*runs_for_event) (WpEventHook * self, WpEvent * event);

  void (*run) (WpEventHook * self, WpEvent * event, GCancellable * cancellable,
      GAsyncReadyCallback callback, gpointer callback_data);

  gboolean (*finish) (WpEventHook * self, GAsyncResult * res, GError ** error);

  /*< private >*/
  WP_PADDING(5)
};

WP_API
const gchar * wp_event_hook_get_name (WpEventHook * self);

WP_API
const gchar * const * wp_event_hook_get_runs_before_hooks (WpEventHook * self);

WP_API
const gchar * const * wp_event_hook_get_runs_after_hooks (WpEventHook * self);

WP_PRIVATE_API
WpEventDispatcher * wp_event_hook_get_dispatcher (WpEventHook * self);

WP_PRIVATE_API
void wp_event_hook_set_dispatcher (WpEventHook * self,
    WpEventDispatcher * dispatcher);

WP_API
gboolean wp_event_hook_runs_for_event (WpEventHook * self, WpEvent * event);

WP_API
void wp_event_hook_run (WpEventHook * self,
    WpEvent * event, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data);

WP_API
gboolean wp_event_hook_finish (WpEventHook * self, GAsyncResult * res,
    GError ** error);


/*!
 * \brief The WpInterestEventHook GType
 * \ingroup wpeventhook
 */
#define WP_TYPE_INTEREST_EVENT_HOOK (wp_interest_event_hook_get_type ())
WP_API
G_DECLARE_DERIVABLE_TYPE (WpInterestEventHook, wp_interest_event_hook,
                          WP, INTEREST_EVENT_HOOK, WpEventHook)

struct _WpInterestEventHookClass
{
  WpEventHookClass parent_class;

  /*< private >*/
  WP_PADDING(4)
};

WP_API
void wp_interest_event_hook_add_interest (WpInterestEventHook * self,
    ...) G_GNUC_NULL_TERMINATED;

WP_API
void wp_interest_event_hook_add_interest_full (WpInterestEventHook * self,
    WpObjectInterest * interest);


/*!
 * \brief The WpSimpleEventHook GType
 * \ingroup wpeventhook
 */
#define WP_TYPE_SIMPLE_EVENT_HOOK (wp_simple_event_hook_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpSimpleEventHook, wp_simple_event_hook,
                      WP, SIMPLE_EVENT_HOOK, WpInterestEventHook)

WP_API
WpEventHook * wp_simple_event_hook_new (const gchar *name,
    const gchar * before[], const gchar * after[],
    GClosure * closure);


/*!
 * \brief The WpAsyncEventHook GType
 * \ingroup wpeventhook
 */
#define WP_TYPE_ASYNC_EVENT_HOOK (wp_async_event_hook_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpAsyncEventHook, wp_async_event_hook,
                      WP, ASYNC_EVENT_HOOK, WpInterestEventHook)

WP_API
WpEventHook * wp_async_event_hook_new (const gchar *name,
    const gchar * before[], const gchar * after[],
    GClosure * get_next_step, GClosure * execute_step);

G_END_DECLS

#endif
   07070100000089000081A400000000000000000000000165F8630400003F47000000000000000000000000000000000000002100000000wireplumber-0.5.0/lib/wp/event.c  /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "event.h"
#include "event-dispatcher.h"
#include "event-hook.h"
#include "log.h"
#include "proxy.h"

#include <spa/utils/defs.h>
#include <spa/utils/list.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")

typedef struct _HookData HookData;
struct _HookData
{
  struct spa_list link;
  WpEventHook *hook;
  GPtrArray *dependencies;
};

static inline HookData *
hook_data_new (WpEventHook * hook)
{
  HookData *hook_data = g_new0 (HookData, 1);
  spa_list_init (&hook_data->link);
  hook_data->hook = g_object_ref (hook);
  hook_data->dependencies = g_ptr_array_new ();
  return hook_data;
}

static void
hook_data_free (HookData *self)
{
  g_clear_object (&self->hook);
  g_clear_pointer (&self->dependencies, g_ptr_array_unref);
  g_free (self);
}

struct _WpEvent
{
  grefcount ref;
  GData *datalist;
  struct spa_list hooks;

  /* immutable fields */
  gint priority;
  WpProperties *properties;
  GObject *source;
  GObject *subject;
  GCancellable *cancellable;
  gchar *name;
};

G_DEFINE_BOXED_TYPE (WpEvent, wp_event, wp_event_ref, wp_event_unref)

static gchar *
form_event_name (WpEvent *e)
{
  WpProperties *props = e->properties;
  const gchar *type = wp_properties_get (props, "event.type");
  const gchar *subject_type = wp_properties_get (props, "event.subject.type");
  const gchar *metadata_name = wp_properties_get (props, "metadata.name");
  const gchar *param = wp_properties_get (props, "event.subject.param-id");

  return g_strdup_printf ("<%p>%s%s%s%s%s%s%s", e, (type ? type : ""),
    ((type && subject_type) ? "@" : ""),
    (subject_type ? subject_type : ""),
    ((subject_type && metadata_name) ? "@" : ""),
    (metadata_name ? metadata_name : ""),
    ((param && subject_type) ? "@" : ""),
    (param ? param : "")
    );
}

/*!
 * \brief Creates a new event
 * \ingroup wpevent
 * \param type the type of the event
 * \param priority the priority of the event
 * \param properties (transfer full)(nullable): properties of the event
 * \param source (transfer none): the source of the event
 * \param subject (transfer none)(nullable): the object that the event is about
 * \return (transfer full): the newly constructed event
 */
WpEvent *
wp_event_new (const gchar * type, gint priority, WpProperties * properties,
    GObject * source, GObject * subject)
{
  WpEvent * self = g_new0 (WpEvent, 1);
  g_ref_count_init (&self->ref);
  g_datalist_init (&self->datalist);
  spa_list_init (&self->hooks);

  self->priority = priority;
  self->properties = properties ?
      wp_properties_ensure_unique_owner (properties) :
      wp_properties_new_empty ();

  self->source = source ? g_object_ref (source) : NULL;
  self->subject = subject ? g_object_ref (subject) : NULL;
  self->cancellable = g_cancellable_new ();

  if (self->subject) {
    /* merge properties from subject */
    /* PW properties */
    GParamSpec *pspec = g_object_class_find_property (
        G_OBJECT_GET_CLASS (self->subject), "properties");
    if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == WP_TYPE_PROPERTIES) {
      g_autoptr (WpProperties) subj_props = NULL;
      g_object_get (self->subject, "properties", &subj_props, NULL);
      if (subj_props) {
        wp_properties_update (self->properties, subj_props);
      }
    }

    /* global properties */
    pspec = g_object_class_find_property ( G_OBJECT_GET_CLASS (self->subject),
        "global-properties");
    if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == WP_TYPE_PROPERTIES) {
      g_autoptr (WpProperties) subj_props = NULL;
      g_object_get (self->subject, "global-properties", &subj_props, NULL);
      if (subj_props) {
        wp_properties_update (self->properties, subj_props);
      }
    }
  }

  wp_properties_set (self->properties, "event.type", type);
  self->name = form_event_name (self);

  wp_trace ("event(%s) created", self->name);
  return self;
}

/*!
 * \brief Gets the name of the event
 * \ingroup wpevent
 * \param self the event
 * \return the event name
 */
const gchar *
wp_event_get_name(WpEvent *self)
{
  g_return_val_if_fail(self != NULL, NULL);
  return self->name;
}

static void
wp_event_free (WpEvent * self)
{
  HookData *hook_data;
  spa_list_consume (hook_data, &self->hooks, link) {
    spa_list_remove (&hook_data->link);
    hook_data_free (hook_data);
  }
  g_datalist_clear (&self->datalist);
  g_clear_pointer (&self->properties, wp_properties_unref);
  g_clear_object (&self->source);
  g_clear_object (&self->subject);
  g_cancellable_cancel (self->cancellable);
  g_clear_object (&self->cancellable);
  g_free (self->name);
  g_free (self);
}

WpEvent *
wp_event_ref (WpEvent * self)
{
  g_ref_count_inc (&self->ref);
  return self;
}

void
wp_event_unref (WpEvent * self)
{
  if (g_ref_count_dec (&self->ref))
    wp_event_free (self);
}

/*!
 * \brief Gets the priority of the event
 * \ingroup wpevent
 * \param self the event
 * \return the event priority
 */
gint
wp_event_get_priority (WpEvent * self)
{
  g_return_val_if_fail (self != NULL, 0);
  return self->priority;
}

/*!
 * \brief Gets the properties of the Event
 * \ingroup wpevent
 * \param self the handle
 * \return (transfer full): the properties of the event
 */
WpProperties *
wp_event_get_properties (WpEvent * self)
{
  g_return_val_if_fail(self != NULL, NULL);
  return wp_properties_ref (self->properties);
}

/*!
 * \brief Gets the Source Object of the Event
 * \ingroup wpevent
 * \param self the handle
 * \return (transfer full): the source of the event
 */
GObject *
wp_event_get_source (WpEvent * self)
{
  g_return_val_if_fail(self != NULL, NULL);
  return self->source ? g_object_ref (self->source) : NULL;
}

/*!
 * \brief Gets the Subject Object of the Event
 * \ingroup wpevent
 * \param self the handle
 * \return (transfer full): the subject of the event
 */
GObject *
wp_event_get_subject (WpEvent * self)
{
  g_return_val_if_fail(self != NULL, NULL);
  return self->subject ? g_object_ref (self->subject) : NULL;
}

/*!
 * \brief Returns the internal GCancellable that is used to track whether this
 *    event has been stopped by wp_event_stop_processing()
 * \ingroup wpevent
 * \param self the event
 * \return (transfer none): the cancellable
 */
GCancellable *
wp_event_get_cancellable (WpEvent * self)
{
  return self->cancellable;
}

/*!
 * \brief Stops processing of this event; any further hooks will not be executed
 *   from this moment onwards and the event will be discarded from the stack
 * \ingroup wpevent
 * \param self the event
 */
void
wp_event_stop_processing (WpEvent * self)
{
  g_return_if_fail (self != NULL);
  wp_debug ("stopping event(%s)", self->name);
  g_cancellable_cancel (self->cancellable);
}

static void
destroy_event_data (gpointer data)
{
  g_value_unset ((GValue *) data);
  g_free (data);
}

/*!
 * \brief Stores \a data on the event, associated with the specified \a key
 *
 * This can be used to exchange arbitrary data between hooks that run for
 * this event.
 *
 * \ingroup wpevent
 * \param self the event
 * \param key the key to associate \a data with
 * \param data (transfer none)(nullable): the data element, or \c NULL to
 *   remove any previous data associated with this \a key
 */
void
wp_event_set_data (WpEvent * self, const gchar * key, const GValue * data)
{
  g_return_if_fail (self != NULL);
  g_return_if_fail (key != NULL);
  GValue *data_copy = NULL;

  if (data && G_IS_VALUE (data)) {
    data_copy = g_new0 (GValue, 1);
    g_value_init (data_copy, G_VALUE_TYPE (data));
    g_value_copy (data, data_copy);
  }

  g_datalist_set_data_full (&self->datalist, key, data_copy,
      data_copy ? destroy_event_data : NULL);
}

/*!
 * \brief Gets the data that was previously associated with \a key by
 *    wp_event_set_data()
 * \ingroup wpevent
 * \param self the event
 * \param key the key
 * \return (transfer none)(nullable): the data associated with \a key or \c NULL
 */
const GValue *
wp_event_get_data (WpEvent * self, const gchar * key)
{
  g_return_val_if_fail (self != NULL, NULL);
  g_return_val_if_fail (key != NULL, NULL);

  return g_datalist_get_data (&self->datalist, key);
}

static inline void
record_dependency (struct spa_list *list, const gchar *target,
    const gchar *dependency)
{
  HookData *hook_data;
  spa_list_for_each (hook_data, list, link) {
    if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
      g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
      break;
    }
  }
}

static inline gboolean
hook_exists_in (const gchar *hook_name, struct spa_list *list)
{
  HookData *hook_data;
  if (!spa_list_is_empty (list)) {
    spa_list_for_each (hook_data, list, link) {
      if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
        return TRUE;
      }
    }
  }
  return FALSE;
}

/*!
 * \brief Collects all the hooks registered in the \a dispatcher that run for
 *    this \a event
 * \ingroup wpevent
 * \param event the event
 * \param dispatcher the event dispatcher
 * \return TRUE if at least one hook has been collected,
 *    FALSE if no hooks run for this event or an error occurred
 */
gboolean
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
{
  struct spa_list collected, result, remaining;
  g_autoptr (WpIterator) all_hooks = NULL;
  g_auto (GValue) value = G_VALUE_INIT;

  g_return_val_if_fail (event != NULL, FALSE);
  g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);

  /* hooks already collected */
  if (!spa_list_is_empty (&event->hooks))
    return TRUE;

  spa_list_init (&collected);
  spa_list_init (&result);
  spa_list_init (&remaining);

  /* collect hooks that run for this event */
  all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher);
  while (wp_iterator_next (all_hooks, &value)) {
    WpEventHook *hook = g_value_get_object (&value);

    if (wp_event_hook_runs_for_event (hook, event)) {
      HookData *hook_data = hook_data_new (hook);

      /* record "after" dependencies directly */
      const gchar * const * strv =
          wp_event_hook_get_runs_after_hooks (hook_data->hook);
      while (strv && *strv) {
        g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
        strv++;
      }

      spa_list_append (&collected, &hook_data->link);

      wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
          WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
    }

    g_value_unset (&value);
  }

  if (!spa_list_is_empty (&collected)) {
    HookData *hook_data;

    /* convert "before" dependencies into "after" dependencies */
    spa_list_for_each (hook_data, &collected, link) {
      const gchar * const * strv =
          wp_event_hook_get_runs_before_hooks (hook_data->hook);
      while (strv && *strv) {
        /* record hook_data->hook as a dependency of the *strv hook */
        record_dependency (&collected, *strv,
            wp_event_hook_get_name (hook_data->hook));
        strv++;
      }
    }

    /* sort */
    while (!spa_list_is_empty (&collected)) {
      gboolean made_progress = FALSE;

      /* examine each hook to see if its dependencies are satisfied in the
         result list; if yes, then append it to the result too */
      spa_list_consume (hook_data, &collected, link) {
        guint deps_satisfied = 0;

        spa_list_remove (&hook_data->link);

        wp_trace_boxed (WP_TYPE_EVENT, event,
              "examining: %s", wp_event_hook_get_name (hook_data->hook));

        for (guint i = 0; i < hook_data->dependencies->len; i++) {
          const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
          /* if the dependency is already in the sorted result list or if
             it doesn't exist at all, we consider it satisfied */
          if (hook_exists_in (dep, &result) ||
              !(hook_exists_in (dep, &collected) ||
                hook_exists_in (dep, &remaining))) {
            deps_satisfied++;
          }

          wp_trace_boxed (WP_TYPE_EVENT, event, "depends: %s, satisfied: %u/%u",
              dep, deps_satisfied, hook_data->dependencies->len);
        }

        if (deps_satisfied == hook_data->dependencies->len) {
          wp_trace_boxed (WP_TYPE_EVENT, event,
              "sorted: "WP_OBJECT_FORMAT"(%s)",
              WP_OBJECT_ARGS (hook_data->hook),
              wp_event_hook_get_name (hook_data->hook));

          spa_list_append (&result, &hook_data->link);
          made_progress = TRUE;
        } else {
          spa_list_append (&remaining, &hook_data->link);
        }
      }

      if (made_progress) {
        /* run again with the remaining hooks */
        spa_list_insert_list (&collected, &remaining);
        spa_list_init (&remaining);
      }
      else if (!spa_list_is_empty (&remaining)) {
        /* if we did not make any progress towards growing the result list,
           it means the dependencies cannot be satisfied because of circles */
        wp_critical_boxed (WP_TYPE_EVENT, event, "detected circular "
            "dependencies in the collected hooks!");

        /* clean up */
        spa_list_consume (hook_data, &result, link) {
          spa_list_remove (&hook_data->link);
          hook_data_free (hook_data);
        }
        spa_list_consume (hook_data, &remaining, link) {
          spa_list_remove (&hook_data->link);
          hook_data_free (hook_data);
        }

        return FALSE;
      }
    }
  }

  spa_list_insert_list (&event->hooks, &result);
  return !spa_list_is_empty (&event->hooks);
}

struct event_hooks_iterator_data
{
  WpEvent *event;
  HookData *cur;
};

static void
event_hooks_iterator_reset (WpIterator *it)
{
  struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
  struct spa_list *list = &it_data->event->hooks;

  if (!spa_list_is_empty (list))
    it_data->cur = spa_list_first (&it_data->event->hooks, HookData, link);
}

static gboolean
event_hooks_iterator_next (WpIterator *it, GValue *item)
{
  struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
  struct spa_list *list = &it_data->event->hooks;

  if (!spa_list_is_empty (list) &&
      !spa_list_is_end (it_data->cur, list, link)) {
    g_value_init (item, WP_TYPE_EVENT_HOOK);
    g_value_set_object (item, it_data->cur->hook);
    it_data->cur = spa_list_next (it_data->cur, link);
    return TRUE;
  }
  return FALSE;
}

static gboolean
event_hooks_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
    gpointer data)
{
  struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
  struct spa_list *list = &it_data->event->hooks;
  HookData *hook_data;

  if (!spa_list_is_empty (list)) {
    spa_list_for_each (hook_data, list, link) {
      g_auto (GValue) item = G_VALUE_INIT;
      g_value_init (&item, WP_TYPE_EVENT_HOOK);
      g_value_set_object (&item, hook_data->hook);
      if (!func (&item, ret, data))
          return FALSE;
    }
  }
  return TRUE;
}

static void
event_hooks_iterator_finalize (WpIterator *it)
{
  struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
  wp_event_unref (it_data->event);
}

static const WpIteratorMethods event_hooks_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = event_hooks_iterator_reset,
  .next = event_hooks_iterator_next,
  .fold = event_hooks_iterator_fold,
  .finalize = event_hooks_iterator_finalize,
};

/*!
 * \brief Returns an iterator that iterates over all the hooks that were
 *    collected by wp_event_collect_hooks()
 * \ingroup wpevent
 * \param event the event
 * \return (transfer full): the new iterator
 */
WpIterator *
wp_event_new_hooks_iterator (WpEvent * event)
{
  WpIterator *it = NULL;
  struct event_hooks_iterator_data *it_data;

  g_return_val_if_fail (event != NULL, NULL);

  it = wp_iterator_new (&event_hooks_iterator_methods,
      sizeof (struct event_hooks_iterator_data));
  it_data = wp_iterator_get_user_data (it);
  it_data->event = wp_event_ref (event);
  event_hooks_iterator_reset (it);
  return it;
}
 0707010000008A000081A400000000000000000000000165F86304000006CC000000000000000000000000000000000000002100000000wireplumber-0.5.0/lib/wp/event.h  /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_EVENT_H__
#define __WIREPLUMBER_EVENT_H__

#include "properties.h"
#include "iterator.h"

G_BEGIN_DECLS

typedef struct _WpEventDispatcher WpEventDispatcher;

/*! \defgroup wpevent WpEvent */
/*!
 * \struct WpEvent
 *
 * WpEvent describes an event, an event is an entity which can be pushed on to
 * event stack and the event dispatcher is going to pick and dispatch it.
 *
 */
#define WP_TYPE_EVENT (wp_event_get_type ())
WP_API
GType wp_event_get_type (void) G_GNUC_CONST;

typedef struct _WpEvent WpEvent;

WP_API
WpEvent * wp_event_new (const gchar * type, gint priority,
    WpProperties * properties, GObject * source, GObject * subject);

WP_API
WpEvent * wp_event_ref (WpEvent * self);

WP_API
void wp_event_unref (WpEvent * self);

WP_API
gint wp_event_get_priority (WpEvent * self);

WP_API
WpProperties * wp_event_get_properties (WpEvent * self);

WP_API
GObject * wp_event_get_source (WpEvent * self);

WP_API
const gchar *wp_event_get_name(WpEvent *self);

WP_API
GObject * wp_event_get_subject (WpEvent * self);

WP_API
GCancellable * wp_event_get_cancellable (WpEvent * self);

WP_API
void wp_event_stop_processing (WpEvent * self);

WP_API
void wp_event_set_data (WpEvent * self, const gchar * key, const GValue * data);

WP_API
const GValue * wp_event_get_data (WpEvent * self, const gchar * key);

WP_API
gboolean wp_event_collect_hooks (WpEvent * event,
    WpEventDispatcher * dispatcher);

WP_API
WpIterator * wp_event_new_hooks_iterator (WpEvent * event);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpEvent, wp_event_unref)

G_END_DECLS

#endif
0707010000008B000081A400000000000000000000000165F8630400000D4C000000000000000000000000000000000000002300000000wireplumber-0.5.0/lib/wp/factory.c    /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "factory.h"
#include "log.h"
#include "private/pipewire-object-mixin.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-factory")

/*! \defgroup wpfactory WpFactory */
/*!
 * \struct WpFactory
 *
 * The WpFactory class allows accessing the properties and methods of
 * PipeWire Factory objects (`struct pw_factory`).
 *
 * A WpFactory is constructed internally by wireplumber, when the pipewire 
 * constructed factory objects are reported in by PipeWire registry 
 * and it is made available for wireplumber clients through the 
 * WpObjectManager API.
 *
 * \since 0.4.5
 */

struct _WpFactory
{
  WpGlobalProxy parent;
};

static void wp_factory_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface);


G_DEFINE_TYPE_WITH_CODE (WpFactory, wp_factory, WP_TYPE_GLOBAL_PROXY,
    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
        wp_pw_object_mixin_object_interface_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
        wp_factory_pw_object_mixin_priv_interface_init))

static void wp_factory_init (WpFactory * self)
{
}

static void
wp_factory_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case WP_PW_OBJECT_MIXIN_STEP_BIND:
  case WP_TRANSITION_STEP_ERROR:
    /* base class can handle BIND and ERROR */
    WP_OBJECT_CLASS (wp_factory_parent_class)->
        activate_execute_step (object, transition, step, missing);
    break;
  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
    /* just wait, info will be emitted anyway after binding */
    break;
  default:
    g_assert_not_reached ();
  }
}

static const struct pw_factory_events factory_events = {
  PW_VERSION_FACTORY_EVENTS,
  .info = (HandleEventInfoFunc(factory)) wp_pw_object_mixin_handle_event_info,
};

static void
wp_factory_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
      factory, &factory_events);
}

static void
wp_factory_pw_proxy_destroyed (WpProxy * proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);

  WP_PROXY_CLASS (wp_factory_parent_class)->pw_proxy_destroyed (proxy);
}

static void
wp_factory_class_init (WpFactoryClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->get_property = wp_pw_object_mixin_get_property;

  wpobject_class->get_supported_features =
      wp_pw_object_mixin_get_supported_features;
  wpobject_class->activate_get_next_step =
      wp_pw_object_mixin_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_factory_activate_execute_step;

  proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Factory;
  proxy_class->pw_iface_version = PW_VERSION_FACTORY;
  proxy_class->pw_proxy_created = wp_factory_pw_proxy_created;
  proxy_class->pw_proxy_destroyed = wp_factory_pw_proxy_destroyed;

  wp_pw_object_mixin_class_override_properties (object_class);
}

static void
wp_factory_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface)
{
  wp_pw_object_mixin_priv_interface_info_init_no_params (iface, factory, FACTORY);
}
0707010000008C000081A400000000000000000000000165F86304000001F0000000000000000000000000000000000000002300000000wireplumber-0.5.0/lib/wp/factory.h    /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author Ashok Sidipotu <ashok.sidipotuc@ollabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_FACTORY_H__
#define __WIREPLUMBER_FACTORY_H__

#include "global-proxy.h"

G_BEGIN_DECLS

struct pw_factory;

/*!
 * \brief The WpFactory GType
 * \ingroup wpfactory
 */
#define WP_TYPE_FACTORY (wp_factory_get_type ())

WP_API
G_DECLARE_FINAL_TYPE (WpFactory, wp_factory, WP, FACTORY, WpGlobalProxy)

G_END_DECLS

#endif
0707010000008D000081A400000000000000000000000165F8630400003078000000000000000000000000000000000000002800000000wireplumber-0.5.0/lib/wp/global-proxy.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "global-proxy.h"
#include "private/registry.h"
#include "core.h"
#include "error.h"
#include "log.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-global-proxy")

/*! \defgroup wpglobalproxy WpGlobalProxy */
/*!
 * \struct WpGlobalProxy
 *
 * A proxy that represents a PipeWire global object, i.e. an
 * object that is made available through the PipeWire registry.
 *
 * \gproperties
 *
 * \gproperty{global, WpGlobal *, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY,
 *   Internal WpGlobal object}
 *
 * \gproperty{factory-name, gchar *, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY,
 *   The factory name}
 *
 * \gproperty{global-properties, WpProperties *,
 *   G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The pipewire global properties}
 *
 * \gproperty{permissions, guint, G_PARAM_READABLE,
 *   The pipewire global permissions}
 */

typedef struct _WpGlobalProxyPrivate WpGlobalProxyPrivate;
struct _WpGlobalProxyPrivate
{
  WpGlobal *global;
  gchar factory_name[96];
  WpProperties *properties;
};

enum {
  PROP_0,
  PROP_GLOBAL,
  PROP_FACTORY_NAME,
  PROP_GLOBAL_PROPERTIES,
  PROP_PERMISSIONS,
};

G_DEFINE_TYPE_WITH_PRIVATE (WpGlobalProxy, wp_global_proxy, WP_TYPE_PROXY)

static void
wp_global_proxy_init (WpGlobalProxy * self)
{
}

static void
wp_global_proxy_dispose (GObject * object)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  if (priv->global)
    wp_global_rm_flag (priv->global, WP_GLOBAL_FLAG_OWNED_BY_PROXY);

  G_OBJECT_CLASS (wp_global_proxy_parent_class)->dispose (object);
}

static void
wp_global_proxy_finalize (GObject * object)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  g_clear_pointer (&priv->properties, wp_properties_unref);
  g_clear_pointer (&priv->global, wp_global_unref);

  G_OBJECT_CLASS (wp_global_proxy_parent_class)->finalize (object);
}

static void
wp_global_proxy_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  switch (property_id) {
  case PROP_GLOBAL:
    priv->global = g_value_dup_boxed (value);
    break;
  case PROP_FACTORY_NAME:
    priv->factory_name[0] = '\0';
    strncpy (priv->factory_name, g_value_get_string (value),
        sizeof (priv->factory_name) - 1);
    break;
  case PROP_GLOBAL_PROPERTIES:
    priv->properties = g_value_dup_boxed (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_global_proxy_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);

  switch (property_id) {
  case PROP_PERMISSIONS:
    g_value_set_uint (value, wp_global_proxy_get_permissions (self));
    break;
  case PROP_GLOBAL_PROPERTIES:
    g_value_take_boxed (value, wp_global_proxy_get_global_properties (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static WpObjectFeatures
wp_global_proxy_get_supported_features (WpObject * object)
{
  return WP_PROXY_FEATURE_BOUND;
}

enum {
  STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
};

static guint
wp_global_proxy_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  /* we only support BOUND, so this is the only
     feature that can be in @em missing */
  g_return_val_if_fail (missing == WP_PROXY_FEATURE_BOUND,
      WP_TRANSITION_STEP_ERROR);

  return STEP_BIND;
}

static void
wp_global_proxy_step_bind (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  WpProxyClass *proxy_klass = WP_PROXY_GET_CLASS (WP_PROXY (object));
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
  WpGlobalProxyPrivate *priv = wp_global_proxy_get_instance_private (self);

  /* Create the pipewire object if global is NULL */
  if (priv->global == NULL && priv->factory_name[0] != '\0') {
    g_autoptr (WpCore) core = NULL;
    struct pw_core *pw_core = NULL;
    struct pw_proxy * p = NULL;

    core = wp_object_get_core (WP_OBJECT (self));
    if (G_UNLIKELY (!core)) {
        wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
            WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "The WirePlumber core is not valid; object cannot be created"));
      return;
    }

    pw_core = wp_core_get_pw_core (core);
    if (G_UNLIKELY (!pw_core)) {
        wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
            WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "The WirePlumber core is not connected; object cannot be created"));
      return;
    }

    p = pw_core_create_object (pw_core, priv->factory_name,
        proxy_klass->pw_iface_type, proxy_klass->pw_iface_version,
        priv->properties ?
            wp_properties_peek_dict (priv->properties) : NULL, 0);
    if (G_UNLIKELY (!p)) {
      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
            WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "Failed to create object with given factory name and properties"));
      return;
    }

    wp_proxy_set_pw_proxy (WP_PROXY (self), p);
  }

  /* Bind */
  if (wp_proxy_get_pw_proxy (WP_PROXY (self)) == NULL) {
    if (!wp_global_proxy_bind (self)) {
      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
          WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
              "global not specified or destroyed; cannot bind proxy"));
    }
  }
}


static void
wp_global_proxy_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case STEP_BIND:
    wp_global_proxy_step_bind (object, transition, step, missing);
    break;
  case WP_TRANSITION_STEP_ERROR:
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
wp_global_proxy_bound (WpProxy * proxy, guint32 global_id)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (proxy);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));

  if (!priv->global) {
    wp_registry_prepare_new_global (wp_core_get_registry (core),
        global_id, PW_PERM_ALL, WP_GLOBAL_FLAG_OWNED_BY_PROXY,
        G_TYPE_FROM_INSTANCE (self), self,
        priv->properties ? wp_properties_peek_dict (priv->properties) : NULL,
        &priv->global);
  }
}

static void
wp_global_proxy_destroyed (WpProxy * proxy)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (proxy);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  g_clear_pointer (&priv->global, wp_global_unref);
}

static void
wp_global_proxy_class_init (WpGlobalProxyClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->finalize = wp_global_proxy_finalize;
  object_class->dispose = wp_global_proxy_dispose;
  object_class->set_property = wp_global_proxy_set_property;
  object_class->get_property = wp_global_proxy_get_property;

  wpobject_class->get_supported_features =
      wp_global_proxy_get_supported_features;
  wpobject_class->activate_get_next_step =
      wp_global_proxy_activate_get_next_step;
  wpobject_class->activate_execute_step =
      wp_global_proxy_activate_execute_step;

  proxy_class->bound = wp_global_proxy_bound;
  proxy_class->pw_proxy_destroyed = wp_global_proxy_destroyed;

  g_object_class_install_property (object_class, PROP_GLOBAL,
      g_param_spec_boxed ("global", "global", "Internal WpGlobal object",
          wp_global_get_type (),
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_FACTORY_NAME,
      g_param_spec_string ("factory-name", "factory-name",
          "The factory name", "",
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_GLOBAL_PROPERTIES,
      g_param_spec_boxed ("global-properties", "global-properties",
          "The pipewire global properties", WP_TYPE_PROPERTIES,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PERMISSIONS,
      g_param_spec_uint ("permissions", "permissions",
          "The pipewire global permissions", 0, G_MAXUINT, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Requests the PipeWire server to destroy the object represented by
 * this proxy.
 *
 * If the server allows it, the object will be destroyed and the
 * WpProxy's `pw-proxy-destroyed` signal will be emitted. If the server does
 * not allow it, nothing will happen.
 *
 * This is mostly useful for destroying WpLink objects.
 *
 * \ingroup wpglobalproxy
 * \param self the pipewire global
 */
void
wp_global_proxy_request_destroy (WpGlobalProxy * self)
{
  g_return_if_fail (WP_IS_GLOBAL_PROXY (self));

  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));

  if (priv->global && core) {
    WpRegistry *reg = wp_core_get_registry (core);
    if (reg->pw_registry)
      pw_registry_destroy (reg->pw_registry, priv->global->id);
  }
}

/*!
 * \brief Gets the permissions of a pipewire global
 * \ingroup wpglobalproxy
 * \param self the pipewire global
 * \returns the permissions that wireplumber has on this object
 */
guint32
wp_global_proxy_get_permissions (WpGlobalProxy * self)
{
  g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), 0);

  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  return priv->global ? priv->global->permissions : PW_PERM_ALL;
}

/*!
 * \brief Gets the global properties of a pipewire global
 * \ingroup wpglobalproxy
 * \param self the pipewire global
 * \returns (transfer full): the global (immutable) properties of this
 *   pipewire object
 */
WpProperties *
wp_global_proxy_get_global_properties (WpGlobalProxy * self)
{
  g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), NULL);

  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  if (priv->global && priv->global->properties)
    return wp_properties_ref (priv->global->properties);
  return NULL;
}

/*!
 * \brief Binds to the global and creates the underlying `pw_proxy`.
 *
 * This is mostly meant to be called internally. It will create the `pw_proxy`
 * and will activate the WP_PROXY_FEATURE_BOUND feature.
 *
 * This may only be called if there is no `pw_proxy` associated with this
 * object yet.
 *
 * \ingroup wpglobalproxy
 * \param self the pipewire global
 * \returns TRUE on success, FALSE if there is no global to bind to
 */
gboolean
wp_global_proxy_bind (WpGlobalProxy * self)
{
  WpGlobalProxyPrivate *priv;
  struct pw_proxy *p;

  g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), FALSE);
  g_return_val_if_fail (wp_proxy_get_pw_proxy (WP_PROXY (self)) == NULL, FALSE);

  priv = wp_global_proxy_get_instance_private (self);

  if (!priv->global || !priv->global->proxy)
    return FALSE;

  /*
   * We can bind only if the WpGlobal will unbind this proxy on removal, so
   * assert here that this is so. Why: pw_registry_bind can race with the global
   * id being replaced with a different object on server side, so we must rely
   * on the registry_global messages to know if the object was replaced.  If the
   * race occurs, and a wrong object is going to be bound here, what will then
   * happen is that WpGlobal will dispose this proxy and no problem arises.
   */
  g_return_val_if_fail (priv->global->proxy == self, FALSE);

  p = wp_global_bind (priv->global);
  if (!p)
    return FALSE;

  wp_proxy_set_pw_proxy (WP_PROXY (self), p);
  return TRUE;
}
0707010000008E000081A400000000000000000000000165F86304000003BC000000000000000000000000000000000000002800000000wireplumber-0.5.0/lib/wp/global-proxy.h   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_GLOBAL_PROXY_H__
#define __WIREPLUMBER_GLOBAL_PROXY_H__

#include "proxy.h"
#include "properties.h"

G_BEGIN_DECLS

/*!
 * \brief The WpGlobalProxy GType
 * \ingroup wpglobalproxy
 */
#define WP_TYPE_GLOBAL_PROXY (wp_global_proxy_get_type ())
WP_API
G_DECLARE_DERIVABLE_TYPE (WpGlobalProxy, wp_global_proxy,
                          WP, GLOBAL_PROXY, WpProxy)

struct _WpGlobalProxyClass
{
  WpProxyClass parent_class;

  /*< private >*/
  WP_PADDING(4)
};

WP_API
void wp_global_proxy_request_destroy (WpGlobalProxy * self);

WP_API
guint32 wp_global_proxy_get_permissions (WpGlobalProxy * self);

WP_API
WpProperties * wp_global_proxy_get_global_properties (
    WpGlobalProxy * self);

WP_API
gboolean wp_global_proxy_bind (WpGlobalProxy * self);

G_END_DECLS

#endif
0707010000008F000081A400000000000000000000000165F86304000020F6000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/iterator.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "iterator.h"
#include "log.h"
#include <spa/utils/defs.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-iterator")

/*! \defgroup wpiterator WpIterator */
/*!
 * \struct WpIterator
 * A generic iterator API
 */
struct _WpIterator
{
  const WpIteratorMethods *methods;
  gpointer user_data;
};

G_DEFINE_BOXED_TYPE (WpIterator, wp_iterator, wp_iterator_ref, wp_iterator_unref)

static gboolean
wp_iterator_default_fold (WpIterator *self, WpIteratorFoldFunc func,
    GValue *item, gpointer data)
{
  GValue next = G_VALUE_INIT;

  wp_iterator_reset (self);

  while (wp_iterator_next (self, &next)) {
    const gboolean res = func (&next, item, data);
    g_value_unset (&next);
    if (!res)
      return FALSE;
  }

  return TRUE;
}

struct foreach_fold_data {
  WpIteratorForeachFunc func;
  gpointer data;
};

static gboolean
foreach_fold_func (const GValue *item, GValue *ret, gpointer data)
{
  struct foreach_fold_data *d = data;
  d->func (item, d->data);
  return TRUE;
}

static gboolean
wp_iterator_default_foreach (WpIterator *self, WpIteratorForeachFunc func,
   gpointer data)
{
  struct foreach_fold_data d = {func, data};
  return wp_iterator_fold (self, foreach_fold_func, NULL, &d);
}

/*!
 * \brief Constructs an iterator that uses the provided \a methods to implement
 * its API.
 *
 * The WpIterator structure is internally allocated with \a user_size additional
 * space at the end. A pointer to this space can be retrieved with
 * wp_iterator_get_user_data() and is available for implementation-specific
 * storage.
 *
 * \ingroup wpiterator
 * \param methods method implementations for the new iterator
 * \param user_size size of the user_data structure to be allocated
 * \returns (transfer full): a new custom iterator
 */
WpIterator *
wp_iterator_new (const WpIteratorMethods *methods, size_t user_size)
{
  WpIterator *self = NULL;

  g_return_val_if_fail (methods, NULL);

  self = g_rc_box_alloc0 (sizeof (WpIterator) + user_size);
  self->methods = methods;
  if (user_size > 0)
    self->user_data = SPA_MEMBER (self, sizeof (WpIterator), void);

  return self;
}

/*!
 * \brief Gets the implementation-specific storage of an iterator
 * \note this only for use by implementations of WpIterator
 *
 * \ingroup wpiterator
 * \param self an iterator object
 * \returns a pointer to the implementation-specific storage area
 */
gpointer
wp_iterator_get_user_data (WpIterator *self)
{
  return self->user_data;
}

/*!
 * \brief Increases the reference count of an iterator
 * \ingroup wpiterator
 * \param self an iterator object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpIterator *
wp_iterator_ref (WpIterator *self)
{
  return (WpIterator *) g_rc_box_acquire ((gpointer) self);
}

static void
wp_iterator_free (WpIterator *self)
{
  if (self->methods->finalize)
    self->methods->finalize (self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 *
 * \ingroup wpiterator
 * \param self (transfer full): an iterator object
 */
void
wp_iterator_unref (WpIterator *self)
{
  g_rc_box_release_full (self, (GDestroyNotify) wp_iterator_free);
}

/*!
 * \brief Resets the iterator so we can iterate again from the beginning.
 *
 * \ingroup wpiterator
 * \param self the iterator
 */
void
wp_iterator_reset (WpIterator *self)
{
  g_return_if_fail (self);
  g_return_if_fail (self->methods->reset);

  self->methods->reset (self);
}

/*!
 * \brief Gets the next item of the iterator.
 *
 * \ingroup wpiterator
 * \param self the iterator
 * \param item (out): the next item of the iterator
 * \returns TRUE if next iterator was obtained, FALSE when the iterator has no
 * more items to iterate through.
 */

gboolean
wp_iterator_next (WpIterator *self, GValue *item)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (self->methods->next, FALSE);

  return self->methods->next (self, item);
}

/*!
 * \brief Fold a function over the items of the iterator.
 *
 * \ingroup wpiterator
 * \param self the iterator
 * \param func (scope call): the fold function
 * \param ret (inout): the accumulator data
 * \param data (closure): the user data
 * \returns TRUE if all the items were processed, FALSE otherwise.
 */
gboolean
wp_iterator_fold (WpIterator *self, WpIteratorFoldFunc func, GValue *ret,
    gpointer data)
{
  g_return_val_if_fail (self, FALSE);

  if (self->methods->fold)
    return self->methods->fold (self, func, ret, data);

  return wp_iterator_default_fold (self, func, ret, data);
}

/*!
 * \brief Iterates over all items of the iterator calling a function.
 *
 * \ingroup wpiterator
 * \param self the iterator
 * \param func (scope call): the foreach function
 * \param data (closure): the user data
 * \returns TRUE if all the items were processed, FALSE otherwise.
 */
gboolean
wp_iterator_foreach (WpIterator *self, WpIteratorForeachFunc func,
   gpointer data)
{
  g_return_val_if_fail (self, FALSE);

  if (self->methods->foreach)
    return self->methods->foreach (self, func, data);

  return wp_iterator_default_foreach (self, func, data);
}

struct ptr_array_iterator_data
{
  GPtrArray *array;
  GType item_type;
  guint index;
  void (*set_value) (GValue *, gpointer);
};

static void
ptr_array_iterator_reset (WpIterator *it)
{
  struct ptr_array_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->index = 0;
}

static gboolean
ptr_array_iterator_next (WpIterator *it, GValue *item)
{
  struct ptr_array_iterator_data *it_data = wp_iterator_get_user_data (it);

  while (it_data->index < it_data->array->len) {
    gpointer ptr = g_ptr_array_index (it_data->array, it_data->index++);
    if (!ptr)
      continue;
    g_value_init (item, it_data->item_type);
    it_data->set_value (item, ptr);
    return TRUE;
  }
  return FALSE;
}

static gboolean
ptr_array_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
    gpointer data)
{
  struct ptr_array_iterator_data *it_data = wp_iterator_get_user_data (it);
  gpointer *ptr, *base;
  guint len;

  ptr = base = it_data->array->pdata;
  len = it_data->array->len;

  while ((ptr - base) < len) {
    if (*ptr) {
      g_auto (GValue) item = G_VALUE_INIT;
      g_value_init (&item, it_data->item_type);
      it_data->set_value (&item, *ptr);
      if (!func (&item, ret, data))
        return FALSE;
    }
    ptr++;
  }
  return TRUE;
}

static void
ptr_array_iterator_finalize (WpIterator *it)
{
  struct ptr_array_iterator_data *it_data = wp_iterator_get_user_data (it);
  g_ptr_array_unref (it_data->array);
}

static const WpIteratorMethods ptr_array_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = ptr_array_iterator_reset,
  .next = ptr_array_iterator_next,
  .fold = ptr_array_iterator_fold,
  .finalize = ptr_array_iterator_finalize,
};

/*!
 * \brief Creates an iterator from a pointer array
 *
 * \ingroup wpiterator
 * \param items (element-type gpointer) (transfer full): the items to iterate over
 * \param item_type the type of each item
 * \returns (transfer full): a new iterator that iterates over \a items
 */
WpIterator *
wp_iterator_new_ptr_array (GPtrArray * items, GType item_type)
{
  g_autoptr (WpIterator) it = NULL;
  struct ptr_array_iterator_data *it_data;

  g_return_val_if_fail (items != NULL, NULL);

  it = wp_iterator_new (&ptr_array_iterator_methods,
      sizeof (struct ptr_array_iterator_data));
  it_data = wp_iterator_get_user_data (it);
  it_data->array = items;
  it_data->item_type = item_type;
  it_data->index = 0;

  if (g_type_is_a (item_type, G_TYPE_POINTER))
    it_data->set_value = g_value_set_pointer;
  else if (g_type_is_a (item_type, G_TYPE_BOXED))
    it_data->set_value = (void (*) (GValue *, gpointer)) g_value_set_boxed;
  else if (g_type_is_a (item_type, G_TYPE_OBJECT))
    it_data->set_value = g_value_set_object;
  else if (g_type_is_a (item_type, G_TYPE_INTERFACE))
    it_data->set_value = g_value_set_object;
  else if (g_type_is_a (item_type, G_TYPE_VARIANT))
    it_data->set_value = (void (*) (GValue *, gpointer)) g_value_set_variant;
  else if (g_type_is_a (item_type, G_TYPE_STRING))
    it_data->set_value = (void (*) (GValue *, gpointer)) g_value_set_string;
  else
    g_return_val_if_reached (NULL);

  return g_steal_pointer (&it);
}
  07070100000090000081A400000000000000000000000165F86304000009F1000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/iterator.h   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_ITERATOR_H__
#define __WIREPLUMBER_ITERATOR_H__

#include <gio/gio.h>
#include "defs.h"

G_BEGIN_DECLS

/*!
 * \brief A function to be passed to wp_iterator_fold()
 * \param item the item to fold
 * \param ret the value collecting the result
 * \param data data passed to wp_iterator_fold()
 * \returns TRUE if the fold should continue, FALSE if it should stop.
 * \ingroup wpiterator
 */
typedef gboolean (*WpIteratorFoldFunc) (const GValue *item, GValue *ret,
    gpointer data);

/*!
 * \brief A function that is called by wp_iterator_foreach().
 * \param item the item
 * \param data the data passed to wp_iterator_foreach()
 * \ingroup wpiterator
 */
typedef void (*WpIteratorForeachFunc) (const GValue *item, gpointer data);

/*!
 * \brief The WpIterator GType
 * \ingroup wpiterator
 */
#define WP_TYPE_ITERATOR (wp_iterator_get_type ())
WP_API
GType wp_iterator_get_type (void);

typedef struct _WpIterator WpIterator;
typedef struct _WpIteratorMethods WpIteratorMethods;

/*!
 * \brief The version to set to _WpIteratorMethods::version.
 * This allows future expansion of the struct
 * \ingroup wpiterator
 */
#define  WP_ITERATOR_METHODS_VERSION 0U

struct _WpIteratorMethods
{
  guint32 version;

  void (*reset) (WpIterator *self);
  gboolean (*next) (WpIterator *self, GValue *item);
  gboolean (*fold) (WpIterator *self, WpIteratorFoldFunc func,
      GValue *ret, gpointer data);
  gboolean (*foreach) (WpIterator *self, WpIteratorForeachFunc func,
      gpointer data);
  void (*finalize) (WpIterator *self);
};

/* ref count */

WP_API
WpIterator *wp_iterator_ref (WpIterator *self);

WP_API
void wp_iterator_unref (WpIterator *self);

/* iteration api */

WP_API
void wp_iterator_reset (WpIterator *self);

WP_API
gboolean wp_iterator_next (WpIterator *self, GValue *item);

WP_API
gboolean wp_iterator_fold (WpIterator *self, WpIteratorFoldFunc func,
    GValue *ret, gpointer data);

WP_API
gboolean wp_iterator_foreach (WpIterator *self, WpIteratorForeachFunc func,
    gpointer data);

/* constructors */

WP_API
WpIterator * wp_iterator_new_ptr_array (GPtrArray * items, GType item_type);

WP_API
WpIterator * wp_iterator_new (const WpIteratorMethods * methods,
    size_t user_size);

/* private */

WP_API
gpointer wp_iterator_get_user_data (WpIterator * self);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpIterator, wp_iterator_unref)

G_END_DECLS

#endif
   07070100000091000081A400000000000000000000000165F8630400002623000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/json-utils.c /* WirePlumber
 *
 * Copyright © 2023 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "json-utils.h"
#include "error.h"
#include "log.h"

#include <pipewire/pipewire.h>
#include <spa/utils/result.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-json-utils")

/*! \defgroup wpjsonutils Json Utilities */

struct match_rules_cb_data
{
  WpRuleMatchCallback callback;
  gpointer data;
  GError **error;
};

static int
match_rules_cb (void *data, const char *location, const char *action,
    const char *str, size_t len)
{
  struct match_rules_cb_data *cb_data = data;
  g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_stringn (str, len);
  return cb_data->callback (cb_data->data, action, json, cb_data->error) ? 0 : -EPIPE;
}

/*!
 * \brief Matches the given properties against a set of rules described in JSON
 * and calls the given callback to perform actions on a successful match.
 *
 * The given JSON should be an array of objects, where each object has a
 * "matches" and an "actions" property. The "matches" value should also be
 * an array of objects, where each object is a set of properties to match.
 * Inside such an object, all properties must match to consider a successful
 * match. However, if multiple objects are provided, only one object needs
 * to match.
 *
 * The "actions" value should be an object where the key is the action name
 * and the value can be any valid JSON. Both the action name and the value are
 * passed as-is on the \a callback.
 *
 * \verbatim
 * [
 *     {
 *         matches = [
 *             # any of the items in matches needs to match, if one does,
 *             # actions are emited.
 *             {
 *                 # all keys must match the value. ! negates. ~ starts regex.
 *                 <key> = <value>
 *                 ...
 *             }
 *             ...
 *         ]
 *         actions = {
 *             <action> = <value>
 *             ...
 *         }
 *     }
 * ]
 * \endverbatim
 *
 * \ingroup wpjsonutils
 * \param json a JSON array containing rules in the described format
 * \param match_props (transfer none): the properties to match against the rules
 * \param callback (scope call): a function to call for each action on a successful match
 * \param data (closure callback): data to be passed to \a callback
 * \param error (out)(optional): the error that occurred, if any
 * \returns FALSE if an error occurred, TRUE otherwise
 */
gboolean
wp_json_utils_match_rules (WpSpaJson *json, WpProperties *match_props,
    WpRuleMatchCallback callback, gpointer data, GError ** error)
{
  g_autoptr (GError) cb_error = NULL;
  struct match_rules_cb_data cb_data = { callback, data, &cb_error };

  int res = pw_conf_match_rules (wp_spa_json_get_data (json),
      wp_spa_json_get_size (json), NULL, wp_properties_peek_dict (match_props),
      match_rules_cb, &cb_data);

  if (res < 0) {
    if (cb_error)
      g_propagate_error (error, g_steal_pointer (&cb_error));
    else
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
          "match rules error: %s", spa_strerror (res));
    return FALSE;
  }

  return TRUE;
}

struct update_props_cb_data
{
  WpProperties *props;
  gint count;
};

static gboolean
update_props_cb (gpointer data, const gchar * action, WpSpaJson * value,
    GError ** error)
{
  struct update_props_cb_data *cb_data = data;
  if (g_str_equal (action, "update-props"))
    cb_data->count += wp_properties_update_from_json (cb_data->props, value);
  return TRUE;
}

/*!
 * \brief Matches the given properties against a set of rules described in JSON
 * and updates the properties if the rule actions include the "update-props"
 * action.
 *
 * \ingroup wpjsonutils
 * \param json a JSON array containing rules in the format accepted by
 *    wp_json_utils_match_rules()
 * \param props (transfer none): the properties to match against the rules
 *    and also update, acting on the "update-props" action
 * \returns the number of properties that were updated
 */
gint
wp_json_utils_match_rules_update_properties (WpSpaJson *json, WpProperties *props)
{
  g_autoptr (GError) cb_error = NULL;
  struct update_props_cb_data cb_data = { props, 0 };

  wp_json_utils_match_rules (json, props, update_props_cb, &cb_data, &cb_error);
  if (cb_error)
    wp_notice ("%s", cb_error->message);

  return cb_data.count;
}


#define OVERRIDE_SECTION_PREFIX "override."

static WpSpaJson *
merge_json_objects (WpSpaJson *a, WpSpaJson *b)
{
  g_autoptr (WpSpaJsonBuilder) builder = NULL;

  builder = wp_spa_json_builder_new_object ();

  /* Add all properties from 'a' that don't exist in 'b' */
  {
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (a);
    g_auto (GValue) item = G_VALUE_INIT;
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      g_autoptr (WpSpaJson) key = NULL;
      g_autoptr (WpSpaJson) val = NULL;
      g_autoptr (WpSpaJson) j = NULL;
      g_autofree gchar *str = NULL;
      const gchar *key_str;
      g_autofree gchar *override_key_str = NULL;
      gboolean override;

      key = g_value_dup_boxed (&item);
      key_str = str = wp_spa_json_parse_string (key);
      g_return_val_if_fail (key_str, NULL);
      override = g_str_has_prefix (str, OVERRIDE_SECTION_PREFIX);
      if (override)
        key_str += strlen (OVERRIDE_SECTION_PREFIX);
      override_key_str = g_strdup_printf (OVERRIDE_SECTION_PREFIX "%s", key_str);

      g_value_unset (&item);
      g_return_val_if_fail (wp_iterator_next (it, &item), NULL);
      val = g_value_dup_boxed (&item);

      if (!wp_spa_json_object_get (b, key_str, "J", &j, NULL) &&
          !wp_spa_json_object_get (b, override_key_str, "J", &j, NULL)) {
        wp_spa_json_builder_add_property (builder, key_str);
        wp_spa_json_builder_add_json (builder, val);
      }
    }
  }

  /* Add properties from 'b' that don't exist in 'a'. If a property
   * exists in 'a' and does not have the 'override.' prefix, recursively
   * merge it before adding it. Otherwise override it. */
  {
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (b);
    g_auto (GValue) item = G_VALUE_INIT;
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      g_autoptr (WpSpaJson) key = NULL;
      g_autoptr (WpSpaJson) val = NULL;
      g_autoptr (WpSpaJson) j = NULL;
      g_autofree gchar *str = NULL;
      const gchar *key_str;
      g_autofree gchar *override_key_str = NULL;
      gboolean override;

      key = g_value_dup_boxed (&item);
      key_str = str = wp_spa_json_parse_string (key);
      g_return_val_if_fail (key_str, NULL);
      override = g_str_has_prefix (str, OVERRIDE_SECTION_PREFIX);
      if (override)
        key_str += strlen (OVERRIDE_SECTION_PREFIX);
      override_key_str = g_strdup_printf (OVERRIDE_SECTION_PREFIX "%s", key_str);

      g_value_unset (&item);
      g_return_val_if_fail (wp_iterator_next (it, &item), NULL);
      val = g_value_dup_boxed (&item);

      if (!override &&
          (wp_spa_json_object_get (a, key_str, "J", &j, NULL) ||
           wp_spa_json_object_get (a, override_key_str, "J", &j, NULL))) {
        g_autoptr (WpSpaJson) merged = wp_json_utils_merge_containers (j, val);
        if (!merged) {
          wp_warning ("skipping merge of %s as JSON values are not compatible",
              key_str);
          continue;
        }
        wp_spa_json_builder_add_property (builder, key_str);
        wp_spa_json_builder_add_json (builder, merged);
      } else {
        wp_spa_json_builder_add_property (builder, key_str);
        wp_spa_json_builder_add_json (builder, val);
      }
    }
  }

  return wp_spa_json_builder_end (builder);
}

static WpSpaJson *
merge_json_arrays (WpSpaJson * a, WpSpaJson * b)
{
  g_autoptr (WpSpaJsonBuilder) builder = NULL;

  builder = wp_spa_json_builder_new_array ();

  /* Add all elements from 'a' */
  {
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (a);
    g_auto (GValue) item = G_VALUE_INIT;
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaJson *j = g_value_get_boxed (&item);
      wp_spa_json_builder_add_json (builder, j);
    }
  }

  /* Add all elements from 'b' */
  {
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (b);
    g_auto (GValue) item = G_VALUE_INIT;
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaJson *j = g_value_get_boxed (&item);
      wp_spa_json_builder_add_json (builder, j);
    }
  }

  return wp_spa_json_builder_end (builder);
}

/*!
 * \brief Merges two JSON containers (objects or arrays) into one.
 *
 * If both \a a and \a b are objects, the result will be a new object
 * containing all properties from both \a a and \a b. If a property exists
 * in both \a a and \a b, the values are recursively merged. If a property
 * exists in both \a a and \a b and the property name starts with the
 * "override." prefix in either of those, the value from the key with the
 * prefix is used.
 *
 * If both \a a and \a b are arrays, the result will be a new array
 * containing all elements from both \a a and \a b.
 *
 * If \a a and \a b are not of the same type, NULL is returned.
 *
 * \ingroup wpjsonutils
 * \param a (transfer none): a JSON container
 * \param b (transfer none): a JSON container
 * \returns a new JSON container containing the merged contents of \a a and \a b
 *    or NULL if \a a and \a b are not of the same type
 */
WpSpaJson *
wp_json_utils_merge_containers (WpSpaJson * a, WpSpaJson * b)
{
  if (wp_spa_json_is_array (a) && wp_spa_json_is_array (b))
    return merge_json_arrays (a, b);
  else if (wp_spa_json_is_object (a) && wp_spa_json_is_object (b))
    return merge_json_objects (a, b);
  return NULL;
}
 07070100000092000081A400000000000000000000000165F86304000004BB000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/json-utils.h /* WirePlumber
 *
 * Copyright © 2023 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_JSON_UTILS_H__
#define __WIREPLUMBER_JSON_UTILS_H__

#include "spa-json.h"
#include "properties.h"

G_BEGIN_DECLS

/*!
 * \brief A function to be called by wp_json_utils_match_rules() when a match is found.
 * \param data (closure): the user data passed to wp_json_utils_match_rules()
 * \param action the rule's action string
 * \param value the value associated with this action
 * \param error (out): return location for a GError
 * \returns FALSE if an error occurred and the match process should stop, TRUE otherwise
 * \ingroup wpjsonutils
 */
typedef gboolean (*WpRuleMatchCallback) (gpointer data, const gchar * action,
    WpSpaJson * value, GError ** error);

WP_API
gboolean wp_json_utils_match_rules (WpSpaJson * json, WpProperties * match_props,
    WpRuleMatchCallback callback, gpointer data, GError ** error);

WP_API
gint wp_json_utils_match_rules_update_properties (WpSpaJson *json, WpProperties *props);

WP_API
WpSpaJson * wp_json_utils_merge_containers (WpSpaJson * a, WpSpaJson * b);

G_END_DECLS

#endif
 07070100000093000081A400000000000000000000000165F86304000020A0000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/link.c   /* WirePlumber
 *
 * Copyright © 2019-2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "link.h"
#include "log.h"
#include "wpenums.h"
#include "private/pipewire-object-mixin.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-link")

/*! \defgroup wplink WpLink */
/*!
 * \struct WpLink
 *
 * The WpLink class allows accessing the properties and methods of a
 * PipeWire link object (`struct pw_link`).
 *
 * A WpLink is constructed internally when a new link appears on the
 * PipeWire registry and it is made available through the WpObjectManager API.
 * Alternatively, a WpLink can also be constructed using
 * wp_link_new_from_factory(), which creates a new link object
 * on the remote PipeWire server by calling into a factory.
 *
 * \gproperties
 *
 * \gproperty{state, WpLinkState, G_PARAM_READABLE, The current state of the link}
 *
 * \gsignals
 *
 * \par state-changed
 * \parblock
 * \code
 * void
 * state_changed_callback (WpLink * self,
 *                         WpLinkState old_state,
 *                         WpLinkState new_state,
 *                         gpointer user_data)
 * \endcode
 *
 * Emitted when the link changes state. This is only emitted when
 * WP_PIPEWIRE_OBJECT_FEATURE_INFO is enabled.
 *
 * Parameters:
 * - `old_state` - the old state
 * - `new_state` - the new state
 *
 * Flags: G_SIGNAL_RUN_LAST
 * \endparblock
 */

enum {
  PROP_STATE = WP_PW_OBJECT_MIXIN_PROP_CUSTOM_START,
};

enum {
  SIGNAL_STATE_CHANGED,
  N_SIGNALS,
};

static guint32 signals[N_SIGNALS] = {0};

struct _WpLink
{
  WpGlobalProxy parent;
};

static void wp_link_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface);

G_DEFINE_TYPE_WITH_CODE (WpLink, wp_link, WP_TYPE_GLOBAL_PROXY,
    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
        wp_pw_object_mixin_object_interface_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
        wp_link_pw_object_mixin_priv_interface_init))

static void
wp_link_init (WpLink * self)
{
}

static void
wp_link_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);

  switch (property_id) {
  case PROP_STATE:
    g_value_set_enum (value, d->info ?
        ((struct pw_link_info *) d->info)->state : 0);
    break;
  default:
    wp_pw_object_mixin_get_property (object, property_id, value, pspec);
    break;
  }
}

static void
wp_link_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case WP_PW_OBJECT_MIXIN_STEP_BIND:
  case WP_TRANSITION_STEP_ERROR:
    /* base class can handle BIND and ERROR */
    WP_OBJECT_CLASS (wp_link_parent_class)->
        activate_execute_step (object, transition, step, missing);
    break;
  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
    /* just wait, info will be emitted anyway after binding */
    break;
  default:
    g_assert_not_reached ();
  }
}

static const struct pw_link_events link_events = {
  PW_VERSION_LINK_EVENTS,
  .info = (HandleEventInfoFunc(link)) wp_pw_object_mixin_handle_event_info,
};

static void
wp_link_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
      link, &link_events);
}

static void
wp_link_pw_proxy_destroyed (WpProxy * proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);

  WP_PROXY_CLASS (wp_link_parent_class)->pw_proxy_destroyed (proxy);
}

static void
wp_link_class_init (WpLinkClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->get_property = wp_link_get_property;

  wpobject_class->get_supported_features =
      wp_pw_object_mixin_get_supported_features;
  wpobject_class->activate_get_next_step =
      wp_pw_object_mixin_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_link_activate_execute_step;

  proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Link;
  proxy_class->pw_iface_version = PW_VERSION_LINK;
  proxy_class->pw_proxy_created = wp_link_pw_proxy_created;
  proxy_class->pw_proxy_destroyed = wp_link_pw_proxy_destroyed;

  wp_pw_object_mixin_class_override_properties (object_class);

  g_object_class_install_property (object_class, PROP_STATE,
      g_param_spec_enum ("state", "state", "state", WP_TYPE_LINK_STATE, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  signals[SIGNAL_STATE_CHANGED] = g_signal_new (
      "state-changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2,
      WP_TYPE_LINK_STATE, WP_TYPE_LINK_STATE);

}

static void
wp_link_process_info (gpointer instance, gpointer old_info, gpointer i)
{
  const struct pw_link_info *info = i;

  if (info->change_mask & PW_LINK_CHANGE_MASK_STATE) {
    enum pw_link_state old_state = old_info ?
        ((struct pw_link_info *) old_info)->state : PW_LINK_STATE_INIT;
    g_signal_emit (instance, signals[SIGNAL_STATE_CHANGED], 0,
        old_state, info->state);
  }
}

static void
wp_link_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface)
{
  wp_pw_object_mixin_priv_interface_info_init_no_params (iface, link, LINK);
  iface->process_info = wp_link_process_info;
}

/*!
 * \brief Constructs a link on the PipeWire server by asking the remote factory
 * \a factory_name to create it.
 *
 * Because of the nature of the PipeWire protocol, this operation completes
 * asynchronously at some point in the future. In order to find out when
 * this is done, you should call wp_object_activate(), requesting at least
 * WP_PROXY_FEATURE_BOUND. When this feature is ready, the link is ready for
 * use on the server. If the link cannot be created, this activation operation
 * will fail.
 *
 * \ingroup wplink
 * \param core the wireplumber core
 * \param factory_name the pipewire factory name to construct the link
 * \param properties (nullable) (transfer full): the properties to pass to the
 *   factory
 * \returns (nullable) (transfer full): the new link or NULL if the core
 *   is not connected and therefore the link cannot be created
 */
WpLink *
wp_link_new_from_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties)
{
  g_autoptr (WpProperties) props = properties;
  return g_object_new (WP_TYPE_LINK,
      "core", core,
      "factory-name", factory_name,
      "global-properties", props,
      NULL);
}

/*!
 * \brief Retrieves the ids of the objects that are linked by this link
 *
 * \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 *
 * \ingroup wplink
 * \param self the link
 * \param output_node (out) (optional): the bound id of the output (source) node
 * \param output_port (out) (optional): the bound id of the output (source) port
 * \param input_node (out) (optional): the bound id of the input (sink) node
 * \param input_port (out) (optional): the bound id of the input (sink) port
 */
void
wp_link_get_linked_object_ids (WpLink * self,
    guint32 * output_node, guint32 * output_port,
    guint32 * input_node, guint32 * input_port)
{
  g_return_if_fail (WP_IS_LINK (self));

  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
  struct pw_link_info *info = d->info;
  g_return_if_fail (info);

  if (output_node)
    *output_node = info->output_node_id;
  if (output_port)
    *output_port = info->output_port_id;
  if (input_node)
    *input_node = info->input_node_id;
  if (input_port)
    *input_port = info->input_port_id;
}

/*!
 * \brief Gets the current state of the link
 * \ingroup wplink
 * \param self the link
 * \param error (out) (optional) (transfer none): the error
 * \returns the current state of the link
 * \since 0.4.11
 */
WpLinkState
wp_link_get_state (WpLink * self, const gchar ** error)
{
  g_return_val_if_fail (WP_IS_LINK (self), WP_LINK_STATE_ERROR);
  g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (self),
          WP_PIPEWIRE_OBJECT_FEATURE_INFO), WP_LINK_STATE_ERROR);

  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
  const struct pw_link_info *info = d->info;

  if (error)
    *error = info->error;
  return (WpLinkState) info->state;
}
07070100000094000081A400000000000000000000000165F863040000054E000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/link.h   /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_LINK_H__
#define __WIREPLUMBER_LINK_H__

#include "global-proxy.h"

G_BEGIN_DECLS

/*!
 * \brief The state of the link
 * \ingroup wplink
 *
 * \since 0.4.11
 */
typedef enum {
  WP_LINK_STATE_ERROR = -2,     /*!< the link is in error */
  WP_LINK_STATE_UNLINKED = -1,  /*!< the link is unlinked */
  WP_LINK_STATE_INIT = 0,       /*!< the link is initialized */
  WP_LINK_STATE_NEGOTIATING = 1,  /*!< the link is negotiating formats */
  WP_LINK_STATE_ALLOCATING = 2, /*!< the link is allocating buffers */
  WP_LINK_STATE_PAUSED = 3,     /*!< the link is paused */
  WP_LINK_STATE_ACTIVE = 4,     /*!< the link is active */
} WpLinkState;

/*!
 * \brief The WpLink GType
 * \ingroup wplink
 */
#define WP_TYPE_LINK (wp_link_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpLink, wp_link, WP, LINK, WpGlobalProxy)

WP_API
WpLink * wp_link_new_from_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties);

WP_API
void wp_link_get_linked_object_ids (WpLink * self,
    guint32 * output_node, guint32 * output_port,
    guint32 * input_node, guint32 * input_port);

WP_API
WpLinkState wp_link_get_state (WpLink * self, const gchar ** error);

G_END_DECLS

#endif
  07070100000095000081A400000000000000000000000165F86304000077D7000000000000000000000000000000000000001F00000000wireplumber-0.5.0/lib/wp/log.c    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "log.h"
#include "wp.h"
#include <pipewire/pipewire.h>
#include <spa/support/log.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-log")

/*!
 * \defgroup wplog Debug Logging
 * \{
 */
/*!
 * \def WP_DEFINE_LOCAL_LOG_TOPIC(name)
 * \brief Defines a static \em WpLogTopic* variable called \em WP_LOCAL_LOG_TOPIC
 *
 * The log topic is automatically intialized to the given topic \a name when
 * it is first used. The default logging macros expect this variable to be
 * defined, so it is a good coding practice in the WirePlumber codebase to
 * start all files at the top with:
 * \code
 * WP_DEFINE_LOCAL_LOG_TOPIC ("some-topic")
 * \endcode
 *
 * \param name The name of the log topic
 */
/*!
 * \def WP_LOG_TOPIC_STATIC(var, name)
 * \brief Defines a static \em WpLogTopic* variable called \a var with the given
 * topic \a name
 * \param var The name of the variable to define
 * \param name The name of the log topic
 */
/*!
 * \def WP_LOG_TOPIC(var, name)
 * \brief Defines a \em WpLogTopic* variable called \a var with the given
 * topic \a name. Unlike WP_LOG_TOPIC_STATIC(), the variable defined here is
 * not static, so it can be linked to by other object files.
 * \param var The name of the variable to define
 * \param name The name of the log topic
 */
/*!
 * \def WP_LOG_TOPIC_EXTERN(var)
 * \brief Declares an extern \em WpLogTopic* variable called \a var.
 * This variable is meant to be defined in a .c file with WP_LOG_TOPIC()
 * \param var The name of the variable to declare
 */
/*!
 * \def WP_OBJECT_FORMAT
 * \brief A format string to print GObjects with WP_OBJECT_ARGS()
 * For example:
 * \code
 * GObject *myobj = ...;
 * wp_debug ("This: " WP_OBJECT_FORMAT " is an object", WP_OBJECT_ARGS (myobj));
 * \endcode
 */
/*!
 * \def WP_OBJECT_ARGS(object)
 * \brief A macro to format an object for printing with WP_OBJECT_FORMAT
 */
/*!
 * \def wp_critical(...)
 * \brief Logs a critical message to the standard log via GLib's logging system.
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_critical_object(object, ...)
 * \brief Logs a critical message to the standard log via GLib's logging system.
 * \param object A GObject associated with the log; this is printed in a special
 *   way to make it easier to track messages from a specific object
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_critical_boxed(type, object, ...)
 * \brief Logs a critical message to the standard log via GLib's logging system.
 * \param type The type of \a object
 * \param object A boxed object associated with the log; this is printed in a
 *   special way to make it easier to track messages from a specific object.
 *   For some object types, contents from the object are also printed (ex WpSpaPod)
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_warning(...)
 * \brief Logs a warning message to the standard log via GLib's logging system.
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_warning_object(object, ...)
 * \brief Logs a warning message to the standard log via GLib's logging system.
 * \param object A GObject associated with the log; this is printed in a special
 *   way to make it easier to track messages from a specific object
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_warning_boxed(type, object, ...)
 * \brief Logs a warning message to the standard log via GLib's logging system.
 * \param type The type of \a object
 * \param object A boxed object associated with the log; this is printed in a
 *   special way to make it easier to track messages from a specific object.
 *   For some object types, contents from the object are also printed (ex WpSpaPod)
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_notice(...)
 * \brief Logs a notice message to the standard log via GLib's logging system.
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_notice_object(object, ...)
 * \brief Logs a notice message to the standard log via GLib's logging system.
 * \param object A GObject associated with the log; this is printed in a special
 *   way to make it easier to track messages from a specific object
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_notice_boxed(type, object, ...)
 * \brief Logs a notice message to the standard log via GLib's logging system.
 * \param type The type of \a object
 * \param object A boxed object associated with the log; this is printed in a
 *   special way to make it easier to track messages from a specific object.
 *   For some object types, contents from the object are also printed (ex WpSpaPod)
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_info(...)
 * \brief Logs a info message to the standard log via GLib's logging system.
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_info_object(object, ...)
 * \brief Logs a info message to the standard log via GLib's logging system.
 * \param object A GObject associated with the log; this is printed in a special
 *   way to make it easier to track messages from a specific object
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_info_boxed(type, object, ...)
 * \brief Logs a info message to the standard log via GLib's logging system.
 * \param type The type of \a object
 * \param object A boxed object associated with the log; this is printed in a
 *   special way to make it easier to track messages from a specific object.
 *   For some object types, contents from the object are also printed (ex WpSpaPod)
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_debug(...)
 * \brief Logs a debug message to the standard log via GLib's logging system.
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_debug_object(object, ...)
 * \brief Logs a debug message to the standard log via GLib's logging system.
 * \param object A GObject associated with the log; this is printed in a special
 *   way to make it easier to track messages from a specific object
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_debug_boxed(type, object, ...)
 * \brief Logs a debug message to the standard log via GLib's logging system.
 * \param type The type of \a object
 * \param object A boxed object associated with the log; this is printed in a
 *   special way to make it easier to track messages from a specific object.
 *   For some object types, contents from the object are also printed (ex WpSpaPod)
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_trace(...)
 * \brief Logs a trace message to the standard log via GLib's logging system.
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_trace_object(object, ...)
 * \brief Logs a trace message to the standard log via GLib's logging system.
 * \param object A GObject associated with the log; this is printed in a special
 *   way to make it easier to track messages from a specific object
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_trace_boxed(type, object, ...)
 * \brief Logs a trace message to the standard log via GLib's logging system.
 * \param type The type of \a object
 * \param object A boxed object associated with the log; this is printed in a
 *   special way to make it easier to track messages from a specific object.
 *   For some object types, contents from the object are also printed (ex WpSpaPod)
 * \param ... A format string, followed by format arguments in printf() style
 */
/*!
 * \def wp_log(level, type, object, ...)
 * \brief The generic form of all the logging macros
 * \remark Don't use this directly, use one of the other logging macros
 */
/*! \} */

static GString *spa_dbg_str = NULL;
#define spa_debug(...) \
({ \
  g_string_append_printf (spa_dbg_str, __VA_ARGS__); \
  g_string_append_c (spa_dbg_str, '\n'); \
})

#include <spa/debug/pod.h>

#define DEFAULT_LOG_LEVEL 4  /* MESSAGE */
#define DEFAULT_LOG_LEVEL_FLAGS (G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR)

struct log_topic_pattern
{
  GPatternSpec *spec;
  gchar *spec_str;
  gint log_level;
};

static struct {
  gboolean use_color;
  gboolean output_is_journal;
  gboolean set_pw_log;
  gint global_log_level;
  GLogLevelFlags global_log_level_flags;
  struct log_topic_pattern *patterns;
  GPtrArray *log_topics;
  GMutex log_topics_lock;
} log_state = {
  .use_color = FALSE,
  .output_is_journal = FALSE,
  .set_pw_log = FALSE,
  .global_log_level = DEFAULT_LOG_LEVEL,
  .global_log_level_flags = DEFAULT_LOG_LEVEL_FLAGS,
  .patterns = NULL,
  .log_topics = NULL,
};

/* reference: https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit */
#define COLOR_RED            "\033[1;31m"
#define COLOR_GREEN          "\033[1;32m"
#define COLOR_YELLOW         "\033[1;33m"
#define COLOR_BLUE           "\033[1;34m"
#define COLOR_MAGENTA        "\033[1;35m"
#define COLOR_CYAN           "\033[1;36m"
#define COLOR_BRIGHT_RED     "\033[1;91m"
#define COLOR_BRIGHT_GREEN   "\033[1;92m"
#define COLOR_BRIGHT_YELLOW  "\033[1;93m"
#define COLOR_BRIGHT_BLUE    "\033[1;94m"
#define COLOR_BRIGHT_MAGENTA "\033[1;95m"
#define COLOR_BRIGHT_CYAN    "\033[1;96m"

#define RESET_COLOR          "\033[0m"

/* our palette */
#define DOMAIN_COLOR    COLOR_MAGENTA
#define LOCATION_COLOR  COLOR_BLUE

/* available colors for object printouts (the <Object:0xfoobar>) */
static const gchar *object_colors[] = {
  COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN,
  COLOR_BRIGHT_RED, COLOR_BRIGHT_GREEN, COLOR_BRIGHT_YELLOW,
  COLOR_BRIGHT_MAGENTA, COLOR_BRIGHT_CYAN
};

/*
 * priority numbers are based on GLib's gmessages.c
 * reference: http://man7.org/linux/man-pages/man3/syslog.3.html#DESCRIPTION
 */
static const struct {
  GLogLevelFlags log_level_flags;
  enum spa_log_level spa_level;
  gchar name;
  gchar priority[2];
  gchar color[8];
} log_level_info[] = {
  { 0,                   0,                  'U', "0", COLOR_BRIGHT_MAGENTA },
  { G_LOG_LEVEL_ERROR,   SPA_LOG_LEVEL_NONE, 'F', "3" /* LOG_ERR */, COLOR_BRIGHT_RED },
  { G_LOG_LEVEL_CRITICAL,SPA_LOG_LEVEL_ERROR,'E', "4" /* LOG_WARNING */, COLOR_RED },
  { G_LOG_LEVEL_WARNING, SPA_LOG_LEVEL_WARN, 'W', "4" /* LOG_WARNING */, COLOR_BRIGHT_YELLOW },
  { G_LOG_LEVEL_MESSAGE, SPA_LOG_LEVEL_WARN, 'N', "5" /* LOG_NOTICE */, COLOR_BRIGHT_GREEN },
  { G_LOG_LEVEL_INFO,    SPA_LOG_LEVEL_INFO, 'I', "6" /* LOG_INFO */, COLOR_GREEN },
  { G_LOG_LEVEL_DEBUG,   SPA_LOG_LEVEL_DEBUG,'D', "7" /* LOG_DEBUG */, COLOR_BRIGHT_CYAN },
  { WP_LOG_LEVEL_TRACE,  SPA_LOG_LEVEL_TRACE,'T', "7" /* LOG_DEBUG */, COLOR_CYAN },
};

/* map glib's log levels, which are flags in the range (1<<2) to (1<<8),
  to the 1-7 range; first calculate the integer part of log2(log_level)
  to bring it down to 2-8 and substract 1 */
static G_GNUC_CONST inline gint
level_index_from_flags (GLogLevelFlags log_level)
{
  gint logarithm = 0;
  while ((log_level >>= 1) != 0)
    logarithm += 1;
  return (logarithm >= 2 && logarithm <= 8) ? (logarithm - 1) : 0;
}

/* map an index in the log_level_info table to a single GLogLevelFlags flag */
static G_GNUC_CONST inline GLogLevelFlags
level_index_to_flag (gint lvl_index)
{
  if (lvl_index < 0 || lvl_index >= (gint) G_N_ELEMENTS (log_level_info))
    return 0;
  return log_level_info [lvl_index].log_level_flags;
}

/* map an index in the log_level_info table to an OR combination of all the
   GLogLevelFlags that are enabled at this level */
static G_GNUC_CONST inline GLogLevelFlags
level_index_to_full_flags (gint lvl_index)
{
  GLogLevelFlags flags = 0;
  for (gint i = 1; i <= lvl_index; i++) {
    flags |= level_index_to_flag (i);
  }
  return flags;
}

/* map a SPA_LOG_LEVEL_* to an index in the log_level_info table;
   if warn_to_notice == TRUE, SPA_LOG_LEVEL_WARN maps to 4 (G_LOG_LEVEL_MESSAGE)
   and index 3 (G_LOG_LEVEL_WARNING) can not be returned
   if warn_to_notice == FALSE, SPA_LOG_LEVEL_WARN maps to 3 (G_LOG_LEVEL_WARNING)
   and index 4 (G_LOG_LEVEL_MESSAGE) can not be returned */
static G_GNUC_CONST inline gint
level_index_from_spa (gint spa_lvl, gboolean warn_to_notice)
{
  if (G_UNLIKELY (spa_lvl <= SPA_LOG_LEVEL_NONE))
    return 1;
  else if (spa_lvl == SPA_LOG_LEVEL_ERROR)
    return 2;
  else if (spa_lvl == SPA_LOG_LEVEL_WARN)
    return warn_to_notice ? 4 : 3;
  else if (G_UNLIKELY (spa_lvl > SPA_LOG_LEVEL_TRACE))
    return (gint) G_N_ELEMENTS (log_level_info) - 1;
  else
    return spa_lvl + 2;
}

/* map an index in the log_level_info table to a SPA_LOG_LEVEL_*
   here, G_LOG_LEVEL_MESSAGE maps to SPA_LOG_LEVEL_WARN */
static G_GNUC_CONST inline gint
level_index_to_spa (gint lvl_index)
{
  if (lvl_index < 0 || lvl_index >= (gint) G_N_ELEMENTS (log_level_info))
    return 0;
  return log_level_info [lvl_index].spa_level;
}

static gboolean
level_index_from_string (const char *str, gint *lvl)
{
  g_return_val_if_fail (str != NULL, FALSE);

  /* level is always 1 character */
  if (str[0] != '\0' && str[1] == '\0') {
    for (guint i = 1; i < G_N_ELEMENTS (log_level_info); i++) {
      if (str[0] == log_level_info[i].name) {
        *lvl = i;
        return TRUE;
      }
    }

    if (str[0] >= '0' && str[0] <= '5') {
      *lvl = level_index_from_spa (str[0] - '0', TRUE);
      return TRUE;
    }
  }
  return FALSE;
}

static gint
find_topic_log_level (const gchar *log_topic, bool *has_custom_level)
{
  struct log_topic_pattern *pttrn = log_state.patterns;
  guint len;
  g_autofree gchar *reverse_topic = NULL;
  gint log_level = log_state.global_log_level;

  /* reverse string and length required for pattern match */
  len = strlen (log_topic);
  reverse_topic = g_strreverse (g_strndup (log_topic, len));

  while (pttrn && pttrn->spec &&
        !g_pattern_match (pttrn->spec, len, log_topic, reverse_topic))
    pttrn++;

  if (pttrn && pttrn->spec) {
    if (has_custom_level)
      *has_custom_level = true;
    log_level = pttrn->log_level;
  } else if (has_custom_level) {
    *has_custom_level = false;
  }

  return log_level;
}

static void
log_topic_update_level (WpLogTopic *topic)
{
  gint log_level = find_topic_log_level (topic->topic_name, NULL);
  gint flags = topic->flags & ~WP_LOG_TOPIC_LEVEL_MASK;

  flags |= level_index_to_full_flags (log_level);

  topic->flags = flags;
}

static void
update_log_topic_levels (void)
{
  guint i;

  g_mutex_lock (&log_state.log_topics_lock);

  if (log_state.log_topics)
    for (i = 0; i < log_state.log_topics->len; ++i)
      log_topic_update_level (g_ptr_array_index (log_state.log_topics, i));

  g_mutex_unlock (&log_state.log_topics_lock);
}

static void
free_patterns (struct log_topic_pattern *patterns)
{
  struct log_topic_pattern *p = patterns;

  while (p && p->spec) {
    g_clear_pointer (&p->spec, g_pattern_spec_free);
    g_clear_pointer (&p->spec_str, g_free);
    ++p;
  }

  g_free (patterns);
}

/* Parse value to log level and patterns. If no global level in string,
   global_log_level is not modified. */
static gboolean
parse_log_level (const gchar *level_str, struct log_topic_pattern **global_patterns, gint *global_log_level)
{
  struct log_topic_pattern *patterns = NULL, *pttrn;
  gint n_tokens = 0;
  gchar **tokens = NULL;
  int level = *global_log_level;

  *global_patterns = NULL;

  if (level_str && level_str[0] != '\0') {
    /* [<glob>:]<level>,..., */
    tokens = pw_split_strv (level_str, ",", INT_MAX, &n_tokens);
  }

  /* allocate enough space to hold all pattern specs */
  patterns = g_malloc_n ((n_tokens + 2), sizeof (struct log_topic_pattern));
  pttrn = patterns;
  if (!patterns)
    g_error ("unable to allocate space for %d log patterns", n_tokens + 2);

  for (gint i = 0; i < n_tokens; i++) {
    gint n_tok;
    gchar **tok;
    gint lvl;

    tok = pw_split_strv (tokens[i], ":", 2, &n_tok);
    if (n_tok == 2 && level_index_from_string (tok[1], &lvl)) {
      pttrn->spec = g_pattern_spec_new (tok[0]);
      pttrn->spec_str = g_strdup (tok[0]);
      pttrn->log_level = lvl;
      pttrn++;
    } else if (n_tok == 1 && level_index_from_string (tok[0], &lvl)) {
      level = lvl;
    } else {
      pttrn->spec = NULL;
      pw_free_strv (tok);
      free_patterns (patterns);
      return FALSE;
    }

    pw_free_strv (tok);
  }

  /* disable pipewire connection trace by default */
  pttrn->spec = g_pattern_spec_new ("conn.*");
  pttrn->spec_str = g_strdup ("conn.*");
  pttrn->log_level = 0;
  pttrn++;

  /* terminate with NULL */
  pttrn->spec = NULL;
  pttrn->spec_str = NULL;
  pttrn->log_level = 0;

  pw_free_strv (tokens);

  *global_patterns = patterns;
  *global_log_level = level;
  return TRUE;
}

static gchar *
format_pw_log_level_string (gint level, const struct log_topic_pattern *patterns)
{
  GString *str = g_string_new (NULL);
  const struct log_topic_pattern *p;

  g_string_printf (str, "%d", level_index_to_spa (level));

  for (p = patterns; p && p->spec; ++p)
    g_string_append_printf (str, ",%s:%d", p->spec_str, level_index_to_spa (p->log_level));

  return g_string_free (str, FALSE);
}

gboolean
wp_log_set_level (const gchar *level_str)
{
  gint level;
  GLogLevelFlags flags;
  struct log_topic_pattern *patterns;

  level = DEFAULT_LOG_LEVEL;
  if (!parse_log_level (level_str, &patterns, &level))
    return FALSE;

  flags = level_index_to_full_flags (level);

  g_mutex_lock (&log_state.log_topics_lock);
  log_state.global_log_level = level;
  log_state.global_log_level_flags = flags;
  SPA_SWAP (log_state.patterns, patterns);
  g_mutex_unlock (&log_state.log_topics_lock);

  free_patterns (patterns);

  update_log_topic_levels ();

  wp_spa_log_get_instance()->level = level_index_to_spa (level);

  if (log_state.set_pw_log) {
#if PW_CHECK_VERSION(1,1,0)
    g_autofree gchar *pw_pattern = format_pw_log_level_string (log_state.global_log_level, log_state.patterns);
    pw_log_set_level_string (pw_pattern);
#else
    pw_log_set_level (level_index_to_spa (level));
#endif
  }

  return TRUE;
}

/* private, called from wp_init() */
void
wp_log_init (gint flags)
{
  log_state.use_color = g_log_writer_supports_color (fileno (stderr));
  log_state.output_is_journal = g_log_writer_is_journald (fileno (stderr));
  log_state.set_pw_log = flags & WP_INIT_SET_PW_LOG && !g_getenv ("WIREPLUMBER_NO_PW_LOG");

  if (flags & WP_INIT_SET_GLIB_LOG)
    g_log_set_writer_func (wp_log_writer_default, NULL, NULL);

  /* set the spa_log interface that pipewire will use */
  if (log_state.set_pw_log)
    pw_log_set (wp_spa_log_get_instance ());

  if (!wp_log_set_level (g_getenv ("WIREPLUMBER_DEBUG"))) {
    wp_warning ("Ignoring invalid value in WIREPLUMBER_DEBUG");
    wp_log_set_level (NULL);
  }

  if (log_state.set_pw_log) {
    /* always set PIPEWIRE_DEBUG for 2 reasons:
     * 1. to overwrite it from the environment, in case the user has set it
     * 2. to prevent pw_context from parsing "log.level" from the config file;
     *    we do this ourselves here and allows us to have more control over
     *    the whole process.
     */
    g_autofree gchar *lvl_str = format_pw_log_level_string (log_state.global_log_level, log_state.patterns);
    g_warn_if_fail (g_setenv ("PIPEWIRE_DEBUG", lvl_str, TRUE));
  }
}

static void
log_topic_register (WpLogTopic *topic)
{
  if (!log_state.log_topics)
    log_state.log_topics = g_ptr_array_new ();

  g_ptr_array_add (log_state.log_topics, topic);

  log_topic_update_level (topic);
  topic->flags |= WP_LOG_TOPIC_FLAG_INITIALIZED;
}

static void
log_topic_unregister (WpLogTopic *topic)
{
  if (!log_state.log_topics)
    return;

  g_ptr_array_remove_fast (log_state.log_topics, topic);

  if (log_state.log_topics->len == 0) {
    g_ptr_array_free (log_state.log_topics, TRUE);
    log_state.log_topics = NULL;
  }
}

/*!
 * \brief Registers a log topic.
 *
 * The log topic must be unregistered using \ref wp_log_topic_unregister
 * before its lifetime ends.
 *
 * This function is threadsafe.
 *
 * \ingroup wplog
 */
void
wp_log_topic_register (WpLogTopic *topic)
{
  g_mutex_lock (&log_state.log_topics_lock);
  log_topic_register (topic);
  g_mutex_unlock (&log_state.log_topics_lock);
}

/*!
 * \brief Unregisters a log topic.
 *
 * This function is threadsafe.
 *
 * \ingroup wplog
 */
void
wp_log_topic_unregister (WpLogTopic *topic)
{
  g_mutex_lock (&log_state.log_topics_lock);
  log_topic_unregister (topic);
  g_mutex_unlock (&log_state.log_topics_lock);
}

/*!
 * \brief Initializes a log topic. Internal function, don't use it directly
 * \ingroup wplog
 */
void
wp_log_topic_init (WpLogTopic *topic)
{
  g_mutex_lock (&log_state.log_topics_lock);
  if ((topic->flags & WP_LOG_TOPIC_FLAG_INITIALIZED) == 0) {
    if (topic->flags & WP_LOG_TOPIC_FLAG_STATIC) {
      /* Auto-register log topics that have infinite lifetime */
      log_topic_register (topic);
    } else {
      log_topic_update_level (topic);
      topic->flags |= WP_LOG_TOPIC_FLAG_INITIALIZED;
    }
  }
  g_mutex_unlock (&log_state.log_topics_lock);
}

typedef struct _WpLogFields WpLogFields;
struct _WpLogFields
{
  const gchar *log_topic;
  const gchar *file;
  const gchar *line;
  const gchar *func;
  const gchar *message;
  gint log_level;
  GType object_type;
  gconstpointer object;
};

static void
wp_log_fields_init (WpLogFields *lf,
    const gchar *log_topic,
    gint log_level,
    const gchar *file,
    const gchar *line,
    const gchar *func,
    GType object_type,
    gconstpointer object,
    const gchar *message)
{
  lf->log_topic = log_topic ? log_topic : "default";
  lf->log_level = log_level;
  lf->file = file;
  lf->line = line;
  lf->func = func;
  lf->object_type = object_type;
  lf->object = object;
  lf->message = message ? message : "(null)";
}

static void
wp_log_fields_init_from_glib (WpLogFields *lf, GLogLevelFlags log_level_flags,
    const GLogField *fields, gsize n_fields)
{
  wp_log_fields_init (lf, NULL, level_index_from_flags (log_level_flags),
      NULL, NULL, NULL, 0, NULL, NULL);

  for (guint i = 0; i < n_fields; i++) {
    if (g_strcmp0 (fields[i].key, "GLIB_DOMAIN") == 0 && fields[i].value) {
      lf->log_topic = fields[i].value;
    }
    else if (g_strcmp0 (fields[i].key, "MESSAGE") == 0 && fields[i].value) {
      lf->message = fields[i].value;
    }
    else if (g_strcmp0 (fields[i].key, "CODE_FILE") == 0) {
      lf->file = fields[i].value;
    }
    else if (g_strcmp0 (fields[i].key, "CODE_LINE") == 0) {
      lf->line = fields[i].value;
    }
    else if (g_strcmp0 (fields[i].key, "CODE_FUNC") == 0) {
      lf->func = fields[i].value;
    }
  }
}

static void
wp_log_fields_write_to_stream (WpLogFields *lf, FILE *s)
{
  gint64 now;
  time_t now_secs;
  struct tm now_tm;
  gchar time_buf[128];

  now = g_get_real_time ();
  now_secs = (time_t) (now / G_USEC_PER_SEC);
  localtime_r (&now_secs, &now_tm);
  strftime (time_buf, sizeof (time_buf), "%H:%M:%S", &now_tm);

  fprintf (s, "%s%c %s.%06d %s%18.18s %s%s:%s:%s:%s %s\n",
      /* level */
      log_state.use_color ? log_level_info[lf->log_level].color : "",
      log_level_info[lf->log_level].name,
      /* timestamp */
      time_buf,
      (gint) (now % G_USEC_PER_SEC),
      /* domain */
      log_state.use_color ? DOMAIN_COLOR : "",
      lf->log_topic,
      /* file, line, function */
      log_state.use_color ? LOCATION_COLOR : "",
      lf->file,
      lf->line,
      lf->func,
      log_state.use_color ? RESET_COLOR : "",
      /* message */
      lf->message);
  fflush (s);
}

static gboolean
wp_log_fields_write_to_journal (WpLogFields *lf)
{
  gsize n_fields = 6;
  GLogField fields[6] = {
    { "PRIORITY", log_level_info[lf->log_level].priority, -1 },
    { "CODE_FILE", lf->file ? lf->file : "", -1 },
    { "CODE_LINE", lf->line ? lf->line : "", -1 },
    { "CODE_FUNC", lf->func ? lf->func : "", -1 },
    { "TOPIC", lf->log_topic ? lf->log_topic : "", -1 },
    { "MESSAGE", lf->message ? lf->message : "", -1 },
  };

  /* the log level flags are not used in this function, so we can pass 0 */
  return (g_log_writer_journald (0, fields, n_fields, NULL) == G_LOG_WRITER_HANDLED);
}

static inline gchar *
wp_log_fields_format_message (WpLogFields *lf)
{
  g_autofree gchar *extra_message = NULL;
  g_autofree gchar *extra_object = NULL;
  const gchar *object_color = "";

  if (log_state.use_color) {
    guint h = g_direct_hash (lf->object) % G_N_ELEMENTS (object_colors);
    object_color = object_colors[h];
  }

  if (lf->object_type == WP_TYPE_SPA_POD && lf->object && !spa_dbg_str) {
    spa_dbg_str = g_string_new (lf->message);
    g_string_append (spa_dbg_str, ":\n");
    spa_debug_pod (2, NULL, wp_spa_pod_get_spa_pod (lf->object));
    extra_message = g_string_free (spa_dbg_str, FALSE);
    spa_dbg_str = NULL;
  }
  else if (lf->object && g_type_is_a (lf->object_type, WP_TYPE_PROXY) &&
      (wp_object_test_active_features ((WpObject *) lf->object, WP_PROXY_FEATURE_BOUND))) {
    extra_object = g_strdup_printf (":%u:",
        wp_proxy_get_bound_id ((WpProxy *) lf->object));
  }

  return g_strdup_printf ("%s<%s%s%p>%s %s",
      object_color,
      lf->object_type != 0 ? g_type_name (lf->object_type) : "",
      extra_object ? extra_object : ":",
      lf->object,
      log_state.use_color ? RESET_COLOR : "",
      extra_message ? extra_message : lf->message);
}

static GLogWriterOutput
wp_log_fields_log (WpLogFields *lf)
{
  g_autofree gchar *full_message = NULL;

  /* in the unlikely event that someone messed with stderr... */
  if (G_UNLIKELY (!stderr || fileno (stderr) < 0))
    return G_LOG_WRITER_UNHANDLED;

  /* format the message to include the object */
  if (lf->object_type) {
    lf->message = full_message = wp_log_fields_format_message (lf);
  }

  /* write complete field information to the journal if we are logging to it */
  if (log_state.output_is_journal && wp_log_fields_write_to_journal (lf))
    return G_LOG_WRITER_HANDLED;

  wp_log_fields_write_to_stream (lf, stderr);
  return G_LOG_WRITER_HANDLED;
}

/*!
 * \brief WirePlumber's GLogWriterFunc
 *
 * This is installed automatically when you call wp_init() with
 * WP_INIT_SET_GLIB_LOG set in the flags
 * \ingroup wplog
 */
GLogWriterOutput
wp_log_writer_default (GLogLevelFlags log_level_flags,
    const GLogField *fields, gsize n_fields, gpointer user_data)
{
  WpLogFields lf = {0};

  g_return_val_if_fail (fields != NULL, G_LOG_WRITER_UNHANDLED);
  g_return_val_if_fail (n_fields > 0, G_LOG_WRITER_UNHANDLED);

  wp_log_fields_init_from_glib (&lf, log_level_flags, fields, n_fields);

  /* check if debug level & topic is enabled */
  if (lf.log_level > find_topic_log_level (lf.log_topic, NULL))
    return G_LOG_WRITER_HANDLED;

  return wp_log_fields_log (&lf);
}

/*!
 * \brief Used internally by the debug logging macros. Avoid using it directly.
 *
 * This assumes that the arguments are correct and that the log_topic is
 * enabled for the given log_level. No additional checks are performed.
 * \ingroup wplog
 */
void
wp_log_checked (
    const gchar *log_topic,
    GLogLevelFlags log_level_flags,
    const gchar *file,
    const gchar *line,
    const gchar *func,
    GType object_type,
    gconstpointer object,
    const gchar *message_format,
    ...)
{
  WpLogFields lf = {0};
  g_autofree gchar *message = NULL;
  va_list args;

  va_start (args, message_format);
  message = g_strdup_vprintf (message_format, args);
  va_end (args);

  wp_log_fields_init (&lf, log_topic, level_index_from_flags (log_level_flags),
      file, line, func, object_type, object, message);
  wp_log_fields_log (&lf);
}

static G_GNUC_PRINTF (7, 0) void
wp_spa_log_logtv (void *object,
    enum spa_log_level level,
    const struct spa_log_topic *topic,
    const char *file,
    int line,
    const char *func,
    const char *fmt,
    va_list args)
{
  WpLogFields lf = {0};
  gint log_level = level_index_from_spa (level, FALSE);
  g_autofree gchar *message = NULL;
  gchar line_str[11];

  sprintf (line_str, "%d", line);
  message = g_strdup_vprintf (fmt, args);

  wp_log_fields_init (&lf, topic ? topic->topic : NULL, log_level,
      file, line_str, func, 0, NULL, message);
  wp_log_fields_log (&lf);
}

static G_GNUC_PRINTF (7, 8) void
wp_spa_log_logt (void *object,
    enum spa_log_level level,
    const struct spa_log_topic *topic,
    const char *file,
    int line,
    const char *func,
    const char *fmt, ...)
{
  va_list args;
  va_start (args, fmt);
  wp_spa_log_logtv (object, level, topic, file, line, func, fmt, args);
  va_end (args);
}

static G_GNUC_PRINTF (6, 0) void
wp_spa_log_logv (void *object,
    enum spa_log_level level,
    const char *file,
    int line,
    const char *func,
    const char *fmt,
    va_list args)
{
  wp_spa_log_logtv (object, level, NULL, file, line, func, fmt, args);
}

static G_GNUC_PRINTF (6, 7) void
wp_spa_log_log (void *object,
    enum spa_log_level level,
    const char *file,
    int line,
    const char *func,
    const char *fmt, ...)
{
  va_list args;
  va_start (args, fmt);
  wp_spa_log_logtv (object, level, NULL, file, line, func, fmt, args);
  va_end (args);
}

static void
wp_spa_log_topic_init (void *object, struct spa_log_topic *topic)
{
  gint log_level = find_topic_log_level (topic->topic, &topic->has_custom_level);
  topic->level = level_index_to_spa (log_level);
}

static const struct spa_log_methods wp_spa_log_methods = {
  SPA_VERSION_LOG_METHODS,
  .log = wp_spa_log_log,
  .logv = wp_spa_log_logv,
  .logt = wp_spa_log_logt,
  .logtv = wp_spa_log_logtv,
  .topic_init = wp_spa_log_topic_init,
};

static struct spa_log wp_spa_log = {
  .iface = { SPA_TYPE_INTERFACE_Log, SPA_VERSION_LOG, { &wp_spa_log_methods, NULL } },
  .level = SPA_LOG_LEVEL_WARN,
};

/*!
 * \brief Gets WirePlumber's instance of `spa_log`
 * \ingroup wplog
 * \returns WirePlumber's instance of `spa_log`, which can be used to redirect
 *   PipeWire's log messages to the currently installed GLogWriterFunc.
 *   This is installed automatically when you call wp_init() with
 *   WP_INIT_SET_PW_LOG set in the flags
 */
struct spa_log *
wp_spa_log_get_instance (void)
{
  return &wp_spa_log;
}
 07070100000096000081A400000000000000000000000165F863040000187B000000000000000000000000000000000000001F00000000wireplumber-0.5.0/lib/wp/log.h    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_LOG_H__
#define __WIREPLUMBER_LOG_H__

#include <glib-object.h>
#include "defs.h"

G_BEGIN_DECLS

/*!
 * \brief A custom GLib log level for trace messages (extension of GLogLevelFlags)
 * \ingroup wplog
 */
static const guint WP_LOG_LEVEL_TRACE = (1 << 8);

/*
  The above WP_LOG_LEVEL_TRACE constant is intended to be defined as
  (1 << G_LOG_LEVEL_USER_SHIFT), but due to a gobject-introspection bug
  we define it with the value of G_LOG_LEVEL_USER_SHIFT, which is 8, so
  that it ends up correctly in the bindings. To avoid value mismatches,
  we statically verify here that G_LOG_LEVEL_USER_SHIFT is indeed 8.
  See https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/540
*/
G_STATIC_ASSERT (G_LOG_LEVEL_USER_SHIFT == 8);

#define WP_OBJECT_FORMAT "<%s:%p>"
#define WP_OBJECT_ARGS(object) \
    (object ? G_OBJECT_TYPE_NAME(object) : "invalid"), object

WP_PRIVATE_API
void wp_log_init (gint flags);

WP_API
gboolean wp_log_set_level (const gchar *log_level);

/*!
 * \brief WpLogTopic flags
 * \ingroup wplog
 */
typedef enum { /*< flags >*/
  /*! the lower 16 bits of the flags are GLogLevelFlags */
  WP_LOG_TOPIC_LEVEL_MASK = 0xFFFF,
  /*! the log topic has infinite lifetime (lives on static storage) */
  WP_LOG_TOPIC_FLAG_STATIC = 1u << 30,
  /*! the log topic has been initialized */
  WP_LOG_TOPIC_FLAG_INITIALIZED = 1u << 31,
} WpLogTopicFlags;

/*!
 * \brief A structure representing a log topic
 * \ingroup wplog
 */
typedef struct {
  const char *topic_name;
  WpLogTopicFlags flags;

  /*< private >*/
  WP_PADDING(3)
} WpLogTopic;

#define WP_LOG_TOPIC_EXTERN(var) \
  extern WpLogTopic * var;

#define WP_LOG_TOPIC(var, name) \
  WpLogTopic var##_struct = { .topic_name = name, .flags = WP_LOG_TOPIC_FLAG_STATIC };  \
  WpLogTopic * var = &(var##_struct);

#define WP_LOG_TOPIC_STATIC(var, name) \
  static WpLogTopic var##_struct = { .topic_name = name, .flags = WP_LOG_TOPIC_FLAG_STATIC }; \
  static G_GNUC_UNUSED WpLogTopic * var = &(var##_struct);

#define WP_DEFINE_LOCAL_LOG_TOPIC(name) \
  WP_LOG_TOPIC_STATIC(WP_LOCAL_LOG_TOPIC, name)

/* make glib log functions also use the local log topic */
#ifdef WP_USE_LOCAL_LOG_TOPIC_IN_G_LOG
# ifdef G_LOG_DOMAIN
#  undef G_LOG_DOMAIN
# endif
# define G_LOG_DOMAIN (WP_LOCAL_LOG_TOPIC->topic_name)
#endif

WP_API
void wp_log_topic_init (WpLogTopic *topic);

WP_API
void wp_log_topic_register (WpLogTopic *topic);

WP_API
void wp_log_topic_unregister (WpLogTopic *topic);

static inline gboolean
wp_log_topic_is_initialized (WpLogTopic *topic)
{
  return (topic->flags & WP_LOG_TOPIC_FLAG_INITIALIZED) != 0;
}

static inline gboolean
wp_log_topic_is_enabled (WpLogTopic *topic, GLogLevelFlags log_level)
{
  /* first time initialization */
  if (G_UNLIKELY (!wp_log_topic_is_initialized (topic)))
    wp_log_topic_init (topic);

  return (topic->flags & log_level & WP_LOG_TOPIC_LEVEL_MASK) != 0;
}

#define wp_local_log_topic_is_enabled(log_level) \
  (wp_log_topic_is_enabled (WP_LOCAL_LOG_TOPIC, log_level))

WP_API
GLogWriterOutput wp_log_writer_default (GLogLevelFlags log_level,
    const GLogField *fields, gsize n_fields, gpointer user_data);

WP_API
void wp_log_checked (const gchar *log_topic, GLogLevelFlags log_level,
    const gchar *file, const gchar *line, const gchar *func,
    GType object_type, gconstpointer object,
    const gchar *message_format, ...) G_GNUC_PRINTF (8, 9);

#define wp_log(topic, level, type, object, ...) \
({ \
  if (G_UNLIKELY (wp_log_topic_is_enabled (topic, level))) \
    wp_log_checked (topic->topic_name, level, __FILE__, G_STRINGIFY (__LINE__), \
        G_STRFUNC, type, object, __VA_ARGS__); \
})

#define wp_critical(...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_CRITICAL, 0, NULL, __VA_ARGS__)
#define wp_warning(...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_WARNING, 0, NULL, __VA_ARGS__)
#define wp_notice(...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_MESSAGE, 0, NULL, __VA_ARGS__)
#define wp_info(...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_INFO, 0, NULL, __VA_ARGS__)
#define wp_debug(...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_DEBUG, 0, NULL, __VA_ARGS__)
#define wp_trace(...) \
    wp_log (WP_LOCAL_LOG_TOPIC, WP_LOG_LEVEL_TRACE, 0, NULL, __VA_ARGS__)

#define wp_critical_object(object, ...)  \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_CRITICAL, (object) ? G_TYPE_FROM_INSTANCE (object) : G_TYPE_NONE, object, __VA_ARGS__)
#define wp_warning_object(object, ...)  \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_WARNING, (object) ? G_TYPE_FROM_INSTANCE (object) : G_TYPE_NONE, object, __VA_ARGS__)
#define wp_notice_object(object, ...)  \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_MESSAGE, (object) ? G_TYPE_FROM_INSTANCE (object) : G_TYPE_NONE, object, __VA_ARGS__)
#define wp_info_object(object, ...)  \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_INFO, (object) ? G_TYPE_FROM_INSTANCE (object) : G_TYPE_NONE, object, __VA_ARGS__)
#define wp_debug_object(object, ...)  \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_DEBUG, (object) ? G_TYPE_FROM_INSTANCE (object) : G_TYPE_NONE, object, __VA_ARGS__)
#define wp_trace_object(object, ...)  \
    wp_log (WP_LOCAL_LOG_TOPIC, WP_LOG_LEVEL_TRACE, (object) ? G_TYPE_FROM_INSTANCE (object) : G_TYPE_NONE, object, __VA_ARGS__)

#define wp_critical_boxed(type, object, ...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_CRITICAL, type, object, __VA_ARGS__)
#define wp_warning_boxed(type, object, ...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_WARNING, type, object, __VA_ARGS__)
#define wp_notice_boxed(type, object, ...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_MESSAGE, type, object, __VA_ARGS__)
#define wp_info_boxed(type, object, ...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_INFO, type, object, __VA_ARGS__)
#define wp_debug_boxed(type, object, ...) \
    wp_log (WP_LOCAL_LOG_TOPIC, G_LOG_LEVEL_DEBUG, type, object, __VA_ARGS__)
#define wp_trace_boxed(type, object, ...) \
    wp_log (WP_LOCAL_LOG_TOPIC, WP_LOG_LEVEL_TRACE, type, object, __VA_ARGS__)

struct spa_log;

WP_API
struct spa_log * wp_spa_log_get_instance (void);

G_END_DECLS

#endif
 07070100000097000081A400000000000000000000000165F8630400000D38000000000000000000000000000000000000002500000000wireplumber-0.5.0/lib/wp/meson.build  wp_lib_sources = files(
  'base-dirs.c',
  'client.c',
  'component-loader.c',
  'conf.c',
  'core.c',
  'device.c',
  'error.c',
  'event.c',
  'event-dispatcher.c',
  'event-hook.c',
  'factory.c',
  'global-proxy.c',
  'iterator.c',
  'json-utils.c',
  'link.c',
  'log.c',
  'metadata.c',
  'settings.c',
  'module.c',
  'node.c',
  'object.c',
  'object-interest.c',
  'object-manager.c',
  'plugin.c',
  'port.c',
  'properties.c',
  'proxy.c',
  'proxy-interfaces.c',
  'session-item.c',
  'si-factory.c',
  'si-interfaces.c',
  'spa-json.c',
  'spa-pod.c',
  'spa-type.c',
  'state.c',
  'transition.c',
  'wp.c',
)

wp_lib_priv_sources = files(
  'private/pipewire-object-mixin.c',
  'private/internal-comp-loader.c',
  'private/registry.c',
)

wp_lib_headers = files(
  'base-dirs.h',
  'client.h',
  'component-loader.h',
  'conf.h',
  'core.h',
  'defs.h',
  'device.h',
  'error.h',
  'event.h',
  'event-dispatcher.h',
  'event-hook.h',
  'global-proxy.h',
  'iterator.h',
  'json-utils.h',
  'link.h',
  'log.h',
  'metadata.h',
  'settings.h',
  'module.h',
  'node.h',
  'object.h',
  'object-interest.h',
  'object-manager.h',
  'plugin.h',
  'port.h',
  'properties.h',
  'proxy.h',
  'proxy-interfaces.h',
  'session-item.h',
  'si-factory.h',
  'si-interfaces.h',
  'spa-json.h',
  'spa-pod.h',
  'spa-type.h',
  'state.h',
  'transition.h',
  'wp.h',
  'factory.h',
)

install_headers(wp_lib_headers,
  install_dir : wireplumber_headers_dir
)

enums = gnome.mkenums_simple('wpenums',
  sources: wp_lib_headers,
  header_prefix: '#include "wp/defs.h"',
  decorator: 'WP_API',
  install_header: true,
  install_dir: wireplumber_headers_dir,
)
wpenums_h = enums[1]
wpenums_c = enums[0]
wp_gen_sources = [wpenums_h]
wpenums_include_dir = include_directories('.')

wpversion_data = configuration_data()
wpversion_data.set('version', meson.project_version())
wpversion_data.set('api_version', wireplumber_api_version)
wpversion = configure_file(
  input : 'wpversion.h.in',
  output : 'wpversion.h',
  configuration : wpversion_data,
  install_dir: wireplumber_headers_dir
)
wp_gen_sources += [wpversion]

wpbuildbasedirs_data = configuration_data()
wpbuildbasedirs_data.set('BUILD_SYSCONFDIR', '"@0@"'.format(get_option('prefix') / get_option('sysconfdir')))
wpbuildbasedirs_data.set('BUILD_DATADIR', '"@0@"'.format(get_option('prefix') / get_option('datadir')))
wpbuildbasedirs_data.set('BUILD_LIBDIR', '"@0@"'.format(get_option('prefix') / get_option('libdir')))
wpbuildbasedirs_data.set('BUILD_LOCALEDIR', '"@0@"'.format(get_option('prefix') / get_option('localedir')))
wpbuildbasedirs = configure_file (
  output : 'wpbuildbasedirs.h',
  configuration : wpbuildbasedirs_data,
)

wp_lib = library('wireplumber-' + wireplumber_api_version,
  wp_lib_sources, wp_lib_priv_sources, wpenums_c, wpenums_h, wpversion, wpbuildbasedirs,
  c_args : [
    '-DBUILDING_WP',
  ],
  install: true,
  include_directories: wp_lib_include_dir,
  dependencies : [gobject_dep, gmodule_dep, gio_dep, pipewire_dep, libintl_dep],
  soversion: wireplumber_so_version,
  version: meson.project_version(),
)

wp_dep = declare_dependency(
  link_with: wp_lib,
  sources: wp_gen_sources,
  include_directories: wp_lib_include_dir,
  dependencies: [gobject_dep, gio_dep]
)

pkgconfig.generate(wp_lib,
  libraries: [gobject_dep, gio_dep],
  subdirs: 'wireplumber-' + wireplumber_api_version
)
07070100000098000081A400000000000000000000000165F8630400005B36000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/metadata.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Raghavendra Rao <raghavendra.rao@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "metadata.h"
#include "core.h"
#include "log.h"
#include "error.h"
#include "wpenums.h"

#include <pipewire/impl.h>
#include <pipewire/pipewire.h>
#include <pipewire/extensions/metadata.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-metadata")

/*! \defgroup wpmetadata WpMetadata */
/*!
 * \struct WpMetadata
 *
 * The WpMetadata class allows accessing the properties and methods of
 * PipeWire metadata object (`struct pw_metadata`).
 *
 * A WpMetadata is constructed internally when a new metadata object appears on the
 * PipeWire registry and it is made available through the WpObjectManager API.
 *
 * \gsignals
 *
 * \par changed
 * \parblock
 * \code
 * void
 * changed_callback (WpMetadata * self,
 *                   guint subject,
 *                   gchar * key,
 *                   gchar * type,
 *                   gchar * value,
 *                   gpointer user_data)
 * \endcode
 * Emited when metadata change
 *
 * Parameters:
 * - `subject` - the metadata subject id
 * - `key` - the metadata key
 * - `type` - the value type
 * - `value` - the metadata value
 *
 * Flags: G_SIGNAL_RUN_LAST
 * \endparblock
 */
enum {
  SIGNAL_CHANGED,
  N_SIGNALS,
};

static guint32 signals[N_SIGNALS] = {0};

/* data structure */

struct item
{
  uint32_t subject;
  gchar *key;
  gchar *type;
  gchar *value;
};

static void
set_item (struct item * item, uint32_t subject, const char * key,
    const char * type, const char * value)
{
  item->subject = subject;
  item->key = g_strdup (key);
  item->type = g_strdup (type);
  item->value = g_strdup (value);
}

static void
clear_item (struct item * item)
{
  g_free (item->key);
  g_free (item->type);
  g_free (item->value);
  spa_zero (*item);
}

static struct item *
find_item (struct pw_array * metadata, uint32_t subject, const char * key)
{
  struct item *item;

  pw_array_for_each (item, metadata) {
    if (item->subject == subject && (key == NULL || !strcmp (item->key, key))) {
      return item;
    }
  }
  return NULL;
}

static int
clear_subject (struct pw_array * metadata, uint32_t subject)
{
  struct item *item;
  uint32_t removed = 0;

  while (true) {
    item = find_item (metadata, subject, NULL);
    if (item == NULL)
      break;
    clear_item (item);
    pw_array_remove (metadata, item);
    removed++;
  }

  return removed;
}

static void
clear_items (struct pw_array * metadata)
{
  struct item *item;

  pw_array_consume (item, metadata) {
    clear_item (item);
    pw_array_remove (metadata, item);
  }
  pw_array_reset (metadata);
}

typedef struct _WpMetadataPrivate WpMetadataPrivate;
struct _WpMetadataPrivate
{
  struct pw_metadata *iface;
  struct spa_hook listener;
  struct pw_array metadata;
  gboolean remove_listener;
};

G_DEFINE_TYPE_WITH_PRIVATE (WpMetadata, wp_metadata, WP_TYPE_GLOBAL_PROXY)

static void
wp_metadata_init (WpMetadata * self)
{
  WpMetadataPrivate *priv = wp_metadata_get_instance_private (self);
  pw_array_init (&priv->metadata, 4096);
}

static void
wp_metadata_finalize (GObject * object)
{
  WpMetadataPrivate *priv =
      wp_metadata_get_instance_private (WP_METADATA (object));

  pw_array_clear (&priv->metadata);

  G_OBJECT_CLASS (wp_metadata_parent_class)->finalize (object);
}

static WpObjectFeatures
wp_metadata_get_supported_features (WpObject * object)
{
  return WP_PROXY_FEATURE_BOUND | WP_METADATA_FEATURE_DATA;
}

enum {
  STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_CACHE
};

static guint
wp_metadata_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  g_return_val_if_fail (
      missing & (WP_PROXY_FEATURE_BOUND | WP_METADATA_FEATURE_DATA),
      WP_TRANSITION_STEP_ERROR);

  /* bind if not already bound */
  if (missing & WP_PROXY_FEATURE_BOUND)
    return STEP_BIND;
  else
    return STEP_CACHE;
}

static void
wp_metadata_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case STEP_CACHE:
    /* just wait for initial_sync_done() */
    break;
  default:
    WP_OBJECT_CLASS (wp_metadata_parent_class)->
        activate_execute_step (object, transition, step, missing);
    break;
  }
}

static int
metadata_event_property (void *object, uint32_t subject, const char *key,
    const char *type, const char *value)
{
  WpMetadata *self = WP_METADATA (object);
  WpMetadataPrivate *priv =
      wp_metadata_get_instance_private (WP_METADATA (self));
  struct item *item = NULL;

  if (key == NULL) {
    if (clear_subject (&priv->metadata, subject) > 0) {
      wp_debug_object (self, "remove id:%d", subject);
      g_signal_emit (self, signals[SIGNAL_CHANGED], 0, subject, NULL, NULL,
          NULL);
    }
    return 0;
  }

  item = find_item (&priv->metadata, subject, key);
  if (item == NULL) {
    if (value == NULL)
      return 0;
    item = pw_array_add (&priv->metadata, sizeof (*item));
    if (item == NULL)
      return -errno;
  } else {
    clear_item (item);
  }

  if (value != NULL) {
    if (type == NULL)
      type = "string";
    set_item (item, subject, key, type, value);
    wp_debug_object (self, "add id:%d key:%s type:%s value:%s",
        subject, key, type, value);
  } else {
    type = NULL;
    pw_array_remove (&priv->metadata, item);
    wp_debug_object (self, "remove id:%d key:%s", subject, key);
  }

  g_signal_emit (self, signals[SIGNAL_CHANGED], 0, subject, key, type, value);
  return 0;
}

static const struct pw_metadata_events metadata_events = {
  PW_VERSION_METADATA_EVENTS,
  .property = metadata_event_property,
};

static void
initial_sync_done (WpCore * core, GAsyncResult * res, WpMetadata * self)
{
  g_autoptr (GError) error = NULL;
  if (!wp_core_sync_finish (core, res, &error)) {
    wp_warning_object (self, "core sync error: %s", error->message);
    return;
  }

  wp_object_update_features (WP_OBJECT (self), WP_METADATA_FEATURE_DATA, 0);
}

static void
wp_metadata_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
{
  WpMetadata *self = WP_METADATA (proxy);
  WpMetadataPrivate *priv = wp_metadata_get_instance_private (self);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));

  priv->iface = (struct pw_metadata *) pw_proxy;
  pw_metadata_add_listener (priv->iface, &priv->listener,
      &metadata_events, self);
  priv->remove_listener = TRUE;
  wp_core_sync_closure (core, NULL,
      g_cclosure_new_object ((GCallback) initial_sync_done, G_OBJECT (self)));
}

static void
wp_metadata_pw_proxy_destroyed (WpProxy * proxy)
{
  WpMetadata *self = WP_METADATA (proxy);
  WpMetadataPrivate *priv = wp_metadata_get_instance_private (self);

  if (priv->remove_listener) {
    spa_hook_remove (&priv->listener);
    priv->remove_listener = FALSE;
  }
  clear_items (&priv->metadata);
  wp_object_update_features (WP_OBJECT (self), 0, WP_METADATA_FEATURE_DATA);

  WP_PROXY_CLASS (wp_metadata_parent_class)->pw_proxy_destroyed (proxy);
}

static void
wp_metadata_class_init (WpMetadataClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->finalize = wp_metadata_finalize;

  wpobject_class->get_supported_features = wp_metadata_get_supported_features;
  wpobject_class->activate_get_next_step = wp_metadata_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_metadata_activate_execute_step;

  proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Metadata;
  proxy_class->pw_iface_version = PW_VERSION_METADATA;
  proxy_class->pw_proxy_created = wp_metadata_pw_proxy_created;
  proxy_class->pw_proxy_destroyed = wp_metadata_pw_proxy_destroyed;

  signals[SIGNAL_CHANGED] = g_signal_new ("changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 4,
      G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
}

/*!
 * \struct WpMetadataItem
 *
 * WpMetadataItem holds the subject, key, type and value of a metadata entry.
 */
struct _WpMetadataItem
{
  WpMetadata *metadata;
  guint32 subject;
  const gchar *key;
  const gchar *type;
  const gchar *value;
};

G_DEFINE_BOXED_TYPE (WpMetadataItem, wp_metadata_item,
    wp_metadata_item_ref, wp_metadata_item_unref)

static WpMetadataItem *
wp_metadata_item_new (WpMetadata *metadata, guint32 subject, const gchar *key,
    const gchar *type, const gchar *value)
{
  WpMetadataItem *self = g_rc_box_new0 (WpMetadataItem);
  self->metadata = g_object_ref (metadata);
  self->subject = subject;
  self->key = key;
  self->type = type;
  self->value = value;
  return self;
}

static void
wp_metadata_item_free (gpointer p)
{
  WpMetadataItem *self = p;
  g_clear_object (&self->metadata);
}

/*!
 * \brief Increases the reference count of a metadata item object
 * \ingroup wpmetadata
 * \param self a metadata item object
 * \returns (transfer full): \a self with an additional reference count on it
 * \since 0.5.0
 */
WpMetadataItem *
wp_metadata_item_ref (WpMetadataItem *self)
{
  return g_rc_box_acquire (self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 * \ingroup wpmetadata
 * \param self (transfer full): a metadata item object
 * \since 0.5.0
 */
void
wp_metadata_item_unref (WpMetadataItem *self)
{
  g_rc_box_release_full (self, wp_metadata_item_free);
}

/*!
 * \brief Gets the subject from a metadata item
 *
 * \ingroup wpmetadata
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_metadata_new_iterator()
 * \returns (transfer none): the metadata subject of the \a item
 * \since 0.5.0
 */
guint32
wp_metadata_item_get_subject (WpMetadataItem * self)
{
  return self->subject;
}

/*!
 * \brief Gets the key from a metadata item
 *
 * \ingroup wpmetadata
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_metadata_new_iterator()
 * \returns (transfer none): the metadata key of the \a item
 * \since 0.5.0
 */
const gchar *
wp_metadata_item_get_key (WpMetadataItem * self)
{
  return self->key;
}

/*!
 * \brief Gets the value type from a metadata item
 *
 * \ingroup wpmetadata
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_metadata_new_iterator()
 * \returns (transfer none): the metadata value type of the \a item
 * \since 0.5.0
 */
const gchar *
wp_metadata_item_get_value_type (WpMetadataItem * self)
{
  return self->type;
}

/*!
 * \brief Gets the value from a metadata item
 *
 * \ingroup wpmetadata
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_metadata_new_iterator()
 * \returns (transfer none): the metadata value of the \a item
 * \since 0.5.0
 */
const gchar *
wp_metadata_item_get_value (WpMetadataItem * self)
{
  return self->value;
}

struct metadata_iterator_data
{
  WpMetadata *metadata;
  const struct item *item;
  guint32 subject;
};

static void
metadata_iterator_reset (WpIterator *it)
{
  struct metadata_iterator_data *it_data = wp_iterator_get_user_data (it);
  WpMetadataPrivate *priv =
      wp_metadata_get_instance_private (it_data->metadata);

  it_data->item = pw_array_first (&priv->metadata);
}

static gboolean
metadata_iterator_next (WpIterator *it, GValue *item)
{
  struct metadata_iterator_data *it_data = wp_iterator_get_user_data (it);
  WpMetadataPrivate *priv =
      wp_metadata_get_instance_private (it_data->metadata);

  while (pw_array_check (&priv->metadata, it_data->item)) {
    if ((it_data->subject == PW_ID_ANY ||
            it_data->subject == it_data->item->subject)) {
      g_autoptr (WpMetadataItem) mi = wp_metadata_item_new (it_data->metadata,
          it_data->item->subject, it_data->item->key, it_data->item->type,
          it_data->item->value);
      g_value_init (item, WP_TYPE_METADATA_ITEM);
      g_value_take_boxed (item, g_steal_pointer (&mi));
      it_data->item++;
      return TRUE;
    }
    it_data->item++;
  }
  return FALSE;
}

static gboolean
metadata_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
    gpointer data)
{
  struct metadata_iterator_data *it_data = wp_iterator_get_user_data (it);
  WpMetadataPrivate *priv =
      wp_metadata_get_instance_private (it_data->metadata);
  const struct item *i;

  pw_array_for_each (i, &priv->metadata) {
    if ((it_data->subject == PW_ID_ANY ||
            it_data->subject == it_data->item->subject)) {
      g_auto (GValue) item = G_VALUE_INIT;
      g_autoptr (WpMetadataItem) mi = wp_metadata_item_new (it_data->metadata,
          it_data->item->subject, it_data->item->key, it_data->item->type,
          it_data->item->value);
      g_value_init (&item, WP_TYPE_METADATA_ITEM);
      g_value_take_boxed (&item, g_steal_pointer (&mi));
      if (!func (&item, ret, data))
        return FALSE;
    }
  }
  return TRUE;
}

static void
metadata_iterator_finalize (WpIterator *it)
{
  struct metadata_iterator_data *it_data = wp_iterator_get_user_data (it);
  g_object_unref (it_data->metadata);
}

static const WpIteratorMethods metadata_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = metadata_iterator_reset,
  .next = metadata_iterator_next,
  .fold = metadata_iterator_fold,
  .finalize = metadata_iterator_finalize,
};

/*!
 * \brief Iterates over metadata items that matches the given \a subject.
 *
 * If no constraints are specified, the returned iterator iterates over all the
 * stored metadata.
 *
 * Note that this method works on cached metadata. When you change metadata
 * with wp_metadata_set(), this cache will be updated on the next round-trip
 * with the pipewire server.
 *
 * \ingroup wpmetadata
 * \param self a metadata object
 * \param subject the metadata subject id, or -1 (PW_ID_ANY)
 * \returns (transfer full): an iterator that iterates over the found metadata.
 *   The type of the iterator item is WpMetadataItem.
 */
WpIterator *
wp_metadata_new_iterator (WpMetadata * self, guint32 subject)
{
  WpMetadataPrivate *priv;
  g_autoptr (WpIterator) it = NULL;
  struct metadata_iterator_data *it_data;

  g_return_val_if_fail (self != NULL, NULL);
  priv = wp_metadata_get_instance_private (self);

  it = wp_iterator_new (&metadata_iterator_methods,
      sizeof (struct metadata_iterator_data));
  it_data = wp_iterator_get_user_data (it);
  it_data->metadata = g_object_ref (self);
  it_data->item = pw_array_first (&priv->metadata);
  it_data->subject = subject;
  return g_steal_pointer (&it);
}

/*!
 * \brief Finds the metadata value given its \a subject and \a key.
 *
 * \ingroup wpmetadata
 * \param self a metadata object
 * \param subject the metadata subject id
 * \param key the metadata key name
 * \param type (out)(optional): the metadata type name
 * \returns the metadata string value, or NULL if not found.
 */
const gchar *
wp_metadata_find (WpMetadata * self, guint32 subject, const gchar * key,
  const gchar ** type)
{
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) val = G_VALUE_INIT;
  it = wp_metadata_new_iterator (self, subject);
  for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
    WpMetadataItem *mi = g_value_get_boxed (&val);
    const gchar *k = wp_metadata_item_get_key (mi);
    const gchar *t = wp_metadata_item_get_value_type (mi);
    const gchar *v = wp_metadata_item_get_value (mi);
    if (g_strcmp0 (k, key) == 0) {
      if (type)
        *type = t;
      g_value_unset (&val);
      return v;
    }
  }
  return NULL;
}

/*!
 * \brief Sets the metadata associated with the given \a subject and \a key.
 * Use NULL as a value to unset the given \a key and use NULL in both \a key
 * and \a value to remove all metadata associated with the given \a subject.
 *
 * \ingroup wpmetadata
 * \param self the metadata object
 * \param subject the subject id for which this metadata property is being set
 * \param key (nullable): the key to set, or NULL to remove all metadata for
 *   \a subject
 * \param type (nullable): the type of the value; NULL is synonymous to "string"
 * \param value (nullable): the value to set, or NULL to unset the given \a key
 */
void
wp_metadata_set (WpMetadata * self, guint32 subject,
    const gchar * key, const gchar * type, const gchar * value)
{
  WpMetadataPrivate *priv = wp_metadata_get_instance_private (self);
  pw_metadata_set_property (priv->iface, subject, key, type, value);
}

/*!
 * \brief Clears permanently all stored metadata.
 * \ingroup wpmetadata
 * \param self the metadata object
 */
void
wp_metadata_clear (WpMetadata * self)
{
  WpMetadataPrivate *priv = wp_metadata_get_instance_private (self);
  pw_metadata_clear (priv->iface);
}

/*!
 * \struct WpImplMetadata
 * Implementation of the metadata object.
 *
 * Activate this object with at least WP_PROXY_FEATURE_BOUND to export it to
 * PipeWire.
 */
struct _WpImplMetadata
{
  WpMetadata parent;

  gchar *name;
  WpProperties *properties;

  struct pw_impl_metadata *impl;
  struct spa_hook listener;
};

enum {
  PROP_0,
  PROP_NAME,
  PROP_PROPERTIES,
};

G_DEFINE_TYPE (WpImplMetadata, wp_impl_metadata, WP_TYPE_METADATA)

static void
wp_impl_metadata_init (WpImplMetadata * self)
{
}

static const struct pw_impl_metadata_events impl_metadata_events = {
  PW_VERSION_IMPL_METADATA_EVENTS,
  .property = metadata_event_property,
};

static void
wp_impl_metadata_constructed (GObject *object)
{
  WpImplMetadata *self = WP_IMPL_METADATA (object);
  WpMetadataPrivate *priv =
      wp_metadata_get_instance_private (WP_METADATA (self));
  g_autoptr (WpCore) core = NULL;
  struct pw_context *pw_context;
  struct pw_properties *props = NULL;

  core = wp_object_get_core (WP_OBJECT (self));
  g_return_if_fail (core);
  pw_context = wp_core_get_pw_context (core);
  g_return_if_fail (pw_context);

  if (self->properties)
    props = wp_properties_to_pw_properties (self->properties);

  self->impl = pw_context_create_metadata (pw_context, self->name, props , 0);
  g_return_if_fail (self->impl);
  priv->iface = pw_impl_metadata_get_implementation (self->impl);
  g_return_if_fail (priv->iface);

  pw_impl_metadata_add_listener (self->impl, &self->listener,
      &impl_metadata_events, self);

  wp_object_update_features (WP_OBJECT (self), WP_METADATA_FEATURE_DATA, 0);
  G_OBJECT_CLASS (wp_impl_metadata_parent_class)->constructed (object);
}

static void
wp_impl_metadata_finalize (GObject * object)
{
  WpImplMetadata *self = WP_IMPL_METADATA (object);

  spa_hook_remove (&self->listener);
  g_clear_pointer (&self->impl, pw_impl_metadata_destroy);
  g_clear_pointer (&self->properties, wp_properties_unref);
  g_clear_pointer (&self->name, g_free);

  G_OBJECT_CLASS (wp_impl_metadata_parent_class)->finalize (object);
}

static void
wp_impl_metadata_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpImplMetadata *self = WP_IMPL_METADATA (object);

  switch (property_id) {
  case PROP_NAME:
    g_clear_pointer (&self->name, g_free);
    self->name = g_value_dup_string (value);
    break;
  case PROP_PROPERTIES:
    g_clear_pointer (&self->properties, wp_properties_unref);
    self->properties = g_value_dup_boxed (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_impl_metadata_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpImplMetadata *self = WP_IMPL_METADATA (object);

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, self->name);
    break;
  case PROP_PROPERTIES:
    g_value_set_boxed (value, self->properties);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_impl_metadata_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  WpImplMetadata *self = WP_IMPL_METADATA (object);
  WpMetadataPrivate *priv =
      wp_metadata_get_instance_private (WP_METADATA (self));

  switch (step) {
  case STEP_BIND: {
    g_autoptr (WpCore) core = wp_object_get_core (object);
    struct pw_core *pw_core = wp_core_get_pw_core (core);
    const struct pw_properties *props = NULL;

    /* no pw_core -> we are not connected */
    if (!pw_core) {
      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
              WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
              "The WirePlumber core is not connected; "
              "object cannot be exported to PipeWire"));
      return;
    }

    props = pw_impl_metadata_get_properties (self->impl);
    wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
            PW_TYPE_INTERFACE_Metadata, &props->dict, priv->iface, 0)
    );
    break;
  }
  case STEP_CACHE:
    /* never reached because WP_METADATA_FEATURE_DATA is always enabled */
    g_assert_not_reached ();
    break;
  default:
    WP_OBJECT_CLASS (wp_impl_metadata_parent_class)->
        activate_execute_step (object, transition, step, missing);
    break;
  }
}

static void
wp_impl_metadata_class_init (WpImplMetadataClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->constructed = wp_impl_metadata_constructed;
  object_class->finalize = wp_impl_metadata_finalize;
  object_class->set_property = wp_impl_metadata_set_property;
  object_class->get_property = wp_impl_metadata_get_property;

  wpobject_class->activate_execute_step =
      wp_impl_metadata_activate_execute_step;

  /* disable adding a listener for events */
  proxy_class->pw_proxy_created = NULL;

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name", "The metadata name", "",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PROPERTIES,
      g_param_spec_boxed ("properties", "properties",
          "The metadata properties", WP_TYPE_PROPERTIES,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Creates a new metadata implementation
 * \ingroup wpmetadata
 * \param core the core
 * \returns (transfer full): a new WpImplMetadata
 */
WpImplMetadata *
wp_impl_metadata_new (WpCore * core)
{
  return wp_impl_metadata_new_full (core, NULL, NULL);
}

/*!
 * \brief Creates a new metadata implementation with name and properties
 * \ingroup wpmetadata
 * \param core the core
 * \param name (nullable): the metadata name
 * \param properties (nullable) (transfer full): the metadata properties
 * \returns (transfer full): a new WpImplMetadata
 * \since 0.4.3
 */
WpImplMetadata *
wp_impl_metadata_new_full (WpCore * core, const gchar *name,
    WpProperties *properties)
{
  g_autoptr (WpProperties) props = properties;

  g_return_val_if_fail (WP_IS_CORE (core), NULL);

  return g_object_new (WP_TYPE_IMPL_METADATA,
      "core", core,
      "name", name,
      "properties", props,
      NULL);
}
  07070100000099000081A400000000000000000000000165F863040000092D000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/metadata.h   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Raghavendra Rao <raghavendra.rao@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_METADATA_H__
#define __WIREPLUMBER_METADATA_H__

#include "global-proxy.h"

G_BEGIN_DECLS

/*!
 * \brief The WpMetadataItem GType
 * \ingroup wpmetadata
 */
#define WP_TYPE_METADATA_ITEM (wp_metadata_item_get_type ())
WP_API
GType wp_metadata_item_get_type (void);

typedef struct _WpMetadataItem WpMetadataItem;

WP_API
WpMetadataItem *wp_metadata_item_ref (WpMetadataItem *self);

WP_API
void wp_metadata_item_unref (WpMetadataItem *self);

WP_API
guint32 wp_metadata_item_get_subject (WpMetadataItem * self);

WP_API
const gchar * wp_metadata_item_get_key (WpMetadataItem * self);

WP_API
const gchar * wp_metadata_item_get_value_type (WpMetadataItem * self);

WP_API
const gchar * wp_metadata_item_get_value (WpMetadataItem * self);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpMetadataItem, wp_metadata_item_unref)

/*!
 * \brief An extension of WpProxyFeatures for WpMetadata objects
 * \ingroup wpmetadata
 */
typedef enum { /*< flags >*/
  /*! caches metadata locally */
  WP_METADATA_FEATURE_DATA = (WP_PROXY_FEATURE_CUSTOM_START << 0),
} WpMetadataFeatures;

/*!
 * \brief The WpMetadata GType
 * \ingroup wpmetadata
 */
#define WP_TYPE_METADATA (wp_metadata_get_type ())

WP_API
G_DECLARE_DERIVABLE_TYPE (WpMetadata, wp_metadata, WP, METADATA, WpGlobalProxy)

struct _WpMetadataClass
{
  WpGlobalProxyClass parent_class;

  /*< private >*/
  WP_PADDING(4)
};

WP_API
WpIterator * wp_metadata_new_iterator (WpMetadata * self, guint32 subject);

WP_API
const gchar * wp_metadata_find (WpMetadata * self, guint32 subject,
    const gchar * key, const gchar ** type);

WP_API
void wp_metadata_set (WpMetadata * self, guint32 subject,
    const gchar * key, const gchar * type, const gchar * value);

WP_API
void wp_metadata_clear (WpMetadata * self);

/*!
 * \brief The WpImplMetadata GType
 * \ingroup wpmetadata
 */
#define WP_TYPE_IMPL_METADATA (wp_impl_metadata_get_type ())

WP_API
G_DECLARE_FINAL_TYPE (WpImplMetadata, wp_impl_metadata, WP, IMPL_METADATA, WpMetadata)

WP_API
WpImplMetadata * wp_impl_metadata_new (WpCore * core);

WP_API
WpImplMetadata * wp_impl_metadata_new_full (WpCore * core, const gchar *name,
    WpProperties *properties);

G_END_DECLS

#endif
   0707010000009A000081A400000000000000000000000165F8630400001B97000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/module.c /* WirePlumber
 *
 * Copyright © 2021 Asymptotic
 *    @author Arun Raghavan <arun@asymptotic.io>
 *
 * SPDX-License-Identifier: MIT
 */

#include "module.h"
#include "log.h"

#include <pipewire/impl.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-module")

/*! \defgroup wpimplmodule WpImplModule */
/*!
 * \struct WpImplModule
 * \since 0.4.2
 *
 * Used to load PipeWire modules within the WirePlumber process. This is
 * slightly different from other objects in that the module is not exported to
 * PipeWire, but it may create an export objects itself.
 */

struct _WpImplModule
{
  GObject parent;

  GWeakRef core;
  gchar *name;
  gchar *args;
  WpProperties *props; /* only used during module load */

  struct pw_impl_module *pw_impl_module;
};

G_DEFINE_TYPE (WpImplModule, wp_impl_module, G_TYPE_OBJECT);

enum {
  PROP_0,
  PROP_CORE,
  PROP_NAME,
  PROP_ARGUMENTS,
  PROP_PROPERTIES,
  PROP_PW_IMPL_MODULE,
};

static void
wp_impl_module_init (WpImplModule * self)
{
  g_weak_ref_init (&self->core, NULL);
  self->name = NULL;
  self->args = NULL;
  self->props = NULL;
  self->pw_impl_module = NULL;
}

static void
wp_impl_module_constructed (GObject * object)
{
  WpImplModule *self = WP_IMPL_MODULE (object);
  WpCore *core = g_weak_ref_get (&self->core);
  struct pw_context *context = core ? wp_core_get_pw_context (core) : NULL;
  struct pw_properties *props = NULL;

  if (!core || !context) {
    g_warning ("Tried to load module on unconnected core");
    return;
  }

  if (!self->name) {
    g_warning ("Invalid name while loading warnings");
    return;
  }

  if (self->props)
    props = wp_properties_to_pw_properties (self->props);

  self->pw_impl_module =
    pw_context_load_module (context, self->name, self->args, props);

  if (self->pw_impl_module && self->props) {
    /* With the module loaded, properties are just passthrough now */
    wp_properties_unref (self->props);
    self->props = NULL;
  }

  G_OBJECT_CLASS (wp_impl_module_parent_class)->constructed (object);
}

static void
wp_impl_module_finalize (GObject * object)
{
  WpImplModule *self = WP_IMPL_MODULE (object);

  g_weak_ref_clear (&self->core);

  if (self->pw_impl_module)
    pw_impl_module_destroy (self->pw_impl_module);

  g_free (self->name);
  g_free (self->args);

  if (self->props)
    wp_properties_unref (self->props);
}

static void
wp_impl_module_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  WpImplModule *self = WP_IMPL_MODULE (object);

  switch (prop_id) {
    case PROP_CORE:
      g_value_set_pointer (value, g_weak_ref_get (&self->core));
      break;

    case PROP_NAME:
      g_value_set_string (value, self->name);
      break;

    case PROP_ARGUMENTS:
      g_value_set_string (value, self->args);
      break;

    case PROP_PROPERTIES:
      if (self->pw_impl_module) {
        const struct pw_properties *props =
          pw_impl_module_get_properties (self->pw_impl_module);

        /* Should we just wrap instead of copying? */
        if (props)
          g_value_set_boxed (value, wp_properties_new_copy (props));
        else
          g_value_set_boxed (value, NULL);
      } else {
        g_value_set_boxed (value, self->props);
      }
      break;

    case PROP_PW_IMPL_MODULE:
      g_value_set_pointer (value, self->pw_impl_module);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
wp_impl_module_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  WpImplModule *self = WP_IMPL_MODULE (object);
  WpProperties *props;

  switch (prop_id) {
    case PROP_CORE:
      g_weak_ref_set (&self->core, g_value_get_pointer (value));
      break;

    case PROP_NAME:
      g_free (self->name);
      self->name = g_value_dup_string (value);
      break;

    case PROP_ARGUMENTS:
      g_free (self->args);
      self->args = g_value_dup_string (value);
      break;

    case PROP_PROPERTIES:
      props = g_value_get_boxed (value);

      if (props && self->pw_impl_module) {
        pw_impl_module_update_properties (self->pw_impl_module,
            wp_properties_peek_dict (props));
      } else {
        if (props)
          self->props = wp_properties_ref (props);
        else
          self->props = NULL;
      }
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
wp_impl_module_class_init (WpImplModuleClass * klass)
{
  GObjectClass *gobject_class = (GObjectClass *) klass;

  gobject_class->constructed = wp_impl_module_constructed;
  gobject_class->finalize = wp_impl_module_finalize;
  gobject_class->get_property = wp_impl_module_get_property;
  gobject_class->set_property = wp_impl_module_set_property;

  g_object_class_install_property (gobject_class, PROP_CORE,
      g_param_spec_pointer ("core", "Core", "The WirePlumber core",
        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_NAME,
      g_param_spec_string ("name", "Name", "The name of the PipeWire module",
        NULL,
        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_ARGUMENTS,
      g_param_spec_string ("arguments", "Arguments",
        "The arguments to provide to the module while loading", NULL,
        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_PROPERTIES,
      g_param_spec_boxed ("properties", "Properties",
        "Properties of the module", WP_TYPE_PROPERTIES,
        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_PW_IMPL_MODULE,
      g_param_spec_pointer ("pw-impl-module", "Underlying pw_impl_module",
        "Pointer to the underlying pw_impl_module structure for the module",
        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Loads a PipeWire module into the WirePlumber process
 *
 * \ingroup wpimplmodule
 * \since 0.4.2
 * \param core (transfer none): The WirePlumber core
 * \param name (transfer none): the name of the module to load
 * \param arguments (nullable) (transfer none): arguments to be passed to the module
 * \param properties (nullable) (transfer none): additional properties to be
 *    provided to the module
 * \returns (nullable) (transfer full): the WpImplModule for the module that
 *    was loaded on success, %NULL on failure.
 */
WpImplModule *
wp_impl_module_load (WpCore * core, const gchar * name,
    const gchar * arguments, WpProperties * properties)
{
  WpImplModule *module = WP_IMPL_MODULE (
      g_object_new (WP_TYPE_IMPL_MODULE,
        "core", core,
        "name", name,
        "arguments", arguments,
        "properties", properties,
        NULL)
      );

  if (!module->pw_impl_module) {
    /* Module loading failed, free and return */
    g_object_unref (module);
    return NULL;
  }

  return module;
}
 0707010000009B000081A400000000000000000000000165F86304000002D0000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/module.h /* WirePlumber
 *
 * Copyright © 2021 Asymptotic
 *    @author Arun Raghavan <arun@asymptotic.io>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_MODULE_H__
#define __WIREPLUMBER_MODULE_H__

#include <glib-object.h>

#include "core.h"
#include "defs.h"
#include "properties.h"

G_BEGIN_DECLS

/*!
 * \brief The WpImplModule GType
 * \since 0.4.2
 * \ingroup wpimplmodule
 */
#define WP_TYPE_IMPL_MODULE (wp_impl_module_get_type())
WP_API
G_DECLARE_FINAL_TYPE (WpImplModule, wp_impl_module, WP, IMPL_MODULE, GObject);

WP_API
WpImplModule * wp_impl_module_load (WpCore * core, const gchar * name,
    const gchar * arguments, WpProperties * properties);

G_END_DECLS

#endif /* __WIREPLUMBER_MODULE_H__ */
0707010000009C000081A400000000000000000000000165F8630400006DAA000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/node.c   /* WirePlumber
 *
 * Copyright © 2019-2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "node.h"
#include "core.h"
#include "object-manager.h"
#include "log.h"
#include "wpenums.h"
#include "private/pipewire-object-mixin.h"

#include <pipewire/impl.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-node")

/*! \defgroup wpnode WpNode */
/*!
 * \struct WpNode
 *
 * The WpNode class allows accessing the properties and methods of a
 * PipeWire node object (`struct pw_node`).
 *
 * A WpNode is constructed internally when a new node appears on the
 * PipeWire registry and it is made available through the WpObjectManager API.
 * Alternatively, a WpNode can also be constructed using
 * wp_node_new_from_factory(), which creates a new node object
 * on the remote PipeWire server by calling into a factory.
 *
 * \gproperties
 *
 * \gproperty{state, WpNodeState, G_PARAM_READABLE, The current state of the node}
 *
 * \gproperty{n-input-ports, uint, G_PARAM_READABLE, The number of input ports}
 *
 * \gproperty{n-output-ports, uint, G_PARAM_READABLE, The number of output ports}
 *
 * \gproperty{max-input-ports, uint, G_PARAM_READABLE, The max number of input ports}
 *
 * \gproperty{max-output-ports, uint, G_PARAM_READABLE, The max number of output ports}
 *
 * \gsignals
 *
 * \par ports-changed
 * \parblock
 * \code
 * void
 * ports_changed_callback (WpNode * self,
 *                         gpointer user_data)
 * \endcode
 *
 * Emitted when the node's ports change. This is only emitted when
 * WP_NODE_FEATURE_PORTS is enabled.
 *
 * Flags: G_SIGNAL_RUN_LAST
 * \endparblock
 *
 * \par state-changed
 * \parblock
 * \code
 * void
 * state_changed_callback (WpNode * self,
 *                         WpNodeState old_state,
 *                         WpNodeState new_state,
 *                         gpointer user_data)
 * \endcode
 *
 * Emitted when the node changes state. This is only emitted when
 * WP_PIPEWIRE_OBJECT_FEATURE_INFO is enabled.
 *
 * Parameters:
 * - `old_state` - the old state
 * - `new_state` - the new state
 *
 * Flags: G_SIGNAL_RUN_LAST
 * \endparblock
 */


enum {
  PROP_STATE = WP_PW_OBJECT_MIXIN_PROP_CUSTOM_START,
  PROP_N_INPUT_PORTS,
  PROP_N_OUTPUT_PORTS,
  PROP_MAX_INPUT_PORTS,
  PROP_MAX_OUTPUT_PORTS,
};

enum {
  SIGNAL_STATE_CHANGED,
  SIGNAL_PORTS_CHANGED,
  N_SIGNALS,
};

static guint32 signals[N_SIGNALS] = {0};

struct _WpNode
{
  WpGlobalProxy parent;
  WpObjectManager *ports_om;
};

static void wp_node_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface);

G_DEFINE_TYPE_WITH_CODE (WpNode, wp_node, WP_TYPE_GLOBAL_PROXY,
    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
        wp_pw_object_mixin_object_interface_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
        wp_node_pw_object_mixin_priv_interface_init))

static void
wp_node_init (WpNode * self)
{
}

static void
wp_node_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);

  switch (property_id) {
  case PROP_STATE:
    g_value_set_enum (value, d->info ?
        ((struct pw_node_info *) d->info)->state : 0);
    break;
  case PROP_N_INPUT_PORTS:
    g_value_set_uint (value, d->info ?
        ((struct pw_node_info *) d->info)->n_input_ports : 0);
    break;
  case PROP_N_OUTPUT_PORTS:
    g_value_set_uint (value, d->info ?
        ((struct pw_node_info *) d->info)->n_output_ports : 0);
    break;
  case PROP_MAX_INPUT_PORTS:
    g_value_set_uint (value, d->info ?
        ((struct pw_node_info *) d->info)->max_input_ports : 0);
    break;
  case PROP_MAX_OUTPUT_PORTS:
    g_value_set_uint (value, d->info ?
        ((struct pw_node_info *) d->info)->max_output_ports : 0);
    break;
  default:
    wp_pw_object_mixin_get_property (object, property_id, value, pspec);
    break;
  }
}

static void
wp_node_on_ports_om_installed (WpObjectManager *ports_om, WpNode * self)
{
  wp_object_update_features (WP_OBJECT (self), WP_NODE_FEATURE_PORTS, 0);
}

static void
wp_node_emit_ports_changed (WpObjectManager *ports_om, WpNode * self)
{
  g_signal_emit (self, signals[SIGNAL_PORTS_CHANGED], 0);
}

static void
wp_node_enable_feature_ports (WpNode * self)
{
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  guint32 bound_id = wp_proxy_get_bound_id (WP_PROXY (self));

  wp_debug_object (self, "enabling WP_NODE_FEATURE_PORTS, bound_id:%u",
      bound_id);

  self->ports_om = wp_object_manager_new ();
  wp_object_manager_add_interest (self->ports_om,
      WP_TYPE_PORT,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, PW_KEY_NODE_ID, "=u", bound_id,
      NULL);
  wp_object_manager_request_object_features (self->ports_om,
      WP_TYPE_PORT, WP_OBJECT_FEATURES_ALL);

  g_signal_connect_object (self->ports_om, "installed",
      G_CALLBACK (wp_node_on_ports_om_installed), self, 0);
  g_signal_connect_object (self->ports_om, "objects-changed",
      G_CALLBACK (wp_node_emit_ports_changed), self, 0);

  wp_core_install_object_manager (core, self->ports_om);
}

static WpObjectFeatures
wp_node_get_supported_features (WpObject * object)
{
  return wp_pw_object_mixin_get_supported_features (object)
      | WP_NODE_FEATURE_PORTS;
}

enum {
  STEP_PORTS = WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START,
};

static void
wp_node_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case WP_PW_OBJECT_MIXIN_STEP_BIND:
  case WP_TRANSITION_STEP_ERROR:
    /* base class can handle BIND and ERROR */
    WP_OBJECT_CLASS (wp_node_parent_class)->
        activate_execute_step (object, transition, step, missing);
    break;
  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
    /* just wait, info will be emitted anyway after binding */
    break;
  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
    wp_pw_object_mixin_cache_params (object, missing);
    break;
  case STEP_PORTS:
    wp_node_enable_feature_ports (WP_NODE (object));
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
wp_node_deactivate (WpObject * object, WpObjectFeatures features)
{
  wp_pw_object_mixin_deactivate (object, features);

  if (features & WP_NODE_FEATURE_PORTS) {
    WpNode *self = WP_NODE (object);
    g_clear_object (&self->ports_om);
    wp_object_update_features (object, 0, WP_NODE_FEATURE_PORTS);
  }

  WP_OBJECT_CLASS (wp_node_parent_class)->deactivate (object, features);
}

static const struct pw_node_events node_events = {
  PW_VERSION_NODE_EVENTS,
  .info = (HandleEventInfoFunc(node)) wp_pw_object_mixin_handle_event_info,
  .param = wp_pw_object_mixin_handle_event_param,
};

static void
wp_node_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
      node, &node_events);
}

static void
wp_node_pw_proxy_destroyed (WpProxy * proxy)
{
  WpNode *self = WP_NODE (proxy);

  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);

  g_clear_object (&self->ports_om);
  wp_object_update_features (WP_OBJECT (self), 0, WP_NODE_FEATURE_PORTS);

  WP_PROXY_CLASS (wp_node_parent_class)->pw_proxy_destroyed (proxy);
}

static void
wp_node_class_init (WpNodeClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->get_property = wp_node_get_property;

  wpobject_class->get_supported_features = wp_node_get_supported_features;
  wpobject_class->activate_get_next_step =
      wp_pw_object_mixin_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_node_activate_execute_step;
  wpobject_class->deactivate = wp_node_deactivate;

  proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Node;
  proxy_class->pw_iface_version = PW_VERSION_NODE;
  proxy_class->pw_proxy_created = wp_node_pw_proxy_created;
  proxy_class->pw_proxy_destroyed = wp_node_pw_proxy_destroyed;

  wp_pw_object_mixin_class_override_properties (object_class);

  g_object_class_install_property (object_class, PROP_STATE,
      g_param_spec_enum ("state", "state", "state", WP_TYPE_NODE_STATE, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_N_INPUT_PORTS,
      g_param_spec_uint ("n-input-ports", "n-input-ports", "n-input-ports",
          0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_N_OUTPUT_PORTS,
      g_param_spec_uint ("n-output-ports", "n-output-ports", "n-output-ports",
          0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_MAX_INPUT_PORTS,
      g_param_spec_uint ("max-input-ports", "max-input-ports", "max-input-ports",
          0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_MAX_OUTPUT_PORTS,
      g_param_spec_uint ("max-output-ports", "max-output-ports", "max-output-ports",
          0, G_MAXUINT, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  signals[SIGNAL_STATE_CHANGED] = g_signal_new (
      "state-changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2,
      WP_TYPE_NODE_STATE, WP_TYPE_NODE_STATE);

  signals[SIGNAL_PORTS_CHANGED] = g_signal_new (
      "ports-changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
}

static void
wp_node_process_info (gpointer instance, gpointer old_info, gpointer i)
{
  const struct pw_node_info *info = i;

  if (info->change_mask & PW_NODE_CHANGE_MASK_STATE) {
    enum pw_node_state old_state = old_info ?
        ((struct pw_node_info *) old_info)->state : PW_NODE_STATE_CREATING;
    g_signal_emit (instance, signals[SIGNAL_STATE_CHANGED], 0,
        old_state, info->state);
  }
  if (info->change_mask & PW_NODE_CHANGE_MASK_INPUT_PORTS) {
    g_object_notify (G_OBJECT (instance), "n-input-ports");
    g_object_notify (G_OBJECT (instance), "max-input-ports");
  }
  if (info->change_mask & PW_NODE_CHANGE_MASK_OUTPUT_PORTS) {
    g_object_notify (G_OBJECT (instance), "n-output-ports");
    g_object_notify (G_OBJECT (instance), "max-output-ports");
  }
}

static gint
wp_node_enum_params (gpointer instance, guint32 id,
    guint32 start, guint32 num, WpSpaPod *filter)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  return pw_node_enum_params (d->iface, 0, id, start, num,
      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
}

static gint
wp_node_set_param (gpointer instance, guint32 id, guint32 flags,
    WpSpaPod * param)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  g_autoptr (WpSpaPod) p = param;
  return pw_node_set_param (d->iface, id, flags,
      wp_spa_pod_get_spa_pod (p));
}

static void
wp_node_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface)
{
  wp_pw_object_mixin_priv_interface_info_init (iface, node, NODE);
  iface->process_info = wp_node_process_info;
  iface->enum_params = wp_node_enum_params;
  iface->set_param = wp_node_set_param;
}

/*!
 * \brief Constructs a node on the PipeWire server by asking the remote factory
 * \a factory_name to create it.
 *
 * Because of the nature of the PipeWire protocol, this operation completes
 * asynchronously at some point in the future. In order to find out when
 * this is done, you should call wp_object_activate(), requesting at least
 * WP_PROXY_FEATURE_BOUND. When this feature is ready, the node is ready for
 * use on the server. If the node cannot be created, this activation operation
 * will fail.
 *
 * \ingroup wpnode
 * \param core the wireplumber core
 * \param factory_name the pipewire factory name to construct the node
 * \param properties (nullable) (transfer full): the properties to pass to the
 *   factory
 * \returns (nullable) (transfer full): the new node or NULL if the core
 *   is not connected and therefore the node cannot be created
 */
WpNode *
wp_node_new_from_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties)
{
  g_autoptr (WpProperties) props = properties;
  return g_object_new (WP_TYPE_NODE,
      "core", core,
      "factory-name", factory_name,
      "global-properties", props,
      NULL);
}

/*!
 * \brief Gets the current state of the node
 * \ingroup wpnode
 * \param self the node
 * \param error (out) (optional) (transfer none): the error
 * \returns the current state of the node
 */
WpNodeState
wp_node_get_state (WpNode * self, const gchar ** error)
{
  g_return_val_if_fail (WP_IS_NODE (self), WP_NODE_STATE_ERROR);
  g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (self),
          WP_PIPEWIRE_OBJECT_FEATURE_INFO), WP_NODE_STATE_ERROR);

  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
  const struct pw_node_info *info = d->info;

  if (error)
    *error = info->error;
  return (WpNodeState) info->state;
}

/*!
 * \brief Gets the number of input ports of this node
 *
 * \remarks Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 *
 * \ingroup wpnode
 * \param self the node
 * \param max (out) (optional): the maximum supported number of input ports
 * \returns the number of input ports of this node, as reported by the node info
 */
guint
wp_node_get_n_input_ports (WpNode * self, guint * max)
{
  g_return_val_if_fail (WP_IS_NODE (self), 0);
  g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (self),
          WP_PIPEWIRE_OBJECT_FEATURE_INFO), 0);

  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
  const struct pw_node_info *info = d->info;

  if (max)
    *max = info->max_input_ports;
  return info->n_input_ports;
}

/*!
 * \brief Gets the number of output ports of this node
 *
 * \remarks Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 *
 * \ingroup wpnode
 * \param self the node
 * \param max (out) (optional): the maximum supported number of output ports
 * \returns the number of output ports of this node, as reported by the node info
 */
guint
wp_node_get_n_output_ports (WpNode * self, guint * max)
{
  g_return_val_if_fail (WP_IS_NODE (self), 0);
  g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (self),
          WP_PIPEWIRE_OBJECT_FEATURE_INFO), 0);

  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
  const struct pw_node_info *info = d->info;

  if (max)
    *max = info->max_output_ports;
  return info->n_output_ports;
}

/*!
 * \brief Gets the number of ports of this node
 *
 * Note that this number may not add up to
 * wp_node_get_n_input_ports() + wp_node_get_n_output_ports()
 * because it is discovered by looking at the number of available ports
 * in the registry, however ports may appear there with a delay or may
 * not appear at all if this client does not have permission to read them
 *
 * \remarks Requires WP_NODE_FEATURE_PORTS
 *
 * \ingroup wpnode
 * \param self the node
 * \returns the number of ports of this node.
 */
guint
wp_node_get_n_ports (WpNode * self)
{
  g_return_val_if_fail (WP_IS_NODE (self), 0);
  g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (self),
          WP_NODE_FEATURE_PORTS), 0);

  return wp_object_manager_get_n_objects (self->ports_om);
}

/*!
 * \brief Gets a new iterator that iterates over all the ports that belong
 * to this node
 *
 * \remarks Requires WP_NODE_FEATURE_PORTS
 *
 * \ingroup wpnode
 * \param self the node
 * \returns (transfer full): a WpIterator that iterates over WpPort objects
 */
WpIterator *
wp_node_new_ports_iterator (WpNode * self)
{
  g_return_val_if_fail (WP_IS_NODE (self), NULL);
  g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (self),
          WP_NODE_FEATURE_PORTS), NULL);

  return wp_object_manager_new_iterator (self->ports_om);
}

/*!
 * \brief Gets a new iterator that iterates over all
 * the ports that belong to this node and match the constraints
 *
 * The constraints specified in the variable arguments must follow the rules
 * documented in wp_object_interest_new().
 *
 * \remarks Requires WP_NODE_FEATURE_PORTS
 *
 * \ingroup wpnode
 * \param self the node
 * \param ... a list of constraints, terminated by NULL
 * \returns (transfer full): a WpIterator that iterates over WpPort objects
 */
WpIterator *
wp_node_new_ports_filtered_iterator (WpNode * self, ...)
{
  WpObjectInterest *interest;
  va_list args;
  va_start (args, self);
  interest = wp_object_interest_new_valist (WP_TYPE_PORT, &args);
  va_end (args);
  return wp_node_new_ports_filtered_iterator_full (self, interest);
}

/*!
 * \brief Gets a new iterator that iterates over all
 * the ports that belong to this node and match the \a interest
 *
 * \remarks Requires WP_NODE_FEATURE_PORTS
 *
 * \ingroup wpnode
 * \param self the node
 * \param interest (transfer full): the interest
 * \returns (transfer full): a WpIterator that iterates over WpPort objects
 */
WpIterator *
wp_node_new_ports_filtered_iterator_full (WpNode * self,
    WpObjectInterest * interest)
{
  g_return_val_if_fail (WP_IS_NODE (self), NULL);
  g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (self),
          WP_NODE_FEATURE_PORTS), NULL);

  return wp_object_manager_new_filtered_iterator_full (self->ports_om,
      interest);
}

/*!
 * \brief Retrieves the first port that matches the constraints
 *
 * The constraints specified in the variable arguments must follow the rules
 * documented in wp_object_interest_new().
 *
 * \remarks Requires WP_NODE_FEATURE_PORTS
 *
 * \ingroup wpnode
 * \param self the node
 * \param ... a list of constraints, terminated by NULL
 * \returns (transfer full) (nullable): the first port that matches the
 *    constraints, or NULL if there is no such port
 */
WpPort *
wp_node_lookup_port (WpNode * self, ...)
{
  WpObjectInterest *interest;
  va_list args;
  va_start (args, self);
  interest = wp_object_interest_new_valist (WP_TYPE_PORT, &args);
  va_end (args);
  return wp_node_lookup_port_full (self, interest);
}

/*!
 * \brief Retrieves the first port that matches the \a interest
 *
 * \remarks Requires WP_NODE_FEATURE_PORTS
 *
 * \ingroup wpnode
 * \param self the node
 * \param interest (transfer full): the interest
 * \returns (transfer full) (nullable): the first port that matches the
 *    \a interest, or NULL if there is no such port
 */
WpPort *
wp_node_lookup_port_full (WpNode * self, WpObjectInterest * interest)
{
  g_return_val_if_fail (WP_IS_NODE (self), NULL);
  g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (self),
          WP_NODE_FEATURE_PORTS), NULL);

  return (WpPort *)
      wp_object_manager_lookup_full (self->ports_om, interest);
}

/*!
 * \brief Sends a command to a node
 *
 * Valid commands are the short string reprepsentations of
 * `enum spa_node_command`. For example, "Suspend" or "Flush" are valid commands
 *
 * \ingroup wpnode
 * \param self the node
 * \param command the command
 */
void
wp_node_send_command (WpNode * self, const gchar * command)
{
  WpSpaIdValue command_value = wp_spa_id_value_from_short_name (
      "Spa:Pod:Object:Command:Node", command);

  g_return_if_fail (WP_IS_NODE (self));
  g_return_if_fail (command_value != NULL);
  g_return_if_fail (wp_object_test_active_features (WP_OBJECT (self),
      WP_PROXY_FEATURE_BOUND));

  struct spa_command cmd =
      SPA_NODE_COMMAND_INIT(wp_spa_id_value_number (command_value));
  pw_node_send_command (wp_proxy_get_pw_proxy (WP_PROXY (self)), &cmd);
}

/*! \defgroup wpimplnode WpImplNode */

enum {
  PROP_PW_IMPL_NODE = WP_PW_OBJECT_MIXIN_PROP_CUSTOM_START,
};

struct _WpImplNode
{
  WpProxy parent;
  GWeakRef core;
  struct pw_impl_node *pw_impl_node;
};

static void wp_impl_node_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface);

G_DEFINE_TYPE_WITH_CODE (WpImplNode, wp_impl_node, WP_TYPE_PROXY,
    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
        wp_pw_object_mixin_object_interface_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
        wp_impl_node_pw_object_mixin_priv_interface_init))

static void
wp_impl_node_init (WpImplNode * self)
{
}

static void
wp_impl_node_constructed (GObject * object)
{
  WpImplNode * self = WP_IMPL_NODE (object);
  WpPwObjectMixinData * data = wp_pw_object_mixin_get_data (self);

  data->info = (gpointer) pw_impl_node_get_info (self->pw_impl_node);
  data->iface = pw_impl_node_get_implementation (self->pw_impl_node);

  /* TODO handle the actual node properties */
  data->properties = wp_properties_new_empty();

  WpObjectFeatures ft =
      wp_pw_object_mixin_get_supported_features (WP_OBJECT (self))
      & ~WP_PROXY_FEATURE_BOUND;
  wp_object_update_features (WP_OBJECT (self), ft, 0);

  G_OBJECT_CLASS (wp_impl_node_parent_class)->constructed (object);
}

static void
wp_impl_node_dispose (GObject * object)
{
  WpImplNode *self = WP_IMPL_NODE (object);

  WpObjectFeatures ft =
      wp_pw_object_mixin_get_supported_features (WP_OBJECT (self))
      & ~WP_PROXY_FEATURE_BOUND;
  wp_object_update_features (WP_OBJECT (self), 0, ft);

  G_OBJECT_CLASS (wp_impl_node_parent_class)->dispose (object);
}

static void
wp_impl_node_finalize (GObject * object)
{
  WpImplNode *self = WP_IMPL_NODE (object);

  g_clear_pointer (&self->pw_impl_node, pw_impl_node_destroy);

  G_OBJECT_CLASS (wp_impl_node_parent_class)->finalize (object);
}

static void
wp_impl_node_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpImplNode *self = WP_IMPL_NODE (object);

  switch (property_id) {
  case PROP_PW_IMPL_NODE:
    self->pw_impl_node = g_value_get_pointer (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_impl_node_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpImplNode *self = WP_IMPL_NODE (object);

  switch (property_id) {
  case PROP_PW_IMPL_NODE:
    g_value_set_pointer (value, self->pw_impl_node);
    break;
  default:
    wp_pw_object_mixin_get_property (object, property_id, value, pspec);
    break;
  }
}

enum {
  STEP_EXPORT = WP_TRANSITION_STEP_CUSTOM_START,
};

static guint
wp_impl_node_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  /* BOUND is the only feature that can be in @em missing */
  g_return_val_if_fail (missing == WP_PROXY_FEATURE_BOUND,
      WP_TRANSITION_STEP_ERROR);

  return STEP_EXPORT;
}

static void
wp_impl_node_activate_execute_step (WpObject * object,
      WpFeatureActivationTransition * transition, guint step,
      WpObjectFeatures missing)
{
  WpImplNode *self = WP_IMPL_NODE (object);

  switch (step) {
  case STEP_EXPORT: {
    g_autoptr (WpCore) core = wp_object_get_core (object);
    struct pw_core *pw_core = wp_core_get_pw_core (core);
    g_return_if_fail (pw_core);

    wp_proxy_set_pw_proxy (WP_PROXY (self),
        pw_core_export (pw_core, PW_TYPE_INTERFACE_Node, NULL,
            self->pw_impl_node, 0));
    break;
  }
  default:
    g_assert_not_reached ();
  }
}

/*!
 * \struct WpImplNode
 *
 * A WpImplNode allows running a node implementation (`struct pw_impl_node`)
 * locally, loading the implementation from factory or wrapping a manually
 * constructed `pw_impl_node`. This object can then be exported to PipeWire
 * by requesting WP_PROXY_FEATURE_BOUND.
 *
 * \gproperties
 *
 * \gproperty{pw-impl-node, gpointer, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The actual node implementation\, `struct pw_impl_node *`}
 */
static void
wp_impl_node_class_init (WpImplNodeClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;

  object_class->constructed = wp_impl_node_constructed;
  object_class->dispose = wp_impl_node_dispose;
  object_class->finalize = wp_impl_node_finalize;
  object_class->set_property = wp_impl_node_set_property;
  object_class->get_property = wp_impl_node_get_property;

  wpobject_class->get_supported_features =
      wp_pw_object_mixin_get_supported_features;
  wpobject_class->activate_get_next_step = wp_impl_node_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_impl_node_activate_execute_step;

  wp_pw_object_mixin_class_override_properties (object_class);

  g_object_class_install_property (object_class, PROP_PW_IMPL_NODE,
      g_param_spec_pointer ("pw-impl-node", "pw-impl-node",
          "The actual node implementation, struct pw_impl_node *",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

static int
impl_node_collect_params (void *data, int seq,
    uint32_t id, uint32_t index, uint32_t next, struct spa_pod *param)
{
  GPtrArray *result = data;
  g_ptr_array_add (result, wp_spa_pod_new_wrap_const (param));
  return 0;
}

static GPtrArray *
wp_impl_node_enum_params_sync (gpointer instance, guint32 id,
      guint32 start, guint32 num, WpSpaPod *filter)
{
  WpImplNode *self = WP_IMPL_NODE (instance);
  GPtrArray *result =
      g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);

  pw_impl_node_for_each_param (self->pw_impl_node, 1, id, start, num,
      filter ? wp_spa_pod_get_spa_pod (filter) : NULL,
      impl_node_collect_params, result);
  return result;
}

static gint
wp_impl_node_set_param (gpointer instance, guint32 id, guint32 flags,
    WpSpaPod * param)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  g_autoptr (WpSpaPod) p = param;
  return spa_node_set_param (d->iface, id, flags,
      wp_spa_pod_get_spa_pod (p));
}

static void
wp_impl_node_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface)
{
  wp_pw_object_mixin_priv_interface_info_init (iface, node, NODE);
  iface->flags = WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE;
  iface->enum_params_sync = wp_impl_node_enum_params_sync;
  iface->set_param = wp_impl_node_set_param;
}

/*!
 * \brief Constructs a node object from an existing `pw_impl_node`.
 * \ingroup wpimplnode
 * \param core the wireplumber core
 * \param node an existing `pw_impl_node` to wrap
 * \returns (transfer full): A new WpImplNode wrapping \a node
 */
WpImplNode *
wp_impl_node_new_wrap (WpCore * core, struct pw_impl_node * node)
{
  return g_object_new (WP_TYPE_IMPL_NODE,
      "core", core,
      "pw-impl-node", node,
      NULL);
}

/*!
 * \brief Constructs a new node, locally on this process, using the specified
 * \a factory_name.
 *
 * To export this node to the PipeWire server, you need to call
 * wp_object_activate() requesting WP_PROXY_FEATURE_BOUND and
 * wait for the operation to complete.
 *
 * \ingroup wpimplnode
 * \param core the wireplumber core
 * \param factory_name the name of the pipewire factory
 * \param properties (nullable) (transfer full): properties to be passed to node
 *    constructor
 * \returns (nullable) (transfer full): A new WpImplNode wrapping the
 *   node that was constructed by the factory, or %NULL if the factory
 *   does not exist or was unable to construct the node
 */
WpImplNode *
wp_impl_node_new_from_pw_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties)
{
  g_autoptr (WpProperties) props = properties;
  struct pw_context *pw_context = wp_core_get_pw_context (core);
  struct pw_impl_factory *factory = NULL;
  struct pw_impl_node *node = NULL;

  g_return_val_if_fail (pw_context != NULL, NULL);

  factory = pw_context_find_factory (pw_context, factory_name);
  if (!factory) {
    wp_warning ("pipewire factory '%s' not found", factory_name);
    return NULL;
  }

  node = pw_impl_factory_create_object (factory,
      NULL, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE,
      props ? wp_properties_to_pw_properties (props) : NULL, 0);
  if (!node) {
    wp_warning ("failed to create node from factory '%s'", factory_name);
    return NULL;
  }

  return wp_impl_node_new_wrap (core, node);
}
  0707010000009D000081A400000000000000000000000165F8630400000A55000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/node.h   /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_NODE_H__
#define __WIREPLUMBER_NODE_H__

#include "global-proxy.h"
#include "port.h"
#include "iterator.h"
#include "object-interest.h"

G_BEGIN_DECLS

struct pw_impl_node;

/*!
 * \brief The state of the node
 * \ingroup wpnode
 */
typedef enum {
  /*! error state */
  WP_NODE_STATE_ERROR = -1,
  /*! the node is being created */
  WP_NODE_STATE_CREATING = 0,
  /*! the node is suspended, the device might be closed */
  WP_NODE_STATE_SUSPENDED = 1,
  /*! the node is running but there is no active port */
  WP_NODE_STATE_IDLE = 2,
  /*! the node is running */
  WP_NODE_STATE_RUNNING = 3,
} WpNodeState;

/*!
 * \brief An extension of WpProxyFeatures
 * \ingroup wpnode
 */
typedef enum { /*< flags >*/
  /*! caches information about ports, enabling
   * the use of wp_node_get_n_ports(), wp_node_lookup_port(),
   * wp_node_new_ports_iterator() and related methods */
  WP_NODE_FEATURE_PORTS = (WP_PROXY_FEATURE_CUSTOM_START << 0),
} WpNodeFeatures;

/*!
 * \brief The WpNode GType
 * \ingroup wpnode
 */
#define WP_TYPE_NODE (wp_node_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpNode, wp_node, WP, NODE, WpGlobalProxy)

WP_API
WpNode * wp_node_new_from_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties);

WP_API
WpNodeState wp_node_get_state (WpNode * self, const gchar ** error);

WP_API
guint wp_node_get_n_input_ports (WpNode * self, guint * max);

WP_API
guint wp_node_get_n_output_ports (WpNode * self, guint * max);

WP_API
guint wp_node_get_n_ports (WpNode * self);

WP_API
WpIterator * wp_node_new_ports_iterator (WpNode * self);

WP_API
WpIterator * wp_node_new_ports_filtered_iterator (WpNode * self, ...)
    G_GNUC_NULL_TERMINATED;

WP_API
WpIterator * wp_node_new_ports_filtered_iterator_full (WpNode * self,
    WpObjectInterest * interest);

WP_API
WpPort * wp_node_lookup_port (WpNode * self, ...) G_GNUC_NULL_TERMINATED;

WP_API
WpPort * wp_node_lookup_port_full (WpNode * self, WpObjectInterest * interest);

WP_API
void wp_node_send_command (WpNode * self, const gchar *command);

/*!
 * \brief The WpImplNode GType
 * \ingroup wpimplnode
 */
#define WP_TYPE_IMPL_NODE (wp_impl_node_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpImplNode, wp_impl_node, WP, IMPL_NODE, WpProxy)

WP_API
WpImplNode * wp_impl_node_new_wrap (WpCore * core, struct pw_impl_node * node);

WP_API
WpImplNode * wp_impl_node_new_from_pw_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties);

G_END_DECLS

#endif
   0707010000009E000081A400000000000000000000000165F8630400007BA9000000000000000000000000000000000000002B00000000wireplumber-0.5.0/lib/wp/object-interest.c    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "object-interest.h"
#include "global-proxy.h"
#include "session-item.h"
#include "proxy-interfaces.h"
#include "event-dispatcher.h"
#include "log.h"
#include "error.h"

#include <pipewire/pipewire.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-object-interest")

/*! \defgroup wpobjectinterest WpObjectInterest */
/*!
 * \struct WpObjectInterest
 *
 * An object interest is a helper that is used in WpObjectManager to
 * declare interest in certain kinds of objects.
 *
 * An interest is defined by a GType and a set of constraints on the object's
 * properties. An object "matches" the interest if it is of the specified
 * GType (either the same type or a descendant of it) and all the constraints
 * are satisfied.
 */

struct constraint
{
  WpConstraintType type;
  WpConstraintVerb verb;
  gchar subject_type; /* a basic GVariantType as a single char */
  gchar *subject;
  GVariant *value;
};

struct _WpObjectInterest
{
  grefcount ref;
  gboolean valid;
  GType gtype;
  struct pw_array constraints;
};

G_DEFINE_BOXED_TYPE (WpObjectInterest, wp_object_interest,
                     wp_object_interest_ref, wp_object_interest_unref)

/*!
 * \brief Creates a new interest that declares interest in objects of the specified
 * \a gtype, with the constraints specified in the variable arguments.
 *
 * The variable arguments should be a list of constraints terminated with NULL,
 * where each constraint consists of the following arguments:
 *  - a `WpConstraintType`: the constraint type
 *  - a `const gchar *`: the subject name
 *  - a `const gchar *`: the format string
 *  - 0 or more arguments according to the format string
 *
 * The format string is interpreted as follows:
 *  - the first character is the constraint verb:
 *     - `=`: WP_CONSTRAINT_VERB_EQUALS
 *     - `!`: WP_CONSTRAINT_VERB_NOT_EQUALS
 *     - `c`: WP_CONSTRAINT_VERB_IN_LIST
 *     - `~`: WP_CONSTRAINT_VERB_IN_RANGE
 *     - `#`: WP_CONSTRAINT_VERB_MATCHES
 *     - `+`: WP_CONSTRAINT_VERB_IS_PRESENT
 *     - `-`: WP_CONSTRAINT_VERB_IS_ABSENT
 *  - the rest of the characters are interpreted as a GVariant format string,
 *    as it would be used in g_variant_new()
 *
 * The rest of this function's arguments up to the start of the next constraint
 * depend on the GVariant format part of the format string and are used to
 * construct a GVariant for the constraint's value argument.
 *
 * For further reading on the constraint's arguments, see
 * wp_object_interest_add_constraint()
 *
 * For example, this interest matches objects that are descendands of WpProxy
 * with a "bound-id" between 0 and 100 (inclusive), with a pipewire property
 * called "format.dsp" that contains the string "audio" somewhere in the value
 * and with a pipewire property "port.name" being present (with any value):
 * \code
 * interest = wp_object_interest_new (WP_TYPE_PROXY,
 *     WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "~(uu)", 0, 100,
 *     WP_CONSTRAINT_TYPE_PW_PROPERTY, "format.dsp", "#s", "*audio*",
 *     WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.name", "+",
 *     NULL);
 * \endcode
 *
 * \ingroup wpobjectinterest
 * \param gtype the type of the object to declare interest in
 * \param ... a set of constraints, terminated with NULL
 * \returns (transfer full): the new object interest
 */
WpObjectInterest *
wp_object_interest_new (GType gtype, ...)
{
  WpObjectInterest *self;
  va_list args;
  va_start (args, gtype);
  self = wp_object_interest_new_valist (gtype, &args);
  va_end (args);
  return self;
}

/*!
 * \brief va_list version of wp_object_interest_new()
 *
 * \ingroup wpobjectinterest
 * \param gtype the type of the object to declare interest in
 * \param args pointer to va_list containing the constraints
 * \returns (transfer full): the new object interest
 */
WpObjectInterest *
wp_object_interest_new_valist (GType gtype, va_list *args)
{
  WpObjectInterest *self = wp_object_interest_new_type (gtype);
  WpConstraintType type;

  g_return_val_if_fail (self != NULL, NULL);

  for (type = va_arg (*args, WpConstraintType);
       type != WP_CONSTRAINT_TYPE_NONE;
       type = va_arg (*args, WpConstraintType))
  {
    const gchar *subject, *format;
    WpConstraintVerb verb = 0;
    GVariant *value = NULL;

    subject = va_arg (*args, const gchar *);
    g_return_val_if_fail (subject != NULL, NULL);

    format = va_arg (*args, const gchar *);
    g_return_val_if_fail (format != NULL, NULL);

    verb = format[0];
    if (verb != 0 && format[1] != '\0')
      value = g_variant_new_va (format + 1, NULL, args);

    wp_object_interest_add_constraint (self, type, subject, verb, value);
  }
  return self;
}

/*!
 * \brief Creates a new interest that declares interest in objects of the
 * specified \a gtype, without any property constraints.
 *
 * To add property constraints, you can call wp_object_interest_add_constraint()
 * afterwards.
 *
 * \ingroup wpobjectinterest
 * \param gtype the type of the object to declare interest in
 * \returns (transfer full): the new object interest
 */
WpObjectInterest *
wp_object_interest_new_type (GType gtype)
{
  WpObjectInterest *self = g_slice_new0 (WpObjectInterest);
  g_return_val_if_fail (self != NULL, NULL);
  g_ref_count_init (&self->ref);
  self->gtype = gtype;
  pw_array_init (&self->constraints, sizeof (struct constraint));
  return self;
}

/*!
 * \brief Adds a constraint to this interest. Constraints consist of a \a type,
 * a \a subject, a \a verb and, depending on the \a verb, a \a value.
 *
 * Constraints are almost like a spoken language sentence that declare a
 * condition that must be true in order to consider that an object can match
 * this interest. For instance, a constraint can be "pipewire property
 * 'object.id' equals 10". This would be translated to:
 * \code
 * wp_object_interest_add_constraint (i,
 *    WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id",
 *    WP_CONSTRAINT_VERB_EQUALS, g_variant_new_int (10));
 * \endcode
 *
 * Some verbs require a \a value and some others do not. For those that do,
 * the \a value must be of a specific type:
 *  - WP_CONSTRAINT_VERB_EQUALS: \a value can be a string, a (u)int32,
 *    a (u)int64, a double or a boolean. The \a subject value must equal this
 *    value for the constraint to be satisfied
 *  - WP_CONSTRAINT_VERB_IN_LIST: \a value must be a tuple that contains any
 *    number of items of the same type; the items can be string, (u)int32,
 *    (u)int64 or double. These items make a list that the \a subject's value
 *    will be checked against. If any of the items equals the \a subject value,
 *    the constraint is satisfied
 *  - WP_CONSTRAINT_VERB_IN_RANGE: \a value must be a tuple that contains exactly
 *    2 numbers of the same type ((u)int32, (u)int64 or double), meaning the
 *    minimum and maximum (inclusive) of the range. If the \a subject value is a
 *    number within this range, the constraint is satisfied
 *  - WP_CONSTRAINT_VERB_MATCHES: \a value must be a string that defines a
 *    pattern usable with GPatternSpec If the \a subject value matches this
 *    pattern, the constraint is satisfied
 *
 * In case the type of the \a subject value is not the same type as the one
 * requested by the type of the \a value, the \a subject value is converted.
 * For GObject properties, this conversion is done using g_value_transform(),
 * so limitations of this function apply. In the case of PipeWire properties,
 * which are *always* strings, conversion is done as follows:
 *  - to boolean: `"true"` or `"1"` means TRUE, `"false"` or `"0"` means FALSE
 *  - to int / uint / int64 / uint64: One of the `strtol()` family of functions
 *    is used to convert, using base 10
 *  - to double: `strtod()` is used
 *
 * This method does not fail if invalid arguments are given. However,
 * wp_object_interest_validate() should be called after adding all the
 * constraints on an interest in order to catch errors.
 *
 * \ingroup wpobjectinterest
 * \param self the object interest
 * \param type the constraint type
 * \param subject the subject that the constraint applies to
 * \param verb the operation that is performed to check the constraint
 * \param value (transfer floating)(nullable): the value to check for
 */
void
wp_object_interest_add_constraint (WpObjectInterest * self,
    WpConstraintType type, const gchar * subject,
    WpConstraintVerb verb, GVariant * value)
{
  struct constraint *c;

  g_return_if_fail (self != NULL);

  c = pw_array_add (&self->constraints, sizeof (struct constraint));
  g_return_if_fail (c != NULL);
  c->type = type;
  c->verb = verb;
  /* subject_type is filled in by _validate() */
  c->subject_type = '\0';
  c->subject = g_strdup (subject);
  c->value = value ? g_variant_ref_sink (value) : NULL;

  /* mark as invalid to force validation */
  self->valid = FALSE;
}

/*!
 * \brief Increases the reference count of an object interest
 * \ingroup wpobjectinterest
 * \param self the object interest to ref
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpObjectInterest *
wp_object_interest_ref (WpObjectInterest *self)
{
  g_ref_count_inc (&self->ref);
  return self;
}

static void
wp_object_interest_free (WpObjectInterest * self)
{
  struct constraint *c;

  g_return_if_fail (self != NULL);

  pw_array_for_each (c, &self->constraints) {
    g_clear_pointer (&c->subject, g_free);
    g_clear_pointer (&c->value, g_variant_unref);
  }
  pw_array_clear (&self->constraints);
  g_slice_free (WpObjectInterest, self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 *
 * \ingroup wpobjectinterest
 * \param self (transfer full): the object interest to unref
 */
void
wp_object_interest_unref (WpObjectInterest * self)
{
  if (g_ref_count_dec (&self->ref))
    wp_object_interest_free (self);
}

/*!
 * \brief Validates the interest, ensuring that the interest GType
 * is a valid object and that all the constraints have been expressed properly.
 *
 * \remark This is called internally when \a self is first used to find a match,
 * so it is not necessary to call it explicitly
 *
 * \ingroup wpobjectinterest
 * \param self the object interest to validate
 * \param error (out) (optional): the error, in case validation failed
 * \returns TRUE if the interest is valid and can be used in a match,
 *   FALSE otherwise
 */
gboolean
wp_object_interest_validate (WpObjectInterest * self, GError ** error)
{
  struct constraint *c;
  gboolean is_props;

  g_return_val_if_fail (self != NULL, FALSE);

  /* if already validated, we are done */
  if (self->valid)
    return TRUE;

  if (!G_TYPE_IS_OBJECT (self->gtype) && !G_TYPE_IS_INTERFACE (self->gtype) &&
          !g_type_is_a (self->gtype, WP_TYPE_PROPERTIES) &&
          !g_type_is_a (self->gtype, WP_TYPE_EVENT)) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
        "type '%s' is not a valid interest type", g_type_name (self->gtype));
    return FALSE;
  }

  is_props = g_type_is_a (self->gtype, WP_TYPE_PROPERTIES);

  pw_array_for_each (c, &self->constraints) {
    const GVariantType *value_type = NULL;

    if (c->type <= WP_CONSTRAINT_TYPE_NONE ||
        c->type > WP_CONSTRAINT_TYPE_G_PROPERTY) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
          "invalid constraint type %d", c->type);
      return FALSE;
    }

    if (is_props && c->type == WP_CONSTRAINT_TYPE_G_PROPERTY) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
          "constraint type %d cannot apply to type '%s'",
          c->type, g_type_name (self->gtype));
      return FALSE;
    }

    if (!c->subject) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
          "constraint subject cannot be NULL");
      return FALSE;
    }

    switch (c->verb) {
      case WP_CONSTRAINT_VERB_EQUALS:
      case WP_CONSTRAINT_VERB_NOT_EQUALS:
      case WP_CONSTRAINT_VERB_IN_LIST:
      case WP_CONSTRAINT_VERB_IN_RANGE:
      case WP_CONSTRAINT_VERB_MATCHES:
        if (!c->value) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "verb %d (%c) requires a value", c->verb, (gchar) c->verb);
          return FALSE;
        }
        value_type = g_variant_get_type (c->value);
        break;

      case WP_CONSTRAINT_VERB_IS_PRESENT:
      case WP_CONSTRAINT_VERB_IS_ABSENT:
        if (c->value) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "verb %d (%c) should not have a value", c->verb, (gchar) c->verb);
          return FALSE;
        }
        break;

      default:
        g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "invalid constraint verb %d (%c)", c->verb, (gchar) c->verb);
        return FALSE;
    }

    switch (c->verb) {
      case WP_CONSTRAINT_VERB_EQUALS:
      case WP_CONSTRAINT_VERB_NOT_EQUALS:
        if (!g_variant_type_equal (value_type, G_VARIANT_TYPE_STRING) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_BOOLEAN) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT32) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT32) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT64) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT64) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_DOUBLE)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "WP_CONSTRAINT_VERB_{NOT_,}EQUALS requires a basic GVariant type"
              " (actual type was '%s')", g_variant_get_type_string (c->value));
          return FALSE;
        }

        break;
      case WP_CONSTRAINT_VERB_IN_LIST: {
        const GVariantType *tuple_type;

        if (!g_variant_type_is_definite (value_type) ||
            !g_variant_type_is_tuple (value_type)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "WP_CONSTRAINT_VERB_IN_LIST requires a tuple GVariant type"
              " (actual type was '%s')", g_variant_get_type_string (c->value));
          return FALSE;
        }

        for (tuple_type = value_type = g_variant_type_first (value_type);
            tuple_type != NULL;
            tuple_type = g_variant_type_next (tuple_type)) {
          if (!g_variant_type_equal (tuple_type, value_type)) {
            g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
                "tuple must contain children of the same type"
                " (mismatching type was '%s' at '%.*s')",
                g_variant_get_type_string (c->value),
                (int) g_variant_type_get_string_length (tuple_type),
                g_variant_type_peek_string (tuple_type));
            return FALSE;
          }
        }

        if (!g_variant_type_equal (value_type, G_VARIANT_TYPE_STRING) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT32) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT32) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT64) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT64) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_DOUBLE)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "list tuple must contain string, (u)int32, (u)int64 or double"
              " (mismatching type was '%s' at '%.*s')",
              g_variant_get_type_string (c->value),
              (int) g_variant_type_get_string_length (value_type),
              g_variant_type_peek_string (value_type));
          return FALSE;
        }

        break;
      }
      case WP_CONSTRAINT_VERB_IN_RANGE: {
        const GVariantType *tuple_type;

        if (!g_variant_type_is_definite (value_type) ||
            !g_variant_type_is_tuple (value_type)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range requires a tuple GVariant type (actual type was '%s')",
              g_variant_get_type_string (c->value));
          return FALSE;
        }

        tuple_type = value_type = g_variant_type_first (value_type);
        if (!tuple_type) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range requires a non-empty tuple (actual type was '%s')",
              g_variant_get_type_string (c->value));
          return FALSE;
        }

        if (!g_variant_type_equal (tuple_type, G_VARIANT_TYPE_INT32) &&
            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_UINT32) &&
            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_INT64) &&
            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_UINT64) &&
            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_DOUBLE)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range tuple must contain (u)int32, (u)int64 or double"
              " (mismatching type was '%s' at '%.*s')",
              g_variant_get_type_string (c->value),
              (int) g_variant_type_get_string_length (tuple_type),
              g_variant_type_peek_string (tuple_type));
          return FALSE;
        }

        tuple_type = g_variant_type_next (tuple_type);
        if (!tuple_type || !g_variant_type_equal (tuple_type, value_type)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range tuple must contain 2 children of the same type"
              " (mismatching type was '%s' at '%.*s')",
              g_variant_get_type_string (c->value),
              (int) g_variant_type_get_string_length (tuple_type),
              g_variant_type_peek_string (tuple_type));
          return FALSE;
        }

        tuple_type = g_variant_type_next (tuple_type);
        if (tuple_type) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range tuple must contain exactly 2 children, not more"
              " (mismatching type was '%s')",
              g_variant_get_type_string (c->value));
          return FALSE;
        }

        break;
      }
      case WP_CONSTRAINT_VERB_MATCHES:
        if (!g_variant_type_equal (value_type, G_VARIANT_TYPE_STRING)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "WP_CONSTRAINT_VERB_MATCHES requires a string GVariant"
              " (actual type was '%s')", g_variant_get_type_string (c->value));
          return FALSE;
        }

        break;
      case WP_CONSTRAINT_VERB_IS_PRESENT:
      case WP_CONSTRAINT_VERB_IS_ABSENT:
        break;
      default:
        g_return_val_if_reached (FALSE);
    }

    /* cache the type that the property must have */
    if (value_type)
      c->subject_type = *g_variant_type_peek_string (value_type);
  }

  return (self->valid = TRUE);
}

G_GNUC_CONST static GType
subject_type_to_gtype (gchar type)
{
  switch (type) {
    case 'b': return G_TYPE_BOOLEAN;
    case 'i': return G_TYPE_INT;
    case 'u': return G_TYPE_UINT;
    case 'x': return G_TYPE_INT64;
    case 't': return G_TYPE_UINT64;
    case 'd': return G_TYPE_DOUBLE;
    case 's': return G_TYPE_STRING;
    default: g_return_val_if_reached (G_TYPE_INVALID);
  }
}

static inline gboolean
property_string_to_gvalue (gchar subj_type, const gchar * str, GValue * val)
{
  g_value_init (val, subject_type_to_gtype (subj_type));

  switch (subj_type) {
    case 'b':
      if (!strcmp (str, "true") || !strcmp (str, "1"))
        g_value_set_boolean (val, TRUE);
      else if (!strcmp (str, "false") || !strcmp (str, "0"))
        g_value_set_boolean (val, FALSE);
      else {
        wp_trace ("failed to convert '%s' to boolean", str);
        return FALSE;
      }
      break;
    case 's':
      g_value_set_static_string (val, str);
      break;

#define CASE_NUMBER(l, T, convert) \
    case l: { \
      g##T number; \
      errno = 0; \
      number = convert; \
      if (errno != 0) { \
        wp_trace ("failed to convert '%s' to " #T, str); \
        return FALSE; \
      } \
      g_value_set_##T (val, number); \
      break; \
    }
    CASE_NUMBER ('i', int, strtol (str, NULL, 10))
    CASE_NUMBER ('u', uint, strtoul (str, NULL, 10))
    CASE_NUMBER ('x', int64, strtoll (str, NULL, 10))
    CASE_NUMBER ('t', uint64, strtoull (str, NULL, 10))
    CASE_NUMBER ('d', double, strtod (str, NULL))
#undef CASE_NUMBER
    default:
      g_return_val_if_reached (FALSE);
  }
  return TRUE;
}

static inline gboolean
constraint_verb_equals (gchar subj_type, const GValue * subj_val,
    GVariant * check_val)
{
  switch (subj_type) {
    case 'd': {
      gdouble a = g_value_get_double (subj_val);
      gdouble b = g_variant_get_double (check_val);
      return G_APPROX_VALUE (a, b, FLT_EPSILON);
    }
    case 's':
      return !g_strcmp0 (g_value_get_string (subj_val),
                         g_variant_get_string (check_val, NULL));
#define CASE_BASIC(l, T, R) \
    case l: \
      return (g_value_get_##T (subj_val) == g_variant_get_##R (check_val));
    CASE_BASIC ('b', boolean, boolean)
    CASE_BASIC ('i', int, int32)
    CASE_BASIC ('u', uint, uint32)
    CASE_BASIC ('x', int64, int64)
    CASE_BASIC ('t', uint64, uint64)
#undef CASE_BASIC
    default:
      g_return_val_if_reached (FALSE);
  }
}

static inline gboolean
constraint_verb_matches (gchar subj_type, const GValue * subj_val,
    GVariant * check_val)
{
  switch (subj_type) {
    case 's': {
      const gchar *check_str = g_variant_get_string (check_val, NULL);
      const gchar *subj_str = g_value_get_string (subj_val);
      if (!check_str || !subj_str)
        return FALSE;
      return g_pattern_match_simple (check_str, subj_str);
    }
    default:
      g_return_val_if_reached (FALSE);
  }
  return TRUE;
}

static inline gboolean
constraint_verb_in_list (gchar subj_type, const GValue * subj_val,
    GVariant * check_val)
{
  GVariantIter iter;
  g_autoptr (GVariant) child = NULL;

  g_variant_iter_init (&iter, check_val);
  while ((child = g_variant_iter_next_value (&iter))) {
    if (constraint_verb_equals (subj_type, subj_val, child))
      return TRUE;
    g_variant_unref (child);
  }
  return FALSE;
}

static inline gboolean
constraint_verb_in_range (gchar subj_type, const GValue * subj_val,
    GVariant * check_val)
{
  switch (subj_type) {
#define CASE_RANGE(l, t, T) \
    case l: { \
      g##T val, min, max; \
      g_variant_get (check_val, "("#t#t")", &min, &max); \
      val = g_value_get_##T (subj_val); \
      if (val < min || val > max) \
        return FALSE; \
      break; \
    }
    CASE_RANGE('i', i, int)
    CASE_RANGE('u', u, uint)
    CASE_RANGE('x', x, int64)
    CASE_RANGE('t', t, uint64)
    CASE_RANGE('d', d, double)
#undef CASE_RANGE
    default:
      g_return_val_if_reached (FALSE);
  }
  return TRUE;
}

/*!
 * \brief Checks if the specified \a object matches the type and all the
 * constraints that are described in \a self
 *
 * If \a self is configured to match GObject subclasses, this is equivalent to
 * `wp_object_interest_matches_full (self, G_OBJECT_TYPE (object), object,
 * NULL, NULL)` and if it is configured to match WpProperties, this is
 * equivalent to `wp_object_interest_matches_full (self, self->gtype, NULL,
 * (WpProperties *) object, NULL);`
 *
 * \ingroup wpobjectinterest
 * \param self the object interest
 * \param object the target object to check for a match
 * \returns TRUE if the object matches, FALSE otherwise
 */
gboolean
wp_object_interest_matches (WpObjectInterest * self, gpointer object)
{
  if (g_type_is_a (self->gtype, WP_TYPE_PROPERTIES)) {
    g_return_val_if_fail (object != NULL, FALSE);
    return wp_object_interest_matches_full (self, 0, self->gtype, NULL,
        (WpProperties *) object, NULL) == WP_INTEREST_MATCH_ALL;
  }
  else {
    g_return_val_if_fail (G_IS_OBJECT (object), FALSE);
    return wp_object_interest_matches_full (self, 0, G_OBJECT_TYPE (object),
        object, NULL, NULL) == WP_INTEREST_MATCH_ALL;
  }
}

/*!
 * \brief A low-level version of wp_object_interest_matches().
 *
 * In this version, the object's type is directly given in \a object_type and
 * is not inferred from the \a object. \a object is only used to check for
 * constraints against GObject properties.
 *
 * \a pw_props and \a pw_global_props are used to check constraints against
 * PipeWire object properties and global properties, respectively.
 *
 * \a object, \a pw_props and \a pw_global_props may be NULL, but in case there
 * are any constraints that require them, the match will fail.
 * As a special case, if \a object is not NULL and is a subclass of WpProxy,
 * then \a pw_props and \a pw_global_props, if required, will be internally
 * retrieved from \a object by calling wp_pipewire_object_get_properties() and
 * wp_global_proxy_get_global_properties() respectively.
 *
 * When \a flags contains WP_INTEREST_MATCH_FLAGS_CHECK_ALL, all the constraints
 * are checked and the returned value contains accurate information about which
 * types of constraints have failed to match, if any. When this flag is not
 * present, this function returns after the first failure has been encountered.
 * This means that the returned flags set will contain all but one flag, which
 * will indicate the kind of constraint that failed (more could have failed,
 * but they are not checked...)
 *
 * \ingroup wpobjectinterest
 * \param self the object interest
 * \param flags flags to alter the behavior of this function
 * \param object_type the type to be checked against the interest's type
 * \param object (type GObject)(transfer none)(nullable): the object to be used for
 *   checking constraints of type WP_CONSTRAINT_TYPE_G_PROPERTY
 * \param pw_props (transfer none)(nullable): the properties to be used for
 *   checking constraints of type WP_CONSTRAINT_TYPE_PW_PROPERTY
 * \param pw_global_props (transfer none)(nullable): the properties to be used for
 *   checking constraints of type WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY
 * \returns flags that indicate which components of the interest match.
 *   WP_INTEREST_MATCH_ALL indicates a fully successful match; any other
 *   combination indicates a failure on the component(s) that do not appear on
 *   the flag set
 */
WpInterestMatch
wp_object_interest_matches_full (WpObjectInterest * self,
    WpInterestMatchFlags flags, GType object_type, gpointer object,
    WpProperties * pw_props, WpProperties * pw_global_props)
{
  WpInterestMatch result = WP_INTEREST_MATCH_ALL;
  g_autoptr (WpProperties) props = NULL;
  g_autoptr (WpProperties) global_props = NULL;
  g_autoptr (GError) error = NULL;
  struct constraint *c;

  g_return_val_if_fail (self != NULL, WP_INTEREST_MATCH_NONE);

  if (G_UNLIKELY (!wp_object_interest_validate (self, &error))) {
    wp_critical_boxed (WP_TYPE_OBJECT_INTEREST, self, "validation failed: %s",
        error->message);
    return WP_INTEREST_MATCH_NONE;
  }

  /* check if the GType matches */
  if (!g_type_is_a (object_type, self->gtype))
    result &= ~WP_INTEREST_MATCH_GTYPE;

  /* prepare for constraint lookups on proxy properties */
  if (object) {
    if (!pw_global_props && WP_IS_GLOBAL_PROXY (object)) {
      WpGlobalProxy *pwg = (WpGlobalProxy *) object;
      pw_global_props = global_props =
          wp_global_proxy_get_global_properties (pwg);
    }

    if (!pw_props && WP_IS_PIPEWIRE_OBJECT (object)) {
      WpObject *oo = (WpObject *) object;
      WpPipewireObject *pwo = (WpPipewireObject *) object;

      if (wp_object_test_active_features (oo, WP_PIPEWIRE_OBJECT_FEATURE_INFO))
        pw_props = props = wp_pipewire_object_get_properties (pwo);
    }

    if (!pw_global_props && WP_IS_SESSION_ITEM (object)) {
      WpSessionItem *si = (WpSessionItem *) object;
      pw_global_props = props = wp_session_item_get_properties (si);
    }
  }

  /* check all constraints; if any of them fails at any point, fail the match */
  pw_array_for_each (c, &self->constraints) {
    WpProperties *lookup_props = pw_global_props;
    g_auto (GValue) value = G_VALUE_INIT;
    gboolean exists = FALSE;

    /* return early if the match failed and CHECK_ALL is not specified */
    if (!(flags & WP_INTEREST_MATCH_FLAGS_CHECK_ALL) &&
        result != WP_INTEREST_MATCH_ALL)
      return result;

    /* collect, check & convert the subject property */
    switch (c->type) {
      case WP_CONSTRAINT_TYPE_PW_PROPERTY:
        lookup_props = pw_props;
        SPA_FALLTHROUGH;

      case WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY: {
        const gchar *lookup_str = NULL;

        if (lookup_props)
          exists = !!(lookup_str = wp_properties_get (lookup_props, c->subject));

        if (exists && c->subject_type)
          property_string_to_gvalue (c->subject_type, lookup_str, &value);
        break;
      }
      case WP_CONSTRAINT_TYPE_G_PROPERTY: {
        GType value_type;
        GParamSpec *pspec = NULL;

        if (object)
          exists = !!(pspec = g_object_class_find_property (
              G_OBJECT_GET_CLASS (object), c->subject));

        if (exists && c->subject_type) {
          g_value_init (&value, pspec->value_type);
          g_object_get_property (object, c->subject, &value);
          value_type = G_VALUE_TYPE (&value);

          /* transform if not compatible */
          if (value_type != subject_type_to_gtype (c->subject_type)) {
            if (g_value_type_transformable (value_type,
                    subject_type_to_gtype (c->subject_type))) {
              g_auto (GValue) orig = G_VALUE_INIT;
              g_value_init (&orig, value_type);
              g_value_copy (&value, &orig);
              g_value_unset (&value);
              g_value_init (&value, subject_type_to_gtype (c->subject_type));
              g_value_transform (&orig, &value);
            }
            else {
              result &= ~(1 << c->type);
              continue;
            }
          }
        }

        break;
      }
      default:
        g_return_val_if_reached (WP_INTEREST_MATCH_NONE);
    }

    /* match the subject to the constraint's value,
       according to the operation defined by the verb */
    switch (c->verb) {
      case WP_CONSTRAINT_VERB_EQUALS:
        if (!exists ||
            !constraint_verb_equals (c->subject_type, &value, c->value))
          result &= ~(1 << c->type);
        break;
      case WP_CONSTRAINT_VERB_NOT_EQUALS:
        if (exists &&
            constraint_verb_equals (c->subject_type, &value, c->value))
          result &= ~(1 << c->type);
        break;
      case WP_CONSTRAINT_VERB_MATCHES:
        if (!exists ||
            !constraint_verb_matches (c->subject_type, &value, c->value))
          result &= ~(1 << c->type);
        break;
      case WP_CONSTRAINT_VERB_IN_LIST:
        if (!exists ||
            !constraint_verb_in_list (c->subject_type, &value, c->value))
          result &= ~(1 << c->type);
        break;
      case WP_CONSTRAINT_VERB_IN_RANGE:
        if (!exists ||
            !constraint_verb_in_range (c->subject_type, &value, c->value))
          result &= ~(1 << c->type);
        break;
      case WP_CONSTRAINT_VERB_IS_PRESENT:
        if (!exists)
          result &= ~(1 << c->type);
        break;
      case WP_CONSTRAINT_VERB_IS_ABSENT:
        if (exists)
          result &= ~(1 << c->type);
        break;
      default:
        g_return_val_if_reached (WP_INTEREST_MATCH_NONE);
    }
  }
  return result;
}
   0707010000009F000081A400000000000000000000000165F8630400001089000000000000000000000000000000000000002B00000000wireplumber-0.5.0/lib/wp/object-interest.h    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_OBJECT_INTEREST_H__
#define __WIREPLUMBER_OBJECT_INTEREST_H__

#include <glib-object.h>
#include "defs.h"
#include "properties.h"

G_BEGIN_DECLS

/*!
 * \brief Constraint types for wp_object_interest_add_constraint()
 * \ingroup wpobjectinterest
 */
typedef enum {
  /*! invalid constraint type */
  WP_CONSTRAINT_TYPE_NONE = 0,
  /*! constraint applies to a PipeWire global property of the object
   *  (the ones returned by wp_global_proxy_get_global_properties()) */
  WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY,
  /*! constraint applies to a PipeWire property of the object
   *  (the ones returned by wp_pipewire_object_get_properties()) */
  WP_CONSTRAINT_TYPE_PW_PROPERTY,
  /*! constraint applies to a GObject property of the object */
  WP_CONSTRAINT_TYPE_G_PROPERTY,
} WpConstraintType;

/*!
 * \brief Verbs to use with wp_object_interest_add_constraint()
 * \ingroup wpobjectinterest
 */
typedef enum {
  /*! the subject's value must equal the constraint's value */
  WP_CONSTRAINT_VERB_EQUALS = '=',
  /*! the subject's value must be different from the constraint's value */
  WP_CONSTRAINT_VERB_NOT_EQUALS = '!',
  /*! the subject's value must equal at least
   *  one of the values in the list given as the constraint's value */
  WP_CONSTRAINT_VERB_IN_LIST = 'c',
  /*! the subject's value must be a number in the range defined
   *  by the constraint's value */
  WP_CONSTRAINT_VERB_IN_RANGE = '~',
  /*! the subject's value must match the pattern specified in the
   *  constraint's value */
  WP_CONSTRAINT_VERB_MATCHES = '#',
  /*! the subject property must exist */
  WP_CONSTRAINT_VERB_IS_PRESENT = '+',
  /*! the subject property must not exist */
  WP_CONSTRAINT_VERB_IS_ABSENT = '-',
} WpConstraintVerb;

/*!
 * \brief Flags that indicate which constraints have been matched in
 *  wp_object_interest_matches_full()
 * \ingroup wpobjectinterest
 */
typedef enum { /*< flags >*/
  WP_INTEREST_MATCH_NONE = 0,
  WP_INTEREST_MATCH_GTYPE = (1 << 0),
  WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES = (1 << 1),
  WP_INTEREST_MATCH_PW_PROPERTIES = (1 << 2),
  WP_INTEREST_MATCH_G_PROPERTIES = (1 << 3),

  /*!
   * Special WpInterestMatch value that indicates that all constraints
   * have been matched
   */
  WP_INTEREST_MATCH_ALL =
    (WP_INTEREST_MATCH_GTYPE |
     WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES |
     WP_INTEREST_MATCH_PW_PROPERTIES |
     WP_INTEREST_MATCH_G_PROPERTIES),
} WpInterestMatch;

/*!
 * \brief Flags to alter the behaviour of wp_object_interest_matches_full()
 * \ingroup wpobjectinterest
 */
typedef enum { /*< flags >*/
  WP_INTEREST_MATCH_FLAGS_NONE = 0,
  /*! check all the constraints instead of returning after the first mis-match */
  WP_INTEREST_MATCH_FLAGS_CHECK_ALL = (1 << 0),
} WpInterestMatchFlags;

/*!
 * \brief The WpObjectInterest GType
 * \ingroup wpobjectinterest
 */
#define WP_TYPE_OBJECT_INTEREST (wp_object_interest_get_type ())
WP_API
GType wp_object_interest_get_type (void) G_GNUC_CONST;

typedef struct _WpObjectInterest WpObjectInterest;

WP_API
WpObjectInterest * wp_object_interest_new (GType gtype, ...) G_GNUC_NULL_TERMINATED;

WP_API
WpObjectInterest * wp_object_interest_new_valist (GType gtype, va_list * args);

WP_API
WpObjectInterest * wp_object_interest_new_type (GType gtype);

WP_API
void wp_object_interest_add_constraint (WpObjectInterest * self,
    WpConstraintType type, const gchar * subject,
    WpConstraintVerb verb, GVariant * value);

WP_API
WpObjectInterest * wp_object_interest_ref (WpObjectInterest *self);

WP_API
void wp_object_interest_unref (WpObjectInterest * self);

WP_API
gboolean wp_object_interest_validate (WpObjectInterest * self, GError ** error);

WP_API
gboolean wp_object_interest_matches (WpObjectInterest * self, gpointer object);

WP_API
WpInterestMatch wp_object_interest_matches_full (WpObjectInterest * self,
    WpInterestMatchFlags flags, GType object_type, gpointer object,
    WpProperties * pw_props, WpProperties * pw_global_props);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpObjectInterest, wp_object_interest_unref)

G_END_DECLS

#endif
   070701000000A0000081A400000000000000000000000165F86304000062E3000000000000000000000000000000000000002A00000000wireplumber-0.5.0/lib/wp/object-manager.c /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "object-manager.h"
#include "log.h"
#include "proxy-interfaces.h"
#include "private/registry.h"

#include <pipewire/pipewire.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-object-manager")

/*! \defgroup wpobjectmanager WpObjectManager */
/*!
 * \struct WpObjectManager
 *
 * The WpObjectManager class provides a way to collect a set of objects and
 * be notified when objects that fulfill a certain set of criteria are created
 * or destroyed.
 *
 * There are 4 kinds of objects that can be managed by a
 * WpObjectManager:
 *   * remote PipeWire global objects that are advertised on the registry;
 *     these are bound locally to subclasses of WpGlobalProxy
 *   * remote PipeWire global objects that are created by calling a remote
 *     factory through the WirePlumber API; these are very similar to other
 *     global objects but it should be noted that the same WpGlobalProxy
 *     instance that created them appears in the WpObjectManager (as soon as
 *     its WP_PROXY_FEATURE_BOUND is enabled)
 *   * local PipeWire objects that are being exported to PipeWire
 *     (WpImplMetadata, WpImplNode, etc); these appear in the
 *     WpObjectManager as soon as they are exported (so, when their
 *     WP_PROXY_FEATURE_BOUND is enabled)
 *   * WirePlumber-specific objects, such as plugins, factories and session items
 *
 * To start an object manager, you first need to declare interest in a certain
 * kind of object by calling wp_object_manager_add_interest() and then install
 * it on the WpCore with wp_core_install_object_manager().
 *
 * Upon installing a WpObjectManager on a WpCore, any pre-existing objects
 * that match the interests of this WpObjectManager will immediately become
 * available to get through wp_object_manager_new_iterator() and the
 * WpObjectManager \c object-added signal will be emitted for all of them.
 * However, note that if these objects need to be prepared (to activate some
 * features on them), the emission of \c object-added will be delayed. To know
 * when it is safe to access the initial set of objects, wait until the
 * \c installed signal has been emitted. That signal is emitted asynchronously
 * after all the initial objects have been prepared.
 *
 * \gproperties
 *
 * \gproperty{core, WpCore *, G_PARAM_READABLE, The core}
 *
 * \gsignals
 *
 * \par installed
 * \parblock
 * \code
 * void
 * installed_callback (WpObjectManager * self,
 *                     gpointer user_data)
 * \endcode
 *
 * This is emitted once after the object manager is installed with
 * wp_core_install_object_manager(). If there are objects that need to be
 * prepared asynchronously internally, emission of this signal is delayed
 * until all objects are ready.
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par object-added
 * \parblock
 * \code
 * void
 * object_added_callback (WpObjectManager * self,
 *                        gpointer object,
 *                        gpointer user_data)
 * \endcode
 *
 * Emitted when an object that matches the interests of this object manager
 * is made available.
 *
 * Parameters:
 * - `object` - the managed object that was just added
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par object-removed
 * \parblock
 * \code
 * void
 * object_removed_callback (WpObjectManager * self,
 *                          gpointer object,
 *                          gpointer user_data)
 * \endcode
 *
 * Emitted when an object that was previously added on this object manager is
 * now being removed (and most likely destroyed). At the time that this signal
 * is emitted, the object is still alive.
 *
 * Parameters:
 * - `object` - the managed object that is being removed
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par objects-changed
 * \parblock
 * \code
 * void
 * objects_changed_callback (WpObjectManager * self,
 *                          gpointer user_data)
 * \endcode
 *
 * Emitted when one or more objects have been recently added or removed from
 * this object manager. This signal is useful to get notified only once when
 * multiple changes happen in a short timespan. The receiving callback may
 * retrieve the updated list of objects by calling wp_object_manager_new_iterator()
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 */

struct _WpObjectManager
{
  GObject parent;
  GWeakRef core;

  /* element-type: WpObjectInterest* */
  GPtrArray *interests;
  /* element-type: <GType, WpProxyFeatures> */
  GHashTable *features;
  /* objects that we are interested in, without a ref */
  GPtrArray *objects;

  gboolean installed;
  gboolean changed;
  guint pending_objects;
  GSource *idle_source;
};

enum {
  PROP_0,
  PROP_CORE,
};

enum {
  SIGNAL_OBJECT_ADDED,
  SIGNAL_OBJECT_REMOVED,
  SIGNAL_OBJECTS_CHANGED,
  SIGNAL_INSTALLED,
  LAST_SIGNAL,
};

static guint signals[LAST_SIGNAL] = { 0 };

G_DEFINE_TYPE (WpObjectManager, wp_object_manager, G_TYPE_OBJECT)

static void
wp_object_manager_init (WpObjectManager * self)
{
  g_weak_ref_init (&self->core, NULL);
  self->interests = g_ptr_array_new_with_free_func (
      (GDestroyNotify) wp_object_interest_unref);
  self->features = g_hash_table_new (g_direct_hash, g_direct_equal);
  self->objects = g_ptr_array_new ();
  self->installed = FALSE;
  self->changed = FALSE;
  self->pending_objects = 0;
}

static void
wp_object_manager_finalize (GObject * object)
{
  WpObjectManager *self = WP_OBJECT_MANAGER (object);

  if (self->idle_source) {
    g_source_destroy (self->idle_source);
    g_clear_pointer (&self->idle_source, g_source_unref);
  }
  g_clear_pointer (&self->objects, g_ptr_array_unref);
  g_clear_pointer (&self->features, g_hash_table_unref);
  g_clear_pointer (&self->interests, g_ptr_array_unref);
  g_weak_ref_clear (&self->core);

  G_OBJECT_CLASS (wp_object_manager_parent_class)->finalize (object);
}

static void
wp_object_manager_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpObjectManager *self = WP_OBJECT_MANAGER (object);

  switch (property_id) {
  case PROP_CORE:
    g_value_take_object (value, g_weak_ref_get (&self->core));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_object_manager_class_init (WpObjectManagerClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->finalize = wp_object_manager_finalize;
  object_class->get_property = wp_object_manager_get_property;

  /* Install the properties */

  g_object_class_install_property (object_class, PROP_CORE,
      g_param_spec_object ("core", "core", "The WpCore", WP_TYPE_CORE,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  signals[SIGNAL_OBJECT_ADDED] = g_signal_new (
      "object-added", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_OBJECT);

  signals[SIGNAL_OBJECT_REMOVED] = g_signal_new (
      "object-removed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_OBJECT);

  signals[SIGNAL_OBJECTS_CHANGED] = g_signal_new (
      "objects-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      0, NULL, NULL, NULL, G_TYPE_NONE, 0);

  signals[SIGNAL_INSTALLED] = g_signal_new (
      "installed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      0, NULL, NULL, NULL, G_TYPE_NONE, 0);
}

/*!
 * \brief Constructs a new object manager.
 * \ingroup wpobjectmanager
 * \returns (transfer full): the newly constructed object manager
 */
WpObjectManager *
wp_object_manager_new (void)
{
  return g_object_new (WP_TYPE_OBJECT_MANAGER, NULL);
}

/*!
 * \brief Checks if an object manager is installed.
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \returns TRUE if the object manager is installed (i.e. the
 *   WpObjectManager \c installed signal has been emitted), FALSE otherwise
 */
gboolean
wp_object_manager_is_installed (WpObjectManager * self)
{
  g_return_val_if_fail (WP_IS_OBJECT_MANAGER (self), FALSE);
  return self->installed;
}

/*!
 * \brief Equivalent to:
 * \code
 * WpObjectInterest *i = wp_object_interest_new (gtype, ...);
 * wp_object_manager_add_interest_full (self, i);
 * \endcode
 *
 * The constraints specified in the variable arguments must follow the rules
 * documented in wp_object_interest_new().
 *
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \param gtype the GType of the objects that we are declaring interest in
 * \param ... a list of constraints, terminated by NULL
 */
void
wp_object_manager_add_interest (WpObjectManager * self, GType gtype, ...)
{
  WpObjectInterest *interest;
  va_list args;

  g_return_if_fail (WP_IS_OBJECT_MANAGER (self));

  va_start (args, gtype);
  interest = wp_object_interest_new_valist (gtype, &args);
  wp_object_manager_add_interest_full (self, interest);
  va_end (args);
}

/*!
 * \brief Declares interest in a certain kind of object.
 *
 * Interest consists of a GType that the object must be an ancestor of
 * (g_type_is_a() must match) and optionally, a set of additional constraints
 * on certain properties of the object. Refer to WpObjectInterest for more details.
 *
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \param interest (transfer full): the interest
 */
void
wp_object_manager_add_interest_full (WpObjectManager *self,
    WpObjectInterest * interest)
{
  g_autoptr (GError) error = NULL;

  g_return_if_fail (WP_IS_OBJECT_MANAGER (self));

  if (G_UNLIKELY (!wp_object_interest_validate (interest, &error))) {
    wp_critical_object (self, "interest validation failed: %s",
        error->message);
    wp_object_interest_unref (interest);
    return;
  }
  g_ptr_array_add (self->interests, interest);
}

static void
store_children_object_features (GHashTable *store, GType object_type,
    WpObjectFeatures wanted_features)
{
  g_autofree GType *children = NULL;
  GType *child;

  child = children = g_type_children (object_type, NULL);
  while (*child) {
    WpObjectFeatures existing_ft = (WpObjectFeatures) GPOINTER_TO_UINT (
        g_hash_table_lookup (store, GSIZE_TO_POINTER (*child)));
    g_hash_table_insert (store, GSIZE_TO_POINTER (*child),
        GUINT_TO_POINTER (existing_ft | wanted_features));
    store_children_object_features (store, *child, wanted_features);
    child++;
  }
}

/*!
 * \brief Requests the object manager to automatically prepare the
 * \a wanted_features on any managed object that is of the specified
 * \a object_type.
 *
 * These features will always be prepared before the object appears on the
 * object manager.
 *
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \param object_type the WpProxy descendant type
 * \param wanted_features the features to enable on this kind of object
 */
void
wp_object_manager_request_object_features (WpObjectManager *self,
    GType object_type, WpObjectFeatures wanted_features)
{
  g_return_if_fail (WP_IS_OBJECT_MANAGER (self));
  g_return_if_fail (g_type_is_a (object_type, WP_TYPE_OBJECT));

  g_hash_table_insert (self->features, GSIZE_TO_POINTER (object_type),
      GUINT_TO_POINTER (wanted_features));
  store_children_object_features (self->features, object_type, wanted_features);
}

/*!
 * \brief Gets the number of objects managed by the object manager.
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \returns the number of objects managed by this WpObjectManager
 */
guint
wp_object_manager_get_n_objects (WpObjectManager * self)
{
  g_return_val_if_fail (WP_IS_OBJECT_MANAGER (self), 0);
  return self->objects->len;
}

struct om_iterator_data
{
  WpObjectManager *om;
  GPtrArray *objects;
  WpObjectInterest *interest;
  guint index;
};

static void
om_iterator_reset (WpIterator *it)
{
  struct om_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->index = 0;
}

static gboolean
om_iterator_next (WpIterator *it, GValue *item)
{
  struct om_iterator_data *it_data = wp_iterator_get_user_data (it);

  while (it_data->index < it_data->objects->len) {
    gpointer obj = g_ptr_array_index (it_data->objects, it_data->index++);

    /* take the next object that matches the interest, if any */
    if (!it_data->interest ||
        wp_object_interest_matches (it_data->interest, obj)) {
      g_value_init_from_instance (item, obj);
      return TRUE;
    }
  }
  return FALSE;
}

static gboolean
om_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
    gpointer data)
{
  struct om_iterator_data *it_data = wp_iterator_get_user_data (it);
  gpointer *obj, *base;
  guint len;

  obj = base = it_data->objects->pdata;
  len = it_data->objects->len;

  while ((obj - base) < len) {
    /* only pass matching objects to the fold func if we have an interest */
    if (!it_data->interest ||
        wp_object_interest_matches (it_data->interest, *obj)) {
      g_auto (GValue) item = G_VALUE_INIT;
      g_value_init_from_instance (&item, *obj);
      if (!func (&item, ret, data))
        return FALSE;
    }
    obj++;
  }
  return TRUE;
}

static void
om_iterator_finalize (WpIterator *it)
{
  struct om_iterator_data *it_data = wp_iterator_get_user_data (it);
  g_clear_pointer (&it_data->objects, g_ptr_array_unref);
  g_clear_pointer (&it_data->interest, wp_object_interest_unref);
  g_object_unref (it_data->om);
}

static const WpIteratorMethods om_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = om_iterator_reset,
  .next = om_iterator_next,
  .fold = om_iterator_fold,
  .finalize = om_iterator_finalize,
};

/*!
 * \brief Iterates through all the objects managed by this object manager.
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \returns (transfer full): a WpIterator that iterates over all the managed
 *   objects of this object manager
 */
WpIterator *
wp_object_manager_new_iterator (WpObjectManager * self)
{
  WpIterator *it;
  struct om_iterator_data *it_data;

  g_return_val_if_fail (WP_IS_OBJECT_MANAGER (self), NULL);

  it = wp_iterator_new (&om_iterator_methods, sizeof (struct om_iterator_data));
  it_data = wp_iterator_get_user_data (it);
  it_data->om = g_object_ref (self);
  it_data->objects = g_ptr_array_copy (self->objects, NULL, NULL);
  it_data->index = 0;
  return it;
}

/*!
 * \brief Equivalent to:
 * \code
 * WpObjectInterest *i = wp_object_interest_new (gtype, ...);
 * return wp_object_manager_new_filtered_iterator_full (self, i);
 * \endcode
 *
 * The constraints specified in the variable arguments must follow the rules
 * documented in wp_object_interest_new().
 *
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \param gtype the GType of the objects to iterate through
 * \param ... a list of constraints, terminated by NULL
 * \returns (transfer full): a WpIterator that iterates over all the matching
 *   objects of this object manager
 */
WpIterator *
wp_object_manager_new_filtered_iterator (WpObjectManager * self, GType gtype,
    ...)
{
  WpObjectInterest *interest;
  va_list args;

  g_return_val_if_fail (WP_IS_OBJECT_MANAGER (self), NULL);

  va_start (args, gtype);
  interest = wp_object_interest_new_valist (gtype, &args);
  va_end (args);

  return wp_object_manager_new_filtered_iterator_full (self, interest);
}

/*!
 * \brief Iterates through all the objects managed by this object manager that
 * match the specified \a interest.
 *
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \param interest (transfer full): the interest
 * \returns (transfer full): a WpIterator that iterates over all the matching
 *   objects of this object manager
 */
WpIterator *
wp_object_manager_new_filtered_iterator_full (WpObjectManager * self,
    WpObjectInterest * interest)
{
  WpIterator *it;
  struct om_iterator_data *it_data;
  g_autoptr (GError) error = NULL;

  g_return_val_if_fail (WP_IS_OBJECT_MANAGER (self), NULL);

  if (G_UNLIKELY (!wp_object_interest_validate (interest, &error))) {
    wp_critical_object (self, "interest validation failed: %s",
        error->message);
    wp_object_interest_unref (interest);
    return NULL;
  }

  it = wp_iterator_new (&om_iterator_methods, sizeof (struct om_iterator_data));
  it_data = wp_iterator_get_user_data (it);
  it_data->om = g_object_ref (self);
  it_data->objects = g_ptr_array_copy (self->objects, NULL, NULL);
  it_data->interest = interest;
  it_data->index = 0;
  return it;
}

/*!
 * \brief Equivalent to:
 * \code
 * WpObjectInterest *i = wp_object_interest_new (gtype, ...);
 * return wp_object_manager_lookup_full (self, i);
 * \endcode
 *
 * The constraints specified in the variable arguments must follow the rules
 * documented in wp_object_interest_new().
 *
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \param gtype the GType of the object to lookup
 * \param ... a list of constraints, terminated by NULL
 * \returns (type GObject)(transfer full)(nullable): the first managed object
 *    that matches the lookup interest, or NULL if no object matches
 */
gpointer
wp_object_manager_lookup (WpObjectManager * self, GType gtype, ...)
{
  WpObjectInterest *interest;
  va_list args;

  g_return_val_if_fail (WP_IS_OBJECT_MANAGER (self), NULL);

  va_start (args, gtype);
  interest = wp_object_interest_new_valist (gtype, &args);
  va_end (args);

  return wp_object_manager_lookup_full (self, interest);
}

/*!
 * \brief Searches for an object that matches the specified \a interest and
 * returns it, if found.
 *
 * If more than one objects match, only the first one is returned.
 * To find multiple objects that match certain criteria,
 * wp_object_manager_new_filtered_iterator() is more suitable.
 *
 * \ingroup wpobjectmanager
 * \param self the object manager
 * \param interest (transfer full): the interst
 * \returns (type GObject)(transfer full)(nullable): the first managed object
 *    that matches the lookup interest, or NULL if no object matches
 */
gpointer
wp_object_manager_lookup_full (WpObjectManager * self,
    WpObjectInterest * interest)
{
  g_auto (GValue) ret = G_VALUE_INIT;
  g_autoptr (WpIterator) it =
      wp_object_manager_new_filtered_iterator_full (self, interest);

  if (wp_iterator_next (it, &ret))
    return g_value_dup_object (&ret);

  return NULL;
}

static gboolean
wp_object_manager_is_interested_in_object (WpObjectManager * self,
    GObject * object)
{
  guint i;
  WpObjectInterest *interest = NULL;

  for (i = 0; i < self->interests->len; i++) {
    interest = g_ptr_array_index (self->interests, i);
    if (wp_object_interest_matches (interest, object))
      return TRUE;
  }
  return FALSE;
}

static gboolean
wp_object_manager_is_interested_in_global (WpObjectManager * self,
    WpGlobal * global, WpObjectFeatures * wanted_features)
{
  guint i;
  WpObjectInterest *interest = NULL;

  for (i = 0; i < self->interests->len; i++) {
    interest = g_ptr_array_index (self->interests, i);

    /* check all constraints */
    WpInterestMatch match = wp_object_interest_matches_full (interest,
        WP_INTEREST_MATCH_FLAGS_CHECK_ALL, global->type, global->proxy,
        NULL, global->properties);

    /* and consider the manager interested if the type and the globals match...
       if pw_properties / g_properties fail, that's ok because they are not
       known yet (the proxy is likely NULL and properties not yet retrieved) */
    if (SPA_FLAG_IS_SET (match, (WP_INTEREST_MATCH_GTYPE |
                                 WP_INTEREST_MATCH_PW_GLOBAL_PROPERTIES))) {
      gpointer ft = g_hash_table_lookup (self->features,
          GSIZE_TO_POINTER (global->type));
      *wanted_features = (WpObjectFeatures) GPOINTER_TO_UINT (ft);

      /* force INFO to be present so that we can check PW_PROPERTIES constraints */
      if (!(match & WP_INTEREST_MATCH_PW_PROPERTIES) &&
            !(*wanted_features & WP_PIPEWIRE_OBJECT_FEATURE_INFO) &&
            g_type_is_a (global->type, WP_TYPE_PIPEWIRE_OBJECT))
        *wanted_features |= WP_PIPEWIRE_OBJECT_FEATURE_INFO;

      return TRUE;
    }
  }
  return FALSE;
}

static gboolean
idle_emit_objects_changed (WpObjectManager * self)
{
  g_clear_pointer (&self->idle_source, g_source_unref);

  if (G_UNLIKELY (!self->installed)) {
    wp_trace_object (self, "installed");
    self->installed = TRUE;
    g_signal_emit (self, signals[SIGNAL_INSTALLED], 0);
  }
  wp_trace_object (self, "emit objects-changed");
  g_signal_emit (self, signals[SIGNAL_OBJECTS_CHANGED], 0);

  return G_SOURCE_REMOVE;
}

void
wp_object_manager_maybe_objects_changed (WpObjectManager * self)
{
  wp_trace_object (self, "pending:%u changed:%d idle_source:%p installed:%d",
      self->pending_objects, self->changed, self->idle_source, self->installed);

  /* always wait until there are no pending objects */
  if (self->pending_objects > 0)
    return;

  /* Emit 'objects-changed' when:
   * - there are no pending objects
   * - object-added or object-removed has been emitted at least once
   */
  if (self->changed) {
    self->changed = FALSE;

    /* schedule emission in idle; if it is already scheduled from earlier,
       there is nothing to do; we will emit objects-changed once for all
       changes... win-win */
    if (!self->idle_source) {
      g_autoptr (WpCore) core = g_weak_ref_get (&self->core);
      if (core) {
        wp_core_idle_add_closure (core, &self->idle_source,
            g_cclosure_new_object (
                G_CALLBACK (idle_emit_objects_changed), G_OBJECT (self)));
      }
    }
  }
  /* Emit 'installed' when:
   * - there are no pending objects
   * - !changed: there was no object added
   * - !installed: not already installed
   * - the registry does not have pending globals; these may be interesting
   * to our object manager, so let's wait a bit until they are released
   * and re-evaluate again later
   * - the registry has globals; if we are on early startup where we don't
   * have any globals yet, wait...
   */
  else if (!self->installed) {
    g_autoptr (WpCore) core = g_weak_ref_get (&self->core);
    if (core) {
      WpRegistry *reg = wp_core_get_registry (core);
      if (reg->tmp_globals->len == 0 && reg->globals->len != 0) {
        wp_trace_object (self, "installed");
        self->installed = TRUE;
        g_signal_emit (self, signals[SIGNAL_INSTALLED], 0);
      }
    }
  }
}

/* caller must also call wp_object_manager_maybe_objects_changed() after */
void
wp_object_manager_add_object (WpObjectManager * self, gpointer object)
{
  if (wp_object_manager_is_interested_in_object (self, object)) {
    wp_trace_object (self, "added: " WP_OBJECT_FORMAT, WP_OBJECT_ARGS (object));
    g_ptr_array_add (self->objects, object);
    g_signal_emit (self, signals[SIGNAL_OBJECT_ADDED], 0, object);
    self->changed = TRUE;
  }
}

/* caller must also call wp_object_manager_maybe_objects_changed() after */
void
wp_object_manager_rm_object (WpObjectManager * self, gpointer object)
{
  guint index;
  if (g_ptr_array_find (self->objects, object, &index)) {
    g_ptr_array_remove_index_fast (self->objects, index);
    g_signal_emit (self, signals[SIGNAL_OBJECT_REMOVED], 0, object);
    self->changed = TRUE;
  }
}

static void
on_proxy_ready (GObject * proxy, GAsyncResult * res, gpointer data)
{
  g_autoptr (WpObjectManager) self = WP_OBJECT_MANAGER (data);
  g_autoptr (GError) error = NULL;

  self->pending_objects--;

  if (!wp_object_activate_finish (WP_OBJECT (proxy), res, &error)) {
    wp_debug_object (self, "proxy activation failed: %s", error->message);
  } else {
    wp_object_manager_add_object (self, proxy);
  }

  wp_object_manager_maybe_objects_changed (self);
}

/* caller must also call wp_object_manager_maybe_objects_changed() after */
void
wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
{
  WpProxyFeatures features = 0;

  /* do not allow proxies that don't have a defined subclass;
     bind will fail because proxy_class->pw_iface_type is NULL */
  if (global->type == WP_TYPE_GLOBAL_PROXY)
    return;

  if (wp_object_manager_is_interested_in_global (self, global, &features)) {
    g_autoptr (WpCore) core = g_weak_ref_get (&self->core);

    self->pending_objects++;

    if (!global->proxy)
      global->proxy = g_object_new (global->type,
          "core", core,
          "global", global,
          NULL);

    wp_trace_object (self, "adding global:%u -> " WP_OBJECT_FORMAT,
        global->id, WP_OBJECT_ARGS (global->proxy));

    wp_object_activate (WP_OBJECT (global->proxy), features, NULL,
        on_proxy_ready, g_object_ref (self));
  }
}

/*!
 * \brief Installs the object manager on this core, activating its internal
 * management engine.
 *
 * This will immediately emit signals about objects added on \a om
 * if objects that the \a om is interested in were in existence already.
 *
 * \ingroup wpobjectmanager
 * \param self the core
 * \param om (transfer none): a WpObjectManager
 */
void
wp_core_install_object_manager (WpCore * self, WpObjectManager * om)
{
  WpRegistry *reg;

  g_return_if_fail (WP_IS_CORE (self));
  g_return_if_fail (WP_IS_OBJECT_MANAGER (om));

  g_weak_ref_set (&om->core, self);

  reg = wp_core_get_registry (self);
  wp_registry_install_object_manager (reg, om);
}
 070701000000A1000081A400000000000000000000000165F863040000089B000000000000000000000000000000000000002A00000000wireplumber-0.5.0/lib/wp/object-manager.h /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_OBJECT_MANAGER_H__
#define __WIREPLUMBER_OBJECT_MANAGER_H__

#include <glib-object.h>
#include "object.h"
#include "iterator.h"
#include "object-interest.h"

G_BEGIN_DECLS

/*!
 * \brief The WpObjectManager GType
 * \ingroup wpobjectmanager
 */
#define WP_TYPE_OBJECT_MANAGER (wp_object_manager_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpObjectManager, wp_object_manager, WP, OBJECT_MANAGER, GObject)

WP_API
WpObjectManager * wp_object_manager_new (void);

/* installation */

WP_API
gboolean wp_object_manager_is_installed (WpObjectManager * self);

/* interest */

WP_API
void wp_object_manager_add_interest (WpObjectManager * self,
    GType gtype, ...) G_GNUC_NULL_TERMINATED;

WP_API
void wp_object_manager_add_interest_full (WpObjectManager * self,
    WpObjectInterest * interest);

/* object features */

WP_API
void wp_object_manager_request_object_features (WpObjectManager *self,
    GType object_type, WpObjectFeatures wanted_features);

/* object inspection */

WP_API
guint wp_object_manager_get_n_objects (WpObjectManager * self);

WP_API
WpIterator * wp_object_manager_new_iterator (WpObjectManager * self);

WP_API
WpIterator * wp_object_manager_new_filtered_iterator (WpObjectManager * self,
    GType gtype, ...);

WP_API
WpIterator * wp_object_manager_new_filtered_iterator_full (
    WpObjectManager * self, WpObjectInterest * interest);

WP_API
gpointer wp_object_manager_lookup (WpObjectManager * self,
    GType gtype, ...) G_GNUC_NULL_TERMINATED;

WP_API
gpointer wp_object_manager_lookup_full (WpObjectManager * self,
    WpObjectInterest * interest);

/* private */

typedef struct _WpGlobal WpGlobal;

WP_PRIVATE_API
void wp_object_manager_maybe_objects_changed (WpObjectManager * self);

WP_PRIVATE_API
void wp_object_manager_add_object (WpObjectManager * self, gpointer object);

WP_PRIVATE_API
void wp_object_manager_rm_object (WpObjectManager * self, gpointer object);

WP_PRIVATE_API
void wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global);

G_END_DECLS

#endif
 070701000000A2000081A400000000000000000000000165F8630400004B48000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/object.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <spa/utils/defs.h>

#include "object.h"
#include "log.h"
#include "core.h"
#include "error.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-object")

/*! \defgroup wpfeatureactivationtransition WpFeatureActivationTransition */
/*!
 * \struct WpFeatureActivationTransition
 * A WpTransition that is used by WpObject to implement feature activation.
 */
struct _WpFeatureActivationTransition
{
  WpTransition parent;
  WpObjectFeatures missing;
};

G_DEFINE_TYPE (WpFeatureActivationTransition,
               wp_feature_activation_transition,
               WP_TYPE_TRANSITION)

static void
wp_feature_activation_transition_init (
    WpFeatureActivationTransition * transition)
{
}

static WpObjectFeatures
wp_feature_activation_transition_calc_missing_features (
    WpFeatureActivationTransition * self, WpObject * object)
{
  /* missing features = features that have been requested,
     they are supported and they are not active yet;
     note that supported features may change while the transition is ongoing,
     which is why we store the requested features as they were originally
     and keep trying to activate everything that is supported at the time */
  WpObjectFeatures requested =
      wp_feature_activation_transition_get_requested_features (self);
  WpObjectFeatures supported = wp_object_get_supported_features (object);
  WpObjectFeatures active = wp_object_get_active_features (object);
  return (requested & supported & ~active);
}

static guint
wp_feature_activation_transition_get_next_step (
    WpTransition * transition, guint step)
{
  WpFeatureActivationTransition *self =
      WP_FEATURE_ACTIVATION_TRANSITION (transition);
  WpObject *object = wp_transition_get_source_object (transition);

  self->missing =
      wp_feature_activation_transition_calc_missing_features (self, object);
  wp_trace_object (object, "missing features to activate: 0x%x",
      self->missing);

  /* nothing to activate, we are done */
  if (self->missing == 0)
    return WP_TRANSITION_STEP_NONE;

  g_return_val_if_fail (WP_OBJECT_GET_CLASS (object)->activate_get_next_step,
      WP_TRANSITION_STEP_ERROR);

  step = WP_OBJECT_GET_CLASS (object)->activate_get_next_step (object, self,
      step, self->missing);

  g_return_val_if_fail (step == WP_TRANSITION_STEP_NONE ||
          WP_OBJECT_GET_CLASS (object)->activate_execute_step,
      WP_TRANSITION_STEP_ERROR);
  return step;
}

static void
wp_feature_activation_transition_execute_step (
    WpTransition * transition, guint step)
{
  WpFeatureActivationTransition *self =
      WP_FEATURE_ACTIVATION_TRANSITION (transition);
  WpObject *object = wp_transition_get_source_object (transition);

  WP_OBJECT_GET_CLASS (object)->activate_execute_step (object, self, step,
      self->missing);
}

static void
wp_feature_activation_transition_class_init (
    WpFeatureActivationTransitionClass * klass)
{
  WpTransitionClass *transition_class = (WpTransitionClass *) klass;

  transition_class->get_next_step =
      wp_feature_activation_transition_get_next_step;
  transition_class->execute_step =
      wp_feature_activation_transition_execute_step;
}

/*!
 * \brief Gets the features requested to be activated in this transition.
 * \ingroup wpfeatureactivationtransition
 * \param self the transition
 * \returns the features that were requested to be activated in this transition;
 *   this contains the features as they were passed in wp_object_activate() and
 *   therefore it may contain unsupported or already active features
 */
WpObjectFeatures
wp_feature_activation_transition_get_requested_features (
    WpFeatureActivationTransition * self)
{
  return GPOINTER_TO_UINT (wp_transition_get_data (WP_TRANSITION (self)));
}

/*! \defgroup wpobject WpObject */
/*!
 * \struct WpObject
 *
 * Base class for objects that have activatable features.
 *
 * \gproperties
 *
 * \gproperty{core, WpCore *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The WpCore associated with this object}
 *
 * \gproperty{active-features, guint (WpObjectFeatures), G_PARAM_READABLE,
 *   The active WpObjectFeatures on this proxy}
 *
 * \gproperty{supported-features, guint (WpObjectFeatures), G_PARAM_READABLE,
 *   The supported WpObjectFeatures on this proxy}
 */
typedef struct _WpObjectPrivate WpObjectPrivate;
struct _WpObjectPrivate
{
  /* properties */
  guint id;
  GWeakRef core;

  /* features state */
  WpObjectFeatures ft_active;
  GQueue *transitions; // element-type: WpFeatureActivationTransition*
  GSource *idle_advnc_source;
  GWeakRef ongoing_transition;
};

enum {
  PROP_0,
  PROP_ID,
  PROP_CORE,
  PROP_ACTIVE_FEATURES,
  PROP_SUPPORTED_FEATURES,
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpObject, wp_object, G_TYPE_OBJECT)

static guint
get_next_id ()
{
  static guint next_id = 0;
  g_atomic_int_inc (&next_id);
  return next_id;
}

static void
wp_object_init (WpObject * self)
{
  WpObjectPrivate *priv = wp_object_get_instance_private (self);

  priv->id = get_next_id ();
  g_weak_ref_init (&priv->core, NULL);
  priv->transitions = g_queue_new ();
  g_weak_ref_init (&priv->ongoing_transition, NULL);
}

static void
wp_object_dispose (GObject * object)
{
  WpObject *self = WP_OBJECT (object);
  WpObjectPrivate *priv = wp_object_get_instance_private (self);

  wp_trace_object (self, "dispose");

  wp_object_deactivate (self, WP_OBJECT_FEATURES_ALL);

  if (priv->idle_advnc_source)
    g_source_destroy (priv->idle_advnc_source);

  G_OBJECT_CLASS (wp_object_parent_class)->dispose (object);
}

static void
wp_object_finalize (GObject * object)
{
  WpObject *self = WP_OBJECT (object);
  WpObjectPrivate *priv = wp_object_get_instance_private (self);

  /* there should be no transitions, since transitions hold a ref on WpObject */
  g_warn_if_fail (g_queue_is_empty (priv->transitions));
  g_clear_pointer (&priv->transitions, g_queue_free);
  g_clear_pointer (&priv->idle_advnc_source, g_source_unref);
  g_weak_ref_clear (&priv->ongoing_transition);
  g_weak_ref_clear (&priv->core);

  /* everything must have been deactivated in dispose() */
  g_warn_if_fail (priv->ft_active == 0);

  G_OBJECT_CLASS (wp_object_parent_class)->finalize (object);
}

static void
wp_object_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpObject *self = WP_OBJECT (object);
  WpObjectPrivate *priv = wp_object_get_instance_private (self);

  switch (property_id) {
  case PROP_CORE:
    g_weak_ref_set (&priv->core, g_value_get_object (value));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_object_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpObject *self = WP_OBJECT (object);
  WpObjectPrivate *priv = wp_object_get_instance_private (self);

  switch (property_id) {
  case PROP_ID:
    g_value_set_uint (value, priv->id);
    break;
  case PROP_CORE:
    g_value_take_object (value, wp_object_get_core (self));
    break;
  case PROP_ACTIVE_FEATURES:
    g_value_set_uint (value, priv->ft_active);
    break;
  case PROP_SUPPORTED_FEATURES:
    g_value_set_uint (value, wp_object_get_supported_features (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_object_class_init (WpObjectClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->dispose = wp_object_dispose;
  object_class->finalize = wp_object_finalize;
  object_class->get_property = wp_object_get_property;
  object_class->set_property = wp_object_set_property;

  g_object_class_install_property (object_class, PROP_ID,
      g_param_spec_uint ("id", "id",
          "The object unique id", 0, G_MAXUINT, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_CORE,
      g_param_spec_object ("core", "core", "The WpCore", WP_TYPE_CORE,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_ACTIVE_FEATURES,
      g_param_spec_uint ("active-features", "active-features",
          "The active WpObjectFeatures on this proxy", 0, G_MAXUINT, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_SUPPORTED_FEATURES,
      g_param_spec_uint ("supported-features", "supported-features",
          "The supported WpObjectFeatures on this proxy", 0, G_MAXUINT, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Gets the unique wireplumber Id of this object
 * \ingroup wpsessionitem
 * \param self the session item
 */
guint
wp_object_get_id (WpObject * self)
{
  g_return_val_if_fail (WP_IS_OBJECT (self), SPA_ID_INVALID);

  WpObjectPrivate *priv = wp_object_get_instance_private (self);
  return priv->id;
}

/*!
 * \brief Gets the core associated with this object.
 *
 * \ingroup wpobject
 * \param self the object
 * \returns (transfer full): the core associated with this object
 */
WpCore *
wp_object_get_core (WpObject * self)
{
  g_return_val_if_fail (WP_IS_OBJECT (self), NULL);

  WpObjectPrivate *priv = wp_object_get_instance_private (self);
  WpCore *core = g_weak_ref_get (&priv->core);
  if (!core && WP_IS_CORE (self))
    core = WP_CORE (g_object_ref (self));
  return core;
}

/*!
 * \brief Gets the active features of this object.
 * \ingroup wpobject
 * \param self the object
 * \returns A bitset containing the active features of this object
 */
WpObjectFeatures
wp_object_get_active_features (WpObject * self)
{
  g_return_val_if_fail (WP_IS_OBJECT (self), 0);

  WpObjectPrivate *priv = wp_object_get_instance_private (self);
  return priv->ft_active;
}

/*!
 * \brief Checks if the given features are active on this object.
 * \param self the object
 * \param features the features to check
 * \returns TRUE if all the given features are active on this object
 * \ingroup wpobject
 * \since 0.5.0
 */
gboolean
wp_object_test_active_features (WpObject * self, WpObjectFeatures features)
{
  g_return_val_if_fail (WP_IS_OBJECT (self), FALSE);

  WpObjectPrivate *priv = wp_object_get_instance_private (self);
  return (priv->ft_active & features) == features;
}

/*!
 * \brief Gets the supported features of this object.
 * \ingroup wpobject
 * \param self the object
 * \returns A bitset containing the supported features of this object;
 *   note that supported features may change at runtime
 */
WpObjectFeatures
wp_object_get_supported_features (WpObject * self)
{
  g_return_val_if_fail (WP_IS_OBJECT (self), 0);
  g_return_val_if_fail (WP_OBJECT_GET_CLASS (self)->get_supported_features, 0);

  return WP_OBJECT_GET_CLASS (self)->get_supported_features (self);
}

/*!
 * \brief Checks if the given features are supported on this object.
 * \param self the object
 * \param features the features to check
 * \returns TRUE if all the given features are supported on this object
 * \ingroup wpobject
 * \since 0.5.0
 */
gboolean
wp_object_test_supported_features (WpObject * self, WpObjectFeatures features)
{
  return (wp_object_get_supported_features (self) & features) == features;
}

static gboolean
wp_object_advance_transitions (WpObject * self)
{
  WpObjectPrivate *priv = wp_object_get_instance_private (self);
  g_autoptr (WpTransition) t = NULL;

  /* clear before advancing; a transition may need to schedule
     a new call to wp_object_advance_transitions() */
  g_clear_pointer (&priv->idle_advnc_source, g_source_unref);

  /* advance ongoing transition if any */
  t = g_weak_ref_get (&priv->ongoing_transition);
  if (t) {
    wp_transition_advance (t);
    if (!wp_transition_get_completed (t))
      return G_SOURCE_REMOVE;
  }

  /* set next transition and advance */
  if (!g_queue_is_empty (priv->transitions)) {
    WpTransition *next = g_queue_pop_head (priv->transitions);
    g_weak_ref_set (&priv->ongoing_transition, next);
    wp_transition_advance (next);
  }

  return G_SOURCE_REMOVE;
}

static void
on_transition_completed (WpTransition * transition, GParamSpec * param,
    WpObject * self)
{
  WpObjectPrivate *priv = wp_object_get_instance_private (self);

  /* abort activation if a transition failed */
  if (wp_transition_had_error (transition)) {
    wp_object_abort_activation (self, "a transition failed");
    return;
  }

  /* advance pending transitions */
  if (!g_queue_is_empty (priv->transitions) && !priv->idle_advnc_source) {
    g_autoptr (WpCore) core = wp_object_get_core (self);
    g_return_if_fail (core != NULL);

    wp_core_idle_add (core, &priv->idle_advnc_source,
        G_SOURCE_FUNC (wp_object_advance_transitions), g_object_ref (self),
        g_object_unref);
  }
}

/*!
 * \brief Callback version of wp_object_activate_closure()
 *
 * \ingroup wpobject
 * \param self the object
 * \param features the features to enable
 * \param cancellable (nullable): a cancellable for the async operation
 * \param callback (scope async): a function to call when activation is complete
 * \param user_data (closure): data for \a callback
 */
void
wp_object_activate (WpObject * self,
    WpObjectFeatures features, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer user_data)
{
  g_return_if_fail (WP_IS_OBJECT (self));

  GClosure *closure = g_cclosure_new (G_CALLBACK (callback), user_data, NULL);

  wp_object_activate_closure (self, features, cancellable, closure);
}

/*!
 * \brief Activates the requested \a features and invokes \a closure when this
 * is done. \a features may contain unsupported or already active features.
 * The operation will filter them and activate only ones that are supported and
 * inactive.
 *
 * If multiple calls to this method is done, the operations will be executed
 * one after the other to ensure features only get activated once.
 *
 * \note \a closure may be invoked in sync while this method is being called,
 * if there are no features to activate.
 *
 * \ingroup wpobject
 * \param self the object
 * \param features the features to enable
 * \param cancellable (nullable): a cancellable for the async operation
 * \param closure (transfer full): the closure to use when activation is completed
 */
void
wp_object_activate_closure (WpObject * self,
    WpObjectFeatures features, GCancellable * cancellable,
    GClosure *closure)
{
  g_return_if_fail (WP_IS_OBJECT (self));

  WpObjectPrivate *priv = wp_object_get_instance_private (self);
  g_autoptr (WpCore) core = wp_object_get_core (self);

  g_return_if_fail (core != NULL);

  WpTransition *transition = wp_transition_new_closure (
      WP_TYPE_FEATURE_ACTIVATION_TRANSITION, self, cancellable, closure);
  wp_transition_set_source_tag (transition, wp_object_activate);
  wp_transition_set_data (transition, GUINT_TO_POINTER (features), NULL);
  g_signal_connect_object (transition, "notify::completed",
      G_CALLBACK (on_transition_completed), self, 0);

  g_queue_push_tail (priv->transitions, transition);

  if (!priv->idle_advnc_source) {
    wp_core_idle_add (core, &priv->idle_advnc_source,
        G_SOURCE_FUNC (wp_object_advance_transitions), g_object_ref (self),
        g_object_unref);
  }
}

/*!
 * \brief Finishes the async operation that was started with wp_object_activate()
 *
 * \ingroup wpobject
 * \param self the object
 * \param res the async operation result
 * \param error (out) (optional): the error of the operation, if any
 * \returns TRUE if the requested features were activated,
 *   FALSE if there was an error
 */
gboolean
wp_object_activate_finish (WpObject * self, GAsyncResult * res, GError ** error)
{
  g_return_val_if_fail (WP_IS_OBJECT (self), FALSE);
  g_return_val_if_fail (
      g_async_result_is_tagged (res, wp_object_activate), FALSE);
  return wp_transition_finish (res, error);
}

/*!
 * \brief Deactivates the given \a features, leaving the object in the state
 * it was before they were enabled.
 *
 * This is seldom needed to call manually, but it can be used to save
 * resources if some features are no longer needed.
 *
 * \ingroup wpobject
 * \param self the object
 * \param features the features to deactivate
 */
void
wp_object_deactivate (WpObject * self, WpObjectFeatures features)
{
  g_return_if_fail (WP_IS_OBJECT (self));
  g_return_if_fail (WP_OBJECT_GET_CLASS (self)->deactivate);

  WpObjectPrivate *priv = wp_object_get_instance_private (self);
  WP_OBJECT_GET_CLASS (self)->deactivate (self, features & priv->ft_active);
}

/*!
 * \brief Aborts the current object activation by returning a transition error
 * if any transitions are pending.
 *
 * This is usually used to stop any pending activation if an error happened.
 *
 * \ingroup wpobject
 * \param self the object
 * \param msg the message used in the transition error
 * \since 0.4.6
 */
void
wp_object_abort_activation (WpObject * self, const gchar *msg)
{
  WpObjectPrivate *priv;
  g_autoptr (WpTransition) t = NULL;

  g_return_if_fail (WP_IS_OBJECT (self));

  priv =  wp_object_get_instance_private (self);

  g_clear_pointer (&priv->idle_advnc_source, g_source_unref);

  /* abort ongoing transition if any */
  t = g_weak_ref_get (&priv->ongoing_transition);
  if (t && !wp_transition_get_completed (t)) {
    wp_transition_return_error (t, g_error_new (WP_DOMAIN_LIBRARY,
            WP_LIBRARY_ERROR_OPERATION_FAILED,
            "Object activation aborted: %s", msg));
    return;
  }

  /* recursively abort the queued transitions if any */
  if (!g_queue_is_empty (priv->transitions)) {
    WpTransition *next = g_queue_pop_head (priv->transitions);
    g_weak_ref_set (&priv->ongoing_transition, next);
    wp_object_abort_activation (self, msg);
  }
}

/*!
 * \brief Allows subclasses to update the currently active features.
 *
 * \a activated should contain new features and \a deactivated
 * should contain features that were just deactivated.
 * Calling this method also advances the activation transitions.
 *
 * \remark Private method to be called by subclasses only.
 *
 * \ingroup wpobject
 * \param self the object
 * \param activated the features that were activated, or 0
 * \param deactivated the features that were deactivated, or 0
 */
void
wp_object_update_features (WpObject * self, WpObjectFeatures activated,
    WpObjectFeatures deactivated)
{
  g_return_if_fail (WP_IS_OBJECT (self));

  WpObjectPrivate *priv = wp_object_get_instance_private (self);
  guint old_ft = priv->ft_active;
  g_autoptr (WpTransition) t = NULL;

  priv->ft_active |= activated;
  priv->ft_active &= ~deactivated;

  if (priv->ft_active != old_ft) {
    wp_debug_object (self, "features changed 0x%x -> 0x%x", old_ft,
        priv->ft_active);
    g_object_notify (G_OBJECT (self), "active-features");
  }

  t = g_weak_ref_get (&priv->ongoing_transition);
  if ((t || !g_queue_is_empty (priv->transitions)) && !priv->idle_advnc_source) {
    g_autoptr (WpCore) core = wp_object_get_core (self);
    g_return_if_fail (core != NULL);

    wp_core_idle_add (core, &priv->idle_advnc_source,
        G_SOURCE_FUNC (wp_object_advance_transitions), g_object_ref (self),
        g_object_unref);
  }
}
070701000000A3000081A400000000000000000000000165F8630400000DC7000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/object.h /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_OBJECT_H__
#define __WIREPLUMBER_OBJECT_H__

#include "transition.h"

G_BEGIN_DECLS

typedef struct _WpCore WpCore;

/*!
 * \brief Flags that specify functionality that is available on this class.
 *
 * Use wp_object_activate() to enable more features,
 * wp_object_get_supported_features() to see which features are supported and
 * wp_object_get_active_features() to find out which features are already
 * enabled. Features can also be deactivated later using wp_object_deactivate().
 *
 * Actual feature flags are to be specified by subclasses and their interfaces.
 * WP_OBJECT_FEATURES_ALL is a special value that can be used to activate
 * all the supported features in any given object.
 *
 * \ingroup wpobject
 */
typedef guint WpObjectFeatures;

/*!
 * \brief Special value that can be used to activate
 * all the supported features in any given object.
 * \ingroup wpobject
 */
static const WpObjectFeatures WP_OBJECT_FEATURES_ALL = 0xffffffff;

/*!
 * \brief The WpFeatureActivationTransition GType
 * \ingroup wpfeatureactivationtransition
 */
#define WP_TYPE_FEATURE_ACTIVATION_TRANSITION \
    (wp_feature_activation_transition_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpFeatureActivationTransition,
                      wp_feature_activation_transition,
                      WP, FEATURE_ACTIVATION_TRANSITION, WpTransition)

WP_API
WpObjectFeatures wp_feature_activation_transition_get_requested_features (
    WpFeatureActivationTransition * self);

/*!
 * \brief The WpObject GType
 * \ingroup wpobject
 */
#define WP_TYPE_OBJECT (wp_object_get_type ())
WP_API
G_DECLARE_DERIVABLE_TYPE (WpObject, wp_object, WP, OBJECT, GObject)

struct _WpObjectClass
{
  GObjectClass parent_class;

  WpObjectFeatures (*get_supported_features) (WpObject * self);

  guint (*activate_get_next_step) (WpObject * self,
      WpFeatureActivationTransition * transition, guint step,
      WpObjectFeatures missing);
  void (*activate_execute_step) (WpObject * self,
      WpFeatureActivationTransition * transition, guint step,
      WpObjectFeatures missing);

  void (*deactivate) (WpObject * self, WpObjectFeatures features);

  /*< private >*/
  WP_PADDING(8)
};

WP_API
guint wp_object_get_id (WpObject * self);

WP_API
WpCore * wp_object_get_core (WpObject * self);

WP_API
WpObjectFeatures wp_object_get_active_features (WpObject * self);

WP_API
gboolean wp_object_test_active_features (WpObject * self, WpObjectFeatures features);

WP_API
WpObjectFeatures wp_object_get_supported_features (WpObject * self);

WP_API
gboolean wp_object_test_supported_features (WpObject * self, WpObjectFeatures features);

WP_API
void wp_object_activate (WpObject * self,
    WpObjectFeatures features, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer user_data);

WP_API
void wp_object_activate_closure (WpObject * self,
    WpObjectFeatures features, GCancellable * cancellable, GClosure *closure);

WP_API
gboolean wp_object_activate_finish (WpObject * self, GAsyncResult * res,
    GError ** error);

WP_API
void wp_object_deactivate (WpObject * self, WpObjectFeatures features);

/* for subclasses only */

WP_API
void wp_object_abort_activation (WpObject * self, const gchar *msg);

WP_API
void wp_object_update_features (WpObject * self, WpObjectFeatures activated,
    WpObjectFeatures deactivated);

G_END_DECLS

#endif
 070701000000A4000081A400000000000000000000000165F863040000199F000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/plugin.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "plugin.h"
#include "core.h"
#include "log.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-plugin")

/*! \defgroup wpplugin WpPlugin */
/*!
 * \struct WpPlugin
 *
 * WpPlugin is a base class for objects that provide functionality to the
 * WirePlumber daemon.
 *
 * Typically, a plugin is created within a module and then registered to
 * make it available for use by the daemon. The daemon is responsible for
 * calling wp_object_activate() on it after all modules have been loaded,
 * the core is connected and the initial discovery of global objects is
 * done.
 *
 * Being a WpObject subclass, the plugin inherits WpObject's activation system.
 * For most implementations, there is only need for activating one
 * feature, WP_PLUGIN_FEATURE_ENABLED, and this can be done by implementing
 * only WpPluginClass::enable() and WpPluginClass::disable().
 * For more advanced plugins that need to have more features, you may
 * implement directly the functions of WpObjectClass and ignore the ones of
 * WpPluginClass.
 *
 * \gproperties
 *
 * \gproperty{name, gchar *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The name of this plugin}
 */

enum {
  PROP_0,
  PROP_NAME,
};

typedef struct _WpPluginPrivate WpPluginPrivate;
struct _WpPluginPrivate
{
  GQuark name_quark;
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpPlugin, wp_plugin, WP_TYPE_OBJECT)

static void
wp_plugin_init (WpPlugin * self)
{
}

static void
wp_plugin_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpPlugin *self = WP_PLUGIN (object);
  WpPluginPrivate *priv = wp_plugin_get_instance_private (self);

  switch (property_id) {
  case PROP_NAME:
    priv->name_quark = g_quark_from_string (g_value_get_string (value));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_plugin_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpPlugin *self = WP_PLUGIN (object);
  WpPluginPrivate *priv = wp_plugin_get_instance_private (self);

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, g_quark_to_string (priv->name_quark));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static WpObjectFeatures
wp_plugin_get_supported_features (WpObject * self)
{
  return WP_PLUGIN_FEATURE_ENABLED;
}

enum {
  STEP_ENABLE = WP_TRANSITION_STEP_CUSTOM_START,
};

static guint
wp_plugin_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  /* we only support ENABLED, so this is the only
     feature that can be in @em missing */
  g_return_val_if_fail (missing == WP_PLUGIN_FEATURE_ENABLED,
      WP_TRANSITION_STEP_ERROR);

  return STEP_ENABLE;
}

static void
wp_plugin_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case STEP_ENABLE: {
    WpPlugin *self = WP_PLUGIN (object);
    wp_info_object (self, "enabling plugin '%s'", wp_plugin_get_name (self));
    g_return_if_fail (WP_PLUGIN_GET_CLASS (self)->enable);
    WP_PLUGIN_GET_CLASS (self)->enable (self, WP_TRANSITION (transition));
    break;
  }
  case WP_TRANSITION_STEP_ERROR:
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
wp_plugin_deactivate (WpObject * object, WpObjectFeatures features)
{
  if (features & WP_PLUGIN_FEATURE_ENABLED) {
    WpPlugin *self = WP_PLUGIN (object);
    wp_info_object (self, "disabling plugin '%s'", wp_plugin_get_name (self));
    if (WP_PLUGIN_GET_CLASS (self)->disable)
      WP_PLUGIN_GET_CLASS (self)->disable (self);
    wp_object_update_features (WP_OBJECT (self), 0, WP_PLUGIN_FEATURE_ENABLED);
  }
}

static void
wp_plugin_class_init (WpPluginClass * klass)
{
  GObjectClass * object_class = (GObjectClass *) klass;
  WpObjectClass * wpobject_class = (WpObjectClass *) klass;

  object_class->set_property = wp_plugin_set_property;
  object_class->get_property = wp_plugin_get_property;

  wpobject_class->get_supported_features = wp_plugin_get_supported_features;
  wpobject_class->activate_get_next_step = wp_plugin_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_plugin_activate_execute_step;
  wpobject_class->deactivate = wp_plugin_deactivate;

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name",
          "The name of this plugin", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

static gboolean
find_plugin_func (gpointer plugin, gpointer name_quark)
{
  if (!WP_IS_PLUGIN (plugin))
    return FALSE;

  WpPluginPrivate *priv = wp_plugin_get_instance_private (plugin);
  return priv->name_quark == GPOINTER_TO_UINT (name_quark);
}

/*!
 * \brief Looks up a plugin.
 *
 * \ingroup wpplugin
 * \param core the core
 * \param plugin_name the lookup name
 * \returns (transfer full) (nullable): the plugin matching the lookup name
 */
WpPlugin *
wp_plugin_find (WpCore * core, const gchar * plugin_name)
{
  g_return_val_if_fail (WP_IS_CORE (core), NULL);

  GQuark q = g_quark_try_string (plugin_name);
  if (q == 0)
    return NULL;
  GObject *p = wp_core_find_object (core,
      (GEqualFunc) find_plugin_func, GUINT_TO_POINTER (q));
  return p ? WP_PLUGIN (p) : NULL;
}

/*!
 * \brief Retreives the name of a plugin.
 *
 * \ingroup wpplugin
 * \param self the plugin
 * \returns the name of this plugin
 */
const gchar *
wp_plugin_get_name (WpPlugin * self)
{
  g_return_val_if_fail (WP_IS_PLUGIN (self), NULL);

  WpPluginPrivate *priv = wp_plugin_get_instance_private (self);
  return g_quark_to_string (priv->name_quark);
}

/**
 * \var _WpPluginClass::enable
 *
 * \brief Enables the plugin. The plugin is required to start any operations
 * only when this method is called and not before.
 *
 * When enabling the plugin is done, you must call wp_object_update_features()
 * with WP_PLUGIN_FEATURE_ENABLED marked as activated, or return an error
 * on \a transition.
 *
 * \param self the plugin
 * \param transition the activation transition
 */

/**
 * \var _WpPluginClass::disable
 *
 * \brief Disables the plugin. The plugin is required to stop all operations
 * and release all resources associated with it.
 *
 * \param self the plugin
 */
 070701000000A5000081A400000000000000000000000165F86304000003F9000000000000000000000000000000000000002200000000wireplumber-0.5.0/lib/wp/plugin.h /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_PLUGIN_H__
#define __WIREPLUMBER_PLUGIN_H__

#include "object.h"

G_BEGIN_DECLS

/*!
 * \brief Flags to be used as WpObjectFeatures on WpPlugin subclasses.
 * \ingroup wpplugin
 */
typedef enum { /*< flags >*/
  /*! enables the plugin */
  WP_PLUGIN_FEATURE_ENABLED = (1 << 0),
} WpPluginFeatures;

/*!
 * \brief The WpPlugin GType
 * \ingroup wpplugin
 */
#define WP_TYPE_PLUGIN (wp_plugin_get_type ())
WP_API
G_DECLARE_DERIVABLE_TYPE (WpPlugin, wp_plugin, WP, PLUGIN, WpObject)

struct _WpPluginClass
{
  WpObjectClass parent_class;

  void (*enable) (WpPlugin * self, WpTransition * transition);
  void (*disable) (WpPlugin * self);

  /*< private >*/
  WP_PADDING(6)
};

WP_API
WpPlugin * wp_plugin_find (WpCore * core, const gchar * plugin_name);

WP_API
const gchar * wp_plugin_get_name (WpPlugin * self);

G_END_DECLS

#endif
   070701000000A6000081A400000000000000000000000165F86304000011AD000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/port.c   /* WirePlumber
 *
 * Copyright © 2019-2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "port.h"
#include "log.h"
#include "private/pipewire-object-mixin.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-port")

/*! \defgroup wpport WpPort */
/*!
 * \struct WpPort
 *
 * The WpPort class allows accessing the properties
 * and methods of a PipeWire port object (`struct pw_port`).
 *
 * A WpPort is constructed internally when a new port appears
 * on the PipeWire registry and it is made available through the
 * WpObjectManager API.
 */

struct _WpPort
{
  WpGlobalProxy parent;
};

static void wp_port_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface);

G_DEFINE_TYPE_WITH_CODE (WpPort, wp_port, WP_TYPE_GLOBAL_PROXY,
    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
        wp_pw_object_mixin_object_interface_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
        wp_port_pw_object_mixin_priv_interface_init))

static void
wp_port_init (WpPort * self)
{
}

static void
wp_port_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case WP_PW_OBJECT_MIXIN_STEP_BIND:
  case WP_TRANSITION_STEP_ERROR:
    /* base class can handle BIND and ERROR */
    WP_OBJECT_CLASS (wp_port_parent_class)->
        activate_execute_step (object, transition, step, missing);
    break;
  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
    /* just wait, info will be emitted anyway after binding */
    break;
  case WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS:
    wp_pw_object_mixin_cache_params (object, missing);
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
wp_port_deactivate (WpObject * object, WpObjectFeatures features)
{
  wp_pw_object_mixin_deactivate (object, features);
  WP_OBJECT_CLASS (wp_port_parent_class)->deactivate (object, features);
}

static const struct pw_port_events port_events = {
  PW_VERSION_PORT_EVENTS,
  .info = (HandleEventInfoFunc(port)) wp_pw_object_mixin_handle_event_info,
  .param = wp_pw_object_mixin_handle_event_param,
};

static void
wp_port_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
      port, &port_events);
}

static void
wp_port_pw_proxy_destroyed (WpProxy * proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);

  WP_PROXY_CLASS (wp_port_parent_class)->pw_proxy_destroyed (proxy);
}

static void
wp_port_class_init (WpPortClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->get_property = wp_pw_object_mixin_get_property;

  wpobject_class->get_supported_features =
      wp_pw_object_mixin_get_supported_features;
  wpobject_class->activate_get_next_step =
      wp_pw_object_mixin_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_port_activate_execute_step;
  wpobject_class->deactivate = wp_port_deactivate;

  proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Port;
  proxy_class->pw_iface_version = PW_VERSION_PORT;
  proxy_class->pw_proxy_created = wp_port_pw_proxy_created;
  proxy_class->pw_proxy_destroyed = wp_port_pw_proxy_destroyed;

  wp_pw_object_mixin_class_override_properties (object_class);
}

static gint
wp_port_enum_params (gpointer instance, guint32 id,
    guint32 start, guint32 num, WpSpaPod *filter)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  return pw_port_enum_params (d->iface, 0, id, start, num,
      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
}

static void
wp_port_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface)
{
  wp_pw_object_mixin_priv_interface_info_init (iface, port, PORT);
  iface->enum_params = wp_port_enum_params;
}

/*!
 * \brief Gets the current direction of the port
 * \remarks Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 * \ingroup wpport
 * \param self the port
 * \returns the current direction of the port
 */
WpDirection
wp_port_get_direction (WpPort * self)
{
  g_return_val_if_fail (WP_IS_PORT (self), 0);
  g_return_val_if_fail (wp_object_test_active_features (WP_OBJECT (self),
          WP_PIPEWIRE_OBJECT_FEATURE_INFO), 0);

  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
  const struct pw_port_info *info = d->info;

  return (WpDirection) info->direction;
}
   070701000000A7000081A400000000000000000000000165F86304000002DB000000000000000000000000000000000000002000000000wireplumber-0.5.0/lib/wp/port.h   /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_PORT_H__
#define __WIREPLUMBER_PORT_H__

#include "global-proxy.h"

G_BEGIN_DECLS

/*!
 * \brief The different directions that a port can have
 * \ingroup wpport
 */
typedef enum {
  WP_DIRECTION_INPUT, /*!< a sink, consuming input */
  WP_DIRECTION_OUTPUT, /*!< a source, producing output */
} WpDirection;

/*!
 * \brief The WpPort GType
 * \ingroup wpport
 */
#define WP_TYPE_PORT (wp_port_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpPort, wp_port, WP, PORT, WpGlobalProxy)

WP_API
WpDirection wp_port_get_direction (WpPort * self);

G_END_DECLS

#endif
 070701000000A8000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002100000000wireplumber-0.5.0/lib/wp/private  070701000000A9000081A400000000000000000000000165F8630400006FE1000000000000000000000000000000000000003800000000wireplumber-0.5.0/lib/wp/private/internal-comp-loader.c   /* WirePlumber
 *
 * Copyright © 2023 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "internal-comp-loader.h"
#include "wp.h"
#include "registry.h"

#include <pipewire/impl.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-internal-comp-loader")

/*** ComponentData ***/

typedef enum {
  FEATURE_STATE_DISABLED,
  FEATURE_STATE_OPTIONAL,
  FEATURE_STATE_REQUIRED
} FeatureState;

typedef struct _ComponentData ComponentData;
struct _ComponentData
{
  grefcount ref;
  /* an identifier for this component that is understandable by the end user */
  gchar *printable_id;
  /* the provided feature name (points to same storage as the id) or NULL */
  gchar *provides;
  /* the original state of the feature (required / optional / disabled) */
  FeatureState state;

  /* other fields extracted as-is from the json description */
  gchar *name;
  gchar *type;
  WpSpaJson *arguments;
  GPtrArray *requires;  /* value-type: string (owned) */
  GPtrArray *wants;     /* value-type: string (owned) */

  /* TRUE when the component is in the final sorted list */
  gboolean visited;
  /* one of the components that requires this one with a strong
    dependency chain (i.e. there is a required component that requires
    this one, directly or indirectly) */
  ComponentData *required_by;
};

static void component_data_free (ComponentData * self);

static ComponentData *
component_data_ref (ComponentData *self)
{
  g_ref_count_inc (&self->ref);
  return self;
}

static void
component_data_unref (ComponentData *self)
{
  if (self && g_ref_count_dec (&self->ref))
    component_data_free (self);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (ComponentData, component_data_unref)

static FeatureState
get_feature_state (WpProperties * dict, const gchar * feature)
{
  const gchar *value = wp_properties_get (dict, feature);

  if (!value || g_str_equal (value, "optional"))
    return FEATURE_STATE_OPTIONAL;
  else if (g_str_equal (value, "required"))
    return FEATURE_STATE_REQUIRED;
  else if (g_str_equal (value, "disabled"))
    return FEATURE_STATE_DISABLED;
  else {
    wp_warning ("invalid feature state '%s' specified in configuration for '%s'",
        value, feature);
    wp_warning ("considering '%s' to be optional", feature);
    return FEATURE_STATE_OPTIONAL;
  }
}

static gboolean
component_rule_match_cb (gpointer data, const gchar * action, WpSpaJson * value,
    GError ** error)
{
  WpProperties *props = data;
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;
  gboolean merge;

  if (!wp_spa_json_is_object (value)) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "expected JSON object instead of: %.*s", (int) wp_spa_json_get_size (value),
        wp_spa_json_get_data (value));
    return FALSE;
  }

  if (g_str_equal (action, "merge")) {
    merge = TRUE;
  } else if (g_str_equal (action, "override")) {
    merge = FALSE;
  } else {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "invalid action '%s' in component rules", action);
    return FALSE;
  }

  it = wp_spa_json_new_iterator (value);

  do {
    g_autofree gchar *key = NULL;
    g_autofree gchar *val = NULL;
    const gchar *old_val = NULL;

    /* extract key */
    if (!wp_iterator_next (it, &item))
      break;
    key = wp_spa_json_to_string (g_value_get_boxed (&item));
    g_value_unset (&item);

    /* extract value */
    if (!wp_iterator_next (it, &item)) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
          "expected value for key '%s' in component rules", key);
      return FALSE;
    }
    val = wp_spa_json_to_string (g_value_get_boxed (&item));
    g_value_unset (&item);

    old_val = wp_properties_get (props, key);

    /* override if not merging or if the value is not a container */
    if (!merge || !old_val || (*old_val != '[' && *old_val != '{')) {
      wp_properties_set (props, key, val);
    }
    else {
      g_autoptr (WpSpaJson) old_json = NULL;
      g_autoptr (WpSpaJson) new_json = NULL;
      g_autoptr (WpSpaJson) merged_json = NULL;

      old_json = wp_spa_json_new_wrap_string (old_val);
      new_json = wp_spa_json_new_wrap_string (val);
      merged_json = wp_json_utils_merge_containers (old_json, new_json);
      wp_properties_set (props, key,
          merged_json ? wp_spa_json_get_data (merged_json) : val);
    }
  } while (TRUE);

  return TRUE;
}

static ComponentData *
component_data_new_from_json (WpSpaJson * json, WpProperties * features,
    WpSpaJson * rules, GError ** error)
{
  g_autoptr (ComponentData) comp = NULL;
  g_autoptr (WpProperties) props = NULL;
  const gchar *str;

  if (!wp_spa_json_is_object (json)) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "expected JSON object instead of: %.*s", (int) wp_spa_json_get_size (json),
        wp_spa_json_get_data (json));
    return NULL;
  }

  comp = g_new0 (ComponentData, 1);
  g_ref_count_init (&comp->ref);
  comp->requires = g_ptr_array_new_with_free_func (g_free);
  comp->wants = g_ptr_array_new_with_free_func (g_free);

  props = wp_properties_new_json (json);
  if (rules && !wp_json_utils_match_rules (rules, props, component_rule_match_cb,
          props, error))
    return NULL;

  if (!(comp->type = g_strdup (wp_properties_get (props, "type")))) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "component 'type' is required at: %.*s", (int) wp_spa_json_get_size (json),
        wp_spa_json_get_data (json));
    return NULL;
  }

  comp->name = g_strdup (wp_properties_get (props, "name"));
  str = wp_properties_get (props, "arguments");
  comp->arguments = str ? wp_spa_json_new_from_string (str) : NULL;

  if ((str = wp_properties_get (props, "provides"))) {
    comp->provides = g_strdup (str);
    comp->state = get_feature_state (features, comp->provides);
    if (comp->name) {
      comp->printable_id =
          g_strdup_printf ("%s [%s: %s]", comp->provides, comp->type, comp->name);
    } else {
      comp->printable_id = g_strdup_printf ("%s [%s]", comp->provides, comp->type);
    }
  } else {
    comp->provides = NULL;
    comp->state = FEATURE_STATE_REQUIRED;
    comp->printable_id = g_strdup_printf ("[%s: %s]", comp->type, comp->name);
  }

  if ((str = wp_properties_get (props, "requires"))) {
    g_autoptr (WpSpaJson) comp_reqs = wp_spa_json_new_wrap_string (str);
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_reqs);
    g_auto (GValue) item = G_VALUE_INIT;

    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaJson *dep = g_value_get_boxed (&item);
      g_ptr_array_add (comp->requires, wp_spa_json_to_string (dep));
    }
  }

  if ((str = wp_properties_get (props, "wants"))) {
    g_autoptr (WpSpaJson) comp_wants = wp_spa_json_new_wrap_string (str);
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_wants);
    g_auto (GValue) item = G_VALUE_INIT;

    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaJson *dep = g_value_get_boxed (&item);
      g_ptr_array_add (comp->wants, wp_spa_json_to_string (dep));
    }
  }

  return g_steal_pointer (&comp);
}

static void
component_data_free (ComponentData * self)
{
  g_clear_pointer (&self->provides, g_free);
  g_clear_pointer (&self->printable_id, g_free);
  g_clear_pointer (&self->name, g_free);
  g_clear_pointer (&self->type, g_free);
  g_clear_pointer (&self->arguments, wp_spa_json_unref);
  g_clear_pointer (&self->requires, g_ptr_array_unref);
  g_clear_pointer (&self->wants, g_ptr_array_unref);
  g_free (self);
}

/*** WpComponentArrayLoadTask ***/

struct _WpComponentArrayLoadTask
{
  WpTransition parent;
  /* the input json object */
  WpSpaJson *json;
  /* the features profile */
  WpProperties *profile;
  /* the rules to apply on each component description */
  WpSpaJson *rules;
  /* all components that provide a feature; key: comp->provides, value: comp */
  GHashTable *feat_components;
  /* the final sorted list of components to load */
  GPtrArray *components;
  /* iterator in the components array above */
  ComponentData **components_iter;
  /* the current component being loaded */
  ComponentData *curr_component;
};

enum {
  STEP_PARSE = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_GET_NEXT,
  STEP_LOAD_NEXT,
};

G_DECLARE_FINAL_TYPE (WpComponentArrayLoadTask, wp_component_array_load_task,
                      WP, COMPONENT_ARRAY_LOAD_TASK, WpTransition)
G_DEFINE_TYPE (WpComponentArrayLoadTask, wp_component_array_load_task,
               WP_TYPE_TRANSITION)

static void
wp_component_array_load_task_init (WpComponentArrayLoadTask * self)
{
}

static guint
wp_component_array_load_task_get_next_step (WpTransition * transition, guint step)
{
  WpComponentArrayLoadTask *self = WP_COMPONENT_ARRAY_LOAD_TASK (transition);

  switch (step) {
  case WP_TRANSITION_STEP_NONE:     return STEP_PARSE;
  case STEP_PARSE:                  return STEP_GET_NEXT;
  case STEP_GET_NEXT:
    return (self->curr_component) ? STEP_LOAD_NEXT : WP_TRANSITION_STEP_NONE;
  case STEP_LOAD_NEXT:              return STEP_GET_NEXT;
  default:
    g_return_val_if_reached (WP_TRANSITION_STEP_ERROR);
  }
}

static gchar *
print_dep_chain (ComponentData *comp)
{
  GString *str = g_string_new (NULL);

  while (comp->required_by) {
    comp = comp->required_by;
    g_string_prepend (str, comp->printable_id);
    if (comp->required_by)
      g_string_prepend (str, " -> ");
  }
  return g_string_free (str, FALSE);
}

static gboolean
add_component (ComponentData * comp, gboolean strongly_required,
    WpComponentArrayLoadTask * self, GError ** error)
{
  if (comp->visited || comp->state == FEATURE_STATE_DISABLED)
    return TRUE;

  comp->visited = TRUE;

  /* recursively visit all the required features */
  for (guint i = 0; i < comp->requires->len; i++) {
    const gchar *dependency = g_ptr_array_index (comp->requires, i);
    ComponentData *req_comp =
        g_hash_table_lookup (self->feat_components, dependency);
    if (!req_comp) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
          "no component provides '%s', required by '%s'", dependency,
          comp->printable_id);
      return FALSE;
    }

    /* make a note if there is a strong dependency chain */
    if (strongly_required && !req_comp->required_by) {
      if (req_comp->state == FEATURE_STATE_OPTIONAL) {
        req_comp->required_by = comp;
      }
      else if (req_comp->state == FEATURE_STATE_DISABLED) {
        g_autofree gchar *dep_chain = print_dep_chain (comp);
        g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
            "component '%s' is disabled, required by %s",
            req_comp->printable_id, dep_chain);
        return FALSE;
      }
    }

    if (!add_component (req_comp, strongly_required, self, error))
      return FALSE;
  }

  /* recursively visit all the optionally wanted features */
  for (guint i = 0; i < comp->wants->len; i++) {
    const gchar *dependency = g_ptr_array_index (comp->wants, i);
    ComponentData *wanted_comp =
        g_hash_table_lookup (self->feat_components, dependency);
    if (!wanted_comp) {
      /* in theory we could ignore this, but it's most likely a typo,
         so let's be strict about it and let the user correct it */
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
          "no component provides '%s', wanted by '%s'", dependency,
          comp->printable_id);
      return FALSE;
    }
    if (!add_component (wanted_comp, FALSE, self, error))
      return FALSE;
  }

  /* append component to the sorted list after all its dependencies */
  g_ptr_array_add (self->components, component_data_ref (comp));
  return TRUE;
}

static gboolean
parse_components (WpComponentArrayLoadTask * self, GError ** error)
{
  /* all the parsed components that are explicitly required */
  g_autoptr (GPtrArray) required_components = NULL;
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;

  if (!wp_spa_json_is_array (self->json)) {
    g_set_error (error,
        WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "components section is not a JSON array");
    return FALSE;
  }

  self->feat_components = g_hash_table_new_full (g_str_hash, g_str_equal,
      NULL, (GDestroyNotify) component_data_unref);
  self->components = g_ptr_array_new_with_free_func (
      (GDestroyNotify) component_data_unref);
  required_components = g_ptr_array_new_with_free_func (
      (GDestroyNotify) component_data_unref);

  /* first parse each component from its json description */
  it = wp_spa_json_new_iterator (self->json);
  for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
    WpSpaJson *cjson = g_value_get_boxed (&item);
    GError *e = NULL;
    g_autoptr (ComponentData) comp = NULL;

    if (!(comp = component_data_new_from_json (cjson, self->profile, self->rules, &e))) {
      g_propagate_error (error, e);
      return FALSE;
    }

    if (comp->state == FEATURE_STATE_REQUIRED)
      g_ptr_array_add (required_components, component_data_ref (comp));

    if (comp->provides)
      g_hash_table_insert (self->feat_components, comp->provides,
          component_data_ref (comp));
  }

  /* topological sorting based on depth-first search */
  for (guint i = 0; i < required_components->len; i++) {
    ComponentData *comp = g_ptr_array_index (required_components, i);
    GError *e = NULL;
    if (!add_component (comp, TRUE, self, &e)) {
      g_propagate_error (error, e);
      return FALSE;
    }
  }

  /* terminate the array with NULL */
  g_ptr_array_add (self->components, NULL);

  /* clear feat_components, they are no longer needed */
  g_clear_pointer (&self->feat_components, g_hash_table_unref);
  return TRUE;
}

static void
on_component_loaded (WpCore *core, GAsyncResult *res, gpointer data)
{
  WpComponentArrayLoadTask *self = WP_COMPONENT_ARRAY_LOAD_TASK (data);
  g_autoptr (GError) error = NULL;

  g_return_if_fail (self->curr_component);

  if (!wp_core_load_component_finish (core, res, &error)) {
    // if it was required, fail
    if (self->curr_component->state == FEATURE_STATE_REQUIRED) {
      wp_transition_return_error (WP_TRANSITION (self), g_error_new (
          WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
          "failed to load required component '%s': %s",
          self->curr_component->printable_id, error->message));
      return;
    }
    // if it was optional, check if strongly_required
    else if (self->curr_component->state == FEATURE_STATE_OPTIONAL &&
             self->curr_component->required_by) {
      g_autofree gchar *dep_chain = print_dep_chain (self->curr_component);
      wp_transition_return_error (WP_TRANSITION (self), g_error_new (
          WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
          "failed to load component '%s' (required by %s): %s",
          self->curr_component->printable_id, dep_chain, error->message));
      return;
    }
    else {
      wp_notice_object (core, "optional component '%s' failed to load: %s",
          self->curr_component->printable_id, error->message);
    }
  }

  wp_transition_advance (WP_TRANSITION (self));
}

static void
wp_component_array_load_task_execute_step (WpTransition * transition, guint step)
{
  WpComponentArrayLoadTask *self = WP_COMPONENT_ARRAY_LOAD_TASK (transition);
  WpCore *core = wp_transition_get_data(transition);

  switch (step) {
  case STEP_PARSE: {
    g_autoptr (GError) error = NULL;
    if (parse_components (self, &error)) {
      self->components_iter =
          (ComponentData **) &g_ptr_array_index (self->components, 0);
      wp_transition_advance (transition);
    } else {
      wp_transition_return_error (transition, g_steal_pointer (&error));
    }
    break;
  }
  case STEP_GET_NEXT:
    /* get the next enabled component */
    do {
      self->curr_component = (ComponentData *) *self->components_iter;
      self->components_iter++;
    } while (self->curr_component &&
             self->curr_component->state == FEATURE_STATE_DISABLED);
    wp_transition_advance (transition);
    break;

  case STEP_LOAD_NEXT: {
    /* verify that dependencies have been loaded */
    gboolean dependencies_ok = TRUE;
    for (guint i = 0; i < self->curr_component->requires->len; i++) {
      const gchar *dependency =
          g_ptr_array_index (self->curr_component->requires, i);
      if (!wp_core_test_feature (core, dependency)) {
        dependencies_ok = FALSE;
        break;
      }
    }

    if (!dependencies_ok) {
      /* this component must be optional, because if it wasn't, the dependency
         failing to load would have caused an error earlier */
      g_assert (self->curr_component->state == FEATURE_STATE_OPTIONAL);
      wp_notice_object (core, "skipping component '%s' because some of its "
          "dependencies were not loaded", self->curr_component->printable_id);
      wp_transition_advance (transition);
      return;
    }

    /* Load the component */
    wp_debug_object (self, "loading component '%s'",
        self->curr_component->printable_id);
    wp_core_load_component (core, self->curr_component->name,
        self->curr_component->type, self->curr_component->arguments,
        self->curr_component->provides, NULL,
        (GAsyncReadyCallback) on_component_loaded, self);
    break;
  }
  case WP_TRANSITION_STEP_ERROR:
    break;

  default:
    g_assert_not_reached ();
  }
}

static void
wp_component_array_load_task_finalize (GObject * object)
{
  WpComponentArrayLoadTask *self = WP_COMPONENT_ARRAY_LOAD_TASK (object);

  g_clear_pointer (&self->feat_components, g_hash_table_unref);
  g_clear_pointer (&self->components, g_ptr_array_unref);
  g_clear_pointer (&self->profile, wp_properties_unref);
  g_clear_pointer (&self->rules, wp_spa_json_unref);
  g_clear_pointer (&self->json, wp_spa_json_unref);

  G_OBJECT_CLASS (wp_component_array_load_task_parent_class)->finalize (object);
}

static void
wp_component_array_load_task_class_init (WpComponentArrayLoadTaskClass * klass)
{
  GObjectClass * object_class = (GObjectClass *) klass;
  WpTransitionClass * transition_class = (WpTransitionClass *) klass;

  object_class->finalize = wp_component_array_load_task_finalize;

  transition_class->get_next_step = wp_component_array_load_task_get_next_step;
  transition_class->execute_step = wp_component_array_load_task_execute_step;
}

static WpTransition *
wp_component_array_load_task_new (WpSpaJson * json, WpProperties * profile,
    WpSpaJson * rules, gpointer source_object, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data)
{
  WpTransition *t = wp_transition_new (wp_component_array_load_task_get_type (),
      source_object, cancellable, callback, callback_data);
  WpComponentArrayLoadTask *task = WP_COMPONENT_ARRAY_LOAD_TASK (t);
  task->json = wp_spa_json_ref (json);
  task->profile = wp_properties_ref (profile);
  task->rules = rules ? wp_spa_json_ref (rules) : NULL;
  return t;
}

/*** built-in components ***/

static void
ensure_no_media_session_om_installed (WpObjectManager * om, GTask * task)
{
  if (wp_object_manager_get_n_objects (om) > 0) {
    g_task_return_new_error (task,
        WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "pipewire-media-session appears to be running; "
        "please stop it before starting wireplumber");
    return;
  }
  g_task_return_pointer (task, NULL, NULL);
}

static gboolean
ensure_no_media_session_task_idle (GTask * task)
{
  /* removing this idle source will cause the task to be destroyed */
  return g_task_get_completed (task) ? G_SOURCE_REMOVE : G_SOURCE_CONTINUE;
}

static void
ensure_no_media_session (GTask * task, WpCore * core, WpSpaJson * args)
{
  WpObjectManager *om = wp_object_manager_new ();

  wp_info_object (core, "checking if pipewire-media-session is running...");

  /* make the object manager owned by the task and the task owned by the core;
     use an idle callback to test when it is ok to unref the task */
  g_task_set_task_data (task, om, g_object_unref);
  wp_core_idle_add (core, NULL, (GSourceFunc) ensure_no_media_session_task_idle,
      g_object_ref (task), g_object_unref);

  wp_object_manager_add_interest (om, WP_TYPE_CLIENT,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY,
      "application.name", "=s", "pipewire-media-session", NULL);
  g_signal_connect_object (om, "installed",
      G_CALLBACK (ensure_no_media_session_om_installed), task, 0);
  wp_core_install_object_manager (core, om);
}

static void
load_export_core (GTask * task, WpCore * core, WpSpaJson * args)
{
  g_autofree gchar *export_core_name = NULL;
  g_autoptr (WpCore) export_core = NULL;
  g_autoptr (WpProperties) props = wp_core_get_properties (core);
  const gchar *str = NULL;

  wp_info_object (core, "connecting export core to pipewire...");

  str = wp_properties_get (props, PW_KEY_APP_NAME);
  export_core_name =
      g_strdup_printf ("%s [export]", str ? str : "WirePlumber");

  export_core = wp_core_clone (core);
  wp_core_update_properties (export_core, wp_properties_new (
        PW_KEY_APP_NAME, export_core_name,
        "wireplumber.export-core", "true",
        NULL));

  g_task_return_pointer (task, g_steal_pointer (&export_core), g_object_unref);
}

static void
load_settings_instance (GTask * task, WpCore * core, WpSpaJson * args)
{
  g_autofree gchar *metadata_name = NULL;
  if (args)
    wp_spa_json_object_get (args, "metadata.name", "s", &metadata_name, NULL);

  wp_info_object (core, "loading settings instance '%s'...",
      metadata_name ? metadata_name : "(default: sm-settings)");

  WpSettings *settings = wp_settings_new (core, metadata_name);
  g_task_return_pointer (task, settings, g_object_unref);
}

static const struct {
  const gchar * name;
  void (*load) (GTask *, WpCore *, WpSpaJson *);
} builtin_components[] = {
  { "ensure-no-media-session", ensure_no_media_session },
  { "export-core", load_export_core },
  { "settings-instance", load_settings_instance },
};

/*** WpInternalCompLoader ***/

struct _WpInternalCompLoader
{
  GObject parent;
};
static void wp_internal_comp_loader_iface_init (WpComponentLoaderInterface * iface);

G_DEFINE_TYPE_WITH_CODE (WpInternalCompLoader, wp_internal_comp_loader,
                         G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (
                            WP_TYPE_COMPONENT_LOADER,
                            wp_internal_comp_loader_iface_init))

#define WP_MODULE_INIT_SYMBOL "wireplumber__module_init"
typedef GObject * (*WpModuleInitFunc) (WpCore *, WpSpaJson *, GError **);

static void
wp_internal_comp_loader_init (WpInternalCompLoader * self)
{
}

static void
wp_internal_comp_loader_class_init (WpInternalCompLoaderClass * klass)
{
}

static GObject *
load_module (WpCore * core, const gchar * module_name, WpSpaJson * args,
    GError ** error)
{
  g_autofree gchar *module_path = NULL;
  GModule *gmodule;
  gpointer module_init;

  module_path = wp_base_dirs_find_file (WP_BASE_DIRS_MODULE, NULL, module_name);
  if (!module_path) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "Failed to locate module %s", module_name);
    return NULL;
  }

  wp_trace_object (core, "loading %s from %s", module_name, module_path);

  gmodule = g_module_open (module_path, G_MODULE_BIND_LOCAL);
  if (!gmodule) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "Failed to open %s: %s", module_path, g_module_error ());
    return NULL;
  }

  if (!g_module_symbol (gmodule, WP_MODULE_INIT_SYMBOL, &module_init)) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "Failed to locate symbol " WP_MODULE_INIT_SYMBOL " in %s",
        module_path);
    g_module_close (gmodule);
    return NULL;
  }

  return ((WpModuleInitFunc) module_init) (core, args, error);
}

static gboolean
wp_internal_comp_loader_supports_type (WpComponentLoader * cl,
    const gchar * type)
{
  return g_str_equal (type, "module") ||
         g_str_equal (type, "pw-module") ||
         g_str_equal (type, "virtual") ||
         g_str_equal (type, "built-in") ||
         g_str_equal (type, "profile") ||
         g_str_equal (type, "array");
}

static void
wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core,
    const gchar * component, const gchar * type, WpSpaJson * args,
    GCancellable * cancellable, GAsyncReadyCallback callback, gpointer data)
{
  if (g_str_equal (type, "profile") || g_str_equal (type, "array")) {
    WpTransition *task = NULL;
    g_autoptr (WpSpaJson) components = NULL;
    g_autoptr (WpSpaJson) rules = NULL;
    g_autoptr (WpProperties) profile = wp_properties_new_empty ();

    if (g_str_equal (type, "profile")) {
      /* component name is the profile name;
         component list and profile features are loaded from config */
      g_autoptr (WpConf) conf = wp_core_get_conf (core);
      g_autoptr (WpSpaJson) all_profiles_j = NULL;
      g_autoptr (WpSpaJson) profile_j = NULL;
      const gchar *profile_name = component;

      all_profiles_j = wp_conf_get_section (conf, "wireplumber.profiles");
      if (all_profiles_j)
        wp_spa_json_object_get (all_profiles_j, profile_name, "J", &profile_j, NULL);

      if (!profile_j) {
        g_autoptr (GTask) task = g_task_new (self, cancellable, callback, data);
        g_task_set_source_tag (task, wp_internal_comp_loader_load);
        g_task_return_new_error (G_TASK (task), WP_DOMAIN_LIBRARY,
            WP_LIBRARY_ERROR_INVALID_ARGUMENT,
            "profile '%s' not found in configuration", profile_name);
        return;
      }

      wp_properties_update_from_json (profile, profile_j);

      components = wp_conf_get_section (conf, "wireplumber.components");
      rules = wp_conf_get_section (conf, "wireplumber.components.rules");
    }
    else {
      /* component list is retrieved from args; profile features are empty */
      components = wp_spa_json_ref (args);
    }

    task = wp_component_array_load_task_new (components, profile, rules, self,
        cancellable, callback, data);
    wp_transition_set_data (task, g_object_ref (core), g_object_unref);
    wp_transition_set_source_tag (task, wp_internal_comp_loader_load);
    wp_transition_advance (task);
  }
  else {
    g_autoptr (GTask) task = g_task_new (self, cancellable, callback, data);
    g_task_set_source_tag (task, wp_internal_comp_loader_load);

    if (g_str_equal (type, "module")) {
      g_autoptr (GError) error = NULL;
      g_autoptr (GObject) o = NULL;

      o = load_module (core, component, args, &error);
      if (o)
        g_task_return_pointer (task, g_steal_pointer (&o), g_object_unref);
      else
        g_task_return_error (task, g_steal_pointer (&error));
    }
    else if (g_str_equal (type, "pw-module")) {
      if (!pw_context_load_module (wp_core_get_pw_context (core), component,
              args ? wp_spa_json_get_data (args) : NULL, NULL)) {
        g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
            WP_LIBRARY_ERROR_OPERATION_FAILED,
            "Failed to load pipewire module %s: %s", component, strerror (errno));
      }
      else {
        g_task_return_pointer (task, NULL, NULL);
      }
    }
    else if (g_str_equal (type, "virtual")) {
      g_task_return_pointer (task, NULL, NULL);
    }
    else if (g_str_equal (type, "built-in")) {
      for (guint i = 0; i < G_N_ELEMENTS (builtin_components); i++) {
        if (g_str_equal (component, builtin_components[i].name)) {
          builtin_components[i].load (task, core, args);
          return;
        }
      }
      g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
          WP_LIBRARY_ERROR_INVALID_ARGUMENT,
          "invalid 'built-in' component: %s", component);
    }
    else {
      g_assert_not_reached ();
    }
  }
}

static GObject *
wp_internal_comp_loader_load_finish (WpComponentLoader * self,
    GAsyncResult * res, GError ** error)
{
  g_return_val_if_fail (
    g_async_result_is_tagged (res, wp_internal_comp_loader_load), NULL);

  if (G_IS_TASK (res))
    return g_task_propagate_pointer (G_TASK (res), error);
  else {
    wp_transition_finish (res, error);
    return NULL;
  }
}

static void
wp_internal_comp_loader_iface_init (WpComponentLoaderInterface * iface)
{
  iface->supports_type = wp_internal_comp_loader_supports_type;
  iface->load = wp_internal_comp_loader_load;
  iface->load_finish = wp_internal_comp_loader_load_finish;
}
   070701000000AA000081A400000000000000000000000165F8630400000214000000000000000000000000000000000000003800000000wireplumber-0.5.0/lib/wp/private/internal-comp-loader.h   /* WirePlumber
 *
 * Copyright © 2023 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_INTERNAL_COMP_LOADER_H__
#define __WIREPLUMBER_INTERNAL_COMP_LOADER_H__

#include "component-loader.h"

G_BEGIN_DECLS

#define WP_TYPE_INTERNAL_COMP_LOADER (wp_internal_comp_loader_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpInternalCompLoader, wp_internal_comp_loader,
                      WP, INTERNAL_COMP_LOADER, GObject)

G_END_DECLS

#endif
070701000000AB000081A400000000000000000000000165F8630400001032000000000000000000000000000000000000003600000000wireplumber-0.5.0/lib/wp/private/parse-conf-section.c /* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */
/* SPDX-License-Identifier: MIT */

/*
  This is a partial copy of functions from libpipewire's conf.c that is meant to
  live here temporarily until pw_context_parse_conf_section() is fixed upstream.
  See https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/1925
*/

#include <string.h>

#include <spa/utils/result.h>
#include <spa/utils/string.h>
#include <spa/utils/json.h>
#include <spa/utils/cleanup.h>

#include <pipewire/impl.h>

struct data {
	struct pw_context *context;
	struct pw_properties *props;
	int count;
};

/* context.spa-libs = {
 *  <factory-name regex> = <library-name>
 * }
 */
static int parse_spa_libs(void *user_data, const char *location,
		const char *section, const char *str, size_t len)
{
	struct data *d = user_data;
	struct pw_context *context = d->context;
	struct spa_json it[2];
	char key[512], value[512];

	spa_json_init(&it[0], str, len);
	if (spa_json_enter_object(&it[0], &it[1]) < 0) {
		pw_log_error("config file error: context.spa-libs is not an object");
		return -EINVAL;
	}

	while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
		if (spa_json_get_string(&it[1], value, sizeof(value)) > 0) {
			pw_context_add_spa_lib(context, key, value);
			d->count++;
		}
	}
	return 0;
}



static int load_module(struct pw_context *context, const char *key, const char *args, const char *flags)
{
	if (pw_context_load_module(context, key, args, NULL) == NULL) {
		if (errno == ENOENT && flags && strstr(flags, "ifexists") != NULL) {
			pw_log_info("%p: skipping unavailable module %s",
					context, key);
		} else if (flags == NULL || strstr(flags, "nofail") == NULL) {
			pw_log_error("%p: could not load mandatory module \"%s\": %m",
					context, key);
			return -errno;
		} else {
			pw_log_info("%p: could not load optional module \"%s\": %m",
					context, key);
		}
	} else {
		pw_log_info("%p: loaded module %s", context, key);
	}
	return 0;
}

/*
 * context.modules = [
 *   {   name = <module-name>
 *       ( args = { <key> = <value> ... } )
 *       ( flags = [ ( ifexists ) ( nofail ) ]
 *       ( condition = [ { key = value, .. } .. ] )
 *   }
 * ]
 */
static int parse_modules(void *user_data, const char *location,
		const char *section, const char *str, size_t len)
{
	struct data *d = user_data;
	struct pw_context *context = d->context;
	struct spa_json it[4];
	char key[512];
	int res = 0;

	spa_autofree char *s = strndup(str, len);
	spa_json_init(&it[0], s, len);
	if (spa_json_enter_array(&it[0], &it[1]) < 0) {
		pw_log_error("config file error: context.modules is not an array");
		return -EINVAL;
	}

	while (spa_json_enter_object(&it[1], &it[2]) > 0) {
		char *name = NULL, *args = NULL, *flags = NULL;
		bool have_match = true;

		while (spa_json_get_string(&it[2], key, sizeof(key)) > 0) {
			const char *val;
			int len;

			if ((len = spa_json_next(&it[2], &val)) <= 0)
				break;

			if (spa_streq(key, "name")) {
				name = (char*)val;
				spa_json_parse_stringn(val, len, name, len+1);
			} else if (spa_streq(key, "args")) {
				if (spa_json_is_container(val, len))
					len = spa_json_container_len(&it[2], val, len);

				args = (char*)val;
				spa_json_parse_stringn(val, len, args, len+1);
			} else if (spa_streq(key, "flags")) {
				if (spa_json_is_container(val, len))
					len = spa_json_container_len(&it[2], val, len);
				flags = (char*)val;
				spa_json_parse_stringn(val, len, flags, len+1);
			}
		}
		if (!have_match)
			continue;

		if (name != NULL)
			res = load_module(context, name, args, flags);

		if (res < 0)
			break;

		d->count++;
	}

	return res;
}

static int _pw_context_parse_conf_section(struct pw_context *context,
		struct pw_properties *conf, const char *section)
{
	struct data data = { .context = context };
	int res;

	if (spa_streq(section, "context.spa-libs"))
		res = pw_conf_section_for_each(&conf->dict, section,
				parse_spa_libs, &data);
	else if (spa_streq(section, "context.modules"))
		res = pw_conf_section_for_each(&conf->dict, section,
				parse_modules, &data);
	else
		res = -EINVAL;

	return res == 0 ? data.count : res;
}
  070701000000AC000081A400000000000000000000000165F863040000802C000000000000000000000000000000000000003900000000wireplumber-0.5.0/lib/wp/private/pipewire-object-mixin.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "private/pipewire-object-mixin.h"
#include "core.h"
#include "spa-type.h"
#include "spa-pod.h"
#include "log.h"
#include "error.h"

#include <spa/utils/result.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-pw-obj-mixin")

G_DEFINE_INTERFACE (WpPwObjectMixinPriv, wp_pw_object_mixin_priv, WP_TYPE_PROXY)

static void
wp_pw_object_mixin_priv_default_init (WpPwObjectMixinPrivInterface * iface)
{
}

static struct spa_param_info *
find_param_info (gpointer instance, guint32 id)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);

  /* offsets are 0 on objects that don't support params */
  if (d->info && iface->n_params_offset && iface->param_info_offset) {
    struct spa_param_info * param_info =
        G_STRUCT_MEMBER (struct spa_param_info *, d->info, iface->param_info_offset);
    guint32 n_params =
        G_STRUCT_MEMBER (guint32, d->info, iface->n_params_offset);

    for (guint i = 0; i < n_params; i++) {
      if (param_info[i].id == id)
        return &param_info[i];
    }
  }
  return NULL;
}

/*************/
/* INTERFACE */

static gconstpointer
wp_pw_object_mixin_get_native_info (WpPipewireObject * obj)
{
  return wp_pw_object_mixin_get_data (obj)->info;
}

static WpProperties *
wp_pw_object_mixin_get_properties (WpPipewireObject * obj)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (obj);
  return d->properties ? wp_properties_ref (d->properties) : NULL;
}

static GVariant *
wp_pw_object_mixin_get_param_info (WpPipewireObject * obj)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (obj);
  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
  struct spa_param_info *info;
  guint32 n_params;
  g_auto (GVariantBuilder) b =
      G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_DICTIONARY);

  if (!d->info ||
      iface->param_info_offset == 0 ||
      iface->n_params_offset == 0)
    return NULL;

  info = G_STRUCT_MEMBER (struct spa_param_info *, d->info, iface->param_info_offset);
  n_params = G_STRUCT_MEMBER (guint32, d->info, iface->n_params_offset);

  g_variant_builder_init (&b, G_VARIANT_TYPE ("a{ss}"));

  for (guint i = 0; i < n_params; i++) {
    WpSpaIdValue idval;
    const gchar *nick = NULL;
    gchar flags[3];
    guint flags_idx = 0;

    idval = wp_spa_id_value_from_number ("Spa:Enum:ParamId", info[i].id);
    nick = wp_spa_id_value_short_name (idval);
    g_return_val_if_fail (nick != NULL, NULL);

    if (info[i].flags & SPA_PARAM_INFO_READ)
      flags[flags_idx++] = 'r';
    if (info[i].flags & SPA_PARAM_INFO_WRITE)
      flags[flags_idx++] = 'w';
    flags[flags_idx] = '\0';

    g_variant_builder_add (&b, "{ss}", nick, flags);
  }

  return g_variant_builder_end (&b);
}

static void
enum_params_done (WpCore * core, GAsyncResult * res, gpointer data)
{
  g_autoptr (GTask) task = G_TASK (data);
  GList *taskl = NULL;
  g_autoptr (GError) error = NULL;
  gpointer instance = g_task_get_source_object (G_TASK (data));
  GPtrArray *params = g_task_get_task_data (task);
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);

  /* finish the sync task */
  wp_core_sync_finish (core, res, &error);

  /* return if task was previously removed from the list */
  taskl = g_list_find (d->enum_params_tasks, task);
  if (!taskl)
    return;

  /* remove the task from the stored list; ref is held by the g_autoptr */
  d->enum_params_tasks = g_list_delete_link (d->enum_params_tasks, taskl);

  wp_debug_object (instance, "got %u params, %s, task " WP_OBJECT_FORMAT,
      params->len, error ? "with error" : "ok", WP_OBJECT_ARGS (task));

  if (error)
    g_task_return_error (task, g_steal_pointer (&error));
  else {
    g_task_return_pointer (task, g_ptr_array_ref (params),
        (GDestroyNotify) g_ptr_array_unref);
  }
}

static void
enum_params_error (WpProxy * proxy, int seq, int res, const gchar *msg,
    GTask * task)
{
  gint t_seq = GPOINTER_TO_INT (g_task_get_source_tag (task));

  if (SPA_RESULT_ASYNC_SEQ (t_seq) == SPA_RESULT_ASYNC_SEQ (seq)) {
    gpointer instance = g_task_get_source_object (task);
    WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
    GList *taskl = NULL;

    taskl = g_list_find (d->enum_params_tasks, task);
    if (taskl) {
      d->enum_params_tasks = g_list_delete_link (d->enum_params_tasks, taskl);
      g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
          WP_LIBRARY_ERROR_OPERATION_FAILED, "%s", msg);
    }
  }
}

static void
wp_pw_object_mixin_enum_params_unchecked (gpointer obj,
    guint32 id, WpSpaPod *filter, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer user_data)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (obj);
  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
  g_autoptr (GTask) task = NULL;
  gint seq = 0;
  GPtrArray *params = NULL;

  g_return_if_fail (iface->enum_params_sync || iface->enum_params);

  if (iface->enum_params_sync) {
    params = iface->enum_params_sync (obj, id, 0, -1, filter);
  } else {
    seq = iface->enum_params (obj, id, 0, -1, filter);

    /* return early if seq contains an error */
    if (G_UNLIKELY (SPA_RESULT_IS_ERROR (seq))) {
      wp_notice_object (obj, "enum_params failed: %s", spa_strerror (seq));
      g_task_report_new_error (obj, callback, user_data, NULL,
          WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
          "enum_params failed: %s", spa_strerror (seq));
      return;
    }
  }

  if (!params)
    params = g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);

  /* create task */
  task = g_task_new (obj, cancellable, callback, user_data);

  /* debug */
  if (wp_local_log_topic_is_enabled (G_LOG_LEVEL_DEBUG)) {
    const gchar *name = NULL;
    name = wp_spa_id_value_short_name (
        wp_spa_id_value_from_number ("Spa:Enum:ParamId", id));
    wp_debug_object (obj, "enum id %u (%s), seq 0x%x (%u), task "
        WP_OBJECT_FORMAT "%s", id, name, seq, seq, WP_OBJECT_ARGS (task),
        iface->enum_params_sync ? ", sync" : "");
  }

  if (iface->enum_params_sync) {
    g_task_return_pointer (task, params, (GDestroyNotify) g_ptr_array_unref);
  } else {
    g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (obj));

    /* watch for errors */
    g_signal_connect_object (obj, "error", G_CALLBACK (enum_params_error),
        task, 0);

    /* store */
    g_task_set_task_data (task, params, (GDestroyNotify) g_ptr_array_unref);
    g_task_set_source_tag (task, GINT_TO_POINTER (seq));
    d->enum_params_tasks = g_list_append (d->enum_params_tasks, task);

    /* call sync */
    wp_core_sync (core, cancellable, (GAsyncReadyCallback) enum_params_done,
        g_object_ref (task));
  }
}

static void
wp_pw_object_mixin_enum_params (WpPipewireObject * obj, const gchar * id,
    WpSpaPod *filter, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer user_data)
{
  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
  WpSpaIdValue param_id;

  if (!(iface->enum_params || iface->enum_params_sync)) {
    g_task_report_new_error (obj, callback, user_data, NULL,
        WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
        "enum_params is not supported on this object");
    return;
  }

  /* translate the id */
  param_id = wp_spa_id_value_from_short_name ("Spa:Enum:ParamId", id);
  if (!param_id) {
    wp_critical_object (obj, "invalid param id: %s", id);
    return;
  }

  wp_pw_object_mixin_enum_params_unchecked (obj,
      wp_spa_id_value_number (param_id), filter,
      cancellable, callback, user_data);
}

static WpIterator *
wp_pw_object_mixin_enum_params_finish (WpPipewireObject * obj,
    GAsyncResult * res, GError ** error)
{
  g_return_val_if_fail (g_task_is_valid (res, obj), NULL);

  GPtrArray *array = g_task_propagate_pointer (G_TASK (res), error);
  if (!array)
    return NULL;

  return wp_iterator_new_ptr_array (array, WP_TYPE_SPA_POD);
}

static WpIterator *
wp_pw_object_mixin_enum_params_sync (WpPipewireObject * obj, const gchar * id,
    WpSpaPod * filter)
{
  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
  GPtrArray *params = NULL;
  WpSpaIdValue param_id;

  /* translate the id */
  param_id = wp_spa_id_value_from_short_name ("Spa:Enum:ParamId", id);
  if (!param_id) {
    wp_critical_object (obj, "invalid param id: %s", id);
    return NULL;
  }

  if (iface->enum_params_sync) {
    /* use enum_params_sync if supported */
    params = iface->enum_params_sync (obj, wp_spa_id_value_number (param_id),
        0, -1, filter);
  } else {
    /* otherwise, find and return the cached params */
    WpPwObjectMixinData *data = wp_pw_object_mixin_get_data (obj);
    params = wp_pw_object_mixin_get_stored_params (data,
        wp_spa_id_value_number (param_id));
    /* TODO filter */
  }

  return params ? wp_iterator_new_ptr_array (params, WP_TYPE_SPA_POD) : NULL;
}

static gboolean
wp_pw_object_mixin_set_param (WpPipewireObject * obj, const gchar * id,
    guint32 flags, WpSpaPod * param)
{
  WpPwObjectMixinPrivInterface *iface = WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (obj);
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (obj);
  WpSpaIdValue param_id;
  gint ret;

  if (!d->iface) {
    wp_notice_object (obj, "ignoring set_param on already destroyed objects");
    return FALSE;
  }

  if (!iface->set_param) {
    wp_warning_object (obj, "set_param is not supported on this object");
    return FALSE;
  }

  param_id = wp_spa_id_value_from_short_name ("Spa:Enum:ParamId", id);
  if (!param_id) {
    wp_critical_object (obj, "invalid param id: %s", id);
    wp_spa_pod_unref (param);
    return FALSE;
  }

  ret = iface->set_param (obj, wp_spa_id_value_number (param_id), flags, param);

  if (G_UNLIKELY (SPA_RESULT_IS_ERROR (ret))) {
    wp_notice_object (obj, "set_param failed: %s", spa_strerror (ret));
    return FALSE;
  }
  return TRUE;
}

void
wp_pw_object_mixin_object_interface_init (WpPipewireObjectInterface * iface)
{
  iface->get_native_info = wp_pw_object_mixin_get_native_info;
  iface->get_properties = wp_pw_object_mixin_get_properties;
  iface->get_param_info = wp_pw_object_mixin_get_param_info;
  iface->enum_params = wp_pw_object_mixin_enum_params;
  iface->enum_params_finish = wp_pw_object_mixin_enum_params_finish;
  iface->enum_params_sync = wp_pw_object_mixin_enum_params_sync;
  iface->set_param = wp_pw_object_mixin_set_param;
}

/********/
/* DATA */

G_DEFINE_QUARK (WpPwObjectMixinData, wp_pw_object_mixin_data)

static void wp_pw_object_mixin_param_store_free (gpointer data);

static WpPwObjectMixinData *
wp_pw_object_mixin_data_new (void)
{
  WpPwObjectMixinData *d = g_slice_new0 (WpPwObjectMixinData);
  spa_hook_list_init (&d->hooks);
  return d;
}

static void
wp_pw_object_mixin_data_free (gpointer data)
{
  WpPwObjectMixinData *d = data;
  spa_hook_list_clean (&d->hooks);
  g_clear_pointer (&d->properties, wp_properties_unref);
  g_list_free_full (d->params, wp_pw_object_mixin_param_store_free);
  g_clear_pointer (&d->subscribed_ids, g_array_unref);
  g_warn_if_fail (d->enum_params_tasks == NULL);
  g_slice_free (WpPwObjectMixinData, d);
}

WpPwObjectMixinData *
wp_pw_object_mixin_get_data (gpointer instance)
{
  WpPwObjectMixinData *d = g_object_get_qdata (G_OBJECT (instance),
      wp_pw_object_mixin_data_quark ());
  if (G_UNLIKELY (!d)) {
    d = wp_pw_object_mixin_data_new ();
    g_object_set_qdata_full (G_OBJECT (instance),
        wp_pw_object_mixin_data_quark (), d, wp_pw_object_mixin_data_free);
  }
  return d;
}

/****************/
/* PARAMS STORE */

typedef struct _WpPwObjectMixinParamStore WpPwObjectMixinParamStore;
struct _WpPwObjectMixinParamStore
{
  guint32 param_id;
  GPtrArray *params;
};

static WpPwObjectMixinParamStore *
wp_pw_object_mixin_param_store_new (void)
{
  WpPwObjectMixinParamStore *d = g_slice_new0 (WpPwObjectMixinParamStore);
  return d;
}

static void
wp_pw_object_mixin_param_store_free (gpointer data)
{
  WpPwObjectMixinParamStore * p = data;
  g_clear_pointer (&p->params, g_ptr_array_unref);
  g_slice_free (WpPwObjectMixinParamStore, p);
}

static gint
param_store_has_id (gconstpointer param, gconstpointer id)
{
  guint32 param_id = ((const WpPwObjectMixinParamStore *) param)->param_id;
  return (param_id == GPOINTER_TO_UINT (id)) ? 0 : 1;
}

GPtrArray *
wp_pw_object_mixin_get_stored_params (WpPwObjectMixinData * data, guint32 id)
{
  GList *link = g_list_find_custom (data->params, GUINT_TO_POINTER (id),
      param_store_has_id);
  WpPwObjectMixinParamStore *s = link ? link->data : NULL;
  return (s && s->params) ? g_ptr_array_ref (s->params) : NULL;
}

void
wp_pw_object_mixin_store_param (WpPwObjectMixinData * data, guint32 id,
    guint32 flags, gpointer param)
{
  GList *link = g_list_find_custom (data->params, GUINT_TO_POINTER (id),
      param_store_has_id);
  WpPwObjectMixinParamStore *s = link ? link->data : NULL;
  gint16 index = (gint16) (flags & 0xffff);

  /* if the link exists, data must also exist */
  g_warn_if_fail (!link || link->data);

  if (!s) {
    if (flags & WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE)
      return;
    s = wp_pw_object_mixin_param_store_new ();
    s->param_id = id;
    data->params = g_list_append (data->params, s);
  }
  else if (s && (flags & WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE)) {
    wp_pw_object_mixin_param_store_free (s);
    data->params = g_list_delete_link (data->params, link);
    return;
  }

  if (flags & WP_PW_OBJECT_MIXIN_STORE_PARAM_CLEAR)
    g_clear_pointer (&s->params, g_ptr_array_unref);

  if (!param)
    return;

  if (flags & WP_PW_OBJECT_MIXIN_STORE_PARAM_ARRAY) {
    if (!s->params)
      s->params = (GPtrArray *) param;
    else
      g_ptr_array_extend_and_steal (s->params, (GPtrArray *) param);
  }
  else {
    WpSpaPod *param_pod = param;

    if (!s->params)
      s->params =
          g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);

    /* copy if necessary to make sure we don't reference
       `const struct spa_pod *` data allocated on the stack */
    param_pod = wp_spa_pod_ensure_unique_owner (param_pod);
    g_ptr_array_insert (s->params, index, param_pod);
  }
}

/******************/
/* PROPERTIES API */

void
wp_pw_object_mixin_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  switch (property_id) {
  case WP_PW_OBJECT_MIXIN_PROP_NATIVE_INFO:
    g_value_set_pointer (value, (gpointer)
        wp_pipewire_object_get_native_info (WP_PIPEWIRE_OBJECT (object)));
    break;
  case WP_PW_OBJECT_MIXIN_PROP_PROPERTIES:
    g_value_take_boxed (value,
        wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (object)));
    break;
  case WP_PW_OBJECT_MIXIN_PROP_PARAM_INFO:
    g_value_set_variant (value,
        wp_pipewire_object_get_param_info (WP_PIPEWIRE_OBJECT (object)));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

void
wp_pw_object_mixin_class_override_properties (GObjectClass * klass)
{
  g_object_class_override_property (klass,
      WP_PW_OBJECT_MIXIN_PROP_NATIVE_INFO, "native-info");
  g_object_class_override_property (klass,
      WP_PW_OBJECT_MIXIN_PROP_PROPERTIES, "properties");
  g_object_class_override_property (klass,
      WP_PW_OBJECT_MIXIN_PROP_PARAM_INFO, "param-info");
}

/****************/
/* FEATURES API */

static const struct {
  WpObjectFeatures feature;
  guint32 param_ids[2];
} params_features[] = {
  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS, { SPA_PARAM_PropInfo, SPA_PARAM_Props } },
  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT, { SPA_PARAM_EnumFormat, SPA_PARAM_Format } },
  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE, { SPA_PARAM_EnumProfile, SPA_PARAM_Profile } },
  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG, { SPA_PARAM_EnumPortConfig, SPA_PARAM_PortConfig } },
  { WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE, { SPA_PARAM_EnumRoute, SPA_PARAM_Route } },
};

static WpObjectFeatures
get_feature_for_param_id (guint32 param_id)
{
  for (guint i = 0; i < G_N_ELEMENTS (params_features); i++) {
    if (params_features[i].param_ids[0] == param_id ||
        params_features[i].param_ids[1] == param_id)
      return params_features[i].feature;
  }
  return 0;
}

WpObjectFeatures
wp_pw_object_mixin_get_supported_features (WpObject * object)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (object);
  WpObjectFeatures ft =
      WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO;

  if (d->info && iface->n_params_offset && iface->param_info_offset) {
    struct spa_param_info * param_info =
        G_STRUCT_MEMBER (struct spa_param_info *, d->info, iface->param_info_offset);
    guint32 n_params =
        G_STRUCT_MEMBER (guint32, d->info, iface->n_params_offset);

    for (guint i = 0; i < n_params; i++)
      ft |= get_feature_for_param_id (param_info[i].id);
  }
  return ft;
}

guint
wp_pw_object_mixin_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);

  /* bind if not already bound */
  if (missing & WP_PROXY_FEATURE_BOUND || !d->iface)
    return WP_PW_OBJECT_MIXIN_STEP_BIND;
  /* wait for info before proceeding, if necessary */
  else if ((missing & WP_PIPEWIRE_OBJECT_FEATURES_ALL) && !d->info)
    return WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO;
  /* then cache params */
  else if (missing & WP_PIPEWIRE_OBJECT_FEATURES_ALL)
    return WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS;
  else
    return WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START;

  /* returning to STEP_NONE is handled by WpFeatureActivationTransition */
}

static void
enum_params_for_cache_done (GObject * object, GAsyncResult * res, gpointer data)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);
  guint32 param_id = GPOINTER_TO_UINT (data);
  g_autoptr (GError) error = NULL;
  g_autoptr (GPtrArray) params = NULL;
  const gchar *name = NULL;

  params = g_task_propagate_pointer (G_TASK (res), &error);
  if (error) {
    wp_debug_object (object, "enum params failed: %s", error->message);
    return;
  }

  name = wp_spa_id_value_short_name (wp_spa_id_value_from_number (
        "Spa:Enum:ParamId", param_id));

  wp_debug_object (object, "cached params id:%u (%s), n_params:%u", param_id,
      name, params->len);

  wp_pw_object_mixin_store_param (d, param_id,
      WP_PW_OBJECT_MIXIN_STORE_PARAM_ARRAY |
      WP_PW_OBJECT_MIXIN_STORE_PARAM_CLEAR |
      WP_PW_OBJECT_MIXIN_STORE_PARAM_APPEND,
      g_steal_pointer (&params));

  g_signal_emit_by_name (object, "params-changed", name);
}

G_DEFINE_QUARK (WpPwObjectMixinParamCacheActivatedFeatures, activated_features)

static void
param_cache_features_enabled (WpCore * core, GAsyncResult * res, gpointer data)
{
  WpObject *object = WP_OBJECT (data);
  WpObjectFeatures activated = GPOINTER_TO_UINT (
      g_object_get_qdata (G_OBJECT (object), activated_features_quark ()));
  wp_object_update_features (object, activated, 0);
}

void
wp_pw_object_mixin_cache_params (WpObject * object, WpObjectFeatures missing)
{
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (object);
  g_autoptr (WpCore) core = wp_object_get_core (object);
  struct spa_param_info * param_info;
  WpObjectFeatures activated = 0;

  g_return_if_fail (!(iface->flags & WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE));

  for (guint i = 0; i < G_N_ELEMENTS (params_features); i++) {
    if (missing & params_features[i].feature) {
      param_info = find_param_info (object, params_features[i].param_ids[0]);
      if (param_info && param_info->flags & SPA_PARAM_INFO_READ) {
        wp_pw_object_mixin_enum_params_unchecked (object,
            param_info->id, NULL, NULL, enum_params_for_cache_done,
            GUINT_TO_POINTER (param_info->id));
      }

      param_info = find_param_info (object, params_features[i].param_ids[1]);
      if (param_info && param_info->flags & SPA_PARAM_INFO_READ) {
        wp_pw_object_mixin_enum_params_unchecked (object,
            param_info->id, NULL, NULL, enum_params_for_cache_done,
            GUINT_TO_POINTER (param_info->id));
      }

      activated |= params_features[i].feature;
    }
  }

  g_object_set_qdata (G_OBJECT (object),
      activated_features_quark (), GUINT_TO_POINTER (activated));
  wp_core_sync_closure (core, NULL, g_cclosure_new_object (
          G_CALLBACK (param_cache_features_enabled),
          G_OBJECT (object)));
}

void
wp_pw_object_mixin_deactivate (WpObject * object, WpObjectFeatures features)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (object);

  /* deactivate param caching */
  if (!(iface->flags & WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE)) {
    for (guint i = 0; i < G_N_ELEMENTS (params_features); i++) {
      if (features & params_features[i].feature) {
        wp_pw_object_mixin_store_param (d, params_features[i].param_ids[0],
            WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE, NULL);
        wp_pw_object_mixin_store_param (d, params_features[i].param_ids[1],
            WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE, NULL);
        wp_object_update_features (object, 0, params_features[i].feature);
      }
    }
  }
}

/************************/
/* PROXY EVENT HANDLERS */

void
wp_pw_object_mixin_handle_pw_proxy_destroyed (WpProxy * proxy)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (proxy);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (proxy);

  spa_hook_remove (&d->listener);
  g_clear_pointer (&d->properties, wp_properties_unref);
  g_clear_pointer (&d->info, iface->free_info);
  d->iface = NULL;

  /* deactivate param caching */
  if (!(iface->flags & WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE)) {
    for (guint i = 0; i < G_N_ELEMENTS (params_features); i++) {
      wp_pw_object_mixin_store_param (d, params_features[i].param_ids[0],
          WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE, NULL);
      wp_pw_object_mixin_store_param (d, params_features[i].param_ids[1],
          WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE, NULL);
    }
  }

  /* cancel enum_params tasks */
  {
    GList *link;
    for (link = g_list_first (d->enum_params_tasks);
         link; link = g_list_first (d->enum_params_tasks)) {
      GTask *task = G_TASK (link->data);
      d->enum_params_tasks = g_list_delete_link (d->enum_params_tasks, link);
      g_task_return_new_error (task,
          WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
          "pipewire proxy destroyed before finishing");
    }
  }

  wp_object_update_features (WP_OBJECT (proxy), 0,
      WP_PIPEWIRE_OBJECT_FEATURES_ALL);
}

/***************************/
/* PIPEWIRE EVENT HANDLERS */

void
wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer update)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
  guint64 change_mask =
      G_STRUCT_MEMBER (guint64, update, iface->change_mask_offset);
  guint64 process_info_change_mask =
      change_mask & ~(iface->CHANGE_MASK_PROPS | iface->CHANGE_MASK_PARAMS);
  gpointer old_info = NULL;

  wp_debug_object (instance, "info, change_mask:0x%"G_GINT64_MODIFIER"x [%s%s]",
      change_mask,
      (change_mask & iface->CHANGE_MASK_PROPS) ? "props," : "",
      (change_mask & iface->CHANGE_MASK_PARAMS) ? "params," : "");

  /* make a copy of d->info for process_info() */
  if (iface->process_info && d->info && process_info_change_mask) {
    /* copy everything that changed except props and params, for efficiency;
       process_info() is only interested in variables that are not PROPS & PARAMS */
    G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) =
        process_info_change_mask;
    old_info = iface->update_info (NULL, d->info);
  }

  /* update params */
  if (!(iface->flags & WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE) &&
       (change_mask & iface->CHANGE_MASK_PARAMS) && d->info) {
    struct spa_param_info * old_param_info =
        G_STRUCT_MEMBER (struct spa_param_info *, d->info, iface->param_info_offset);
    struct spa_param_info * param_info =
        G_STRUCT_MEMBER (struct spa_param_info *, update, iface->param_info_offset);
    guint32 old_n_params =
        G_STRUCT_MEMBER (guint32, d->info, iface->n_params_offset);
    guint32 n_params =
        G_STRUCT_MEMBER (guint32, update, iface->n_params_offset);
    WpObjectFeatures active_ft =
        wp_object_get_active_features (WP_OBJECT (instance));

    for (guint i = 0; i < n_params; i++) {
      /* param changes when flags change */
      if (i >= old_n_params || old_param_info[i].flags != param_info[i].flags) {
        /* update cached params if the relevant feature is active */
        if (active_ft & get_feature_for_param_id (param_info[i].id) &&
            param_info[i].flags & SPA_PARAM_INFO_READ)
        {
          wp_pw_object_mixin_enum_params_unchecked (instance,
              param_info[i].id, NULL, NULL, enum_params_for_cache_done,
              GUINT_TO_POINTER (param_info[i].id));
        }
      }
    }
  }

  /* update our info struct */
  d->info = iface->update_info (d->info, update);

  /* update properties */
  if (change_mask & iface->CHANGE_MASK_PROPS) {
    const struct spa_dict * props =
        G_STRUCT_MEMBER (const struct spa_dict *, d->info, iface->props_offset);

    g_clear_pointer (&d->properties, wp_properties_unref);
    d->properties = wp_properties_new_wrap_dict (props);

    g_object_notify (G_OBJECT (instance), "properties");
  }

  if (change_mask & iface->CHANGE_MASK_PARAMS)
    g_object_notify (G_OBJECT (instance), "param-info");

  /* custom handling, if required */
  if (iface->process_info && process_info_change_mask) {
    iface->process_info (instance, old_info, d->info);
    g_clear_pointer (&old_info, iface->free_info);
  }

  wp_object_update_features (WP_OBJECT (instance),
      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
}

static gint
task_has_seq (gconstpointer task, gconstpointer seq)
{
  gpointer t_seq = g_task_get_source_tag (G_TASK (task));
  return (GPOINTER_TO_INT (t_seq) == GPOINTER_TO_INT (seq)) ? 0 : 1;
}

void
wp_pw_object_mixin_handle_event_param (gpointer instance, int seq,
    uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  g_autoptr (WpSpaPod) w_param = wp_spa_pod_new_wrap_const (param);
  GList *list;
  GTask *task;

  list = g_list_find_custom (d->enum_params_tasks, GINT_TO_POINTER (seq),
      task_has_seq);
  task = list ? G_TASK (list->data) : NULL;

  wp_trace_boxed (WP_TYPE_SPA_POD, w_param,
      WP_OBJECT_FORMAT " param id:%u, index:%u",
      WP_OBJECT_ARGS (instance), id, index);

  if (task) {
    GPtrArray *array = g_task_get_task_data (task);
    g_ptr_array_add (array, wp_spa_pod_copy (w_param));
  } else {
    /* this should never happen */
    wp_warning_object (instance,
        "param event was received without calling enum_params");
  }
}

/***********************************/
/* PIPEWIRE METHOD IMPLEMENTATIONS */

int
wp_pw_object_mixin_impl_add_listener (gpointer instance,
    struct spa_hook *listener, gconstpointer events, gpointer data)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
  struct spa_hook_list save;

  spa_hook_list_isolate (&d->hooks, &save, listener, events, data);

  G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = iface->CHANGE_MASK_ALL;
  iface->emit_info (&d->hooks, d->info);
  G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = 0;

  spa_hook_list_join (&d->hooks, &save);
  return 0;
}

int
wp_pw_object_mixin_impl_enum_params (gpointer instance, int seq,
    guint32 id, guint32 start, guint32 num, const struct spa_pod *filter)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
  g_autoptr (GPtrArray) params = NULL;
  g_autoptr (WpSpaPod) filter_pod = NULL;

  if (!iface->enum_params_sync)
    return -ENOTSUP;

  struct spa_param_info * info = find_param_info (instance, id);
  if (!info || !(info->flags & SPA_PARAM_INFO_READ))
    return -EINVAL;

  filter_pod = filter ? wp_spa_pod_new_wrap_const (filter) : NULL;
  params = iface->enum_params_sync (instance, id, start, num, filter_pod);

  if (params) {
    for (guint i = 0; i < params->len; i++) {
      WpSpaPod *pod = g_ptr_array_index (params, i);

      wp_trace_boxed (WP_TYPE_SPA_POD, pod,
          WP_OBJECT_FORMAT " emit param id:%u, index:%u",
          WP_OBJECT_ARGS (instance), id, start+i);

      iface->emit_param (&d->hooks, seq, id, start+i, start+i+1,
          wp_spa_pod_get_spa_pod (pod));
    }
  }
  return 0;
}

int
wp_pw_object_mixin_impl_subscribe_params (gpointer instance,
    guint32 *ids, guint32 n_ids)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);

  if (!iface->enum_params_sync)
    return -ENOTSUP;

  for (guint i = 0; i < n_ids; i++)
    wp_pw_object_mixin_impl_enum_params (instance, 1, ids[i], 0, -1, NULL);

  if (!d->subscribed_ids)
    d->subscribed_ids = g_array_new (FALSE, FALSE, sizeof (guint32));

  /* FIXME: deduplicate stored ids */
  g_array_append_vals (d->subscribed_ids, ids, n_ids);
  return 0;
}

int
wp_pw_object_mixin_impl_set_param (gpointer instance, guint32 id,
    guint32 flags, const struct spa_pod *param)
{
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);

  if (!iface->set_param)
    return -ENOTSUP;

  struct spa_param_info * info = find_param_info (instance, id);
  if (!info || !(info->flags & SPA_PARAM_INFO_WRITE))
    return -EINVAL;

  WpSpaPod *param_pod = wp_spa_pod_new_wrap_const (param);

  wp_trace_boxed (WP_TYPE_SPA_POD, param_pod,
          WP_OBJECT_FORMAT " set_param id:%u flags:0x%x",
          WP_OBJECT_ARGS (instance), id, flags);

  return iface->set_param (instance, id, flags, param_pod);
}

/**********************/
/*      NOTIFIERS     */

void
wp_pw_object_mixin_notify_info (gpointer instance, guint32 change_mask)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);

  wp_debug_object (instance, "notify info, change_mask:0x%x [%s%s]",
      change_mask,
      (change_mask & iface->CHANGE_MASK_PROPS) ? "props," : "",
      (change_mask & iface->CHANGE_MASK_PARAMS) ? "params," : "");

  G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) =
      (change_mask & iface->CHANGE_MASK_ALL);
  iface->emit_info (&d->hooks, d->info);
  G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = 0;

  if (change_mask & iface->CHANGE_MASK_PROPS)
    g_object_notify (G_OBJECT (instance), "properties");

  if (change_mask & iface->CHANGE_MASK_PARAMS)
    g_object_notify (G_OBJECT (instance), "param-info");
}

void
wp_pw_object_mixin_notify_params_changed (gpointer instance, guint32 id)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance);
  WpPwObjectMixinPrivInterface *iface =
      WP_PW_OBJECT_MIXIN_PRIV_GET_IFACE (instance);
  gboolean subscribed = FALSE;
  const gchar *name = NULL;

  struct spa_param_info * info = find_param_info (instance, id);
  g_return_if_fail (info);

  if (d->subscribed_ids) {
    for (guint i = 0; i < d->subscribed_ids->len; i++) {
      if (g_array_index (d->subscribed_ids, guint32, i) == id) {
        subscribed = TRUE;
        break;
      }
    }
  }

  name = wp_spa_id_value_short_name (wp_spa_id_value_from_number (
        "Spa:Enum:ParamId", id));

  wp_debug_object (instance, "notify param id:%u (%s)", id, name);

  /* toggle the serial flag; this notifies that there is a data change */
  info->flags ^= SPA_PARAM_INFO_SERIAL;

  G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) =
      iface->CHANGE_MASK_PARAMS;
  iface->emit_info (&d->hooks, d->info);
  G_STRUCT_MEMBER (guint64, d->info, iface->change_mask_offset) = 0;

  if (subscribed)
    wp_pw_object_mixin_impl_enum_params (instance, 1, id, 0, -1, NULL);

  g_signal_emit_by_name (instance, "params-changed", name);
}
070701000000AD000081A400000000000000000000000165F863040000225F000000000000000000000000000000000000003900000000wireplumber-0.5.0/lib/wp/private/pipewire-object-mixin.h  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_PIPEWIRE_OBJECT_MIXIN_H__
#define __WIREPLUMBER_PIPEWIRE_OBJECT_MIXIN_H__

#include "proxy-interfaces.h"

#include <pipewire/pipewire.h>

G_BEGIN_DECLS

enum {
  /* this is the same STEP_BIND as in WpGlobalProxy */
  WP_PW_OBJECT_MIXIN_STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
  WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO,
  WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS,

  WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START,
};

enum {
  WP_PW_OBJECT_MIXIN_PROP_0,

  // WpPipewireObject
  WP_PW_OBJECT_MIXIN_PROP_NATIVE_INFO,
  WP_PW_OBJECT_MIXIN_PROP_PROPERTIES,
  WP_PW_OBJECT_MIXIN_PROP_PARAM_INFO,

  WP_PW_OBJECT_MIXIN_PROP_CUSTOM_START,
};

#define WP_TYPE_PW_OBJECT_MIXIN_PRIV (wp_pw_object_mixin_priv_get_type ())
G_DECLARE_INTERFACE (WpPwObjectMixinPriv, wp_pw_object_mixin_priv,
                     WP, PW_OBJECT_MIXIN_PRIV, WpProxy)

struct _WpPwObjectMixinPrivInterface
{
  GTypeInterface parent;

  /* WpPwObjectMixinPriv-specific flags */
#define WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE (1 << 0)
  guint32 flags;

  /* pipewire info struct abstraction layer */
  gsize info_size;
  gsize change_mask_offset;
  gsize props_offset;
  gsize param_info_offset;
  gsize n_params_offset;

  guint64 CHANGE_MASK_ALL;
  guint64 CHANGE_MASK_PROPS;
  guint64 CHANGE_MASK_PARAMS;

  gpointer (*update_info) (gpointer info, gconstpointer update);
  void (*free_info) (gpointer info);

  /* to further process info struct updates - for proxy objects only */
  void (*process_info) (gpointer instance, gpointer old_info, gpointer info);

  /* pipewire interface methods - proxy & impl */
  gint (*enum_params) (gpointer instance, guint32 id,
      guint32 start, guint32 num, WpSpaPod *filter);
  GPtrArray * (*enum_params_sync) (gpointer instance, guint32 id,
      guint32 start, guint32 num, WpSpaPod *filter);
  gint (*set_param) (gpointer instance, guint32 id, guint32 flags,
      WpSpaPod *param /* transfer full */);

  /* pipewire interface events - for impl objects only */
  void (*emit_info) (struct spa_hook_list * hooks, gconstpointer info);
  void (*emit_param) (struct spa_hook_list * hooks, int seq,
      guint32 id, guint32 index, guint32 next, const struct spa_pod *param);
};

/* fills in info struct abstraction layer in WpPwObjectMixinPrivInterface */
#define wp_pw_object_mixin_priv_interface_info_init(iface, type, TYPE) \
({ \
  iface->info_size = sizeof (struct pw_ ## type ## _info); \
  iface->change_mask_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, change_mask); \
  iface->props_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, props); \
  iface->param_info_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, params); \
  iface->n_params_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, n_params); \
  iface->CHANGE_MASK_ALL = PW_ ## TYPE ## _CHANGE_MASK_ALL; \
  iface->CHANGE_MASK_PROPS = PW_ ## TYPE ## _CHANGE_MASK_PROPS; \
  iface->CHANGE_MASK_PARAMS = PW_ ## TYPE ## _CHANGE_MASK_PARAMS; \
  iface->update_info = (gpointer (*)(gpointer, gconstpointer)) pw_ ## type ## _info_update; \
  iface->free_info = (void (*)(gpointer)) pw_ ## type ## _info_free; \
})

/* same as above, for types that don't have params */
#define wp_pw_object_mixin_priv_interface_info_init_no_params(iface, type, TYPE) \
({ \
  iface->flags = WP_PW_OBJECT_MIXIN_PRIV_NO_PARAM_CACHE; \
  iface->info_size = sizeof (struct pw_ ## type ## _info); \
  iface->change_mask_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, change_mask); \
  iface->props_offset = G_STRUCT_OFFSET (struct pw_ ## type ## _info, props); \
  iface->param_info_offset = 0; \
  iface->n_params_offset = 0; \
  iface->CHANGE_MASK_ALL = PW_ ## TYPE ## _CHANGE_MASK_ALL; \
  iface->CHANGE_MASK_PROPS = PW_ ## TYPE ## _CHANGE_MASK_PROPS; \
  iface->CHANGE_MASK_PARAMS = 0; \
  iface->update_info = (gpointer (*)(gpointer, gconstpointer)) pw_ ## type ## _info_update; \
  iface->free_info = (void (*)(gpointer)) pw_ ## type ## _info_free; \
})

/*************/
/* INTERFACE */

/* implements WpPipewireObject for an object that implements WpPwObjectMixinPriv */
void wp_pw_object_mixin_object_interface_init (WpPipewireObjectInterface * iface);

/********/
/* DATA */

typedef struct _WpPwObjectMixinData WpPwObjectMixinData;
struct _WpPwObjectMixinData
{
  gpointer info;            /* pointer to the info struct */
  gpointer iface;           /* pointer to the interface (ex. pw_node) */
  struct spa_hook listener;
  struct spa_hook_list hooks;
  WpProperties *properties;
  GList *enum_params_tasks;  /* element-type: GTask* */
  GList *params;             /* element-type: WpPwObjectMixinParamStore* */
  GArray *subscribed_ids;    /* element-type: guint32 */
};

/* get mixin data (stored as qdata on the @em instance) */
WpPwObjectMixinData * wp_pw_object_mixin_get_data (gpointer instance);

/****************/
/* PARAMS STORE */

/* param store access; (transfer container) */
GPtrArray * wp_pw_object_mixin_get_stored_params (WpPwObjectMixinData * data,
    guint32 id);

/* param store manipulation
 * @em flags: see below
 * @em param: (transfer full): WpSpaPod* or GPtrArray* */
void wp_pw_object_mixin_store_param (WpPwObjectMixinData * data, guint32 id,
    guint32 flags, gpointer param);

/* set the index at which to store the new param */
#define WP_PW_OBJECT_MIXIN_STORE_PARAM_SET(x)  ((x) & 0x7fff)
#define WP_PW_OBJECT_MIXIN_STORE_PARAM_APPEND  (0xffff)
#define WP_PW_OBJECT_MIXIN_STORE_PARAM_PREPEND (0)
/* @em param is a GPtrArray* */
#define WP_PW_OBJECT_MIXIN_STORE_PARAM_ARRAY   (1 << 16)
/* clear the existing array of params before storing */
#define WP_PW_OBJECT_MIXIN_STORE_PARAM_CLEAR   (1 << 17)
/* completely remove stored params for @id */
#define WP_PW_OBJECT_MIXIN_STORE_PARAM_REMOVE  (1 << 18)

/******************/
/* PROPERTIES API */

/* assign to get_property or chain it from there */
void wp_pw_object_mixin_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec);

/* call from class_init */
void wp_pw_object_mixin_class_override_properties (GObjectClass * klass);

/****************/
/* FEATURES API */

/* call from get_supported_features */
WpObjectFeatures wp_pw_object_mixin_get_supported_features (WpObject * object);

/* assign directly to activate_get_next_step */
guint wp_pw_object_mixin_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing);

/* call from activate_execute_step when
   step == WP_PW_OBJECT_MIXIN_STEP_CACHE_PARAMS */
void wp_pw_object_mixin_cache_params (WpObject * object,
    WpObjectFeatures missing);

/* handle deactivation of PARAM_* caching features */
void wp_pw_object_mixin_deactivate (WpObject * object,
    WpObjectFeatures features);

/************************/
/* PROXY EVENT HANDLERS */
/*  (for proxy objects) */

#define wp_pw_object_mixin_handle_pw_proxy_created(instance, pw_proxy, type, events) \
({ \
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (instance); \
  d->iface = pw_proxy; \
  pw_ ## type ## _add_listener ((struct pw_ ## type *) pw_proxy, \
      &d->listener, events, instance); \
})

void wp_pw_object_mixin_handle_pw_proxy_destroyed (WpProxy * proxy);

/***************************/
/* PIPEWIRE EVENT HANDLERS */
/*   (for proxy objects)   */

#define HandleEventInfoFunc(type) \
  void (*)(void *, const struct pw_ ## type ## _info *)

void wp_pw_object_mixin_handle_event_info (gpointer instance, gconstpointer info);

/* assign as the param event callback */
void wp_pw_object_mixin_handle_event_param (gpointer instance, int seq,
    guint32 id, guint32 index, guint32 next, const struct spa_pod *param);

/***********************************/
/* PIPEWIRE METHOD IMPLEMENTATIONS */
/*       (for impl objects)        */

#define ImplAddListenerFunc(type) \
  int (*)(void *, struct spa_hook *, const struct pw_ ## type ## _events *, void *)

int wp_pw_object_mixin_impl_add_listener (gpointer instance,
    struct spa_hook *listener, gconstpointer events, gpointer data);

int wp_pw_object_mixin_impl_enum_params (gpointer instance, int seq,
    guint32 id, guint32 start, guint32 num, const struct spa_pod *filter);

int wp_pw_object_mixin_impl_subscribe_params (gpointer instance,
    guint32 *ids, guint32 n_ids);

int wp_pw_object_mixin_impl_set_param (gpointer instance, guint32 id,
    guint32 flags, const struct spa_pod *param);

/**********************/
/*      NOTIFIERS     */
/* (for impl objects) */

void wp_pw_object_mixin_notify_info (gpointer instance, guint32 change_mask);

void wp_pw_object_mixin_notify_params_changed (gpointer instance, guint32 id);

G_END_DECLS

#endif
 070701000000AE000081A400000000000000000000000165F86304000044ED000000000000000000000000000000000000002C00000000wireplumber-0.5.0/lib/wp/private/registry.c   /* WirePlumber
 *
 * Copyright © 2019-2024 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "registry.h"
#include "object-manager.h"
#include "log.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-registry")

/*
 * WpRegistry:
 *
 * The registry keeps track of registered objects on the wireplumber core.
 * There are 3 kinds of registered objects:
 *
 * 1) PipeWire global objects, which live in another process.
 *
 *    These objects are represented by a WpGlobal with the
 *    WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag set. They appear when
 *    the registry_global() event is fired and are removed by
 *    registry_global_remove(). These objects do not have an associated
 *    WpProxy, unless there is at least one WpObjectManager that is interested
 *    in them. In this case, a WpProxy is constructed and it is owned by the
 *    WpGlobal until the global is removed by the registry_global_remove() event.
 *
 * 2) PipeWire global objects, which were constructed by this process, either
 *    by calling into a remove factory (see wp_node_new_from_factory()) or
 *    by exporting a local object (WpImplSession etc...).
 *
 *    These objects are also represented by a WpGlobal, which may however be
 *    constructed before they appear on the registry. The associated WpProxy
 *    calls into wp_registry_prepare_new_global() at the time it receives
 *    the 'bound' event and creates a global that has the
 *    WP_GLOBAL_FLAG_OWNED_BY_PROXY flag enabled. As the flag name suggests,
 *    these globals are "owned" by the WpProxy and the WpGlobal has no ref
 *    on the WpProxy itself. This allows destroying the proxy in client code
 *    by dropping its last reference.
 *
 *    Normally, these global objects also appear on the pipewire registry. When
 *    this happens, the WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY flag is also added
 *    and that keeps an additional reference on the global (both flags must
 *    be dropped before the WpGlobal is destroyed).
 *
 *    In some cases, such an object might appear first on the registry and
 *    then receive the 'bound' event. In order to handle this situation, globals
 *    are not advertised immediately when they appear on the registry, but
 *    they are added on a tmp_globals list instead, which is emptied on the
 *    next core sync. In all cases, the proxy 'bound' and the registry 'global'
 *    events will be fired in the same sync cycle, so we can catch a late
 *    'bound' event and still associate the proxy with the WpGlobal before
 *    object managers are notified about the existence of this global.
 *
 * 3) WirePlumber global objects (WpModule, WpPlugin, WpSiFactory).
 *
 *    These are local objects that have nothing to do with PipeWire. They do not
 *    have a global id and they are also not subclasses of WpProxy. The registry
 *    always owns a reference on them, so that they are kept alive for as long
 *    as the WpCore is alive.
 */

static void
object_manager_destroyed (gpointer data, GObject * om)
{
  WpRegistry *self = data;
  g_ptr_array_remove_fast (self->object_managers, om);
}

/* find the subclass of WpPipewireGloabl that can handle
   the given pipewire interface type of the given version */
static inline GType
find_proxy_instance_type (const char * type, guint32 version)
{
  g_autofree GType *children;
  guint n_children;

  children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children);

  for (guint i = 0; i < n_children; i++) {
    WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]);
    if (g_strcmp0 (klass->pw_iface_type, type) == 0 &&
        klass->pw_iface_version == version) {
      g_type_class_unref (klass);
      return children[i];
    }

    g_type_class_unref (klass);
  }

  return WP_TYPE_GLOBAL_PROXY;
}

/* called by the registry when a global appears */
static void
registry_global (void *data, uint32_t id, uint32_t permissions,
    const char *type, uint32_t version, const struct spa_dict *props)
{
  WpRegistry *self = data;
  GType gtype = find_proxy_instance_type (type, version);

  wp_debug_object (wp_registry_get_core (self),
      "global:%u perm:0x%x type:%s/%u -> %s",
      id, permissions, type, version, g_type_name (gtype));

  wp_registry_prepare_new_global (self, id, permissions,
      WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY, gtype, NULL, props, NULL);
}

/* called by the registry when a global is removed */
static void
registry_global_remove (void *data, uint32_t id)
{
  WpRegistry *self = data;
  WpGlobal *global = NULL;

  if (id < self->globals->len)
    global = g_ptr_array_index (self->globals, id);

  /* if not found, look in the tmp_globals, as it may still not be exposed */
  if (!global) {
    for (guint i = 0; i < self->tmp_globals->len; i++) {
      WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
      if (g->id == id) {
        global = g;
        break;
      }
    }
  }

  g_return_if_fail (global &&
      global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);

  wp_debug_object (wp_registry_get_core (self),
      "global removed:%u type:%s", id, g_type_name (global->type));

  wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
}

static const struct pw_registry_events registry_events = {
  PW_VERSION_REGISTRY_EVENTS,
  .global = registry_global,
  .global_remove = registry_global_remove,
};

void
wp_registry_init (WpRegistry *self)
{
  self->globals =
      g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
  self->tmp_globals =
      g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);
  self->objects = g_ptr_array_new_with_free_func (g_object_unref);
  self->object_managers = g_ptr_array_new ();
  self->features = g_ptr_array_new_with_free_func (g_free);
}

void
wp_registry_clear (WpRegistry *self)
{
  wp_registry_detach (self);
  g_clear_pointer (&self->globals, g_ptr_array_unref);
  g_clear_pointer (&self->tmp_globals, g_ptr_array_unref);
  g_clear_pointer (&self->features, g_ptr_array_unref);

  /* remove all the registered objects
     this will normally also destroy the object managers, eventually, since
     they are normally ref'ed by modules, which are registered objects */
  {
    g_autoptr (GPtrArray) objlist = g_steal_pointer (&self->objects);

    while (objlist->len > 0) {
      g_autoptr (GObject) object = g_ptr_array_steal_index_fast (objlist,
          objlist->len - 1);
      wp_registry_notify_rm_object (self, object);
    }
  }

  /* in case there are any object managers left,
     remove the weak ref on them and let them be... */
  {
    g_autoptr (GPtrArray) object_mgrs;
    GObject *om;

    object_mgrs = g_steal_pointer (&self->object_managers);

    while (object_mgrs->len > 0) {
      om = g_ptr_array_steal_index_fast (object_mgrs, object_mgrs->len - 1);
      g_object_weak_unref (om, object_manager_destroyed, self);
    }
  }
}

void
wp_registry_attach (WpRegistry *self, struct pw_core *pw_core)
{
  self->pw_registry = pw_core_get_registry (pw_core,
      PW_VERSION_REGISTRY, 0);
  pw_registry_add_listener (self->pw_registry, &self->listener,
      &registry_events, self);
}

void
wp_registry_detach (WpRegistry *self)
{
  if (self->pw_registry) {
    spa_hook_remove (&self->listener);
    pw_proxy_destroy ((struct pw_proxy *) self->pw_registry);
    self->pw_registry = NULL;
  }

  /* remove pipewire globals */
  GPtrArray *objlist = self->globals;
  while (objlist && objlist->len > 0) {
    g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
        objlist->len - 1);

    if (!global)
      continue;

    if (global->proxy)
      wp_registry_notify_rm_object (self, global->proxy);

    /* remove the APPEARS_ON_REGISTRY flag to unref the proxy if it is owned
      by the registry; set registry to NULL to avoid further interference */
    global->registry = NULL;
    wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);

    /* the registry's ref on global is dropped here; it may still live if
      there is a proxy that owns a ref on it, but global->registry is set
      to NULL, so there is no further interference */
  }

  /* drop tmp globals as well */
  objlist = self->tmp_globals;
  while (objlist && objlist->len > 0) {
    g_autoptr (WpGlobal) global = g_ptr_array_steal_index_fast (objlist,
        objlist->len - 1);
    wp_global_rm_flag (global, WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY);
  }
}

static gboolean
expose_tmp_globals (WpCore *core)
{
  WpRegistry *self = wp_core_get_registry (core);
  g_autoptr (GPtrArray) tmp_globals = NULL;
  g_autoptr (GPtrArray) object_managers = NULL;

  /* in case the registry was cleared in the meantime... */
  if (G_UNLIKELY (!self->tmp_globals))
    return G_SOURCE_REMOVE;

  /* steal the tmp_globals list and replace it with an empty one */
  tmp_globals = self->tmp_globals;
  self->tmp_globals =
      g_ptr_array_new_with_free_func ((GDestroyNotify) wp_global_unref);

  wp_debug_object (core, "exposing %u new globals", tmp_globals->len);

  /* traverse in the order that the globals appeared on the registry */
  for (guint i = 0; i < tmp_globals->len; i++) {
    WpGlobal *g = g_ptr_array_index (tmp_globals, i);

    /* if global was already removed, drop it */
    if (g->flags == 0 || g->id == SPA_ID_INVALID)
      continue;

    /* if old global is owned by proxy, remove it */
    if (self->globals->len > g->id) {
      WpGlobal *old_g = g_ptr_array_index (self->globals, g->id);
      if (old_g && (old_g->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY))
        wp_global_rm_flag (old_g, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
    }

    g_return_val_if_fail (self->globals->len <= g->id ||
        g_ptr_array_index (self->globals, g->id) == NULL, G_SOURCE_REMOVE);

    /* set the registry, so that wp_global_rm_flag() can work full-scale */
    g->registry = self;

    /* store it in the globals list */
    if (self->globals->len <= g->id)
      g_ptr_array_set_size (self->globals, g->id + 1);
    g_ptr_array_index (self->globals, g->id) = wp_global_ref (g);
  }

  object_managers = g_ptr_array_copy (self->object_managers,
      (GCopyFunc) g_object_ref, NULL);
  g_ptr_array_set_free_func (object_managers, g_object_unref);

  /* notify object managers */
  for (guint i = 0; i < object_managers->len; i++) {
    WpObjectManager *om = g_ptr_array_index (object_managers, i);

    for (guint i = 0; i < tmp_globals->len; i++) {
      WpGlobal *g = g_ptr_array_index (tmp_globals, i);

      /* if global was already removed, drop it */
      if (g->flags == 0 || g->id == SPA_ID_INVALID)
        continue;

      wp_object_manager_add_global (om, g);
    }
    wp_object_manager_maybe_objects_changed (om);
  }

  return G_SOURCE_REMOVE;
}

/*
 * \param new_global (out) (transfer full) (optional): the new global
 *
 * This is normally called up to 2 times in the same sync cycle:
 * one from registry_global(), another from the proxy bound event
 * Unfortunately the order in which those 2 events happen is specific
 * to the implementation of the object, which is why this is implemented
 * with a temporary globals list that get exposed later to the object managers
 */
void
wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
    guint32 permissions, guint32 flag, GType type,
    WpGlobalProxy *proxy, const struct spa_dict *props,
    WpGlobal ** new_global)
{
  g_autoptr (WpGlobal) global = NULL;
  WpCore *core = wp_registry_get_core (self);

  g_return_if_fail (flag != 0);

  for (guint i = 0; i < self->tmp_globals->len; i++) {
    WpGlobal *g = g_ptr_array_index (self->tmp_globals, i);
    if (g->id == id) {
      global = wp_global_ref (g);
      break;
    }
  }

  wp_debug_object (core, "%s WpGlobal:%u type:%s proxy:%p",
      global ? "reuse" : "new", id, g_type_name (type),
      (global && global->proxy) ? global->proxy : proxy);

  if (!global) {
    global = g_rc_box_new0 (WpGlobal);
    global->flags = flag;
    global->id = id;
    global->type = type;
    global->permissions = permissions;
    global->properties = props ?
        wp_properties_new_copy_dict (props) : wp_properties_new_empty ();
    global->proxy = proxy;
    g_ptr_array_add (self->tmp_globals, wp_global_ref (global));

    /* ensure we have 'object.id' so that we can filter by id on object managers */
    wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, "%u", global->id);

    /* schedule exposing when adding the first global */
    if (self->tmp_globals->len == 1) {
      wp_core_idle_add_closure (core, NULL,
          g_cclosure_new_object (G_CALLBACK (expose_tmp_globals), G_OBJECT (core)));
    }
  } else {
    /* store the most permissive permissions */
    if (permissions > global->permissions)
      global->permissions = permissions;

    global->flags |= flag;

    /* store the most deep type (i.e. WpImplNode instead of WpNode),
       so that object-manager interests can work more accurately
       if the interest is on a specific subclass */
    if (g_type_depth (type) > g_type_depth (global->type))
      global->type = type;

    if (proxy) {
      g_return_if_fail (global->proxy == NULL);
      global->proxy = proxy;
    }

    if (props)
      wp_properties_update_from_dict (global->properties, props);
  }

  if (new_global)
    *new_global = g_steal_pointer (&global);
}

void
wp_registry_notify_add_object (WpRegistry *self, gpointer object)
{
  for (guint i = 0; i < self->object_managers->len; i++) {
    WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
    wp_object_manager_add_object (om, object);
    wp_object_manager_maybe_objects_changed (om);
  }
}

void
wp_registry_notify_rm_object (WpRegistry *self, gpointer object)
{
  for (guint i = 0; i < self->object_managers->len; i++) {
    WpObjectManager *om = g_ptr_array_index (self->object_managers, i);
    wp_object_manager_rm_object (om, object);
    wp_object_manager_maybe_objects_changed (om);
  }
}

void
wp_registry_install_object_manager (WpRegistry * self, WpObjectManager * om)
{
  guint i;

  g_object_weak_ref (G_OBJECT (om), object_manager_destroyed, self);
  g_ptr_array_add (self->object_managers, om);

  /* add pre-existing objects to the object manager,
     in case it's interested in them */
  for (i = 0; i < self->globals->len; i++) {
    WpGlobal *g = g_ptr_array_index (self->globals, i);
    /* check if null because the globals array can have gaps */
    if (g)
      wp_object_manager_add_global (om, g);
  }
  for (i = 0; i < self->objects->len; i++) {
    GObject *o = g_ptr_array_index (self->objects, i);
    wp_object_manager_add_object (om, o);
  }

  wp_object_manager_maybe_objects_changed (om);
}

/* WpGlobal */

G_DEFINE_BOXED_TYPE (WpGlobal, wp_global, wp_global_ref, wp_global_unref)

void
wp_global_rm_flag (WpGlobal *global, guint rm_flag)
{
  WpRegistry *reg = global->registry;
  guint32 id = global->id;

  /* no flag to remove */
  if (!(global->flags & rm_flag))
    return;

  wp_trace_boxed (WP_TYPE_GLOBAL, global,
      "remove global %u flag 0x%x [flags:0x%x, reg:%p]",
      id, rm_flag, global->flags, reg);

  /* global was owned by the proxy; by removing the flag, we clear out
     also the proxy pointer, which is presumably no longer valid and we
     notify all listeners that the proxy is gone */
  if (rm_flag == WP_GLOBAL_FLAG_OWNED_BY_PROXY) {
    global->flags &= ~WP_GLOBAL_FLAG_OWNED_BY_PROXY;
    if (reg && global->proxy) {
      wp_registry_notify_rm_object (reg, global->proxy);
    }
    global->proxy = NULL;
  }
  /* registry removed the global */
  else if (rm_flag == WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) {
    global->flags &= ~WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY;

    /* destroy the proxy if it exists */
    if (global->proxy) {
      /* steal the proxy to avoid calling wp_registry_notify_rm_object()
         again while removing OWNED_BY_PROXY;
         keep a temporary ref so that _deactivate() doesn't crash in case the
         pw-proxy-destroyed signal causes external references to be dropped */
      g_autoptr (WpGlobalProxy) proxy =
          g_object_ref (g_steal_pointer (&global->proxy));

      /* notify all listeners that the proxy is gone */
      if (reg)
        wp_registry_notify_rm_object (reg, proxy);

      /* remove FEATURE_BOUND to destroy the underlying pw_proxy */
      wp_object_deactivate (WP_OBJECT (proxy), WP_PROXY_FEATURE_BOUND);

      /* stop all in-progress activations */
      wp_object_abort_activation (WP_OBJECT (proxy), "PipeWire proxy removed");

      /* if the proxy is not owning the global, unref it */
      if (global->flags == 0)
        g_object_unref (proxy);
    }

    /* It's possible to receive consecutive {add, remove, add} events for the
     * same id. Since the WpGlobal might not be destroyed immediately below,
     * (e.g. it's in tmp_globals list), we must invalidate the id now, so that
     * this WpGlobal is not used in reference to objects added later.
     */
    global->id = SPA_ID_INVALID;
    wp_properties_setf (global->properties, PW_KEY_OBJECT_ID, NULL);
  }

  /* drop the registry's ref on global when it does not appear on the registry anymore */
  if (!(global->flags & WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY) && reg) {
    g_clear_pointer (&g_ptr_array_index (reg->globals, id), wp_global_unref);
  }
}

struct pw_proxy *
wp_global_bind (WpGlobal * global)
{
  g_return_val_if_fail (global->proxy, NULL);
  g_return_val_if_fail (global->registry, NULL);

  WpProxyClass *klass = WP_PROXY_GET_CLASS (global->proxy);
  return pw_registry_bind (global->registry->pw_registry, global->id,
      klass->pw_iface_type, klass->pw_iface_version, 0);
}
   070701000000AF000081A400000000000000000000000165F8630400000A33000000000000000000000000000000000000002C00000000wireplumber-0.5.0/lib/wp/private/registry.h   /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_REGISTRY_H__
#define __WIREPLUMBER_REGISTRY_H__

#include "core.h"
#include "global-proxy.h"

#include <pipewire/pipewire.h>

G_BEGIN_DECLS

typedef struct _WpRegistry WpRegistry;
typedef struct _WpGlobal WpGlobal;

/* registry */

struct _WpRegistry
{
  struct pw_registry *pw_registry;
  struct spa_hook listener;

  GPtrArray *globals; // elementy-type: WpGlobal*
  GPtrArray *tmp_globals; // elementy-type: WpGlobal*
  GPtrArray *objects; // element-type: GObject*
  GPtrArray *object_managers; // element-type: WpObjectManager*
  GPtrArray *features; // element-type: gchar*
};

void wp_registry_init (WpRegistry *self);
void wp_registry_clear (WpRegistry *self);
void wp_registry_attach (WpRegistry *self, struct pw_core *pw_core);
void wp_registry_detach (WpRegistry *self);

void wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
    guint32 permissions, guint32 flag, GType type,
    WpGlobalProxy *proxy, const struct spa_dict *props,
    WpGlobal ** new_global);

void wp_registry_notify_add_object (WpRegistry * self, gpointer object);
void wp_registry_notify_rm_object (WpRegistry * self, gpointer object);

void wp_registry_install_object_manager (WpRegistry * self,
    WpObjectManager * om);

static inline void
wp_registry_mark_feature_provided (WpRegistry * reg, const gchar * feature)
{
  g_ptr_array_add (reg->features, g_strdup (feature));
}

WpCore * wp_registry_get_core (WpRegistry * self) G_GNUC_CONST;

/* core */

WpRegistry * wp_core_get_registry (WpCore * self) G_GNUC_CONST;

/* global */

typedef enum {
  WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY = 0x1,
  WP_GLOBAL_FLAG_OWNED_BY_PROXY = 0x2,
} WpGlobalFlags;

struct _WpGlobal
{
  guint32 flags;
  guint32 id;
  GType type;
  guint32 permissions;
  WpProperties *properties;
  WpGlobalProxy *proxy;
  WpRegistry *registry;
};

#define WP_TYPE_GLOBAL (wp_global_get_type ())
GType wp_global_get_type (void);

static inline void
wp_global_clear (WpGlobal * self)
{
  g_clear_pointer (&self->properties, wp_properties_unref);
}

static inline WpGlobal *
wp_global_ref (WpGlobal * self)
{
  return g_rc_box_acquire (self);
}

static inline void
wp_global_unref (WpGlobal * self)
{
  g_rc_box_release_full (self, (GDestroyNotify) wp_global_clear);
}

void wp_global_rm_flag (WpGlobal *global, guint rm_flag);
struct pw_proxy * wp_global_bind (WpGlobal * global);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpGlobal, wp_global_unref)

G_END_DECLS

#endif
 070701000000B0000081A400000000000000000000000165F8630400007F15000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/properties.c /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "properties.h"
#include "log.h"

#include <errno.h>
#include <pipewire/properties.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-properties")

/*! \defgroup wpproperties WpProperties */
/*!
 * \struct WpProperties
 *
 * WpProperties is a data structure that contains string key-value pairs,
 * which are used to send/receive/attach arbitrary properties to PipeWire objects.
 *
 * This could be thought of as a hash table with strings as both keys and
 * values. However, the reason that this class exists instead of using
 * GHashTable directly is that in reality it wraps the PipeWire native
 * `struct spa_dict` and `struct pw_properties` and therefore it can be
 * easily passed to PipeWire function calls that require a `struct spa_dict *`
 * or a `struct pw_properties *` as arguments. Or alternatively, it can easily
 * wrap a `struct spa_dict *` or a `struct pw_properties *` that was given
 * from the PipeWire API without necessarily doing an expensive copy operation.
 *
 * WpProperties normally wraps a `struct pw_properties`, unless it was created
 * with wp_properties_new_wrap_dict(), in which case it wraps a
 * `struct spa_dict` and it is immutable (you cannot add/remove/modify any
 * key-value pair).
 *
 * In most cases, it actually owns the `struct pw_properties`
 * internally and manages its lifetime. The exception to that rule is when
 * WpProperties is constructed with wp_properties_new_wrap(), in which case
 * the ownership of the `struct pw_properties` remains outside. This must
 * be used with care, as the `struct pw_properties` may be free'ed externally.
 *
 * WpProperties is reference-counted with wp_properties_ref() and
 * wp_properties_unref().
 */

enum {
  FLAG_IS_DICT = (1<<1),
  FLAG_NO_OWNERSHIP = (1<<2),
};

struct _WpProperties
{
  grefcount ref;
  guint32 flags;
  union {
    struct pw_properties *props;
    const struct spa_dict *dict;
  };
};

G_DEFINE_BOXED_TYPE(WpProperties, wp_properties, wp_properties_ref, wp_properties_unref)

/*!
 * \brief Creates a new empty properties set
 * \ingroup wpproperties
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new_empty (void)
{
  WpProperties * self = g_slice_new0 (WpProperties);
  g_ref_count_init (&self->ref);
  self->flags = 0;
  self->props = pw_properties_new (NULL, NULL);
  return self;
}

/*!
 * \brief Constructs a new properties set that contains the given properties
 *
 * \ingroup wpproperties
 * \param key a property name
 * \param ... a property value, followed by any number of further property
 *   key-value pairs, followed by NULL
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new (const gchar * key, ...)
{
  WpProperties * self;
  va_list varargs;

  va_start(varargs, key);
  self = wp_properties_new_valist (key, varargs);
  va_end(varargs);

  return self;
}

/*!
 * \brief This is the `va_list` version of wp_properties_new()
 *
 * \ingroup wpproperties
 * \param key a property name
 * \param args the variable arguments passed to wp_properties_new()
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new_valist (const gchar * key, va_list args)
{
  WpProperties * self = wp_properties_new_empty ();
  const gchar *value;

  while (key != NULL) {
    value = va_arg(args, gchar *);
    if (value && key[0])
      wp_properties_set (self, key, value);
    key = va_arg(args, gchar *);
  }

  return self;
}

/*!
 * \brief Constructs a new properties set that contains the properties that can
 * be parsed from the given string
 *
 * \ingroup wpproperties
 * \param str a string containing either a whitespace separated list of key=value
 *    pairs (ex. "key1=value1 key2=value2") or a JSON object (ex. '{"key1":"value1"}')
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new_string (const gchar * str)
{
  WpProperties * self;

  g_return_val_if_fail (str != NULL, NULL);

  self = g_slice_new0 (WpProperties);
  g_ref_count_init (&self->ref);
  self->flags = 0;
  self->props = pw_properties_new_string (str);
  return self;
}

/*!
 * \brief Constructs a new properties set that contains the properties that can
 * be parsed from the given JSON object
 *
 * \ingroup wpproperties
 * \param json a JSON object
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new_json (const WpSpaJson * json)
{
  WpProperties * self;

  g_return_val_if_fail (json != NULL, NULL);

  self = wp_properties_new_empty ();
  wp_properties_update_from_json (self, json);
  return self;
}

/*!
 * \brief Constructs a new WpProperties that wraps the given \a props structure,
 * allowing reading properties on that \a props structure through
 * the WpProperties API.
 *
 * Care must be taken when using this function, since the returned WpProperties
 * object does not own the \a props structure. Therefore, if the owner decides
 * to free \a props, the returned WpProperties will crash when used. In addition,
 * the returned WpProperties object will not try to free \a props when destroyed.
 *
 * Furthermore, note that the returned WpProperties object is immutable. That
 * means that you cannot add or modify any properties on it, unless you make
 * a copy first.
 *
 * \ingroup wpproperties
 * \param props a native `pw_properties` structure to wrap
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new_wrap (const struct pw_properties * props)
{
  WpProperties * self;

  g_return_val_if_fail (props != NULL, NULL);

  self = g_slice_new0 (WpProperties);
  g_ref_count_init (&self->ref);
  self->flags = FLAG_NO_OWNERSHIP;
  self->props = (struct pw_properties *) props;
  return self;
}

/*!
 * \brief Constructs a new WpProperties that wraps the given \a props structure,
 * allowing reading & writing properties on that \a props structure through
 * the WpProperties API.
 *
 * In constrast with wp_properties_new_wrap(), this function assumes ownership
 * of the \a props structure, so it will try to free \a props when it is destroyed.
 *
 * \ingroup wpproperties
 * \param props a native `pw_properties` structure to wrap
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new_take (struct pw_properties * props)
{
  WpProperties * self;

  g_return_val_if_fail (props != NULL, NULL);

  self = g_slice_new0 (WpProperties);
  g_ref_count_init (&self->ref);
  self->flags = 0;
  self->props = props;
  return self;
}

/*!
 * \brief Constructs a new WpProperties that contains a copy of all the properties
 * contained in the given \a props structure.
 *
 * \ingroup wpproperties
 * \param props a native `pw_properties` structure to copy
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new_copy (const struct pw_properties * props)
{
  WpProperties * self;

  g_return_val_if_fail (props != NULL, NULL);

  self = g_slice_new0 (WpProperties);
  g_ref_count_init (&self->ref);
  self->flags = 0;
  self->props = pw_properties_copy (props);
  return self;
}

/*!
 * \brief Constructs a new WpProperties that wraps the given \a dict structure,
 * allowing reading properties from that \a dict through the WpProperties API.
 *
 * Note that the returned object does not own the \a dict, so care must be taken
 * not to free it externally while this WpProperties object is alive.
 *
 * In addition, note that the returned WpProperties object is immutable. That
 * means that you cannot add or modify any properties on it, since there is
 * no defined method for modifying a `struct spa_dict`. If you need to change
 * this properties set later, you should make a copy with wp_properties_copy().
 *
 * \ingroup wpproperties
 * \param dict a native `spa_dict` structure to wrap
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new_wrap_dict (const struct spa_dict * dict)
{
  WpProperties * self;

  g_return_val_if_fail (dict != NULL, NULL);

  self = g_slice_new0 (WpProperties);
  g_ref_count_init (&self->ref);
  self->flags = FLAG_NO_OWNERSHIP | FLAG_IS_DICT;
  self->dict = dict;
  return self;
}

/*!
 * \brief Constructs a new WpProperties that contains a copy of all the
 * properties contained in the given \a dict structure.
 *
 * \ingroup wpproperties
 * \param dict a native `spa_dict` structure to copy
 * \returns (transfer full): the newly constructed properties set
 */
WpProperties *
wp_properties_new_copy_dict (const struct spa_dict * dict)
{
  WpProperties * self;

  g_return_val_if_fail (dict != NULL, NULL);

  self = g_slice_new0 (WpProperties);
  g_ref_count_init (&self->ref);
  self->flags = 0;
  self->props = pw_properties_new_dict (dict);
  return self;
}

/*!
 * \brief Constructs and returns a new WpProperties object that contains a copy
 * of all the properties contained in \a other.
 *
 * \ingroup wpproperties
 * \param other a properties object
 * \returns (transfer full): the newly constructed properties set
 */

WpProperties *
wp_properties_copy (WpProperties * other)
{
  return wp_properties_new_copy_dict (wp_properties_peek_dict (other));
}

static void
wp_properties_free (WpProperties * self)
{
  if (!(self->flags & FLAG_NO_OWNERSHIP))
    pw_properties_free (self->props);
  g_slice_free (WpProperties, self);
}

/*!
 * \ingroup wpproperties
 * \param self a properties object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpProperties *
wp_properties_ref (WpProperties * self)
{
  g_ref_count_inc (&self->ref);
  return self;
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 *
 * \ingroup wpproperties
 * \param self (transfer full): a properties object
 */
void
wp_properties_unref (WpProperties * self)
{
  if (g_ref_count_dec (&self->ref))
    wp_properties_free (self);
}

/*!
 * \brief Ensures that the given properties set is uniquely owned.
 *
 * "Uniquely owned" means that:
 *  - its reference count is 1
 *  - it is not wrapping a native `spa_dict` or `pw_properties` object
 *
 * If \a self is not uniquely owned already, then it is unrefed and a copy of
 * it is returned instead. You should always consider \a self as unsafe to use
 * after this call and you should use the returned object instead.
 *
 * \ingroup wpproperties
 * \param self (transfer full): a properties object
 * \returns (transfer full): the uniquely owned properties object
 */
WpProperties *
wp_properties_ensure_unique_owner (WpProperties * self)
{
  if (!g_ref_count_compare (&self->ref, 1) ||
      self->flags & (FLAG_IS_DICT | FLAG_NO_OWNERSHIP))
  {
    WpProperties *copy = wp_properties_copy (self);
    wp_properties_unref (self);
    return copy;
  }
  return self;
}

/*!
 * \brief Updates (adds new or modifies existing) properties in \a self,
 * using the given \a props as a source.
 *
 * Any properties that are not contained in \a props are left untouched.
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param props a properties set that contains properties to update
 * \returns the number of properties that were changed
 */
gint
wp_properties_update (WpProperties * self, WpProperties * props)
{
  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  return pw_properties_update (self->props, wp_properties_peek_dict (props));
}

/*!
 * \brief Updates (adds new or modifies existing) properties in \a self,
 * using the given \a dict as a source.
 *
 * Any properties that are not contained in \a dict are left untouched.
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param dict a `spa_dict` that contains properties to update
 * \returns the number of properties that were changed
 */
gint
wp_properties_update_from_dict (WpProperties * self,
    const struct spa_dict * dict)
{
  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  return pw_properties_update (self->props, dict);
}

/*!
 * \brief Updates (adds new or modifies existing) properties in \a self,
 * using the given \a json as a source.
 *
 * Any properties that are not contained in \a json are left untouched.
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param json a JSON object that contains properties to update
 * \returns the number of properties that were changed
 */
gint
wp_properties_update_from_json (WpProperties * self, const WpSpaJson * json)
{
  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  return pw_properties_update_string (self->props, wp_spa_json_get_data (json),
      wp_spa_json_get_size (json));
}

/*!
 * \brief Adds new properties in \a self, using the given \a props as a source.
 *
 * Properties (keys) from \a props that are already contained in \a self
 * are not modified, unlike what happens with wp_properties_update().
 * Properties in \a self that are not contained in \a props are left untouched.
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param props a properties set that contains properties to add
 * \returns the number of properties that were changed
 */
gint
wp_properties_add (WpProperties * self, WpProperties * props)
{
  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  return pw_properties_add (self->props, wp_properties_peek_dict (props));
}

/*!
 * \brief Adds new properties in \a self, using the given \a dict as a source.
 *
 * Properties (keys) from \a dict that are already contained in \a self
 * are not modified, unlike what happens with wp_properties_update_from_dict().
 * Properties in \a self that are not contained in \a dict are left untouched.
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param dict a `spa_dict` that contains properties to add
 * \returns the number of properties that were changed
 */
gint
wp_properties_add_from_dict (WpProperties * self,
    const struct spa_dict * dict)
{
  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  return pw_properties_add (self->props, dict);
}

/*!
 * \brief Updates (adds new or modifies existing) properties in \a self,
 * using the given \a props as a source.
 *
 * Unlike wp_properties_update(), this function only updates properties that
 * have one of the specified keys; the rest is left untouched.
 *
 * \ingroup wpproperties
 * \param self a properties set
 * \param props a properties set that contains properties to update
 * \param key1 a property to update
 * \param ... a list of additional properties to update, followed by NULL
 * \returns the number of properties that were changed
 */
gint
wp_properties_update_keys (WpProperties * self, WpProperties * props,
    const gchar * key1, ...)
{
  gint changed = 0;
  const gchar *value;

  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  va_list args;
  va_start (args, key1);
  for (; key1; key1 = va_arg (args, const gchar *)) {
    if ((value = wp_properties_get (props, key1)) != NULL)
      changed += wp_properties_set (self, key1, value);
  }
  va_end (args);
  return changed;
}

/*!
 * \brief Updates (adds new or modifies existing) properties in \a self,
 * using the given \a dict as a source.
 *
 * Unlike wp_properties_update_from_dict(), this function only updates
 * properties that have one of the specified keys; the rest is left untouched.
 *
 * \ingroup wpproperties
 * \param self a properties set
 * \param dict a `spa_dict` that contains properties to update
 * \param key1 a property to update
 * \param ... a list of additional properties to update, followed by NULL
 * \returns the number of properties that were changed
 */
gint
wp_properties_update_keys_from_dict (WpProperties * self,
    const struct spa_dict * dict, const gchar * key1, ...)
{
  gint changed = 0;
  const gchar *value;

  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  va_list args;
  va_start (args, key1);
  for (; key1; key1 = va_arg (args, const gchar *)) {
    if ((value = spa_dict_lookup (dict, key1)) != NULL)
      changed += wp_properties_set (self, key1, value);
  }
  va_end (args);
  return changed;
}

/*!
 * \brief The same as wp_properties_update_keys(), using a NULL-terminated array
 * for specifying the keys to update
 *
 * \ingroup wpproperties
 * \param self a properties set
 * \param props a properties set that contains properties to update
 * \param keys (array zero-terminated=1): the properties to update
 * \returns the number of properties that were changed
 */
gint
wp_properties_update_keys_array (WpProperties * self, WpProperties * props,
    const gchar * keys[])
{
  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  return pw_properties_update_keys (self->props,
      wp_properties_peek_dict (props), keys);
}

/*!
 * \brief Adds new properties in \a self, using the given \a props as a source.
 *
 * Unlike wp_properties_add(), this function only adds properties that
 * have one of the specified keys; the rest is left untouched.
 *
 * \ingroup wpproperties
 * \param self a properties set
 * \param props a properties set that contains properties to add
 * \param key1 a property to add
 * \param ... a list of additional properties to add, followed by NULL
 * \returns the number of properties that were changed
 */
gint
wp_properties_add_keys (WpProperties * self, WpProperties * props,
    const gchar * key1, ...)
{
  gint changed = 0;
  const gchar *value;

  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  va_list args;
  va_start (args, key1);
  for (; key1; key1 = va_arg (args, const gchar *)) {
    if ((value = wp_properties_get (props, key1)) == NULL)
      continue;
    if (wp_properties_get (self, key1) == NULL)
      changed += wp_properties_set (self, key1, value);
  }
  va_end (args);
  return changed;
}

/*!
 * \brief Adds new properties in \a self, using the given \a dict as a source.
 *
 * Unlike wp_properties_add_from_dict(), this function only adds
 * properties that have one of the specified keys; the rest is left untouched.
 *
 * \ingroup wpproperties
 * \param self a properties set
 * \param dict a `spa_dict` that contains properties to add
 * \param key1 a property to add
 * \param ... a list of additional properties to add, followed by NULL
 * \returns the number of properties that were changed
 */
gint
wp_properties_add_keys_from_dict (WpProperties * self,
    const struct spa_dict * dict, const gchar * key1, ...)
{
  gint changed = 0;
  const gchar *value;

  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  va_list args;
  va_start (args, key1);
  for (; key1; key1 = va_arg (args, const gchar *)) {
    if ((value = spa_dict_lookup (dict, key1)) == NULL)
      continue;
    if (wp_properties_get (self, key1) == NULL)
      changed += wp_properties_set (self, key1, value);
  }
  va_end (args);
  return changed;
}

/*!
 * \brief The same as wp_properties_add_keys(), using a NULL-terminated array
 * for specifying the keys to add
 *
 * \ingroup wpproperties
 * \param self a properties set
 * \param props a properties set that contains properties to add
 * \param keys (array zero-terminated=1): the properties to add
 * \returns the number of properties that were changed
 */
gint
wp_properties_add_keys_array (WpProperties * self, WpProperties * props,
    const gchar * keys[])
{
  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  return pw_properties_add_keys (self->props,
      wp_properties_peek_dict (props), keys);
}

/*!
 * \brief Looks up a given property value from a key
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param key a property key
 * \returns (transfer none) (nullable): the value of the property identified
 *   with \a key, or NULL if this property is not contained in \a self
 */
const gchar *
wp_properties_get (WpProperties * self, const gchar * key)
{
  g_return_val_if_fail (self != NULL, NULL);
  g_return_val_if_fail (key != NULL, NULL);

  return spa_dict_lookup (wp_properties_peek_dict (self), key);
}

/*!
 * \brief Sets the given property \a key - \a value pair on \a self.
 *
 * If the property already existed, the value is overwritten with the new one.
 *
 * If the \a value is NULL, then the specified property is removed from \a self
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param key a property key
 * \param value (nullable): a property value
 * \returns 1 if the property was changed. 0 if nothing was changed because
 *   the property already existed with the same value or because the key to
 *   remove did not exist.
 */
gint
wp_properties_set (WpProperties * self, const gchar * key,
    const gchar * value)
{
  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  return pw_properties_set (self->props, key, value);
}

/*!
 * \brief Formats the given \a format string with the specified arguments
 * and sets the result as a value of the property specified with \a key
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param key a property key
 * \param format a printf-style format to be formatted and set as a value for
 *   this property \a key
 * \param ... arguments for \a format
 * \returns 1 if the property was changed. 0 if nothing was changed because
 *   the property already existed with the same value
 */
gint
wp_properties_setf (WpProperties * self, const gchar * key,
    const gchar * format, ...)
{
  gint res;
  va_list varargs;

  va_start (varargs, format);
  res = wp_properties_setf_valist (self, key, format, varargs);
  va_end (varargs);

  return res;
}

/*!
 * \brief This is the `va_list` version of wp_properties_setf()
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param key a property key
 * \param format a printf-style format to be formatted and set as a value for
 *   this property \a key
 * \param args the variable arguments passed to wp_properties_setf()
 * \returns 1 if the property was changed. 0 if nothing was changed because
 *   the property already existed with the same value
 */
gint
wp_properties_setf_valist (WpProperties * self, const gchar * key,
    const gchar * format, va_list args)
{
  g_return_val_if_fail (self != NULL, -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_IS_DICT), -EINVAL);
  g_return_val_if_fail (!(self->flags & FLAG_NO_OWNERSHIP), -EINVAL);

  return pw_properties_setva (self->props, key, format, args);
}

struct _WpPropertiesItem
{
  WpProperties *props;
  const struct spa_dict_item *item;
};

G_DEFINE_BOXED_TYPE (WpPropertiesItem, wp_properties_item,
    wp_properties_item_ref, wp_properties_item_unref)

static WpPropertiesItem *
wp_properties_item_new (WpProperties *props, const struct spa_dict_item *item)
{
  WpPropertiesItem *self = g_rc_box_new0 (WpPropertiesItem);
  self->props = wp_properties_ref (props);
  self->item = item;
  return self;
}

static void
wp_properties_item_free (gpointer p)
{
  WpPropertiesItem *self = p;
  wp_properties_unref (self->props);
}

/*!
 * \brief Increases the reference count of a properties item object
 * \ingroup wpproperties
 * \param self a properties item object
 * \returns (transfer full): \a self with an additional reference count on it
 * \since 0.4.2
 */
WpPropertiesItem *
wp_properties_item_ref (WpPropertiesItem *self)
{
  return g_rc_box_acquire (self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 * \ingroup wpproperties
 * \param self (transfer full): a properties item object
 * \since 0.4.2
 */
void
wp_properties_item_unref (WpPropertiesItem *self)
{
  g_rc_box_release_full (self, wp_properties_item_free);
}

/*!
 * \brief Gets the key from a properties item
 *
 * \ingroup wpproperties
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_properties_new_iterator()
 * \returns (transfer none): the property key of the \a item
 * \since 0.4.2
 */
const gchar *
wp_properties_item_get_key (WpPropertiesItem * self)
{
  return self->item->key;
}

/*!
 * \brief Gets the value from a properties item
 *
 * \ingroup wpproperties
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_properties_new_iterator()
 * \returns (transfer none): the property value of the \a item
 * \since 0.4.2
 */
const gchar *
wp_properties_item_get_value (WpPropertiesItem * self)
{
  return self->item->value;
}

struct dict_iterator_data
{
  WpProperties *properties;
  const struct spa_dict_item *item;
};

static void
dict_iterator_reset (WpIterator *it)
{
  struct dict_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->item = wp_properties_peek_dict (it_data->properties)->items;
}

static gboolean
dict_iterator_next (WpIterator *it, GValue *item)
{
  struct dict_iterator_data *it_data = wp_iterator_get_user_data (it);
  const struct spa_dict *dict = wp_properties_peek_dict (it_data->properties);

  if ((it_data->item - dict->items) < dict->n_items) {
    g_value_init (item, WP_TYPE_PROPERTIES_ITEM);
    g_autoptr (WpPropertiesItem) pi = wp_properties_item_new (
        it_data->properties, it_data->item);
    g_value_take_boxed (item, g_steal_pointer (&pi));
    it_data->item++;
    return TRUE;
  }
  return FALSE;
}

static gboolean
dict_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
    gpointer data)
{
  struct dict_iterator_data *it_data = wp_iterator_get_user_data (it);
  const struct spa_dict *dict = wp_properties_peek_dict (it_data->properties);
  const struct spa_dict_item *i;

  spa_dict_for_each (i, dict) {
    g_auto (GValue) item = G_VALUE_INIT;
    g_autoptr (WpPropertiesItem) pi = wp_properties_item_new (
        it_data->properties, i);
    g_value_init (&item, WP_TYPE_PROPERTIES_ITEM);
    g_value_take_boxed (&item, g_steal_pointer (&pi));
    if (!func (&item, ret, data))
      return FALSE;
  }
  return TRUE;
}

static void
dict_iterator_finalize (WpIterator *it)
{
  struct dict_iterator_data *it_data = wp_iterator_get_user_data (it);
  wp_properties_unref (it_data->properties);
}

static const WpIteratorMethods dict_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = dict_iterator_reset,
  .next = dict_iterator_next,
  .fold = dict_iterator_fold,
  .finalize = dict_iterator_finalize,
};

/*!
 * \brief Iterates through all the properties in the properties object
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \returns (transfer full): an iterator that iterates over the properties.
 *   The items in the iterator are of type WpPropertiesItem.
 *   Use wp_properties_item_get_key() and
 *   wp_properties_item_get_value() to retrieve their contents.
 */
WpIterator *
wp_properties_new_iterator (WpProperties * self)
{
  g_autoptr (WpIterator) it = NULL;
  struct dict_iterator_data *it_data;

  g_return_val_if_fail (self != NULL, NULL);

  it = wp_iterator_new (&dict_iterator_methods,
      sizeof (struct dict_iterator_data));
  it_data = wp_iterator_get_user_data (it);
  it_data->properties = wp_properties_ref (self);
  it_data->item = wp_properties_peek_dict (it_data->properties)->items;
  return g_steal_pointer (&it);
}

/*!
 * \brief Gets the number of properties contained in this object
 * \ingroup wpproperties
 * \param self a properties object
 * \returns the number of properties contained in this object
 * \since 0.4.10
 */
guint
wp_properties_get_count (WpProperties * self)
{
  const struct spa_dict *dict = wp_properties_peek_dict(self);
  g_return_val_if_fail (dict != NULL, 0);

  return dict->n_items;
}

/*!
 * \brief Sorts the keys in alphabetical order
 * \ingroup wpproperties
 * \param self a properties object
 */
void
wp_properties_sort (WpProperties * self)
{
  g_return_if_fail (self != NULL);
  g_return_if_fail (!(self->flags & FLAG_IS_DICT));
  g_return_if_fail (!(self->flags & FLAG_NO_OWNERSHIP));

  return spa_dict_qsort (&self->props->dict);
}

/*!
 * \brief Gets the dictionary wrapped by a properties object
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \returns (transfer none): the internal properties set as a `struct spa_dict *`
 */
const struct spa_dict *
wp_properties_peek_dict (WpProperties * self)
{
  g_return_val_if_fail (self != NULL, NULL);

  return (self->flags & FLAG_IS_DICT) ? self->dict : &self->props->dict;
}

/*!
 * \brief Gets a copy of the properties object as a `struct pw_properties`
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \returns (transfer full): a copy of the properties in \a self
 *   as a `struct pw_properties`
 */
struct pw_properties *
wp_properties_to_pw_properties (WpProperties * self)
{
  g_return_val_if_fail (self != NULL, NULL);

  return pw_properties_new_dict (wp_properties_peek_dict (self));
}

/*!
 * \brief Similar to wp_properties_to_pw_properties(), but this method avoids making
 * a copy of the properties by returning the `struct pw_properties` that is
 * stored internally and then freeing the WpProperties wrapper.
 *
 * If \a self is not uniquely owned (see wp_properties_ensure_unique_owner()),
 * then this method does make a copy and is the same as
 * wp_properties_to_pw_properties(), performance-wise.
 *
 * \ingroup wpproperties
 * \param self (transfer full): a properties object
 * \returns (transfer full): the properties in \a self as a `struct pw_properties`
 */
struct pw_properties *
wp_properties_unref_and_take_pw_properties (WpProperties * self)
{
  g_return_val_if_fail (self != NULL, NULL);

  g_autoptr (WpProperties) unique = wp_properties_ensure_unique_owner (self);
  /* set the flag so that unref-ing \a unique will not destroy unique->props */
  unique->flags = FLAG_NO_OWNERSHIP;
  return unique->props;
}

/*!
 * \brief Checks if all property values contained in \a other are matching with
 * the values in \a self.
 *
 * If a property is contained in \a other and not in \a self, the result is not
 * matched. If a property is contained in both sets, then the value of the
 * property in \a other is interpreted as a glob-style pattern
 * (using g_pattern_match_simple()) and the value in \a self is checked to
 * see if it matches with this pattern.
 *
 * \ingroup wpproperties
 * \param self a properties object
 * \param other a set of properties to match
 * \returns TRUE if all matches were successfull, FALSE if at least one
 *   property value did not match
 */
gboolean
wp_properties_matches (WpProperties * self, WpProperties *other)
{
  const struct spa_dict * dict;
  const struct spa_dict_item *item;
  const gchar *value;

  g_return_val_if_fail (self != NULL, FALSE);

  /* Check if the property values match the ones from 'self' */
  dict = wp_properties_peek_dict (other);
  spa_dict_for_each(item, dict) {
    value = wp_properties_get (self, item->key);
    if (!value || !g_pattern_match_simple (value, item->value))
      return FALSE;
  }

  return TRUE;
}
   070701000000B1000081A400000000000000000000000165F8630400001233000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/properties.h /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_PROPERTIES_H__
#define __WIREPLUMBER_PROPERTIES_H__

#include <glib-object.h>
#include "defs.h"
#include "iterator.h"
#include "spa-json.h"

G_BEGIN_DECLS

struct pw_properties;
struct spa_dict;

/*!
 * \brief The WpProperties GType
 * \ingroup wpproperties
 */
#define WP_TYPE_PROPERTIES (wp_properties_get_type ())
WP_API
GType wp_properties_get_type (void);

typedef struct _WpProperties WpProperties;

WP_API
WpProperties * wp_properties_new_empty (void);

WP_API
WpProperties * wp_properties_new (const gchar * key, ...) G_GNUC_NULL_TERMINATED;

WP_API
WpProperties * wp_properties_new_valist (const gchar * key, va_list args);

WP_API
WpProperties * wp_properties_new_string (const gchar * str);

WP_API
WpProperties * wp_properties_new_json (const WpSpaJson * json);

WP_API
WpProperties * wp_properties_new_wrap (const struct pw_properties * props);

WP_API
WpProperties * wp_properties_new_take (struct pw_properties * props);

WP_API
WpProperties * wp_properties_new_copy (const struct pw_properties * props);

WP_API
WpProperties * wp_properties_new_wrap_dict (const struct spa_dict * dict);

WP_API
WpProperties * wp_properties_new_copy_dict (const struct spa_dict * dict);

WP_API
WpProperties * wp_properties_copy (WpProperties * other);

/* ref counting */

WP_API
WpProperties * wp_properties_ref (WpProperties * self);

WP_API
void wp_properties_unref (WpProperties * self);

WP_API
WpProperties * wp_properties_ensure_unique_owner (WpProperties * self);

/* update */

WP_API
gint wp_properties_update (WpProperties * self, WpProperties * props);

WP_API
gint wp_properties_update_from_dict (WpProperties * self,
    const struct spa_dict * dict);

WP_API
gint wp_properties_update_from_json (WpProperties * self, const WpSpaJson * json);

/* add */

WP_API
gint wp_properties_add (WpProperties * self, WpProperties * props);

WP_API
gint wp_properties_add_from_dict (WpProperties * self,
    const struct spa_dict * dict);

/* update keys */

WP_API
gint wp_properties_update_keys (WpProperties * self, WpProperties * props,
    const gchar * key1, ...) G_GNUC_NULL_TERMINATED;

WP_API
gint wp_properties_update_keys_from_dict (WpProperties * self,
    const struct spa_dict * dict, const gchar * key1, ...) G_GNUC_NULL_TERMINATED;

WP_API
gint wp_properties_update_keys_array (WpProperties * self, WpProperties * props,
    const gchar * keys[]);

/* add keys */

WP_API
gint wp_properties_add_keys (WpProperties * self, WpProperties * props,
    const gchar * key1, ...) G_GNUC_NULL_TERMINATED;

WP_API
gint wp_properties_add_keys_from_dict (WpProperties * self,
    const struct spa_dict * dict, const gchar * key1, ...) G_GNUC_NULL_TERMINATED;

WP_API
gint wp_properties_add_keys_array (WpProperties * self, WpProperties * props,
    const gchar * keys[]);

/* get/set */

WP_API
const gchar * wp_properties_get (WpProperties * self, const gchar * key);

WP_API
gint wp_properties_set (WpProperties * self, const gchar * key,
    const gchar * value);

WP_API
gint wp_properties_setf (WpProperties * self, const gchar * key,
    const gchar * format, ...) G_GNUC_PRINTF(3, 4);

WP_API
gint wp_properties_setf_valist (WpProperties * self, const gchar * key,
    const gchar * format, va_list args) G_GNUC_PRINTF(3, 0);

/* iterate */

#define WP_TYPE_PROPERTIES_ITEM (wp_properties_item_get_type ())
WP_API
GType wp_properties_item_get_type (void);

typedef struct _WpPropertiesItem WpPropertiesItem;

WP_API
WpPropertiesItem *wp_properties_item_ref (WpPropertiesItem *self);

WP_API
void wp_properties_item_unref (WpPropertiesItem *self);

WP_API
const gchar * wp_properties_item_get_key (WpPropertiesItem * self);

WP_API
const gchar * wp_properties_item_get_value (WpPropertiesItem * self);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpPropertiesItem, wp_properties_item_unref)

WP_API
WpIterator * wp_properties_new_iterator (WpProperties * self);

/* count */

WP_API
guint wp_properties_get_count (WpProperties * self);


/* sort */

WP_API
void wp_properties_sort (WpProperties * self);

/* convert */

WP_API
const struct spa_dict * wp_properties_peek_dict (WpProperties * self);

WP_API
struct pw_properties * wp_properties_to_pw_properties (WpProperties * self);

WP_API
struct pw_properties * wp_properties_unref_and_take_pw_properties (
    WpProperties * self);

/* comparison */

WP_API
gboolean wp_properties_matches (WpProperties * self, WpProperties *other);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpProperties, wp_properties_unref)

G_END_DECLS

#endif
 070701000000B2000081A400000000000000000000000165F8630400002A19000000000000000000000000000000000000002C00000000wireplumber-0.5.0/lib/wp/proxy-interfaces.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "proxy-interfaces.h"
#include "properties.h"
#include "log.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-proxy-ifaces")

/*! \defgroup wppipewireobject WpPipewireObject */
/*!
 * \struct WpPipewireObject
 *
 * An interface for standard PipeWire objects.
 *
 * The common characteristic of all objects that implement this interface is
 * the presence of an "info" structure that contains additional properties
 * for this object (in the form of a spa_dict / pw_properties) and optionally
 * also some parameters that can be enumerated and set on the object.
 *
 * \gproperties
 *
 * \gproperty{properties, WpProperties *, G_PARAM_READABLE,
 *   The properties of the pipewire object}
 *
 * \gproperty{param-info, GVariant * (a{ss}), G_PARAM_READABLE,
 *   The param info of the object}
 *
 * \gproperty{native-info, gpointer, G_PARAM_READABLE,
 *   The native info structure}
 *
 * \gsignals
 *
 * \par params-changed
 * \parblock
 * \code
 * params_changed_callback (WpPipewireObject * self,
 *                          const gchar *id,
 *                          gpointer user_data)
 * \endcode
 *
 * Emitted when the params for id have changed. On proxies that cache params
 * from a remote object, this is emitted after the cached values have changed.
 *
 * You can expect this to be emitted only when the relevant
 * WP_PIPEWIRE_OBJECT_FEATURE_PARAM_* has been activated.
 *
 * Parameters:
 * - `id` - the parameter id as a string (ex "Props", "EnumRoute")
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 */

G_DEFINE_INTERFACE (WpPipewireObject, wp_pipewire_object, WP_TYPE_PROXY)

static void
wp_pipewire_object_default_init (WpPipewireObjectInterface * iface)
{
  g_object_interface_install_property (iface,
      g_param_spec_pointer ("native-info", "native-info",
          "The native info structure",
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_interface_install_property (iface,
      g_param_spec_boxed ("properties", "properties",
          "The properties of the pipewire object", WP_TYPE_PROPERTIES,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_interface_install_property (iface,
      g_param_spec_variant ("param-info", "param-info",
          "The param info of the object", G_VARIANT_TYPE ("a{ss}"), NULL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_signal_new ("params-changed", G_TYPE_FROM_INTERFACE (iface),
      G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
}

/*!
 * \brief Retrieves the native infor structure of this object
 * (pw_node_info, pw_port_info, etc...)
 *
 * \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 *
 * \ingroup wppipewireobject
 * \param self the pipewire object
 * \returns (nullable): the native pipewire info structure of this object
 */
gconstpointer
wp_pipewire_object_get_native_info (WpPipewireObject * self)
{
  g_return_val_if_fail (WP_IS_PIPEWIRE_OBJECT (self), NULL);
  g_return_val_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->get_native_info,
      NULL);

  return WP_PIPEWIRE_OBJECT_GET_IFACE (self)->get_native_info (self);
}

/*!
 * \brief Retrieves the PipeWire properties of this object
 *
 * \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 *
 * \ingroup wppipewireobject
 * \param self the pipewire object
 * \returns (transfer full): the pipewire properties of this object;
 *   normally these are the properties that are part of the info structure
 */
WpProperties *
wp_pipewire_object_get_properties (WpPipewireObject * self)
{
  g_return_val_if_fail (WP_IS_PIPEWIRE_OBJECT (self), NULL);
  g_return_val_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->get_properties,
      NULL);

  return WP_PIPEWIRE_OBJECT_GET_IFACE (self)->get_properties (self);
}

/*!
 * \brief Iterates over the object's PipeWire properties
 *
 * \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 *
 * \ingroup wppipewireobject
 * \param self the pipewire object
 * \returns (transfer full): an iterator that iterates over the pipewire
 *   properties of this object. Use wp_properties_iterator_item_get_key() and
 *   wp_properties_iterator_item_get_value() to parse the items returned by
 *   this iterator.
 */
WpIterator *
wp_pipewire_object_new_properties_iterator (WpPipewireObject * self)
{
  g_autoptr (WpProperties) properties =
      wp_pipewire_object_get_properties (self);
  return properties ? wp_properties_new_iterator (properties) : NULL;
}

/*!
 * \brief Returns the value of a single pipewire property.
 *
 * This is the same as getting the whole properties structure with
 * wp_pipewire_object_get_properties() and accessing a single property with
 * wp_properties_get(), but saves one call and having to clean up the
 * WpProperties reference count afterwards.
 *
 * The value is owned by the proxy, but it is guaranteed to stay alive
 * until execution returns back to the event loop.
 *
 * \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 *
 * \ingroup wppipewireobject
 * \param self the pipewire object
 * \param key the property name
 * \returns (transfer none) (nullable): the value of the pipewire property
 *   \a key or NULL if the property doesn't exist
 */
const gchar *
wp_pipewire_object_get_property (WpPipewireObject * self, const gchar * key)
{
  g_autoptr (WpProperties) properties =
      wp_pipewire_object_get_properties (self);
  return properties ? wp_properties_get (properties, key) : NULL;
}

/*!
 * \brief Returns the available parameters of this pipewire object.
 *
 * The return value is a GVariant of type `a{ss}`, where the key of each map
 * entry is a spa param type id (the same ids that you can pass in
 * wp_pipewire_object_enum_params()) and the value is a string that can
 * contain the following letters, each of them representing a flag:
 *   - `r`: the param is readable (`SPA_PARAM_INFO_READ`)
 *   - `w`: the param is writable (`SPA_PARAM_INFO_WRITE`)
 *
 * For params that are readable, you can query them with
 * wp_pipewire_object_enum_params()
 *
 * Params that are writable can be set with wp_pipewire_object_set_param()
 *
 * \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 *
 * \ingroup wppipewireobject
 * \param self the pipewire object
 * \returns (transfer full) (nullable): a variant of type `a{ss}` or NULL
 *   if the object does not support params at all
 */
GVariant *
wp_pipewire_object_get_param_info (WpPipewireObject * self)
{
  g_return_val_if_fail (WP_IS_PIPEWIRE_OBJECT (self), NULL);
  g_return_val_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->get_param_info,
      NULL);

  return WP_PIPEWIRE_OBJECT_GET_IFACE (self)->get_param_info (self);
}

/*!
 * \brief Enumerate object parameters.
 *
 * This will asynchronously return the result, or an error, by calling the
 * given \a callback. The result is going to be a WpIterator containing
 * WpSpaPod objects, which can be retrieved
 * with wp_pipewire_object_enum_params_finish().
 *
 * \ingroup wppipewireobject
 * \param self the pipewire object
 * \param id (nullable): the parameter id to enumerate or NULL for all parameters
 * \param filter (nullable): a param filter or NULL
 * \param cancellable (nullable): a cancellable for the async operation
 * \param callback (scope async): a callback to call with the result
 * \param user_data (closure): data to pass to \a callback
 */
void
wp_pipewire_object_enum_params (WpPipewireObject * self, const gchar * id,
    WpSpaPod *filter, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer user_data)
{
  g_return_if_fail (WP_IS_PIPEWIRE_OBJECT (self));
  g_return_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_params);

  WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_params (self, id, filter,
      cancellable, callback, user_data);
}

/*!
 * \brief Finishes an asynchronous parameter enumeration operation.
 *
 * \ingroup wppipewireobject
 * \param self the pipewire object
 * \param res the async result
 * \param error (out) (optional): the reported error of the operation, if any
 * \returns (transfer full) (nullable): an iterator to iterate over the
 *   collected params, or NULL if the operation resulted in error;
 *   the items in the iterator are WpSpaPod
 */
WpIterator *
wp_pipewire_object_enum_params_finish (WpPipewireObject * self,
    GAsyncResult * res, GError ** error)
{
  g_return_val_if_fail (WP_IS_PIPEWIRE_OBJECT (self), NULL);
  g_return_val_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_params_finish,
      NULL);

  return WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_params_finish (self, res,
      error);
}

/*!
 * \brief This method can be used to retrieve object parameters in a synchronous
 * way (in contrast with wp_pipewire_object_enum_params(), which is async).
 *
 * The `WP_PIPEWIRE_OBJECT_FEATURE_PARAM_<something>` feature
 * that corresponds to the specified \a id must have been activated earlier.
 * These features enable monitoring and caching of params underneath, so that
 * they are always available for retrieval with this method.
 *
 * Note, however, that cached params may be out-of-date if they have changed
 * very recently on the remote object and the caching mechanism hasn't been
 * able to update them yet, so if you really need up-to-date information you
 * should only rely on wp_pipewire_object_enum_params() instead.
 *
 * \ingroup wppipewireobject
 * \param self the pipewire object
 * \param id the parameter id to enumerate
 * \param filter (nullable): a param filter or NULL
 * \returns (transfer full) (nullable): an iterator to iterate over cached
 *    parameters, or NULL if parameters for this \a id are not cached;
 *    the items in the iterator are WpSpaPod
 */
WpIterator *
wp_pipewire_object_enum_params_sync (WpPipewireObject * self,
    const gchar * id, WpSpaPod * filter)
{
  g_return_val_if_fail (WP_IS_PIPEWIRE_OBJECT (self), NULL);
  g_return_val_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_params_sync,
      NULL);

  return WP_PIPEWIRE_OBJECT_GET_IFACE (self)->enum_params_sync (self, id,
      filter);
}

/*!
 * \brief Sets a parameter on the object.
 *
 * \ingroup wppipewireobject
 * \param self the pipewire object
 * \param id the parameter id to set
 * \param flags optional flags or 0
 * \param param (transfer full): the parameter to set
 * \returns TRUE on success, FALSE if setting the param failed
 */
gboolean
wp_pipewire_object_set_param (WpPipewireObject * self, const gchar * id,
    guint32 flags, WpSpaPod * param)
{
  g_return_val_if_fail (WP_IS_PIPEWIRE_OBJECT (self), FALSE);
  g_return_val_if_fail (WP_PIPEWIRE_OBJECT_GET_IFACE (self)->set_param, FALSE);

  return WP_PIPEWIRE_OBJECT_GET_IFACE (self)->set_param (self, id, flags,
      param);
}
   070701000000B3000081A400000000000000000000000165F8630400000973000000000000000000000000000000000000002C00000000wireplumber-0.5.0/lib/wp/proxy-interfaces.h   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_PROXY_INTERFACES_H__
#define __WIREPLUMBER_PROXY_INTERFACES_H__

#include "proxy.h"
#include "properties.h"
#include "spa-pod.h"

G_BEGIN_DECLS

/*!
 * \brief The WpPipewireObject GType
 * \ingroup wppipewireobject
 */
#define WP_TYPE_PIPEWIRE_OBJECT (wp_pipewire_object_get_type ())
WP_API
G_DECLARE_INTERFACE (WpPipewireObject, wp_pipewire_object,
                     WP, PIPEWIRE_OBJECT, WpProxy)

struct _WpPipewireObjectInterface
{
  GTypeInterface parent_iface;

  gconstpointer (*get_native_info) (WpPipewireObject * self);

  WpProperties * (*get_properties) (WpPipewireObject * self);

  GVariant * (*get_param_info) (WpPipewireObject * self);

  void (*enum_params) (WpPipewireObject * self, const gchar * id,
      WpSpaPod * filter, GCancellable * cancellable,
      GAsyncReadyCallback callback, gpointer user_data);

  WpIterator * (*enum_params_finish) (WpPipewireObject * self,
      GAsyncResult * res, GError ** error);

  WpIterator * (*enum_params_sync) (WpPipewireObject * self,
      const gchar * id, WpSpaPod * filter);

  gboolean (*set_param) (WpPipewireObject * self, const gchar * id,
      guint32 flags, WpSpaPod * param);

  /*< private >*/
  WP_PADDING(5)
};

WP_API
gconstpointer wp_pipewire_object_get_native_info (WpPipewireObject * self);

WP_API
WpProperties * wp_pipewire_object_get_properties (WpPipewireObject * self);

WP_API
WpIterator * wp_pipewire_object_new_properties_iterator (
    WpPipewireObject * self);

WP_API
const gchar * wp_pipewire_object_get_property (WpPipewireObject * self,
    const gchar * key);

WP_API
GVariant * wp_pipewire_object_get_param_info (WpPipewireObject * self);

WP_API
void wp_pipewire_object_enum_params (WpPipewireObject * self, const gchar * id,
    WpSpaPod *filter, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer user_data);

WP_API
WpIterator * wp_pipewire_object_enum_params_finish (WpPipewireObject * self,
    GAsyncResult * res, GError ** error);

WP_API
WpIterator * wp_pipewire_object_enum_params_sync (WpPipewireObject * self,
    const gchar * id, WpSpaPod * filter);

WP_API
gboolean wp_pipewire_object_set_param (WpPipewireObject * self,
    const gchar * id, guint32 flags, WpSpaPod * param);


G_END_DECLS

#endif
 070701000000B4000081A400000000000000000000000165F86304000025FB000000000000000000000000000000000000002100000000wireplumber-0.5.0/lib/wp/proxy.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "proxy.h"
#include "log.h"
#include "error.h"

#include <pipewire/pipewire.h>
#include <spa/utils/hook.h>
#include <spa/utils/result.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-proxy")

/*! \defgroup wpproxy WpProxy */
/*!
 * \struct WpProxy
 *
 * Base class for all objects that expose PipeWire objects using `pw_proxy`
 * underneath.
 *
 * This base class cannot be instantiated. It provides handling of
 * pw_proxy's events and exposes common functionality.
 *
 * \gproperties
 *
 * \gproperty{bound-id, guint, G_PARAM_READABLE,
 *   The id that this object has on the registry}
 *
 * \gproperty{pw-proxy, gpointer, G_PARAM_READABLE,
 *   The `struct pw_proxy *`}
 *
 * \gsignals
 *
 * \par bound
 * \parblock
 * \code
 * void
 * bound_callback (WpProxy * self,
 *                 guint id,
 *                 gpointer user_data)
 * \endcode
 * Parameters:
 * - `id` - the bound id of the proxy
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par pw-proxy-created
 * \parblock
 * \code
 * void
 * pw_proxy_created_callback (WpProxy * self,
 *                            gpointer object,
 *                            gpointer user_data)
 * \endcode
 * Parameters:
 * - `object` - pointer to the pw_proxy that was just created
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par pw-proxy-destroyed
 * \parblock
 * \code
 * void
 * pw_proxy_destroyed_callback (WpProxy * self,
 *                              gpointer user_data)
 * \endcode
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par error
 * \parblock
 * \code
 * void
 * error_callback (WpProxy * self,
 *                 gint seq, gint res, const gchar *message,
 *                 gpointer user_data)
 * \endcode
 * Emitted when an error occurs on the remote object.
 * The parameters are exactly the same as on the underlying pw_proxy error
 * callback.
 *
 * Parameters:
 * - `seq` - the sequence number of the operation that caused the error
 * - `res` - the error code
 * - `message` - a description of the error
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 */

typedef struct _WpProxyPrivate WpProxyPrivate;
struct _WpProxyPrivate
{
  struct pw_proxy *pw_proxy;
  struct spa_hook listener;
};

enum {
  PROP_0,
  PROP_BOUND_ID,
  PROP_PW_PROXY,
};

enum
{
  SIGNAL_PW_PROXY_CREATED,
  SIGNAL_PW_PROXY_DESTROYED,
  SIGNAL_BOUND,
  SIGNAL_ERROR,
  LAST_SIGNAL,
};

static guint signals[LAST_SIGNAL] = { 0 };

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpProxy, wp_proxy, WP_TYPE_OBJECT)

static void
proxy_event_destroy (void *data)
{
  WpProxy *self = WP_PROXY (data);
  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);

  wp_trace_object (self, "destroyed pw_proxy %p (%u)", priv->pw_proxy,
      pw_proxy_get_bound_id (priv->pw_proxy));

  spa_hook_remove (&priv->listener);
  priv->pw_proxy = NULL;
  wp_object_update_features (WP_OBJECT (self), 0, WP_PROXY_FEATURE_BOUND);

  wp_object_abort_activation (WP_OBJECT (self), "PipeWire proxy destroyed");

  g_signal_emit (self, signals[SIGNAL_PW_PROXY_DESTROYED], 0);
}

static void
proxy_event_bound (void *data, uint32_t global_id)
{
  WpProxy *self = WP_PROXY (data);

  wp_trace_object (self, "bound to %u", global_id);

  wp_object_update_features (WP_OBJECT (self), WP_PROXY_FEATURE_BOUND, 0);
  g_signal_emit (self, signals[SIGNAL_BOUND], 0, global_id);
}

static void
proxy_event_removed (void *data)
{
  wp_trace_object (data, "removed");
}

static void
proxy_event_error (void *data, int seq, int res, const char *message)
{
  WpProxy *self = WP_PROXY (data);
  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);

  wp_trace_object (self, "error seq:%d res:%d (%s) %s",
      seq, res, spa_strerror(res), message);

  /* we destroy the proxy on error if feature bound is still not enabled */
  if (priv->pw_proxy &&
      !(wp_object_test_active_features (WP_OBJECT (self), WP_PROXY_FEATURE_BOUND)))
    pw_proxy_destroy (priv->pw_proxy);

  wp_object_abort_activation (WP_OBJECT (self), message);

  g_signal_emit (self, signals[SIGNAL_ERROR], 0, seq, res, message);
}

static const struct pw_proxy_events proxy_events = {
  PW_VERSION_PROXY_EVENTS,
  .destroy = proxy_event_destroy,
  .bound = proxy_event_bound,
  .removed = proxy_event_removed,
  .error = proxy_event_error,
};

static void
wp_proxy_init (WpProxy * self)
{
}

static void
wp_proxy_dispose (GObject * object)
{
  WpProxyPrivate *priv = wp_proxy_get_instance_private (WP_PROXY (object));

  if (priv->pw_proxy)
    pw_proxy_destroy (priv->pw_proxy);

  G_OBJECT_CLASS (wp_proxy_parent_class)->dispose (object);
}

static void
wp_proxy_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpProxy *self = WP_PROXY (object);

  switch (property_id) {
  case PROP_BOUND_ID:
    g_value_set_uint (value, wp_proxy_get_bound_id (self));
    break;
  case PROP_PW_PROXY:
    g_value_set_pointer (value, wp_proxy_get_pw_proxy (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_proxy_deactivate (WpObject * object, WpObjectFeatures features)
{
  if (features & WP_PROXY_FEATURE_BOUND) {
    WpProxyPrivate *priv = wp_proxy_get_instance_private (WP_PROXY (object));
    if (priv->pw_proxy)
      pw_proxy_destroy (priv->pw_proxy);
    wp_object_update_features (object, 0, WP_PROXY_FEATURE_BOUND);
  }
}

static void
wp_proxy_class_init (WpProxyClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;

  object_class->get_property = wp_proxy_get_property;
  object_class->dispose = wp_proxy_dispose;

  wpobject_class->deactivate = wp_proxy_deactivate;

  /* Install the properties */

  g_object_class_install_property (object_class, PROP_BOUND_ID,
      g_param_spec_uint ("bound-id", "bound-id",
          "The id that this object has on the registry", 0, G_MAXUINT, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PW_PROXY,
      g_param_spec_pointer ("pw-proxy", "pw-proxy", "The struct pw_proxy *",
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /* Signals */
  signals[SIGNAL_PW_PROXY_CREATED] = g_signal_new (
      "pw-proxy-created", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      G_STRUCT_OFFSET (WpProxyClass, pw_proxy_created), NULL, NULL, NULL,
      G_TYPE_NONE, 1, G_TYPE_POINTER);

  signals[SIGNAL_PW_PROXY_DESTROYED] = g_signal_new (
      "pw-proxy-destroyed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      G_STRUCT_OFFSET (WpProxyClass, pw_proxy_destroyed), NULL, NULL, NULL,
      G_TYPE_NONE, 0);

  signals[SIGNAL_BOUND] = g_signal_new (
      "bound", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      G_STRUCT_OFFSET (WpProxyClass, bound), NULL, NULL, NULL,
      G_TYPE_NONE, 1, G_TYPE_UINT);

  signals[SIGNAL_ERROR] = g_signal_new (
      "error", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      G_STRUCT_OFFSET (WpProxyClass, error), NULL, NULL, NULL,
      G_TYPE_NONE, 3, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING);
}

/*!
 * \brief Returns the proxy bound id.
 *
 * The bound id is the id that this object has on the
 * PipeWire registry (a.k.a. the global id). The object must have the
 * WP_PROXY_FEATURE_BOUND feature before this method can be called.
 *
 * \remarks Requires WP_PROXY_FEATURE_BOUND
 *
 * \ingroup wpproxy
 * \param self the proxy
 * \returns the bound id of this object
 */
guint32
wp_proxy_get_bound_id (WpProxy * self)
{
  g_return_val_if_fail (WP_IS_PROXY (self), 0);
  g_warn_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
      WP_PROXY_FEATURE_BOUND);

  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
  return priv->pw_proxy ? pw_proxy_get_bound_id (priv->pw_proxy) : SPA_ID_INVALID;
}

/*!
 * \brief Gets the interface type of the proxied object
 * \ingroup wpproxy
 * \param self the proxy
 * \param version (out) (optional): the version of the interface
 * \returns the PipeWire type of the interface that is being proxied
 */
const gchar *
wp_proxy_get_interface_type (WpProxy * self, guint32 * version)
{
  g_return_val_if_fail (WP_IS_PROXY (self), NULL);

  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
  if (priv->pw_proxy)
    return pw_proxy_get_type (priv->pw_proxy, version);
  else {
    WpProxyClass *klass = WP_PROXY_GET_CLASS (self);
    if (version)
      *version = klass->pw_iface_version;
    return klass->pw_iface_type;
  }
}

/*!
 * \brief Gets the `pw_proxy` wrapped by this proxy object
 * \ingroup wpproxy
 * \param self the proxy
 * \returns a pointer to the underlying `pw_proxy` object
 */
struct pw_proxy *
wp_proxy_get_pw_proxy (WpProxy * self)
{
  g_return_val_if_fail (WP_IS_PROXY (self), NULL);

  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
  return priv->pw_proxy;
}

/*!
 * \brief Private method to be used by subclasses to set the `pw_proxy` pointer
 * when it is available.
 *
 * This can be called only if there is no `pw_proxy` already set.
 * Takes ownership of \a proxy.
 *
 * \ingroup wpproxy
 */
void
wp_proxy_set_pw_proxy (WpProxy * self, struct pw_proxy * proxy)
{
  g_return_if_fail (WP_IS_PROXY (self));

  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);

  g_return_if_fail (proxy);

  g_return_if_fail (priv->pw_proxy == NULL);
  priv->pw_proxy = proxy;

  pw_proxy_add_listener (priv->pw_proxy, &priv->listener, &proxy_events,
      self);

  /* inform subclasses and listeners */
  g_signal_emit (self, signals[SIGNAL_PW_PROXY_CREATED], 0, priv->pw_proxy);
}
 070701000000B5000081A400000000000000000000000165F8630400000B22000000000000000000000000000000000000002100000000wireplumber-0.5.0/lib/wp/proxy.h  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_PROXY_H__
#define __WIREPLUMBER_PROXY_H__

#include "object.h"

G_BEGIN_DECLS

struct pw_proxy;

/*!
 * \brief Flags to be used as WpObjectFeatures for WpProxy subclasses.
 * \ingroup wpproxy
 */
typedef enum { /*< flags >*/
  /* standard features */
  WP_PROXY_FEATURE_BOUND                       = (1 << 0),

  /* WpPipewireObjectInterface */
  WP_PIPEWIRE_OBJECT_FEATURE_INFO              = (1 << 4),
  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS       = (1 << 5),
  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT      = (1 << 6),
  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE     = (1 << 7),
  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG = (1 << 8),
  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE       = (1 << 9),

  /*!
   * The minimal feature set for proxies implementing WpPipewireObject.
   * This is a subset of \em WP_PIPEWIRE_OBJECT_FEATURES_ALL
   */
  WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL =
      (WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO),

  /*!
   * The complete common feature set for proxies implementing
   * WpPipewireObject. This is a subset of \em WP_OBJECT_FEATURES_ALL
   */
  WP_PIPEWIRE_OBJECT_FEATURES_ALL =
      (WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL |
       WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS |
       WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT |
       WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE |
       WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG |
       WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE),

  WP_PROXY_FEATURE_CUSTOM_START                = (1 << 16), /*< skip >*/
} WpProxyFeatures;

/*!
 * \brief The WpProxy GType
 * \ingroup wpproxy
 */
#define WP_TYPE_PROXY (wp_proxy_get_type ())
WP_API
G_DECLARE_DERIVABLE_TYPE (WpProxy, wp_proxy, WP, PROXY, WpObject)

struct _WpProxyClass
{
  WpObjectClass parent_class;

  /*! \brief the PipeWire type of the interface that is being proxied by
   *  this class (ex. `PW_TYPE_INTERFACE_Node` for WpNode */
  const gchar * pw_iface_type;

  /*! \brief the PipeWire version of the interface that is being
   *  proxied by this class */
  guint32 pw_iface_version;

  /* signals */

  void (*pw_proxy_created) (WpProxy * self, struct pw_proxy * proxy);
  void (*pw_proxy_destroyed) (WpProxy * self);
  void (*bound) (WpProxy * self, guint32 id);
  void (*error) (WpProxy * self, int seq, int res, const char *message);

  /*< private >*/
  WP_PADDING(6)
};

WP_API
guint32 wp_proxy_get_bound_id (WpProxy * self);

WP_API
const gchar * wp_proxy_get_interface_type (WpProxy * self, guint32 * version);

WP_API
struct pw_proxy * wp_proxy_get_pw_proxy (WpProxy * self);

/* for subclasses only */

WP_API
void wp_proxy_set_pw_proxy (WpProxy * self, struct pw_proxy * proxy);

G_END_DECLS

#endif
  070701000000B6000081A400000000000000000000000165F8630400002EF8000000000000000000000000000000000000002800000000wireplumber-0.5.0/lib/wp/session-item.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "session-item.h"
#include "core.h"
#include "log.h"
#include "error.h"
#include <spa/utils/defs.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-si")

/*! \defgroup wpsessionitem WpSessionItem */
/*!
 * \struct WpSessionItem
 *
 * Session items are high level objects that wrap underlying PipeWire objects
 * and manage them. For example, a session item may be managing a node, taking
 * responsibility for configuring the PortConfig and Format parameters of the
 * node. Or another may be managing links between two nodes.
 *
 * All the implementations are provided by modules and instantiated via the
 * WpSiFactory class.
 *
 * \gproperties
 *
 * \gproperty{id, guint, G_PARAM_READABLE,
 *   The session item unique id}
 *
 * \gproperty{properties, WpProperties *, G_PARAM_READABLE,
 *   The session item properties}
 */

enum {
  STEP_ACTIVATE = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_EXPORT,
};

typedef struct _WpSessionItemPrivate WpSessionItemPrivate;
struct _WpSessionItemPrivate
{
  WpProperties *properties;
};

enum {
  PROP_0,
  PROP_PROPERTIES,
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpSessionItem, wp_session_item,
    WP_TYPE_OBJECT)

static void
wp_session_item_init (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);

  priv->properties = NULL;
}

static void
wp_session_item_default_reset (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);

  g_clear_pointer (&priv->properties, wp_properties_unref);
}

static void
wp_session_item_dispose (GObject * object)
{
  WpSessionItem * self = WP_SESSION_ITEM (object);

  wp_trace_object (self, "dispose");

  wp_session_item_reset (self);

  G_OBJECT_CLASS (wp_session_item_parent_class)->dispose (object);
}

static void
wp_session_item_get_gobject_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpSessionItem *self = WP_SESSION_ITEM (object);

  switch (property_id) {
  case PROP_PROPERTIES:
    g_value_take_boxed (value, wp_session_item_get_properties (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static WpObjectFeatures
session_item_default_get_supported_features (WpObject * self)
{
  return WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED;
}

static guint
session_item_default_activate_get_next_step (WpObject * object,
     WpFeatureActivationTransition * t, guint step, WpObjectFeatures missing)
{
  switch (step) {
    case WP_TRANSITION_STEP_NONE:
      if (missing & WP_SESSION_ITEM_FEATURE_ACTIVE)
        return STEP_ACTIVATE;
      else
        if (missing & WP_SESSION_ITEM_FEATURE_EXPORTED)
          return STEP_EXPORT;
        else
          return WP_TRANSITION_STEP_NONE;

    case STEP_ACTIVATE:
      if (missing & WP_SESSION_ITEM_FEATURE_EXPORTED)
        return STEP_EXPORT;
      else
        return WP_TRANSITION_STEP_NONE;

    case STEP_EXPORT:
      return WP_TRANSITION_STEP_NONE;

    default:
      return WP_TRANSITION_STEP_ERROR;
  }
}

static void
session_item_default_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * t, guint step, WpObjectFeatures missing)
{
  WpSessionItem *self = WP_SESSION_ITEM (object);
  WpTransition *transition = WP_TRANSITION (t);

  switch (step) {
    case STEP_ACTIVATE:
      if (!WP_SESSION_ITEM_GET_CLASS (self)->enable_active) {
        wp_transition_return_error (transition,
            g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "session-item: virtual enable_active method is not defined"));
        return;
      }
      WP_SESSION_ITEM_GET_CLASS (self)->enable_active (self, transition);
      break;

    case STEP_EXPORT:
      if (!WP_SESSION_ITEM_GET_CLASS (self)->enable_exported) {
        wp_transition_return_error (transition,
            g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "session-item: virtual enable_exported method is not defined"));
        return;
      }
      WP_SESSION_ITEM_GET_CLASS (self)->enable_exported (self, transition);
      break;

    case WP_TRANSITION_STEP_ERROR:
      break;

    default:
      g_return_if_reached ();
  }
}

static void
session_item_default_deactivate (WpObject * object, WpObjectFeatures features)
{
  WpSessionItem *self = WP_SESSION_ITEM (object);
  guint current = wp_object_get_active_features (object);

  if (features & current & WP_SESSION_ITEM_FEATURE_ACTIVE) {
    g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->disable_active);
    WP_SESSION_ITEM_GET_CLASS (self)->disable_active (self);
  }

  if (features & current & WP_SESSION_ITEM_FEATURE_EXPORTED) {
    g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->disable_exported);
    WP_SESSION_ITEM_GET_CLASS (self)->disable_exported (self);
  }
}

static void
wp_session_item_class_init (WpSessionItemClass * klass)
{
  WpObjectClass * wpobject_class = (WpObjectClass *) klass;
  GObjectClass * object_class = (GObjectClass *) klass;

  object_class->dispose = wp_session_item_dispose;
  object_class->get_property = wp_session_item_get_gobject_property;

  wpobject_class->get_supported_features =
      session_item_default_get_supported_features;
  wpobject_class->activate_get_next_step =
      session_item_default_activate_get_next_step;
  wpobject_class->activate_execute_step =
      session_item_default_activate_execute_step;
  wpobject_class->deactivate = session_item_default_deactivate;

  klass->reset = wp_session_item_default_reset;

  g_object_class_install_property (object_class, PROP_PROPERTIES,
      g_param_spec_boxed ("properties", "properties",
          "The session item properties", WP_TYPE_PROPERTIES,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Resets the session item.
 *
 * This essentially removes the configuration and deactivates all active features.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 */
void
wp_session_item_reset (WpSessionItem * self)
{
  g_return_if_fail (WP_IS_SESSION_ITEM (self));
  g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->reset);

  return WP_SESSION_ITEM_GET_CLASS (self)->reset (self);
}

/*!
 * \brief Configures the session item with a set of properties
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param props (transfer full): the properties used to configure the item
 * \returns TRUE on success, FALSE if the item could not be configured
 */
gboolean
wp_session_item_configure (WpSessionItem * self, WpProperties * props)
{
  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), FALSE);
  g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->configure,
      FALSE);

  return WP_SESSION_ITEM_GET_CLASS (self)->configure (self, props);
}

/*!
 * \brief Checks if the session item is configured
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \returns TRUE if the item is configured, FALSE otherwise
 */
gboolean
wp_session_item_is_configured (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = NULL;

  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), FALSE);

  priv = wp_session_item_get_instance_private (self);
  return priv->properties != NULL;
}

/*!
 * \brief An associated proxy is a WpProxy subclass instance that
 * is somehow related to this item.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param proxy_type a WpProxy subclass GType
 * \returns (nullable) (transfer full) (type WpProxy): the associated proxy
 *   of the specified @em proxy_type, or NULL if there is no association to
 *   such a proxy
 */
gpointer
wp_session_item_get_associated_proxy (WpSessionItem * self, GType proxy_type)
{
  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), NULL);
  g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->get_associated_proxy,
      NULL);
  g_return_val_if_fail (g_type_is_a (proxy_type, WP_TYPE_PROXY), NULL);

  return WP_SESSION_ITEM_GET_CLASS (self)->get_associated_proxy (self, proxy_type);
}

/*!
 * \brief Gets the bound id of a proxy associated with the session item
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param proxy_type a WpProxy subclass GType
 * \returns the bound id of the associated proxy of the specified @em proxy_type,
 *   or `SPA_ID_INVALID` if there is no association to such a proxy
 */
guint32
wp_session_item_get_associated_proxy_id (WpSessionItem * self, GType proxy_type)
{
  g_autoptr (WpProxy) proxy = wp_session_item_get_associated_proxy (self,
      proxy_type);
  if (!proxy)
    return SPA_ID_INVALID;

  return wp_proxy_get_bound_id (proxy);
}

/*!
 * \brief Registers the session item to its associated core
 *
 * \ingroup wpsessionitem
 * \param self (transfer full): the session item
 */
void
wp_session_item_register (WpSessionItem * self)
{
  g_autoptr (WpCore) core = NULL;

  g_return_if_fail (WP_IS_SESSION_ITEM (self));

  core = wp_object_get_core (WP_OBJECT (self));
  wp_core_register_object (core, self);
}

/*!
 * \brief Removes the session item from its associated core
 *
 * \ingroup wpsessionitem
 * \param self (transfer none): the session item
 */
void
wp_session_item_remove (WpSessionItem * self)
{
  g_autoptr (WpCore) core = NULL;

  g_return_if_fail (WP_IS_SESSION_ITEM (self));

  core = wp_object_get_core (WP_OBJECT (self));
  wp_core_remove_object (core, self);
}

/*!
 * \brief Gets the properties of a session item.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \returns (transfer full): the item's properties.
 */
WpProperties *
wp_session_item_get_properties (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = NULL;

  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), NULL);

  priv = wp_session_item_get_instance_private (self);
  return priv->properties ? wp_properties_ref (priv->properties) : NULL;
}

/*!
 * \brief Looks up a named session item property value for a given key.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param key the property key
 * \returns the item property value for the given key.
 */
const gchar *
wp_session_item_get_property (WpSessionItem * self, const gchar *key)
{
  WpSessionItemPrivate *priv = NULL;

  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), NULL);

  priv = wp_session_item_get_instance_private (self);
  return priv->properties ? wp_properties_get (priv->properties, key) : NULL;
}

/*!
 * \brief Sets the item's properties.
 *
 * This should only be done by sub-classes after the configuration has been done.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param props (transfer full): the new properties to set
 */
void
wp_session_item_set_properties (WpSessionItem * self,
    WpProperties *props)
{
  WpSessionItemPrivate *priv = NULL;

  g_return_if_fail (WP_IS_SESSION_ITEM (self));

  priv = wp_session_item_get_instance_private (self);
  g_clear_pointer (&priv->properties, wp_properties_unref);
  priv->properties = wp_properties_ensure_unique_owner (props);
}

static gboolean
on_session_item_proxy_destroyed_deferred (WpSessionItem * item)
{
  wp_info_object (item, "destroying session item upon request by the server");
  wp_object_deactivate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_EXPORTED);
  return G_SOURCE_REMOVE;
}

/*!
 * \brief Helper callback for sub-classes that deffers and unexports
 * the session item.
 *
 * Only meant to be used when the pipewire proxy destroyed signal is triggered.
 *
 * \ingroup wpsessionitem
 * \param proxy the proxy that was destroyed by the server
 * \param item the associated session item
 */
void
wp_session_item_handle_proxy_destroyed (WpProxy * proxy, WpSessionItem * item)
{
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (proxy));
  if (core)
    wp_core_idle_add_closure (core, NULL, g_cclosure_new_object (
        G_CALLBACK (on_session_item_proxy_destroyed_deferred),
        G_OBJECT (item)));
}
070701000000B7000081A400000000000000000000000165F8630400000B26000000000000000000000000000000000000002800000000wireplumber-0.5.0/lib/wp/session-item.h   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_SESSION_ITEM_H__
#define __WIREPLUMBER_SESSION_ITEM_H__

#include "object.h"
#include "proxy.h"
#include "properties.h"

G_BEGIN_DECLS

/*!
 * \brief Flags to be used as WpObjectFeatures for WpSessionItem subclasses.
 * \ingroup wpsessionitem
 */
typedef enum { /*< flags >*/
  /* main features */
  WP_SESSION_ITEM_FEATURE_ACTIVE       = (1 << 0),
  WP_SESSION_ITEM_FEATURE_EXPORTED     = (1 << 1),

  WP_SESSION_ITEM_FEATURE_CUSTOM_START = (1 << 16), /*< skip >*/
} WpSessionItemFeatures;

/*!
 * \brief The WpSessionItem GType
 * \ingroup wpsessionitem
 */
#define WP_TYPE_SESSION_ITEM (wp_session_item_get_type ())
WP_API
G_DECLARE_DERIVABLE_TYPE (WpSessionItem, wp_session_item,
                          WP, SESSION_ITEM, WpObject)

struct _WpSessionItemClass
{
  WpObjectClass parent_class;

  /*! See wp_session_item_reset() */
  void (*reset) (WpSessionItem * self);
  /*! See wp_session_item_configure() */
  gboolean (*configure) (WpSessionItem * self, WpProperties * props);
  /*! See wp_session_item_get_associated_proxy() */
  gpointer (*get_associated_proxy) (WpSessionItem * self, GType proxy_type);

  /*! disables the active feature of the session item */
  void (*disable_active) (WpSessionItem * self);
  /*! disables the exported feature of the session item */
  void (*disable_exported) (WpSessionItem * self);
  /*! enables the active feature of the session item */
  void (*enable_active) (WpSessionItem * self, WpTransition * transition);
  /*! enables the exported feature of the session item */
  void (*enable_exported) (WpSessionItem * self, WpTransition * transition);

  /*< private >*/
  WP_PADDING(5)
};

/* configuration */

WP_API
void wp_session_item_reset (WpSessionItem * self);

WP_API
gboolean wp_session_item_configure (WpSessionItem * self, WpProperties * props);

WP_API
gboolean wp_session_item_is_configured (WpSessionItem * self);

/* associated proxies */

WP_API
gpointer wp_session_item_get_associated_proxy (WpSessionItem * self,
    GType proxy_type);

WP_API
guint32 wp_session_item_get_associated_proxy_id (WpSessionItem * self,
    GType proxy_type);

/* registry */

WP_API
void wp_session_item_register (WpSessionItem * self);

WP_API
void wp_session_item_remove (WpSessionItem * self);

/* properties */

WP_API
WpProperties * wp_session_item_get_properties (WpSessionItem * self);

WP_API
const gchar * wp_session_item_get_property (WpSessionItem * self,
    const gchar *key);

/* for subclasses only */

WP_API
void wp_session_item_set_properties (WpSessionItem * self, WpProperties *props);

WP_API
void wp_session_item_handle_proxy_destroyed (WpProxy * proxy,
    WpSessionItem * item);

G_END_DECLS

#endif
  070701000000B8000081A400000000000000000000000165F863040000865C000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/settings.c   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "core.h"
#include "settings.h"
#include "metadata.h"
#include "log.h"
#include "object-manager.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-settings")

/*! \defgroup wpsettings WpSettings */

/*!
 * \struct WpSettingsSpec
 *
 * WpSettingSpec holds the specification of a setting.
 */
struct _WpSettingsSpec {
  grefcount ref;
  gchar *desc;
  WpSettingsSpecType type;
  WpSpaJson *def_value;
  WpSpaJson *min_value;
  WpSpaJson *max_value;
};

G_DEFINE_BOXED_TYPE (WpSettingsSpec, wp_settings_spec, wp_settings_spec_ref,
    wp_settings_spec_unref)

/*!
 * \brief Increases the reference count of a settings spec object
 * \ingroup wpsettings
 * \param self a settings spec object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSettingsSpec *
wp_settings_spec_ref (WpSettingsSpec * self)
{
  g_ref_count_inc (&self->ref);
  return self;
}

static void
wp_settings_spec_free (WpSettingsSpec * self)
{
  g_clear_pointer (&self->desc, g_free);
  g_clear_pointer (&self->def_value, wp_spa_json_unref);
  g_clear_pointer (&self->min_value, wp_spa_json_unref);
  g_clear_pointer (&self->max_value, wp_spa_json_unref);
  g_slice_free (WpSettingsSpec, self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 * \ingroup wpsettings
 * \param self (transfer full): a settings spec object
 */
void
wp_settings_spec_unref (WpSettingsSpec * self)
{
  if (g_ref_count_dec (&self->ref))
    wp_settings_spec_free (self);
}

static WpSettingsSpec *
wp_settings_spec_new (WpSpaJson * spec_json)
{
  WpSettingsSpec *self;
  g_autofree gchar *desc = NULL;
  g_autofree gchar *type_str = NULL;
  WpSettingsSpecType type = WP_SETTINGS_SPEC_TYPE_UNKNOWN;
  g_autoptr (WpSpaJson) def_value = NULL;
  g_autoptr (WpSpaJson) min_value = NULL;
  g_autoptr (WpSpaJson) max_value = NULL;

  g_return_val_if_fail (spec_json, NULL);

  if (!wp_spa_json_is_object (spec_json))
    return NULL;

  /* Parse mandatory fields */
  if (!wp_spa_json_object_get (spec_json,
      "description", "s", &desc,
      "type", "s", &type_str,
      "default", "J", &def_value,
      NULL))
    return NULL;

  /* Parse type and check if values are correct */
  if (g_str_equal (type_str, "bool")) {
    type = WP_SETTINGS_SPEC_TYPE_BOOL;
    if (!wp_spa_json_is_boolean (def_value))
      return NULL;
  } else if (g_str_equal (type_str, "int")) {
    type = WP_SETTINGS_SPEC_TYPE_INT;
    if (!wp_spa_json_object_get (spec_json,
        "min", "J", &min_value,
        "max", "J", &max_value,
        NULL))
      return NULL;
    if (!wp_spa_json_is_int (def_value) ||
        !min_value || !wp_spa_json_is_int (min_value) ||
        !max_value || !wp_spa_json_is_int (max_value))
      return NULL;
  } else if (g_str_equal (type_str, "float")) {
    type = WP_SETTINGS_SPEC_TYPE_FLOAT;
    if (!wp_spa_json_object_get (spec_json,
        "min", "J", &min_value,
        "max", "J", &max_value,
        NULL))
      return NULL;
    if (!wp_spa_json_is_float (def_value) ||
        !min_value || !wp_spa_json_is_float (min_value) ||
        !max_value || !wp_spa_json_is_float (max_value))
      return NULL;
  } else if (g_str_equal (type_str, "string")) {
    type = WP_SETTINGS_SPEC_TYPE_STRING;
  } else if (g_str_equal (type_str, "array")) {
    type = WP_SETTINGS_SPEC_TYPE_ARRAY;
    if (!wp_spa_json_is_array (def_value))
      return NULL;
  } else if (g_str_equal (type_str, "object")) {
    type = WP_SETTINGS_SPEC_TYPE_OBJECT;
    if (!wp_spa_json_is_object (def_value))
      return NULL;
  } else {
    return NULL;
  }

  self = g_slice_new0 (WpSettingsSpec);
  g_ref_count_init (&self->ref);
  self->desc = g_steal_pointer (&desc);
  self->type = type;
  self->def_value = g_steal_pointer (&def_value);
  self->min_value = g_steal_pointer (&min_value);
  self->max_value = g_steal_pointer (&max_value);
  return self;
}

/*!
 * \brief Gets the description of a settings spec
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns the description of the settings spec
 */
const gchar *
wp_settings_spec_get_description (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, NULL);
  return self->desc;
}

/*!
 * \brief Gets the type of a settings spec
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns the type of the settings spec
 */
WpSettingsSpecType
wp_settings_spec_get_value_type (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, WP_SETTINGS_SPEC_TYPE_UNKNOWN);
  return self->type;
}

/*!
 * \brief Gets the default value of a settings spec
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns (transfer full): the default value of the settings spec
 */
WpSpaJson *
wp_settings_spec_get_default_value (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, NULL);
  g_return_val_if_fail (self->def_value, NULL);
  return wp_spa_json_ref (self->def_value);
}

/*!
 * \brief Gets the minimum value of a settings spec.
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns (transfer full)(nullable): the minimum value of the settings spec,
 * or NULL if the spec type is not WP_SETTINGS_SPEC_TYPE_INT or
 * WP_SETTINGS_SPEC_TYPE_FLOAT
 */
WpSpaJson *
wp_settings_spec_get_min_value (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, NULL);
  return self->min_value ? wp_spa_json_ref (self->min_value) : NULL;
}

/*!
 * \brief Gets the maximum value of a settings spec.
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns (transfer full)(nullable): the maximum value of the settings spec,
 * or NULL if the spec type is not WP_SETTINGS_SPEC_TYPE_INT or
 * WP_SETTINGS_SPEC_TYPE_FLOAT
 */
WpSpaJson *
wp_settings_spec_get_max_value (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, NULL);
  return self->max_value ? wp_spa_json_ref (self->max_value) : NULL;
}

/*!
 * \brief Checks whether a value is compatible with the spec or not
 * \ingroup wpsettings
 * \param self the settings spec object
 * \param value (transfer none): the value to check
 * \returns TRUE if the value is compatible with the spec, FALSE otherwise
 */
gboolean
wp_settings_spec_check_value (WpSettingsSpec * self, WpSpaJson *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);

  switch (self->type) {
    case WP_SETTINGS_SPEC_TYPE_BOOL:
      return wp_spa_json_is_boolean (value);
    case WP_SETTINGS_SPEC_TYPE_INT: {
      gint val = 0, min = 0, max = 0;
      if (!wp_spa_json_is_int (value) || !wp_spa_json_parse_int (value, &val))
        return FALSE;
      if (!wp_spa_json_parse_int (self->min_value, &min) ||
          !wp_spa_json_parse_int (self->max_value, &max))
        return FALSE;
      return val >= min && val <= max;
    }
    case WP_SETTINGS_SPEC_TYPE_FLOAT: {
      float val = 0.0, min = 0.0, max = 0.0;
      if (wp_spa_json_is_int (value) || !wp_spa_json_is_float (value) ||
          !wp_spa_json_parse_float (value, &val))
        return FALSE;
      if (!wp_spa_json_parse_float (self->min_value, &min) ||
          !wp_spa_json_parse_float (self->max_value, &max))
        return FALSE;
      return val >= min && val <= max;
    }
    case WP_SETTINGS_SPEC_TYPE_STRING:
      /* We also accept strings without quotes, which is why we dont use
       * wp_spa_json_is_string() */
      return !wp_spa_json_is_boolean (value) && !wp_spa_json_is_int (value) &&
        !wp_spa_json_is_float (value) && !wp_spa_json_is_array (value) &&
        !wp_spa_json_is_object (value);
    case WP_SETTINGS_SPEC_TYPE_ARRAY:
      return wp_spa_json_is_array (value);
    case WP_SETTINGS_SPEC_TYPE_OBJECT:
      return wp_spa_json_is_object (value);
    default:
      break;
  }

  return FALSE;
}

/*!
 * \struct WpSettingsItem
 *
 * WpSettingsItem holds the key and value of a setting
 */
struct _WpSettingsItem
{
  WpMetadata *metadata;
  const gchar *key;
  WpSpaJson *value;
};

G_DEFINE_BOXED_TYPE (WpSettingsItem, wp_settings_item,
    wp_settings_item_ref, wp_settings_item_unref)

static WpSettingsItem *
wp_settings_item_new (WpMetadata *metadata, const gchar *key,
    const gchar *value)
{
  WpSettingsItem *self = g_rc_box_new0 (WpSettingsItem);
  self->metadata = g_object_ref (metadata);
  self->key = key;
  self->value = wp_spa_json_new_from_string (value);
  return self;
}

static void
wp_settings_item_free (gpointer p)
{
  WpSettingsItem *self = p;
  g_clear_pointer (&self->value, wp_spa_json_unref);
  g_clear_object (&self->metadata);
}

/*!
 * \brief Increases the reference count of a settings item object
 * \ingroup wpsettings
 * \param self a settings item object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSettingsItem *
wp_settings_item_ref (WpSettingsItem *self)
{
  return g_rc_box_acquire (self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 * \ingroup wpsettings
 * \param self (transfer full): a settings item object
 */
void
wp_settings_item_unref (WpSettingsItem *self)
{
  g_rc_box_release_full (self, wp_settings_item_free);
}

/*!
 * \brief Gets the key from a settings item
 *
 * \ingroup wpsettings
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_settings_new_iterator()
 * \returns (transfer none): the settings key of the \a item
 */
const gchar *
wp_settings_item_get_key (WpSettingsItem * self)
{
  return self->key;
}

/*!
 * \brief Gets the value from a settings item
 *
 * \ingroup wpsettings
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_settings_new_iterator()
 * \returns (transfer full): the settings value of the \a item
 */
WpSpaJson *
wp_settings_item_get_value (WpSettingsItem * self)
{
  return wp_spa_json_ref (self->value);
}

/*!
 * \struct WpSettings
 *
 * WpSettings loads and parses the "sm-settings" (default value) metadata, which
 * contains wireplumber settings, and provides APIs to its clients (modules, lua
 * scripts etc) to access them.
 *
 * Being a WpObject subclass, the settings inherits WpObject's activation
 * system.
 */

struct _WpSettings
{
  WpObject parent;

  /* element-type: Callback* */
  GPtrArray *callbacks;

  gchar *metadata_name;
  gchar *metadata_schema_name;
  gchar *metadata_persistent_name;

  WpObjectManager *metadata_om;
  GWeakRef metadata;
  GWeakRef metadata_schema;
  GWeakRef metadata_persistent;
  GHashTable *schema;
};

typedef struct
{
  GClosure *closure;
  gchar *pattern;
} Callback;

enum {
  PROP_0,
  PROP_METADATA_NAME,
  PROP_PROPERTIES,
};

G_DEFINE_TYPE (WpSettings, wp_settings, WP_TYPE_OBJECT)

static void
wp_settings_init (WpSettings * self)
{
  g_weak_ref_init (&self->metadata, NULL);
  g_weak_ref_init (&self->metadata_schema, NULL);
  g_weak_ref_init (&self->metadata_persistent, NULL);

  self->schema = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
      (GDestroyNotify) wp_settings_spec_unref);
}

static void
wp_settings_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpSettings *self = WP_SETTINGS (object);

  switch (property_id) {
  case PROP_METADATA_NAME:
    self->metadata_name = g_value_dup_string (value);
    self->metadata_schema_name = g_strdup_printf (
        WP_SETTINGS_SCHEMA_METADATA_NAME_PREFIX "%s", self->metadata_name);
    self->metadata_persistent_name = g_strdup_printf (
        WP_SETTINGS_PERSISTENT_METADATA_NAME_PREFIX "%s", self->metadata_name);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_settings_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpSettings *self = WP_SETTINGS (object);

  switch (property_id) {
  case PROP_METADATA_NAME:
    g_value_set_string (value, self->metadata_name);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

enum {
  STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START,
};

static WpObjectFeatures
wp_settings_get_supported_features (WpObject * self)
{
  return WP_SETTINGS_LOADED;
}

static guint
wp_settings_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  g_return_val_if_fail (missing == WP_SETTINGS_LOADED,
      WP_TRANSITION_STEP_ERROR);

  return STEP_LOAD;
}

static void
on_metadata_changed (WpMetadata *m, guint32 subject,
   const gchar *key, const gchar *type, const gchar *value, gpointer d)
{
  WpSettings *self = WP_SETTINGS(d);

  if (value)
    wp_info_object (self, "setting \"%s\" changed to \"%s\"", key, value);
  else
    wp_info_object (self, "setting \"%s\" removed", key);

  for (guint i = 0; i < self->callbacks->len; i++) {
    Callback *cb = g_ptr_array_index (self->callbacks, i);

    if (g_pattern_match_simple (cb->pattern, key)) {
      g_autoptr (WpSpaJson) json = NULL;
      GValue values[3] = { G_VALUE_INIT, G_VALUE_INIT, G_VALUE_INIT };

      g_value_init (&values[0], G_TYPE_OBJECT);
      g_value_init (&values[1], G_TYPE_STRING);
      g_value_init (&values[2], WP_TYPE_SPA_JSON);

      g_value_set_object (&values[0], self);
      g_value_set_string (&values[1], key);
      json = value ? wp_spa_json_new_wrap_string (value) : NULL;
      g_value_set_boxed (&values[2], json);

      g_closure_invoke (cb->closure, NULL, 3, values, NULL);

      g_value_unset (&values[0]);
      g_value_unset (&values[1]);
      g_value_unset (&values[2]);

      wp_debug_object (self, "triggered callback(%p)", cb);
    }
  }
}

static void
on_metadata_added (WpObjectManager *om, WpMetadata *m, gpointer d)
{
  WpTransition * transition = WP_TRANSITION (d);
  WpSettings * self = wp_transition_get_source_object (transition);
  g_autoptr (WpProperties) props = NULL;
  const gchar *metadata_name = NULL;
  g_autoptr (WpMetadata) metadata = NULL;
  g_autoptr (WpMetadata) metadata_schema = NULL;
  g_autoptr (WpMetadata) metadata_persistent = NULL;

  /* make sure the metadata has a name */
  props = wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (m));
  if (props)
    metadata_name = wp_properties_get (props, "metadata.name");
  if (!metadata_name)
    return;

  /* sm-settings */
  if (g_str_equal (metadata_name, self->metadata_name)) {
    g_signal_connect_object (m, "changed", G_CALLBACK (on_metadata_changed),
        self, 0);
    g_weak_ref_set (&self->metadata, m);
  }

  /* schema-sm-settings */
  else if (g_str_equal (metadata_name, self->metadata_schema_name)) {
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) item = G_VALUE_INIT;
    it = wp_metadata_new_iterator (m, 0);
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpMetadataItem *mi = g_value_get_boxed (&item);
      const gchar *key = wp_metadata_item_get_key (mi);
      const gchar *value = wp_metadata_item_get_value (mi);
      g_autoptr (WpSpaJson) spec_json = NULL;
      g_autoptr (WpSettingsSpec) spec = NULL;
      spec_json = wp_spa_json_new_from_string (value);
      spec = wp_settings_spec_new (spec_json);
      if (spec)
        g_hash_table_insert (self->schema, g_strdup (key),
            g_steal_pointer (&spec));
      else
        wp_warning_object (self, "malformed setting spec: %s", value);
    }
    g_weak_ref_set (&self->metadata_schema, m);
  }

  /* presistent-sm-settings */
  else if (g_str_equal (metadata_name, self->metadata_persistent_name)) {
    g_weak_ref_set (&self->metadata_persistent, m);
  }

  /* Finish loading when all metadatas are found */
  metadata = g_weak_ref_get (&self->metadata);
  metadata_schema = g_weak_ref_get (&self->metadata_schema);
  metadata_persistent = g_weak_ref_get (&self->metadata_persistent);
  if (metadata && metadata_schema && metadata_persistent)
    wp_object_update_features (WP_OBJECT (self), WP_SETTINGS_LOADED, 0);
}

static void
callback_unref (Callback * self)
{
  g_free (self->pattern);
  g_clear_pointer (&self->closure, g_closure_unref);
  g_slice_free (Callback, self);
}

static void
wp_settings_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  WpSettings * self = WP_SETTINGS (object);
  g_autoptr (WpCore) core = wp_object_get_core (object);

  switch (step) {
  case STEP_LOAD: {
    self->callbacks = g_ptr_array_new_with_free_func
        ((GDestroyNotify) callback_unref);

    self->metadata_om = wp_object_manager_new ();
    wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s",
            self->metadata_name, NULL);
    wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s",
            self->metadata_schema_name, NULL);
    wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s",
            self->metadata_persistent_name, NULL);
    wp_object_manager_request_object_features (self->metadata_om,
        WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
    g_signal_connect_object (self->metadata_om, "object-added",
        G_CALLBACK (on_metadata_added), transition, 0);
    wp_core_install_object_manager (core, self->metadata_om);

    wp_info_object (self, "looking for metadata object named %s",
        self->metadata_name);
    break;
  }
  case WP_TRANSITION_STEP_ERROR:
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
wp_settings_deactivate (WpObject * object, WpObjectFeatures features)
{
  WpSettings *self = WP_SETTINGS (object);

  g_clear_object (&self->metadata_om);
  g_clear_pointer (&self->callbacks, g_ptr_array_unref);

  wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL);
}

static void
wp_settings_finalize (GObject * object)
{
  WpSettings *self = WP_SETTINGS (object);

  g_clear_pointer (&self->metadata_name, g_free);
  g_clear_pointer (&self->metadata_schema_name, g_free);
  g_clear_pointer (&self->metadata_persistent_name, g_free);

  g_clear_pointer (&self->schema, g_hash_table_unref);

  g_weak_ref_clear (&self->metadata);
  g_weak_ref_clear (&self->metadata_schema);
  g_weak_ref_clear (&self->metadata_persistent);

  G_OBJECT_CLASS (wp_settings_parent_class)->finalize (object);
}

static void
wp_settings_class_init (WpSettingsClass * klass)
{
  GObjectClass * object_class = (GObjectClass *) klass;
  WpObjectClass * wpobject_class = (WpObjectClass *) klass;

  object_class->finalize = wp_settings_finalize;
  object_class->set_property = wp_settings_set_property;
  object_class->get_property = wp_settings_get_property;

  wpobject_class->get_supported_features = wp_settings_get_supported_features;
  wpobject_class->activate_get_next_step = wp_settings_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_settings_activate_execute_step;
  wpobject_class->deactivate = wp_settings_deactivate;

  g_object_class_install_property (object_class, PROP_METADATA_NAME,
      g_param_spec_string ("metadata-name", "metadata-name",
          "The metadata object to look after", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Creates a new WpSettings object
 *
 * \ingroup wpsettings
 * \param core the WpCore
 * \param metadata_name (nullable): the name of the metadata object to
 *    associate with the settings object; NULL means the default "sm-settings"
 * \returns (transfer full): a new WpSettings object
 */
WpSettings *
wp_settings_new (WpCore * core, const gchar * metadata_name)
{
  return g_object_new (WP_TYPE_SETTINGS,
      "core", core,
      "metadata-name", metadata_name ? metadata_name : "sm-settings",
      NULL);
}

static gboolean
find_settings_func (gpointer g_object, gpointer metadata_name)
{
  if (!WP_IS_SETTINGS (g_object))
    return FALSE;

  return g_str_equal (((WpSettings *) g_object)->metadata_name,
      (gchar *) metadata_name);
}

/*!
 * \brief Finds a registered WpSettings object by its metadata name
 *
 * \ingroup wpsettings
 * \param core the WpCore
 * \param metadata_name (nullable): the name of the metadata object that the
 *    settings object is associated with; NULL means the default "sm-settings"
 * \returns (transfer full) (nullable): the WpSettings object, or NULL if not
 *    found
 */
WpSettings *
wp_settings_find (WpCore * core, const gchar * metadata_name)
{
  g_return_val_if_fail (WP_IS_CORE (core), NULL);

  GObject *s = wp_core_find_object (core, (GEqualFunc) find_settings_func,
      metadata_name ? metadata_name : "sm-settings");
  return s ? WP_SETTINGS (s) : NULL;
}

/*!
 * \brief Subscribes callback for a given setting pattern(a glob-style pattern
 * matched using g_pattern_match_simple), this allows clients to look
 * for any changes made in settings through metadata.
 *
 * \ingroup wpsettings
 * \param self the settings object
 * \param pattern name of the pattern to match the settings with
 * \param callback (scope async): the callback triggered when the settings
 *  change.
 * \param user_data data to pass to \a callback
 * \returns the subscription ID (always greater than 0 for successful
 *  subscriptions)
 */
guintptr
wp_settings_subscribe (WpSettings *self,
    const gchar *pattern, WpSettingsChangedCallback callback,
    gpointer user_data)
{
  return wp_settings_subscribe_closure (self, pattern,
      g_cclosure_new (G_CALLBACK (callback), user_data, NULL));
}

/*!
 * \brief Subscribes callback for a given setting pattern(a glob-style pattern
 * matched using g_pattern_match_simple), this allows clients to look
 * for any changes made in settings through metadata.
 *
 * \ingroup wpsettings
 * \param self the settings object
 * \param pattern name of the pattern to match the settings with
 * \param closure (nullable): a GAsyncReadyCallback wrapped in a GClosure
 * \returns the subscription ID (always greater than 0 for success)
 */
guintptr
wp_settings_subscribe_closure (WpSettings *self, const gchar *pattern,
    GClosure *closure)
{
  g_return_val_if_fail (WP_IS_SETTINGS (self), 0);
  g_return_val_if_fail (pattern, 0);
  g_return_val_if_fail (closure, 0);

  Callback *cb = g_slice_new0 (Callback);
  g_return_val_if_fail (cb, 0);

  cb->closure = g_closure_ref (closure);
  g_closure_sink (closure);
  if (G_CLOSURE_NEEDS_MARSHAL (closure))
    g_closure_set_marshal (closure, g_cclosure_marshal_generic);

  cb->pattern = g_strdup (pattern);

  g_ptr_array_add (self->callbacks, cb);

  wp_debug_object (self, "callback(%p) subscribed for pattern(%s)",
      (void *) cb, pattern);

  return (guintptr) cb;
}

/*!
 * \brief Unsubscribes callback for a given subscription_id.
 *
 * \ingroup wpsettings
 * \param self the settings object
 * \param subscription_id identifies the callback
 * \returns TRUE if success, FALSE otherwise
 */
gboolean
wp_settings_unsubscribe (WpSettings *self, guintptr subscription_id)
{
  gboolean ret = FALSE;
  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (subscription_id, FALSE);

  Callback *cb = (Callback *) subscription_id;

  ret = g_ptr_array_remove (self->callbacks, cb);

  wp_debug_object (self, "callback(%p) unsubscription %s", (void *) cb,
      (ret)? "succeeded": "failed");

  return ret;
}

/*!
 * \brief Gets the WpSpaJson value of a setting
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting
 * \returns (transfer full) (nullable): The WpSpaJson value of the setting, or
 * NULL if the setting does not exist
 */
WpSpaJson *
wp_settings_get (WpSettings *self, const gchar *name)
{
  const gchar *value;
  g_autoptr (WpSettingsSpec) spec = NULL;
  g_autoptr (WpSpaJson) def_value = NULL;
  g_autoptr (WpMetadata) m = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), NULL);
  g_return_val_if_fail (name, NULL);

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return NULL;
  }

  m = g_weak_ref_get (&self->metadata);
  if (!m)
    return wp_settings_spec_get_default_value (spec);

  value = wp_metadata_find (m, 0, name, NULL);
  return value ? wp_spa_json_new_wrap_string (value) :
      wp_settings_spec_get_default_value (spec);
}

/*!
 * \brief Gets the WpSpaJson saved value of a setting
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting
 * \returns (transfer full) (nullable): The WpSpaJson saved value of the
 * setting, or NULL if the setting does not exist
 */
WpSpaJson *
wp_settings_get_saved (WpSettings *self, const gchar *name)
{
  const gchar *value;
  g_autoptr (WpSettingsSpec) spec = NULL;
  g_autoptr (WpMetadata) mp = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), NULL);
  g_return_val_if_fail (name, NULL);

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return NULL;
  }

  mp = g_weak_ref_get (&self->metadata_persistent);
  if (!mp)
    return NULL;

  value = wp_metadata_find (mp, 0, name, NULL);
  return value ? wp_spa_json_new_wrap_string (value) : NULL;
}

/*!
 * \brief Gets the specification of a setting
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting
 * \returns (transfer full) (nullable): the specification of the setting
 */
WpSettingsSpec *
wp_settings_get_spec (WpSettings *self, const gchar *name)
{
  WpSettingsSpec *spec;

  g_return_val_if_fail (WP_IS_SETTINGS (self), NULL);
  g_return_val_if_fail (name, NULL);

  spec = g_hash_table_lookup (self->schema, name);
  return spec ? wp_settings_spec_ref (spec) : NULL;
}

/*!
 * \brief Sets a new setting value
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting
 * \param value (transfer none): the JSON value of the setting
 * \returns TRUE if the setting could be set, FALSE otherwise
 */
gboolean
wp_settings_set (WpSettings *self, const gchar *name, WpSpaJson *value)
{
  g_autoptr (WpMetadata) m = NULL;
  g_autoptr (WpSettingsSpec) spec = NULL;
  g_autofree gchar *value_str = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (name, FALSE);
  g_return_val_if_fail (value, FALSE);

  m = g_weak_ref_get (&self->metadata);
  if (!m)
    return FALSE;

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return FALSE;
  }

  value_str = wp_spa_json_to_string (value);
  if (!wp_settings_spec_check_value (spec, value)) {
    wp_warning ("Cannot set setting '%s' with value: %s", name, value_str);
    return FALSE;
  }

  wp_metadata_set (m, 0, name, "Spa:String:JSON", value_str);
  return TRUE;
}

/*!
 * \brief Resets the setting to its default value
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting to reset
 * \returns TRUE if the setting could be reset, FALSE otherwise
 */
gboolean
wp_settings_reset (WpSettings *self, const char *name)
{
  g_autoptr (WpSettingsSpec) spec = NULL;
  g_autoptr (WpSpaJson) def_value = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (name, FALSE);

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return FALSE;
  }

  def_value = wp_settings_spec_get_default_value (spec);
  return wp_settings_set (self, name, def_value);
}

/*!
 * \brief Saves a setting to make it persistent after reboot
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting to be saved
 * \returns TRUE if the setting could be saved, FALSE otherwise
 */
gboolean
wp_settings_save (WpSettings *self, const char *name)
{
  g_autoptr (WpMetadata) mp = NULL;
  g_autoptr (WpSpaJson) value = NULL;
  g_autofree gchar *value_str = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (name, FALSE);

  mp = g_weak_ref_get (&self->metadata_persistent);
  if (!mp)
    return FALSE;

  value = wp_settings_get (self, name);
  if (!value)
    return FALSE;

  value_str = wp_spa_json_to_string (value);
  wp_metadata_set (mp, 0, name, "Spa:String:JSON", value_str);
  return TRUE;
}

/*!
 * \brief Deletes a saved setting to not make it persistent after reboot
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the saved setting to be deleted
 * \returns TRUE if the setting could be deleted, FALSE otherwise
 */
gboolean
wp_settings_delete (WpSettings *self, const char *name)
{
  g_autoptr (WpMetadata) mp = NULL;
  g_autoptr (WpSettingsSpec) spec = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (name, FALSE);

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return FALSE;
  }

  mp = g_weak_ref_get (&self->metadata_persistent);
  if (!mp)
    return FALSE;

  wp_metadata_set (mp, 0, name, NULL, NULL);
  return TRUE;
}

/*!
 * \brief Resets all the settings to their default value
 * \ingroup wpsettings
 * \param self the settings object
 */
void wp_settings_reset_all (WpSettings *self)
{
  g_autoptr (WpMetadata) m = NULL;
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;
  g_autoptr (WpProperties) props = NULL;

  g_return_if_fail (WP_IS_SETTINGS (self));

  m = g_weak_ref_get (&self->metadata);
  if (!m)
    return;

  /* We cannot reset the settings while iterating, as the current iterator
   * won't be valid anyore. Instead, we get a list of all settings, and then
   * we reset them */
  props = wp_properties_new_empty ();
  it = wp_metadata_new_iterator (m, 0);
  for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
    WpMetadataItem *mi = g_value_get_boxed (&item);
    const gchar *key = wp_metadata_item_get_key (mi);
    const gchar *value = wp_metadata_item_get_value (mi);
    wp_properties_set (props, key, value);
  }
  wp_iterator_unref (it);

  /* Now reset all settings */
  it = wp_properties_new_iterator (props);
  for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
    WpPropertiesItem *pi = g_value_get_boxed (&item);
    const gchar *key = wp_properties_item_get_key (pi);
    if (!wp_settings_reset (self, key))
      wp_warning_object (self, "Failed to reset setting %s", key);
  }
}

/*!
 * \brief Saves all the settings to make them persistent after reboot
 * \ingroup wpsettings
 * \param self the settings object
 */
void wp_settings_save_all (WpSettings *self)
{
  g_autoptr (WpMetadata) m = NULL;
  g_autoptr (WpMetadata) mp = NULL;
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;

  g_return_if_fail (WP_IS_SETTINGS (self));

  m = g_weak_ref_get (&self->metadata);
  mp = g_weak_ref_get (&self->metadata_persistent);
  if (!m || !mp)
    return;

  it = wp_metadata_new_iterator (m, 0);
  for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
    WpMetadataItem *mi = g_value_get_boxed (&item);
    const gchar *key = wp_metadata_item_get_key (mi);
    if (!wp_settings_save (self, key))
      wp_warning_object (self, "Failed to save setting %s", key);
  }
}

/*!
 * \brief Deletes all saved setting to not make them persistent after reboot
 * \ingroup wpsettings
 * \param self the settings object
 */
void wp_settings_delete_all (WpSettings *self)
{
  g_autoptr (WpMetadata) mp = NULL;

  g_return_if_fail (WP_IS_SETTINGS (self));

  mp = g_weak_ref_get (&self->metadata_persistent);
  if (!mp)
    return;

  wp_metadata_clear (mp);
}

struct settings_iterator_data
{
  WpSettings *settings;
  WpIterator *metadata_it;
};

static void
settings_iterator_reset (WpIterator *it)
{
  struct settings_iterator_data *it_data = wp_iterator_get_user_data (it);
  g_autoptr (WpMetadata) m = NULL;

  m = g_weak_ref_get (&it_data->settings->metadata);
  g_return_if_fail (m);

  g_clear_pointer (&it_data->metadata_it, wp_iterator_unref);
  it_data->metadata_it = wp_metadata_new_iterator (m, 0);
}

static gboolean
settings_iterator_next (WpIterator *it, GValue *item)
{
  struct settings_iterator_data *it_data = wp_iterator_get_user_data (it);
  g_autoptr (WpMetadata) m = NULL;
  g_autoptr (WpSettingsItem) si = NULL;
  g_auto (GValue) val = G_VALUE_INIT;
  WpMetadataItem *mi;
  const gchar *key, *value;

  m = g_weak_ref_get (&it_data->settings->metadata);
  g_return_val_if_fail (m, FALSE);

  if (!wp_iterator_next (it_data->metadata_it, &val))
    return FALSE;

  mi = g_value_get_boxed (&val);
  key = wp_metadata_item_get_key (mi);
  value = wp_metadata_item_get_value (mi);

  si = wp_settings_item_new (m, key, value);
  g_value_init (item, WP_TYPE_SETTINGS_ITEM);
  g_value_take_boxed (item, g_steal_pointer (&si));
  return TRUE;
}

static void
settings_iterator_finalize (WpIterator *it)
{
  struct settings_iterator_data *it_data = wp_iterator_get_user_data (it);
  g_clear_pointer (&it_data->metadata_it, wp_iterator_unref);
  g_clear_object (&it_data->settings);
}

static const WpIteratorMethods settings_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = settings_iterator_reset,
  .next = settings_iterator_next,
  .fold = NULL,
  .foreach = NULL,
  .finalize = settings_iterator_finalize,
};

/*!
 * \brief Iterates over settings
 * \ingroup wpsettings
 * \param self the settings object
 * \returns (transfer full): an iterator that iterates over the settings.
 */
WpIterator *
wp_settings_new_iterator (WpSettings *self)
{
  g_autoptr (WpIterator) it = NULL;
  struct settings_iterator_data *it_data;
  g_autoptr (WpMetadata) m = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), NULL);

  m = g_weak_ref_get (&self->metadata);
  if (!m)
    return NULL;

  it = wp_iterator_new (&settings_iterator_methods,
      sizeof (struct settings_iterator_data));
  it_data = wp_iterator_get_user_data (it);
  it_data->settings = g_object_ref (self);
  it_data->metadata_it = wp_metadata_new_iterator (m, 0);
  return g_steal_pointer (&it);
}
070701000000B9000081A400000000000000000000000165F8630400001116000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/settings.h   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_SETTINGS_H__
#define __WIREPLUMBER_SETTINGS_H__

#include "object.h"
#include "spa-json.h"

#define WP_SETTINGS_SCHEMA_METADATA_NAME_PREFIX "schema-"
#define WP_SETTINGS_PERSISTENT_METADATA_NAME_PREFIX "persistent-"

G_BEGIN_DECLS

/*!
 * \brief The different spec types of a setting
 * \ingroup wpsettings
 */
typedef enum {
  WP_SETTINGS_SPEC_TYPE_UNKNOWN,
  WP_SETTINGS_SPEC_TYPE_BOOL,
  WP_SETTINGS_SPEC_TYPE_INT,
  WP_SETTINGS_SPEC_TYPE_FLOAT,
  WP_SETTINGS_SPEC_TYPE_STRING,
  WP_SETTINGS_SPEC_TYPE_ARRAY,
  WP_SETTINGS_SPEC_TYPE_OBJECT,
} WpSettingsSpecType;

typedef struct _WpSettingsSpec WpSettingsSpec;

/*!
 * \brief The WpSettingsSpec GType
 * \ingroup wpsettings
 */
#define WP_TYPE_SETTINGS_SPEC (wp_settings_spec_get_type ())
WP_API
GType wp_settings_spec_get_type (void);

WP_API
WpSettingsSpec *wp_settings_spec_ref (WpSettingsSpec * self);

WP_API
void wp_settings_spec_unref (WpSettingsSpec * self);

WP_API
const gchar * wp_settings_spec_get_description (WpSettingsSpec * self);

WP_API
WpSettingsSpecType wp_settings_spec_get_value_type (WpSettingsSpec * self);

WP_API
WpSpaJson * wp_settings_spec_get_default_value (WpSettingsSpec * self);

WP_API
WpSpaJson * wp_settings_spec_get_min_value (WpSettingsSpec * self);

WP_API
WpSpaJson * wp_settings_spec_get_max_value (WpSettingsSpec * self);

WP_API
gboolean wp_settings_spec_check_value (WpSettingsSpec * self, WpSpaJson *value);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSettingsSpec, wp_settings_spec_unref)

/*!
 * \brief The WpSettingsItem GType
 * \ingroup wpsettings
 */
#define WP_TYPE_SETTINGS_ITEM (wp_settings_item_get_type ())
WP_API
GType wp_settings_item_get_type (void);

typedef struct _WpSettingsItem WpSettingsItem;

WP_API
WpSettingsItem *wp_settings_item_ref (WpSettingsItem *self);

WP_API
void wp_settings_item_unref (WpSettingsItem *self);

WP_API
const gchar * wp_settings_item_get_key (WpSettingsItem * self);

WP_API
WpSpaJson * wp_settings_item_get_value (WpSettingsItem * self);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSettingsItem, wp_settings_item_unref)

/*!
 * \brief Flags to be used as WpObjectFeatures on WpSettings subclasses.
 * \ingroup wpsettings
 */
typedef enum { /*< flags >*/
  /*! Loads the settings */
  WP_SETTINGS_LOADED = (1 << 0),
} WpSettingsFeatures;

/*!
 * \brief The WpSettings GType
 * \ingroup wpsettings
 */
#define WP_TYPE_SETTINGS (wp_settings_get_type ())

WP_API
G_DECLARE_FINAL_TYPE (WpSettings, wp_settings, WP, SETTINGS, WpObject)

WP_API
WpSettings * wp_settings_new (WpCore * core, const gchar * metadata_name);

WP_API
WpSettings * wp_settings_find (WpCore * core, const gchar * metadata_name);

/*!
 * \brief callback conveying the changed setting and its json value
 *
 * \ingroup wpsettings
 * \param obj the wpsettings object
 * \param setting the changed setting
 * \param value json value of the the changed setting
 * \param user_data data passed in the \a wp_settings_subscribe
 */
typedef void (*WpSettingsChangedCallback) (WpSettings *obj,
    const gchar *setting, WpSpaJson *value, gpointer user_data);

WP_API
guintptr wp_settings_subscribe (WpSettings *self,
    const gchar *pattern, WpSettingsChangedCallback callback,
    gpointer user_data);

WP_API
guintptr wp_settings_subscribe_closure (WpSettings *self,
    const gchar *pattern,  GClosure * closure);

WP_API
gboolean wp_settings_unsubscribe (WpSettings *self,
    guintptr subscription_id);

WP_API
WpSpaJson * wp_settings_get (WpSettings *self, const gchar *name);

WP_API
WpSpaJson * wp_settings_get_saved (WpSettings *self, const gchar *name);

WP_API
WpSettingsSpec * wp_settings_get_spec (WpSettings *self, const gchar *name);

WP_API
gboolean wp_settings_set (WpSettings *self, const gchar *name,
    WpSpaJson *value);

WP_API
gboolean wp_settings_reset (WpSettings *self, const char *name);

WP_API
gboolean wp_settings_save (WpSettings *self, const char *name);

WP_API
gboolean wp_settings_delete (WpSettings *self, const char *name);

WP_API
void wp_settings_reset_all (WpSettings *self);

WP_API
void wp_settings_save_all (WpSettings *self);

WP_API
void wp_settings_delete_all (WpSettings *self);

WP_API
WpIterator * wp_settings_new_iterator (WpSettings *self);

G_END_DECLS

#endif
  070701000000BA000081A400000000000000000000000165F8630400001912000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/si-factory.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "si-factory.h"
#include "log.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-si-factory")

/*! \defgroup wpsifactory WpSiFactory */
/*!
 * \struct WpSiFactory
 *
 * A factory for session items.
 *
 * The most simple way to register a new item implementation would be:
 * \code
 * wp_si_factory_register (core,
 *    wp_si_factory_new_simple ("foobar", FOO_TYPE_BAR));
 * \endcode
 *
 * And the most simple way to construct an item from a registered factory:
 * \code
 * item = wp_session_item_make (core, "foobar");
 * \endcode
 *
 * \gproperties
 *
 * \gproperty{name, gchar *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The factory's name}
 */
enum {
  PROP_0,
  PROP_NAME,
};

typedef struct _WpSiFactoryPrivate WpSiFactoryPrivate;
struct _WpSiFactoryPrivate
{
  GQuark name_quark;
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpSiFactory, wp_si_factory, G_TYPE_OBJECT)

static void
wp_si_factory_init (WpSiFactory * self)
{
}

static void
wp_si_factory_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpSiFactory *self = WP_SI_FACTORY (object);
  WpSiFactoryPrivate *priv = wp_si_factory_get_instance_private (self);

  switch (property_id) {
  case PROP_NAME:
    priv->name_quark = g_quark_from_string (g_value_get_string (value));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_si_factory_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpSiFactory *self = WP_SI_FACTORY (object);
  WpSiFactoryPrivate *priv = wp_si_factory_get_instance_private (self);

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, g_quark_to_string (priv->name_quark));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_si_factory_class_init (WpSiFactoryClass * klass)
{
  GObjectClass * object_class = (GObjectClass *) klass;

  object_class->get_property = wp_si_factory_get_property;
  object_class->set_property = wp_si_factory_set_property;

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name", "The factory's name", "",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Gets the name of the factory
 *
 * \ingroup wpsifactory
 * \param self the factory
 * \returns the factory name
 */
const gchar *
wp_si_factory_get_name (WpSiFactory * self)
{
  g_return_val_if_fail (WP_IS_SI_FACTORY (self), NULL);

  WpSiFactoryPrivate *priv = wp_si_factory_get_instance_private (self);
  return g_quark_to_string (priv->name_quark);
}

/*!
 * \brief Creates a new instance of the session item that is constructed
 * by this factory
 *
 * \ingroup wpsifactory
 * \param self the factory
 * \param core the core
 * \returns (transfer full): a new session item instance
 */
WpSessionItem *
wp_si_factory_construct (WpSiFactory * self, WpCore * core)
{
  g_return_val_if_fail (WP_IS_SI_FACTORY (self), NULL);
  g_return_val_if_fail (WP_SI_FACTORY_GET_CLASS (self)->construct, NULL);

  return WP_SI_FACTORY_GET_CLASS (self)->construct (self, core);
}

static gboolean
find_factory_func (gpointer factory, gpointer name_quark)
{
  if (!WP_IS_SI_FACTORY (factory))
    return FALSE;

  WpSiFactoryPrivate *priv =
      wp_si_factory_get_instance_private ((WpSiFactory *) factory);
  return priv->name_quark == GPOINTER_TO_UINT (name_quark);
}

/*!
 * \brief Looks up a factory matching a name
 *
 * \ingroup wpsifactory
 * \param core the core
 * \param factory_name the lookup name
 * \returns (transfer full) (nullable): the factory matching the lookup name
 */
WpSiFactory *
wp_si_factory_find (WpCore * core, const gchar * factory_name)
{
  g_return_val_if_fail (WP_IS_CORE (core), NULL);

  GQuark q = g_quark_try_string (factory_name);
  if (q == 0)
    return NULL;
  GObject *f = wp_core_find_object (core,
      (GEqualFunc) find_factory_func, GUINT_TO_POINTER (q));
  return f ? WP_SI_FACTORY (f) : NULL;
}

/*!
 * \brief Finds the factory associated with the given \a name from the \a core
 * and uses it to construct a new WpSessionItem.
 *
 * \ingroup wpsifactory
 * \param core the WpCore
 * \param factory_name the name of the factory to be used for constructing
 *   the object
 * \returns (transfer full) (nullable): the new session item
 */
WpSessionItem *
wp_session_item_make (WpCore * core, const gchar * factory_name)
{
  g_autoptr (WpSiFactory) f = wp_si_factory_find (core, factory_name);
  return f ? wp_si_factory_construct (f, core) : NULL;
}

struct _WpSimpleSiFactory
{
  WpSiFactory parent;
  GType si_type;
};

G_DECLARE_FINAL_TYPE (WpSimpleSiFactory, wp_simple_si_factory,
                      WP, SIMPLE_SI_FACTORY, WpSiFactory)
G_DEFINE_TYPE (WpSimpleSiFactory, wp_simple_si_factory, WP_TYPE_SI_FACTORY)

static void
wp_simple_si_factory_init (WpSimpleSiFactory * self)
{
}

static WpSessionItem *
wp_simple_si_factory_construct (WpSiFactory * self, WpCore *core)
{
  return g_object_new (WP_SIMPLE_SI_FACTORY (self)->si_type,
      "core", core,
      NULL);
}

static void
wp_simple_si_factory_class_init (WpSimpleSiFactoryClass * klass)
{
  WpSiFactoryClass * factory_class = (WpSiFactoryClass *) klass;

  factory_class->construct = wp_simple_si_factory_construct;
}

/*!
 * \brief Creates a simple factory that constructs objects of a given GType.
 *
 * \ingroup wpsifactory
 * \param factory_name the factory name; must be a static string!
 * \param si_type the WpSessionItem subclass type to instantiate for
 *    constructing items
 * \returns (transfer full): the new factory
 */
WpSiFactory *
wp_si_factory_new_simple (const gchar * factory_name, GType si_type)
{
  g_return_val_if_fail (factory_name != NULL, NULL);
  g_return_val_if_fail (g_type_is_a (si_type, WP_TYPE_SESSION_ITEM), NULL);

  WpSimpleSiFactory *self = g_object_new (
      wp_simple_si_factory_get_type (), NULL);

  /* assign the quark directly to use g_quark_from_static_string */
  WpSiFactoryPrivate *priv =
      wp_si_factory_get_instance_private (WP_SI_FACTORY (self));
  priv->name_quark = g_quark_from_static_string (factory_name);

  self->si_type = si_type;

  return WP_SI_FACTORY (self);
}
  070701000000BB000081A400000000000000000000000165F8630400000457000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/si-factory.h /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_SI_FACTORY_H__
#define __WIREPLUMBER_SI_FACTORY_H__

#include "core.h"
#include "session-item.h"

G_BEGIN_DECLS

/*!
 * \brief The WpSiFactory GType
 * \ingroup wpsifactory
 */
#define WP_TYPE_SI_FACTORY (wp_si_factory_get_type ())
WP_API
G_DECLARE_DERIVABLE_TYPE (WpSiFactory, wp_si_factory, WP, SI_FACTORY, GObject)

struct _WpSiFactoryClass
{
  GObjectClass parent_class;

  WpSessionItem * (*construct) (WpSiFactory * self, WpCore * core);

  /*< private >*/
  WP_PADDING(7)
};

WP_API
WpSiFactory * wp_si_factory_new_simple (const gchar * factory_name,
    GType si_type);

WP_API
const gchar * wp_si_factory_get_name (WpSiFactory * self);

WP_API
WpSessionItem * wp_si_factory_construct (WpSiFactory * self, WpCore * core);

WP_API
WpSiFactory * wp_si_factory_find (WpCore * core, const gchar * factory_name);

WP_API
WpSessionItem * wp_session_item_make (WpCore * core, const gchar * factory_name);

G_END_DECLS

#endif
 070701000000BC000081A400000000000000000000000165F8630400003406000000000000000000000000000000000000002900000000wireplumber-0.5.0/lib/wp/si-interfaces.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "si-interfaces.h"
#include "wpenums.h"
#include "log.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-si-interfaces")

/*! \defgroup wpsiinterfaces Session Item Interfaces */

/*!
 * \struct WpSiAdapter
 * An interface for port adapters
 *
 * \gsignals
 *
 * \par adapter-ports-state-changed
 * \parblock
 * \code
 * void
 * adapter_ports_state_changed_callback (WpSiAdapter * self,
 *                                       gpointer user_data)
 * \endcode
 * Emitted when the ports state changes
 *
 * Flags: G_SIGNAL_RUN_LAST
 * \endparblock
 */

G_DEFINE_INTERFACE (WpSiAdapter, wp_si_adapter, WP_TYPE_SESSION_ITEM)

static void
wp_si_adapter_default_init (WpSiAdapterInterface * iface)
{
  g_signal_new ("adapter-ports-state-changed", G_TYPE_FROM_INTERFACE (iface),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE,
      2, WP_TYPE_SI_ADAPTER_PORTS_STATE, WP_TYPE_SI_ADAPTER_PORTS_STATE);
}

/**
 * \brief Gets the ports state
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \returns The state of the ports
 * \since 0.4.10
 */
WpSiAdapterPortsState
wp_si_adapter_get_ports_state (WpSiAdapter * self)
{
  g_return_val_if_fail (WP_IS_SI_ADAPTER (self), WP_SI_ADAPTER_PORTS_STATE_NONE);
  g_return_val_if_fail (WP_SI_ADAPTER_GET_IFACE (self)->get_ports_state,
      WP_SI_ADAPTER_PORTS_STATE_NONE);

  return WP_SI_ADAPTER_GET_IFACE (self)->get_ports_state (self);
}

/**
 * \brief Gets the format used to configure the adapter session item's ports
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \param mode (out) (nullable): the mode
 * \returns (transfer full): The format used to configure the ports of the
 *   adapter session item. Some items automatically choose a format when being
 *   activated, others never set a format on activation and the user needs to
 *   manually set it externally with wp_si_adapter_set_ports_format().
 */
WpSpaPod *
wp_si_adapter_get_ports_format (WpSiAdapter * self, const gchar **mode)
{
  g_return_val_if_fail (WP_IS_SI_ADAPTER (self), NULL);
  g_return_val_if_fail (WP_SI_ADAPTER_GET_IFACE (self)->get_ports_format, NULL);

  return WP_SI_ADAPTER_GET_IFACE (self)->get_ports_format (self, mode);
}

/*!
 * \brief Sets the format and configures the adapter session item ports using
 * the given format.
 *
 * The result of the operation can be checked using the
 * wp_si_adapter_set_ports_format_finish() API. If format is NULL, the adapter
 * will be configured with the default format. If mode is NULL, the adapter
 * will use "dsp" mode.
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \param format (transfer full) (nullable): the format to be set
 * \param mode (nullable): the mode
 * \param callback (scope async): the callback to call when the operation is done
 * \param data (closure): user data for \a callback
 */
void
wp_si_adapter_set_ports_format (WpSiAdapter * self, WpSpaPod *format,
    const gchar *mode, GAsyncReadyCallback callback, gpointer data)
{
  g_return_if_fail (WP_IS_SI_ADAPTER (self));
  g_return_if_fail (WP_SI_ADAPTER_GET_IFACE (self)->set_ports_format);

  WP_SI_ADAPTER_GET_IFACE (self)->set_ports_format (self, format, mode,
      callback, data);
}

/*!
 * \brief Finishes the operation started by wp_si_adapter_set_format().
 * This is meant to be called in the callback that was passed to that method.
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \param res the async result
 * \param error (out) (optional): the operation's error, if it occurred
 * \returns TRUE on success, FALSE if there was an error
 */
gboolean
wp_si_adapter_set_ports_format_finish (WpSiAdapter * self, GAsyncResult * res,
      GError ** error)
{
  g_return_val_if_fail (WP_IS_SI_ADAPTER (self), FALSE);
  g_return_val_if_fail (WP_SI_ADAPTER_GET_IFACE (self)->set_ports_format_finish,
     FALSE);

  return WP_SI_ADAPTER_GET_IFACE (self)->set_ports_format_finish (self, res,
     error);
}

/*!
 * \struct WpSiLinkable
 *
 * An interface for retrieving PipeWire port information from a session item.
 * This information is used to create links in the nodes graph.
 */

G_DEFINE_INTERFACE (WpSiLinkable, wp_si_linkable, WP_TYPE_SESSION_ITEM)

static WpSiAcquisition *
wp_si_linkable_default_get_acquisition (WpSiLinkable * self)
{
  return NULL;
}

static void
wp_si_linkable_default_init (WpSiLinkableInterface * iface)
{
  iface->get_acquisition = wp_si_linkable_default_get_acquisition;
}

/*!
 * \brief This method returns a variant of type "a(uuu)", where each tuple in
 * the array contains the following information:
 *   - u: (guint32) node id
 *   - u: (guint32) port id (the port must belong on the node specified above)
 *   - u: (guint32) the audio channel (enum spa_audio_channel) that this port
 *        makes available, or 0 for non-audio content
 *
 * The order in which ports appear in this array is important when no channel
 * information is available. The link implementation should link the ports
 * in the order they appear. This is normally a good enough substitute for
 * channel matching.
 *
 * The \a context argument can be used to get different sets of ports from
 * the item. The following well-known contexts are defined:
 *   - NULL: get the standard ports to be linked
 *   - "monitor": get the monitor ports
 *   - "control": get the control port
 *   - "reverse": get the reverse direction ports, if this item controls a
 *                filter node, which would have ports on both directions
 *
 * Contexts other than NULL may only be used internally to ease the
 * implementation of more complex item relationships. For example, a
 * WpSessionItem that is in control of an input (sink) adapter node may
 * implement WpSiLinkable where the NULL context will return the standard
 * input ports and the "monitor" context will return the adapter's monitor
 * ports. When linking this item to another item, the NULL context
 * will always be used, but the item may internally spawn a secondary
 * WpSessionItem that implements the "monitor" item. That secondary item
 * may implement WpSiLinkable, chaining calls to the WpSiLinkable
 * of the original item using the "monitor" context. This way, the monitor
 * WpSessionItem does not need to share control of the
 * underlying node; it only proxies calls to satisfy the API.
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \param context (nullable): an optional context for the ports
 * \returns (transfer full): a GVariant containing information about the
 *   ports of this item
 */
GVariant *
wp_si_linkable_get_ports (WpSiLinkable * self, const gchar * context)
{
  g_return_val_if_fail (WP_IS_SI_LINKABLE (self), NULL);
  g_return_val_if_fail (WP_SI_LINKABLE_GET_IFACE (self)->get_ports, NULL);

  return WP_SI_LINKABLE_GET_IFACE (self)->get_ports (self, context);
}

/*!
 * \brief Gets the acquisition interface associated with the item
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 *
 * \returns (transfer none) (nullable): the acquisition interface associated
 *   with this item, or NULL if this item does not require acquiring items
 *   before linking them
 */
WpSiAcquisition *
wp_si_linkable_get_acquisition (WpSiLinkable * self)
{
  g_return_val_if_fail (WP_IS_SI_LINKABLE (self), NULL);
  g_return_val_if_fail (
      WP_SI_LINKABLE_GET_IFACE (self)->get_acquisition, NULL);

  return WP_SI_LINKABLE_GET_IFACE (self)->get_acquisition (self);
}


/*!
 * \struct WpSiLink
 *
 * An interface for session items that provide a PipeWire link.
 *
 * \gsignals
 *
 * \par link-properties-changed
 * \parblock
 * \code
 * void
 * link_properties_changed_callback (WpSiLink * self,
 *                                   gpointer user_data)
 * \endcode
 * Emitted when the properties of the link change
 *
 * Flags: G_SIGNAL_RUN_LAST
 * \endparblock
 */

G_DEFINE_INTERFACE (WpSiLink, wp_si_link, WP_TYPE_SESSION_ITEM)

static WpProperties *
wp_si_link_default_get_properties (WpSiLink * self)
{
  return NULL;
}

static void
wp_si_link_default_init (WpSiLinkInterface * iface)
{
  iface->get_properties = wp_si_link_default_get_properties;

  g_signal_new ("link-properties-changed", G_TYPE_FROM_INTERFACE (iface),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
}

/*!
 * \brief This should return information that is used for registering the link,
 * as a GVariant of type a{ss} that contains additional properties to be
 * added to the list of global properties
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \returns (transfer full): registration info for the link
 */
GVariant *
wp_si_link_get_registration_info (WpSiLink * self)
{
  g_return_val_if_fail (WP_IS_SI_LINK (self), NULL);
  g_return_val_if_fail (WP_SI_LINK_GET_IFACE (self)->get_registration_info, NULL);

  return WP_SI_LINK_GET_IFACE (self)->get_registration_info (self);
}

/*!
 * \brief Gets the properties of the link
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \returns (transfer full) (nullable): the properties of the link
 */
WpProperties *
wp_si_link_get_properties (WpSiLink * self)
{
  g_return_val_if_fail (WP_IS_SI_LINK (self), NULL);
  g_return_val_if_fail (WP_SI_LINK_GET_IFACE (self)->get_properties, NULL);

  return WP_SI_LINK_GET_IFACE (self)->get_properties (self);
}

/*!
 * \brief Gets the output item linked by the link
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \returns (transfer none): the output item that is linked by this link
 */
WpSiLinkable *
wp_si_link_get_out_item (WpSiLink * self)
{
  g_return_val_if_fail (WP_IS_SI_LINK (self), NULL);
  g_return_val_if_fail (WP_SI_LINK_GET_IFACE (self)->get_out_item, NULL);

  return WP_SI_LINK_GET_IFACE (self)->get_out_item (self);
}

/*!
 * \brief Gets the input item linked by the link
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \returns (transfer none): the input item that is linked by this link
 */
WpSiLinkable *
wp_si_link_get_in_item (WpSiLink * self)
{
  g_return_val_if_fail (WP_IS_SI_LINK (self), NULL);
  g_return_val_if_fail (WP_SI_LINK_GET_IFACE (self)->get_in_item, NULL);

  return WP_SI_LINK_GET_IFACE (self)->get_in_item (self);
}

/*!
 * \struct WpSiAcquisition
 *
 * This interface provides a way to request an item for linking before doing
 * so. This allows item implementations to apply internal policy rules.
 *
 * A WpSiAcquisition is associated directly with a WpSiLinkable via
 * wp_si_linkable_get_acquisition(). In order to allow switching
 * policies, it is recommended that port info implementations use a separate
 * session item to implement this interface and allow replacing it.
 */

G_DEFINE_INTERFACE (WpSiAcquisition, wp_si_acquisition, WP_TYPE_SESSION_ITEM)

static void
wp_si_acquisition_default_init (WpSiAcquisitionInterface * iface)
{
}

/*!
 * \brief Acquires the \a item for linking by \a acquisitor.
 *
 * When a link is not allowed by policy, this operation should return
 * an error.
 *
 * When a link needs to be delayed for a short amount of time (ex. to apply
 * a fade out effect on another item), this operation should finish with a
 * delay. It is safe to assume that after this operation completes,
 * the item will be linked immediately.
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \param acquisitor the link that is trying to acquire a port info item
 * \param item the item that is being acquired
 * \param callback (scope async): the callback to call when the operation is done
 * \param data (closure): user data for \a callback
 */
void
wp_si_acquisition_acquire (WpSiAcquisition * self, WpSiLink * acquisitor,
    WpSiLinkable * item, GAsyncReadyCallback callback, gpointer data)
{
  g_return_if_fail (WP_IS_SI_ACQUISITION (self));
  g_return_if_fail (WP_SI_ACQUISITION_GET_IFACE (self)->acquire);

  WP_SI_ACQUISITION_GET_IFACE (self)->acquire (self, acquisitor, item, callback,
      data);
}

/*!
 * \brief Finishes the operation started by wp_si_acquisition_acquire().
 * This is meant to be called in the callback that was passed to that method.
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \param res the async result
 * \param error (out) (optional): the operation's error, if it occurred
 * \returns TRUE on success, FALSE if there was an error
 */
gboolean
wp_si_acquisition_acquire_finish (WpSiAcquisition * self, GAsyncResult * res,
    GError ** error)
{
  g_return_val_if_fail (WP_IS_SI_ACQUISITION (self), FALSE);
  g_return_val_if_fail (WP_SI_ACQUISITION_GET_IFACE (self)->acquire_finish,
      FALSE);

  return WP_SI_ACQUISITION_GET_IFACE (self)->acquire_finish (self, res, error);
}

/*!
 * \brief Releases the \a item, which means that it is being unlinked.
 *
 * \ingroup wpsiinterfaces
 * \param self the session item
 * \param acquisitor the link that had previously acquired the item
 * \param item the port info that is being released
 */
void
wp_si_acquisition_release (WpSiAcquisition * self, WpSiLink * acquisitor,
    WpSiLinkable * item)
{
  g_return_if_fail (WP_IS_SI_ACQUISITION (self));
  g_return_if_fail (WP_SI_ACQUISITION_GET_IFACE (self)->release);

  WP_SI_ACQUISITION_GET_IFACE (self)->release (self, acquisitor, item);
}
  070701000000BD000081A400000000000000000000000165F863040000116B000000000000000000000000000000000000002900000000wireplumber-0.5.0/lib/wp/si-interfaces.h  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_SI_INTERFACES_H__
#define __WIREPLUMBER_SI_INTERFACES_H__

#include "session-item.h"
#include "properties.h"
#include "spa-pod.h"

G_BEGIN_DECLS

typedef struct _WpSiAcquisition WpSiAcquisition;

/*!
 * \brief The WpSiAdapter GType
 * \ingroup wpsiinterfaces
 */
#define WP_TYPE_SI_ADAPTER (wp_si_adapter_get_type ())
WP_API
G_DECLARE_INTERFACE (WpSiAdapter, wp_si_adapter,
                     WP, SI_ADAPTER, WpSessionItem)

/*!
 * \brief The ports configuration state of the adapter
 * \ingroup wpsiinterfaces
 * \since 0.4.10
 */
typedef enum {
  WP_SI_ADAPTER_PORTS_STATE_NONE = 0, /*!< the ports have never been configured */
  WP_SI_ADAPTER_PORTS_STATE_CONFIGURING, /*!< the ports are being configured */
  WP_SI_ADAPTER_PORTS_STATE_CONFIGURED, /*!< the ports are configured */
} WpSiAdapterPortsState;

struct _WpSiAdapterInterface
{
  GTypeInterface interface;

  WpSpaPod * (*get_ports_format) (WpSiAdapter * self, const gchar **mode);
  void (*set_ports_format) (WpSiAdapter * self, WpSpaPod *format,
      const gchar *mode, GAsyncReadyCallback callback, gpointer data);
  gboolean (*set_ports_format_finish) (WpSiAdapter * self, GAsyncResult * res,
      GError ** error);
  WpSiAdapterPortsState (*get_ports_state) (WpSiAdapter * self);

  /*< private >*/
  WP_PADDING(4)
};

WP_API
WpSiAdapterPortsState wp_si_adapter_get_ports_state (WpSiAdapter * self);

WP_API
WpSpaPod *wp_si_adapter_get_ports_format (WpSiAdapter * self,
    const gchar **mode);

WP_API
void wp_si_adapter_set_ports_format (WpSiAdapter * self, WpSpaPod *format,
    const gchar *mode, GAsyncReadyCallback callback, gpointer data);

WP_API
gboolean wp_si_adapter_set_ports_format_finish (WpSiAdapter * self,
    GAsyncResult * res, GError ** error);

/*!
 * \brief The WpSiLinkable GType
 * \ingroup wpsiinterfaces
 */
#define WP_TYPE_SI_LINKABLE (wp_si_linkable_get_type ())
WP_API
G_DECLARE_INTERFACE (WpSiLinkable, wp_si_linkable,
                     WP, SI_LINKABLE, WpSessionItem)

struct _WpSiLinkableInterface
{
  GTypeInterface interface;

  GVariant * (*get_ports) (WpSiLinkable * self, const gchar * context);
  WpSiAcquisition * (*get_acquisition) (WpSiLinkable * self);

  /*< private >*/
  WP_PADDING(6)
};

WP_API
GVariant * wp_si_linkable_get_ports (WpSiLinkable * self,
    const gchar * context);

WP_API
WpSiAcquisition * wp_si_linkable_get_acquisition (WpSiLinkable * self);

/*!
 * \brief The WpSiLink GType
 * \ingroup wpsiinterfaces
 */
#define WP_TYPE_SI_LINK (wp_si_link_get_type ())
WP_API
G_DECLARE_INTERFACE (WpSiLink, wp_si_link,
                     WP, SI_LINK, WpSessionItem)

struct _WpSiLinkInterface
{
  GTypeInterface interface;

  GVariant * (*get_registration_info) (WpSiLink * self);
  WpProperties * (*get_properties) (WpSiLink * self);

  WpSiLinkable * (*get_out_item) (WpSiLink * self);
  WpSiLinkable * (*get_in_item) (WpSiLink * self);

  /*< private >*/
  WP_PADDING(4)
};

WP_API
GVariant * wp_si_link_get_registration_info (WpSiLink * self);

WP_API
WpProperties * wp_si_link_get_properties (WpSiLink * self);

WP_API
WpSiLinkable * wp_si_link_get_out_item (WpSiLink * self);

WP_API
WpSiLinkable * wp_si_link_get_in_item (WpSiLink * self);

/*!
 * \brief The WpSiAcquisition GType
 * \ingroup wpsiinterfaces
 */
#define WP_TYPE_SI_ACQUISITION (wp_si_acquisition_get_type ())
WP_API
G_DECLARE_INTERFACE (WpSiAcquisition, wp_si_acquisition,
                     WP, SI_ACQUISITION, WpSessionItem)

struct _WpSiAcquisitionInterface
{
  GTypeInterface interface;

  void (*acquire) (WpSiAcquisition * self, WpSiLink * acquisitor,
      WpSiLinkable * item, GAsyncReadyCallback callback, gpointer data);
  gboolean (*acquire_finish) (WpSiAcquisition * self, GAsyncResult * res,
      GError ** error);

  void (*release) (WpSiAcquisition * self, WpSiLink * acquisitor,
      WpSiLinkable * item);

  /*< private >*/
  WP_PADDING(5)
};

WP_API
void wp_si_acquisition_acquire (WpSiAcquisition * self, WpSiLink * acquisitor,
    WpSiLinkable * item, GAsyncReadyCallback callback, gpointer data);

WP_API
gboolean wp_si_acquisition_acquire_finish (
    WpSiAcquisition * self, GAsyncResult * res, GError ** error);

WP_API
void wp_si_acquisition_release (WpSiAcquisition * self, WpSiLink * acquisitor,
    WpSiLinkable * item);

G_END_DECLS

#endif
 070701000000BE000081A400000000000000000000000165F863040000B753000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/spa-json.c   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "spa-json.h"
#include "log.h"

#include <spa/utils/defs.h>
#include <spa/utils/json.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-spa-json")

#define WP_SPA_JSON_STRING_INIT_SIZE 64
#define WP_SPA_JSON_BUILDER_INIT_SIZE 64

/*! \defgroup wpspajson WpSpaJson */
/*!
 * \struct WpSpaJson
 *
 * \since 0.4.8
 */
/*!
 * \struct WpSpaJsonBuilder
 *
 * \since 0.4.8
 */
/*!
 * \struct WpSpaJsonParser
 *
 * \since 0.4.8
 */

static void
builder_add_formatted (WpSpaJsonBuilder *self, const gchar *fmt, ...)
    G_GNUC_PRINTF (2, 3);

static WpSpaJsonBuilder *
wp_spa_json_builder_new_formatted (const gchar *fmt, ...)
    G_GNUC_PRINTF (1, 2);

enum {
  FLAG_NO_OWNERSHIP = (1 << 0),
};

struct _WpSpaJson
{
  grefcount ref;
  guint32 flags;

  /* only used if FLAG_NO_OWNERSHIP is not set */
  WpSpaJsonBuilder *builder;

  /* not used if constructed with _new_wrap() */
  struct spa_json json_data;

  /* json data */
  gchar *data;
  size_t size;
  struct spa_json *json;
};

G_DEFINE_BOXED_TYPE (WpSpaJson, wp_spa_json, wp_spa_json_ref, wp_spa_json_unref)

struct _WpSpaJsonBuilder
{
  gboolean add_separator;
  gchar *data;
  size_t size;
  size_t max_size;
};

G_DEFINE_BOXED_TYPE (WpSpaJsonBuilder, wp_spa_json_builder,
    wp_spa_json_builder_ref, wp_spa_json_builder_unref)

struct _WpSpaJsonParser
{
  WpSpaJson *json;
  struct spa_json data[2];
  struct spa_json *pos;
  struct spa_json curr;
};

G_DEFINE_BOXED_TYPE (WpSpaJsonParser, wp_spa_json_parser,
    wp_spa_json_parser_ref, wp_spa_json_parser_unref)

/*!
 * \brief Increases the reference count of a spa json object
 * \ingroup wpspajson
 * \param self a spa json object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSpaJson *
wp_spa_json_ref (WpSpaJson *self)
{
  g_ref_count_inc (&self->ref);
  return self;
}

static void
wp_spa_json_free (WpSpaJson *self)
{
  g_clear_pointer (&self->builder, wp_spa_json_builder_unref);
  g_slice_free (WpSpaJson, self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 * \ingroup wpspajson
 * \param self (transfer full): a spa json object
 */
void
wp_spa_json_unref (WpSpaJson *self)
{
  if (g_ref_count_dec (&self->ref))
    wp_spa_json_free (self);
}

static WpSpaJsonBuilder *
wp_spa_json_builder_new (const gchar *data, size_t size)
{
  WpSpaJsonBuilder *self = g_rc_box_new0 (WpSpaJsonBuilder);
  self->add_separator = FALSE;
  self->data = g_new0 (gchar, size + 1);
  self->max_size = size;
  memcpy (self->data, data, size),
  self->data[size] = '\0';
  self->size = size;
  return self;
}

static WpSpaJsonBuilder *
wp_spa_json_builder_new_empty (size_t size)
{
  WpSpaJsonBuilder *self = g_rc_box_new0 (WpSpaJsonBuilder);
  self->add_separator = FALSE;
  self->data = g_new0 (gchar, size + 1);
  self->max_size = size;
  self->data[0] = '\0';
  self->size = 0;
  return self;
}

static WpSpaJsonBuilder *
wp_spa_json_builder_new_formatted (const gchar *fmt, ...)
{
  va_list args;
  WpSpaJsonBuilder *self = g_rc_box_new0 (WpSpaJsonBuilder);
  self->add_separator = FALSE;
  va_start (args, fmt);
  self->data = g_strdup_vprintf (fmt, args);
  va_end (args);
  self->size = strlen (self->data);
  self->max_size = self->size;
  return self;
}

static WpSpaJson *
wp_spa_json_new_from_builder (WpSpaJsonBuilder *builder)
{
  WpSpaJson *self = g_slice_new0 (WpSpaJson);
  g_ref_count_init (&self->ref);
  self->flags = 0;
  self->builder = builder;
  self->data = builder->data;
  self->size = builder->size;
  spa_json_init (&self->json_data, self->data, self->size);
  self->json = &self->json_data;
  return self;
}

static WpSpaJson *
wp_spa_json_new (const gchar *data, size_t size)
{
  return wp_spa_json_new_from_builder (wp_spa_json_builder_new (data, size));
}

/*!
 * \brief Constructs a new WpSpaJson from a JSON string.
 *
 * \ingroup wpspajson
 * \param json_str a JSON string
 * \returns a new WpSpaJson; unlike the "wrap" variants, this function copies
 *    the data in \a json_str, so it does not need to stay alive.
 * \since 0.5.0
 */
WP_API
WpSpaJson * wp_spa_json_new_from_string (const gchar *json_str)
{
  return wp_spa_json_new (json_str, strlen (json_str));
}

/*!
 * \brief Constructs a new WpSpaJson from a JSON string with specific length.
 *
 * \ingroup wpspajson
 * \param json_str a JSON string
 * \param len the specific length of the string
 * \returns a new WpSpaJson; unlike the "wrap" variants, this function copies
 *    the data in \a json_str, so it does not need to stay alive.
 * \since 0.5.0
 */
WP_API
WpSpaJson * wp_spa_json_new_from_stringn (const gchar *json_str, size_t len)
{
  return wp_spa_json_new (json_str, len);
}

/*!
 * \brief Constructs a new WpSpaJson that wraps a JSON string.
 *
 * \ingroup wpspajson
 * \param json_str a JSON string
 * \returns a new WpSpaJson that references the data in \a json_str. \a json_str
 *   is not copied, so it needs to stay alive.
 * \since 0.5.0
 */
WpSpaJson *
wp_spa_json_new_wrap_string (const gchar *json_str)
{
  return wp_spa_json_new_wrap_stringn(json_str, strlen (json_str));
}

/*!
 * \brief Constructs a new WpSpaJson that wraps a JSON string with specific length.
 *
 * \ingroup wpspajson
 * \param json_str a JSON string
 * \param len the specific length of the string
 * \returns a new WpSpaJson that references the data in \a json_str. \a json_str
 *   is not copied, so it needs to stay alive.
 * \since 0.5.0
 */
WpSpaJson *
wp_spa_json_new_wrap_stringn (const gchar *json_str, size_t len)
{
  WpSpaJson *self = g_slice_new0 (WpSpaJson);
  g_ref_count_init (&self->ref);
  self->flags = FLAG_NO_OWNERSHIP;
  spa_json_init (&self->json_data, json_str, len);
  self->builder = NULL;
  self->data = (gchar *)self->json_data.cur;
  self->size = self->json_data.end - self->json_data.cur;
  self->json = &self->json_data;
  return self;
}

/*!
 * \brief Constructs a new WpSpaJson that wraps the given `spa_json`.
 *
 * \ingroup wpspajson
 * \param json a spa_json
 * \returns a new WpSpaJson that references the data in \a json. \a json is not
 *   copied, so it needs to stay alive.
 */
WpSpaJson *
wp_spa_json_new_wrap (struct spa_json *json)
{
  WpSpaJson *self = g_slice_new0 (WpSpaJson);
  g_ref_count_init (&self->ref);
  self->flags = FLAG_NO_OWNERSHIP;
  self->builder = NULL;
  self->data = (gchar *)json->cur;
  self->size = json->end - json->cur;
  self->json = json;
  return self;
}

/*!
 * \brief Converts a WpSpaJson pointer to a `struct spa_json` one, for use with
 * native pipewire & spa functions. The returned pointer is owned by WpSpaJson
 * and may not be modified or freed.
 *
 * \ingroup wpspajson
 * \param self a spa json object
 * \returns a const pointer to the underlying spa_json structure
 */
const struct spa_json *
wp_spa_json_get_spa_json (const WpSpaJson *self)
{
  return self->json;
}

/*!
 * \brief Returns the json data
 *
 * \ingroup wpspajson
 * \param self a spa json object
 * \returns a const pointer to the json data
 */
const gchar *
wp_spa_json_get_data (const WpSpaJson *self)
{
  return self->data;
}

/*!
 * \brief Returns the json data size
 *
 * \ingroup wpspajson
 * \param self a spa json object
 * \returns the json data size
 */
size_t
wp_spa_json_get_size (const WpSpaJson *self)
{
  return self->size;
}

/*!
 * \brief Returns a newly allocated json string with length matching the size
 *
 * \ingroup wpspajson
 * \param self a spa json object
 * \returns (transfer full): the json string with length matching the size
 * \since 0.4.11
 */
gchar *
wp_spa_json_to_string (const WpSpaJson *self)
{
  return g_strndup (self->data, self->size);
}

/*!
 * \brief Copies a spa json object
 *
 * \ingroup wpspajson
 * \param other a spa json object
 * \returns (transfer full): The newly copied spa json
 */
WpSpaJson *
wp_spa_json_copy (WpSpaJson *other)
{
  g_return_val_if_fail (other, NULL);
  g_return_val_if_fail (other->json, NULL);
  return wp_spa_json_new (other->data, other->size);
}

/*!
 * \brief Checks if the json is the unique owner of its data or not
 *
 * \ingroup wpspajson
 * \param self a spa json object
 * \returns TRUE if the json owns the data, FALSE otherwise.
 */
gboolean
wp_spa_json_is_unique_owner (WpSpaJson *self)
{
  return g_ref_count_compare (&self->ref, 1) &&
      !(self->flags & FLAG_NO_OWNERSHIP);
}

/*!
 * \brief If \a self is not uniquely owned already, then it is unrefed and a
 * copy of it is returned instead. You should always consider \a self as unsafe
 * to use after this call and you should use the returned object instead.
 *
 * \ingroup wpspajson
 * \param self (transfer full): a spa json object
 * \returns (transfer full): the uniquely owned spa json object which may or may
 * not be the same as \a self.
 */
WpSpaJson *
wp_spa_json_ensure_unique_owner (WpSpaJson *self)
{
  WpSpaJson *copy = NULL;

  if (wp_spa_json_is_unique_owner (self))
    return self;

  copy = wp_spa_json_copy (self);
  wp_spa_json_unref (self);
  return copy;
}

/*!
 * \brief Creates a spa json of type NULL
 * \ingroup wpspajson
 * \returns (transfer full): The new spa json
 */
WpSpaJson *
wp_spa_json_new_null (void)
{
  return wp_spa_json_new_from_builder (
      wp_spa_json_builder_new_formatted ("%s", "null"));
}

/*!
 * \brief Creates a spa json of type boolean
 *
 * \ingroup wpspajson
 * \param value the boolean value
 * \returns (transfer full): The new spa json
 */
WpSpaJson *
wp_spa_json_new_boolean (gboolean value)
{
  return wp_spa_json_new_from_builder (
      wp_spa_json_builder_new_formatted ("%s", value ? "true" : "false"));
}

/*!
 * \brief Creates a spa json of type int
 *
 * \ingroup wpspajson
 * \param value the int value
 * \returns (transfer full): The new spa json
 */
WpSpaJson *
wp_spa_json_new_int (gint value)
{
  return wp_spa_json_new_from_builder (
      wp_spa_json_builder_new_formatted ("%d", value));
}

/*!
 * \brief Creates a spa json of type float
 *
 * \ingroup wpspajson
 * \param value the float value
 * \returns (transfer full): The new spa json
 */
WpSpaJson *
wp_spa_json_new_float (float value)
{
  return wp_spa_json_new_from_builder (
      wp_spa_json_builder_new_formatted ("%.6f", value));
}

static void
ensure_allocated_max_size (WpSpaJsonBuilder *self, size_t size)
{
  size_t new_size = self->size + size + 1;  /* '\0' because of vsnprintf */
  if (new_size > self->max_size) {
    size_t next_size = new_size * 2;
    self->data = g_realloc (self->data, next_size);
    self->max_size = next_size;
  }
}

/*!
 * \brief Creates a spa json of type string
 *
 * \ingroup wpspajson
 * \param value the string value
 * \returns (transfer full): The new spa json
 */
WpSpaJson *
wp_spa_json_new_string (const gchar *value)
{
  WpSpaJsonBuilder *b = wp_spa_json_builder_new_empty (strlen (value));
  size_t enc_size = spa_json_encode_string (b->data + b->size,
      b->max_size - b->size, value);
  if (enc_size + 1 > b->max_size - b->size) {
    ensure_allocated_max_size (b, enc_size);
    enc_size = spa_json_encode_string (b->data + b->size,
        b->max_size - b->size, value);
    g_assert (enc_size < b->max_size - b->size);
  }
  b->size += enc_size;
  return wp_spa_json_new_from_builder (b);
}

/* Args is not a pointer in some architectures, so this needs to be a macro to
 * avoid args being copied */
#define wp_spa_json_builder_add_value(self,fmt,args)                           \
do {                                                                           \
  switch (*fmt) {                                                              \
    case 'n':                                                                  \
      wp_spa_json_builder_add_null (self);                                     \
      break;                                                                   \
    case 'b':                                                                  \
      wp_spa_json_builder_add_boolean (self, va_arg(args, gboolean));          \
      break;                                                                   \
    case 'i':                                                                  \
      wp_spa_json_builder_add_int (self, va_arg(args, gint));                  \
      break;                                                                   \
    case 'f':                                                                  \
      wp_spa_json_builder_add_float (self, (float)va_arg(args, double));       \
      break;                                                                   \
    case 's':                                                                  \
      wp_spa_json_builder_add_string (self, va_arg(args, const gchar *));      \
      break;                                                                   \
    case 'J':                                                                  \
      wp_spa_json_builder_add_json (self, va_arg(args, WpSpaJson *));          \
      break;                                                                   \
    default:                                                                   \
      break;                                                                   \
  }								               \
} while(false)

/*!
 * \brief Creates a spa json of type array
 *
 * \ingroup wpspajson
 * \param format (nullable): the first value format ("n", "b", "i", "f", "s" or "J")
 * \param ... a list of array types and values, followed by NULL
 * \returns (transfer full): The new spa json
 */
WpSpaJson *
wp_spa_json_new_array (const gchar *format, ...)
{
  va_list args;
  WpSpaJson *res;

  va_start (args, format);
  res = wp_spa_json_new_array_valist (format, args);
  va_end (args);
  return res;
}

/*!
 * \brief This is the `va_list` version of wp_spa_json_new_array()
 *
 * \ingroup wpspajson
 * \param format (nullable): the first value format ("n", "b", "i", "f", "s" or "J")
 * \param args the variable arguments passed to wp_spa_json_new_array()
 * \returns (transfer full): The new spa json
 */
WpSpaJson *
wp_spa_json_new_array_valist (const gchar *format, va_list args)
{
  g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_array ();

  if (!format)
    return wp_spa_json_builder_end (b);

  wp_spa_json_builder_add_value (b, format, args);
  wp_spa_json_builder_add_valist (b, args);
  return wp_spa_json_builder_end (b);
}

/*!
 * \brief Creates a spa json of type object
 *
 * \ingroup wpspajson
 * \param key (nullable): the first object property key
 * \param format (nullable): the first property format ("n", "b", "i", "f", "s" or "J")
 * \param ... a list of object properties and values, followed by NULL
 * \returns (transfer full): The new spa json
 */
WpSpaJson *
wp_spa_json_new_object (const gchar *key, const gchar *format, ...)
{
  va_list args;
  WpSpaJson *res;

  va_start (args, format);
  res = wp_spa_json_new_object_valist (key, format, args);
  va_end (args);
  return res;
}

/*!
 * \brief This is the `va_list` version of wp_spa_json_new_object()
 *
 * \ingroup wpspajson
 * \param key (nullable): the first object property key
 * \param format (nullable): the first property format ("n", "b", "i", "f", "s" or "J")
 * \param args the variable arguments passed to wp_spa_json_new_object()
 * \returns (transfer full): The new spa json
 */
WpSpaJson *
wp_spa_json_new_object_valist (const gchar *key, const gchar *format,
    va_list args)
{
  g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_object ();

  if (!key || !format)
    return wp_spa_json_builder_end (b);

  wp_spa_json_builder_add_property (b, key);
  wp_spa_json_builder_add_value (b, format, args);
  wp_spa_json_builder_add_valist (b, args);
  return wp_spa_json_builder_end (b);
}

/*!
 * \brief Checks wether the spa json is of type null or not
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns TRUE if it is of type null, FALSE otherwise
 */
gboolean
wp_spa_json_is_null (WpSpaJson *self)
{
  return spa_json_is_null (self->data, self->size);
}

/*!
 * \brief Checks wether the spa json is of type boolean or not
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns TRUE if it is of type boolean, FALSE otherwise
 */
gboolean
wp_spa_json_is_boolean (WpSpaJson *self)
{
  return spa_json_is_bool (self->data, self->size);
}

/*!
 * \brief Checks wether the spa json is of type int or not
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns TRUE if it is of type int, FALSE otherwise
 */
gboolean
wp_spa_json_is_int (WpSpaJson *self)
{
  return spa_json_is_int (self->data, self->size);
}

/*!
 * \brief Checks wether the spa json is of type float or not
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns TRUE if it is of type float, FALSE otherwise
 */
gboolean
wp_spa_json_is_float (WpSpaJson *self)
{
  return spa_json_is_float (self->data, self->size);
}

/*!
 * \brief Checks wether the spa json is of type string or not
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns TRUE if it is of type string, FALSE otherwise
 */
gboolean
wp_spa_json_is_string (WpSpaJson *self)
{
  return spa_json_is_string (self->data, self->size);
}

/*!
 * \brief Checks whether the spa json is of type array or not
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns TRUE if it is of type array, FALSE otherwise
 */
gboolean
wp_spa_json_is_array (WpSpaJson *self)
{
  return spa_json_is_array (self->data, self->size);
}

/*!
 * \brief Checks whether the spa json is of type container or not
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns TRUE if it is of type container, FALSE otherwise
 */
gboolean
wp_spa_json_is_container (WpSpaJson *self)
{
  return spa_json_is_container (self->data, self->size);
}

/*!
 * \brief Checks whether the spa json is of type object or not
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns TRUE if it is of type object, FALSE otherwise
 */
gboolean
wp_spa_json_is_object (WpSpaJson *self)
{
  return spa_json_is_object (self->data, self->size);
}

gboolean
wp_spa_json_parse_boolean_internal (const gchar *data, int len, gboolean *value)
{
  bool v = false;
  if (spa_json_parse_bool (data, len, &v) < 0)
    return FALSE;
  *value = v ? TRUE : FALSE;
  return TRUE;
}

/*!
 * \brief Parses the boolean value of a spa json object
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \param value (out): the boolean value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parse_boolean (WpSpaJson *self, gboolean *value)
{
  return wp_spa_json_parse_boolean_internal (self->data, self->size, value);
}

/*!
 * \brief Parses the int value of a spa json object
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \param value (out): the int value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parse_int (WpSpaJson *self, gint *value)
{
  return spa_json_parse_int (self->data, self->size, value) >= 0;
}

/*!
 * \brief Parses the float value of a spa json object
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \param value (out): the float value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parse_float (WpSpaJson *self, float *value)
{
  return spa_json_parse_float (self->data, self->size, value) >= 0;
}

static gchar *
wp_spa_json_parse_string_internal (const gchar *data, int len)
{
  gchar *res = g_new0 (gchar, len+1);
  if (res)
    spa_json_parse_string (data, len, res);
  return res;
}

/*!
 * \brief Parses the string value of a spa json object
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns (transfer full): The newly allocated parsed string
 */
gchar *
wp_spa_json_parse_string (WpSpaJson *self)
{
  return wp_spa_json_parse_string_internal (self->data, self->size);
}

/*!
 * \brief Parses the array types and values of a spa json object
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \param ... (out): the list of array types and values, followed by NULL
 * \returns TRUE if the types and values were obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parse_array (WpSpaJson *self, ...)
{
  va_list args;
  gboolean res;
  va_start (args, self);
  res = wp_spa_json_parse_array_valist (self, args);
  va_end (args);
  return res;
}

/*!
 * \brief This is the `va_list` version of wp_spa_json_parse_array()
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \param args (out): the variable arguments passed to wp_spa_json_parse_array()
 * \returns TRUE if the types and values were obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parse_array_valist (WpSpaJson *self, va_list args)
{
  g_autoptr (WpSpaJsonParser) p = wp_spa_json_parser_new_array (self);
  gboolean res = wp_spa_json_parser_get_valist (p, args);
  if (res)
    wp_spa_json_parser_end (p);
  return res;
}

/*!
 * \brief Parses the object properties and values of a spa json object
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \param ... (out): the list of object properties and values, followed by NULL
 * \returns TRUE if the properties and values were obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parse_object (WpSpaJson *self, ...)
{
  va_list args;
  gboolean res;
  va_start (args, self);
  res = wp_spa_json_parse_object_valist (self, args);
  va_end (args);
  return res;
}

/*!
 * \brief This is the `va_list` version of wp_spa_json_parse_object()
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \param args (out): the variable arguments passed to wp_spa_json_parse_object()
 * \returns TRUE if the properties and values were obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parse_object_valist (WpSpaJson *self, va_list args)
{
  g_autoptr (WpSpaJsonParser) p = wp_spa_json_parser_new_object (self);
  gboolean res = wp_spa_json_parser_get_valist (p, args);
  if (res)
    wp_spa_json_parser_end (p);
  return res;
}

/* Args is not a pointer in some architectures, so this needs to be a macro to
 * avoid args being copied */
#define wp_spa_json_parse_value(data,len,fmt,args)                             \
do {                                                                           \
  switch (*fmt) {                                                              \
    case 'n':                                                                  \
      if (!spa_json_is_null (data, len))                                       \
        return FALSE;                                                          \
      break;                                                                   \
    case 'b':                                                                  \
      if (!wp_spa_json_parse_boolean_internal (data, len,                      \
          va_arg(args, gboolean *)))                                           \
        return FALSE;                                                          \
      break;                                                                   \
    case 'i':                                                                  \
      if (spa_json_parse_int (data, len, va_arg(args, gint *)) < 0)            \
        return FALSE;                                                          \
      break;                                                                   \
    case 'f':                                                                  \
      if (spa_json_parse_float (data, len, va_arg(args, float *)) < 0)         \
        return FALSE;                                                          \
      break;                                                                   \
    case 's': {                                                                \
      gchar *str = wp_spa_json_parse_string_internal (data, len);              \
      if (!str)                                                                \
        return FALSE;                                                          \
      *va_arg(args, gchar **) = str;                                           \
      break;                                                                   \
    }                                                                          \
    case 'J': {                                                                \
      WpSpaJson *j = wp_spa_json_new (data, len);                              \
      if (!j)                                                                  \
        return FALSE;                                                          \
      *va_arg(args, WpSpaJson **) = j;                                         \
      break;                                                                   \
    }                                                                          \
    default:                                                                   \
      return FALSE;                                                            \
  }                                                                            \
} while(false)

/*!
 * \brief Parses the object property values of a spa json object
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \param ... the list of property names, formats and values, followed by NULL
 * \returns TRUE if the properties and values were obtained, FALSE otherwise
 */
gboolean
wp_spa_json_object_get (WpSpaJson *self, ...)
{
  va_list args;
  gboolean res;
  va_start (args, self);
  res = wp_spa_json_object_get_valist (self, args);
  va_end (args);
  return res;
}

/*!
 * \brief This is the `va_list` version of wp_spa_json_object_get()
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \param args the variable arguments passed to wp_spa_json_object_get()
 * \returns TRUE if the properties and values were obtained, FALSE otherwise
 */
gboolean
wp_spa_json_object_get_valist (WpSpaJson *self, va_list args)
{
  g_auto (GValue) item = G_VALUE_INIT;
  g_autoptr (WpIterator) it = NULL;
  const gchar *lookup_key = NULL;
  const gchar *lookup_fmt = NULL;

  g_return_val_if_fail (wp_spa_json_is_object (self), FALSE);

  lookup_key = va_arg(args, const gchar *);
  if (!lookup_key)
    return TRUE;
  lookup_fmt = va_arg(args, const gchar *);
  if (!lookup_fmt)
    return FALSE;

  it = wp_spa_json_new_iterator (self);
  for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
    WpSpaJson *key = g_value_get_boxed (&item);
    g_autofree gchar *key_str = NULL;
    WpSpaJson *value = NULL;

    key_str = wp_spa_json_parse_string (key);
    g_return_val_if_fail (key_str, FALSE);

    g_value_unset (&item);
    if (!wp_iterator_next (it, &item))
      return FALSE;
    value = g_value_get_boxed (&item);

    if (g_strcmp0 (key_str, lookup_key) == 0) {
      wp_spa_json_parse_value (value->data, value->size, lookup_fmt, args);
      lookup_key = va_arg(args, const gchar *);
      if (!lookup_key)
        return TRUE;
      lookup_fmt = va_arg(args, const gchar *);
      if (!lookup_fmt)
        return FALSE;
      wp_iterator_reset (it);
    }
  }

  return FALSE;
}

/*!
 * \brief Increases the reference count of a spa json builder
 *
 * \ingroup wpspajson
 * \param self a spa json builder object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSpaJsonBuilder *
wp_spa_json_builder_ref (WpSpaJsonBuilder *self)
{
  return (WpSpaJsonBuilder *) g_rc_box_acquire ((gpointer) self);
}

static void
wp_spa_json_builder_free (WpSpaJsonBuilder *self)
{
  g_clear_pointer (&self->data, g_free);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 *
 * \ingroup wpspajson
 * \param self (transfer full): a spa json builder object
 */
void
wp_spa_json_builder_unref (WpSpaJsonBuilder *self)
{
  g_rc_box_release_full (self, (GDestroyNotify) wp_spa_json_builder_free);
}

/*!
 * \brief Creates a spa json builder of type array
 *
 * \ingroup wpspajson
 * \returns (transfer full): the new spa json builder
 */
WpSpaJsonBuilder *
wp_spa_json_builder_new_array (void)
{
  WpSpaJsonBuilder *self = g_rc_box_new0 (WpSpaJsonBuilder);
  self->add_separator = FALSE;
  self->data = g_new0 (gchar, WP_SPA_JSON_BUILDER_INIT_SIZE);
  self->max_size = WP_SPA_JSON_BUILDER_INIT_SIZE;
  self->data[0] = '[';
  self->size = 1;
  return self;
}

/*!
 * \brief Creates a spa json builder of type object
 *
 * \ingroup wpspajson
 * \returns (transfer full): the new spa json builder
 */
WpSpaJsonBuilder *
wp_spa_json_builder_new_object (void)
{
  WpSpaJsonBuilder *self = g_rc_box_new0 (WpSpaJsonBuilder);
  self->add_separator = FALSE;
  self->data = g_new0 (gchar, WP_SPA_JSON_BUILDER_INIT_SIZE);
  self->max_size = WP_SPA_JSON_BUILDER_INIT_SIZE;
  self->data[0] = '{';
  self->size = 1;
  return self;
}

static void
ensure_separator (WpSpaJsonBuilder *self, gboolean for_property)
{
  gboolean insert = (self->data[0] == '{' && for_property) ||
      (self->data[0] == '[' && !for_property);
  if (insert) {
    if (self->add_separator) {
      ensure_allocated_max_size (self, 2);
      self->data[self->size++] = ',';
      self->data[self->size++] = ' ';
    } else {
      self->add_separator = TRUE;
    }
  }
}

static void
builder_add_formatted (WpSpaJsonBuilder *self, const gchar *fmt, ...)
{
  int s;
  va_list args;
  va_start (args, fmt);
  s = vsnprintf (self->data + self->size, self->max_size - self->size, fmt, args);
  va_end (args);
  g_return_if_fail (s > 0);
  self->size += s;
}

static void
builder_add (WpSpaJsonBuilder *self, const gchar *data, size_t size)
{
  g_return_if_fail (self->max_size - self->size >= size + 1);
  snprintf (self->data + self->size, size + 1, "%s", data);
  self->size += size;
}

/*!
 * \brief Adds a property into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param key the name of the property
 */
void
wp_spa_json_builder_add_property (WpSpaJsonBuilder *self, const gchar *key)
{
  size_t enc_size;
  ensure_separator (self, TRUE);
  enc_size = spa_json_encode_string (self->data + self->size,
      self->max_size - self->size, key);
  if (enc_size + 2 > self->max_size - self->size) {
    ensure_allocated_max_size (self, enc_size + 1);
    enc_size = spa_json_encode_string (self->data + self->size,
        self->max_size - self->size, key);
    g_assert (enc_size + 1 < self->max_size - self->size);
  }
  self->data[self->size + enc_size] = ':';
  self->size += enc_size + 1;
}

/*!
 * \brief Adds a null value into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 */
void
wp_spa_json_builder_add_null (WpSpaJsonBuilder *self)
{
  ensure_separator (self, FALSE);
  ensure_allocated_max_size (self, 4);
  builder_add_formatted (self, "%s", "null");
}

/*!
 * \brief Adds a boolean value into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param value the boolean value
 */
void
wp_spa_json_builder_add_boolean (WpSpaJsonBuilder *self, gboolean value)
{
  ensure_separator (self, FALSE);
  ensure_allocated_max_size (self, value ? 4 : 5);
  builder_add_formatted (self, "%s", value ? "true" : "false");
}

/*!
 * \brief Adds a int value into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param value the int value
 */
void
wp_spa_json_builder_add_int (WpSpaJsonBuilder *self, gint value)
{
  ensure_separator (self, FALSE);
  ensure_allocated_max_size (self, 16);
  builder_add_formatted (self, "%d", value);
}

/*!
 * \brief Adds a float value into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param value the float value
 */
void
wp_spa_json_builder_add_float (WpSpaJsonBuilder *self, float value)
{
  ensure_separator (self, FALSE);
  ensure_allocated_max_size (self, 32);
  builder_add_formatted (self, "%.6f", value);
}

/*!
 * \brief Adds a string value into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param value the string value
 */
void
wp_spa_json_builder_add_string (WpSpaJsonBuilder *self, const gchar *value)
{
  size_t enc_size;
  ensure_separator (self, FALSE);
  enc_size = spa_json_encode_string (self->data + self->size,
      self->max_size - self->size, value);
  if (enc_size + 1 > self->max_size - self->size) {
    ensure_allocated_max_size (self, enc_size);
    enc_size = spa_json_encode_string (self->data + self->size,
        self->max_size - self->size, value);
    g_assert (enc_size < self->max_size - self->size);
  }
  self->size += enc_size;
}

/*!
 * \brief Adds a json value into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param json (transfer none): the json value
 */
void
wp_spa_json_builder_add_json (WpSpaJsonBuilder *self, WpSpaJson *json)
{
  ensure_separator (self, FALSE);
  ensure_allocated_max_size (self, json->size);
  builder_add (self, json->data, json->size);
}

/*!
 * \brief Adds a json string into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param json_str the json string
 */
void
wp_spa_json_builder_add_from_string (WpSpaJsonBuilder *self,
    const gchar *json_str)
{
  wp_spa_json_builder_add_from_stringn (self, json_str, strlen (json_str));
}

/*!
 * \brief Adds a json string with specific length into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param json_str the json string
 * \param len the specific length of the json string
 */
void
wp_spa_json_builder_add_from_stringn (WpSpaJsonBuilder *self,
    const gchar *json_str, size_t len)
{
  ensure_separator (self, FALSE);
  ensure_allocated_max_size (self, len);
  builder_add (self, json_str, len);
}

/*!
 * \brief Adds values into the builder
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param ... the json values
 */
void
wp_spa_json_builder_add (WpSpaJsonBuilder *self, ...)
{
  va_list args;
  va_start (args, self);
  wp_spa_json_builder_add_valist (self, args);
  va_end (args);
}

/*!
 * \brief This is the `va_list` version of wp_spa_json_builder_add()
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \param args the variable arguments passed to wp_spa_json_builder_add()
 */
void
wp_spa_json_builder_add_valist (WpSpaJsonBuilder *self, va_list args)
{
  do {
    const char *format = NULL;

    /* add property key if object */
    if (self->data[0] == '{') {
      const gchar *key = va_arg(args, const gchar *);
      if (!key)
        return;
      wp_spa_json_builder_add_property (self, key);
    }

    /* get value format */
    format = va_arg(args, const gchar *);
    if (!format)
      return;

    /* add value */
    wp_spa_json_builder_add_value (self, format, args);
  } while (TRUE);
}

/*!
 * \brief Ends the builder process and returns the constructed spa json object
 *
 * \ingroup wpspajson
 * \param self the spa json builder object
 * \returns (transfer full): the constructed spa json object
 */
WpSpaJson *
wp_spa_json_builder_end (WpSpaJsonBuilder *self)
{
  /* close */
  switch (self->data[0]) {
    case '[':  /* array */
      ensure_allocated_max_size (self, 2);
      self->data[self->size++] = ']';
      self->data[self->size] = '\0';
      break;
    case '{':  /* object */
      ensure_allocated_max_size (self, 2);
      self->data[self->size++] = '}';
      self->data[self->size] = '\0';
      break;
    default:
      break;
  }

  return wp_spa_json_new_from_builder (wp_spa_json_builder_ref (self));
}

/*!
 * \brief Increases the reference count of a spa json parser
 *
 * \ingroup wpspajson
 * \param self a spa json parser object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSpaJsonParser *
wp_spa_json_parser_ref (WpSpaJsonParser *self)
{
  return (WpSpaJsonParser *) g_rc_box_acquire ((gpointer) self);
}

static void
wp_spa_json_parser_free (WpSpaJsonParser *self)
{
  self->json = NULL;
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 *
 * \ingroup wpspajson
 * \param self (transfer full): a spa json parser object
 */
void
wp_spa_json_parser_unref (WpSpaJsonParser *self)
{
  g_rc_box_release_full (self, (GDestroyNotify) wp_spa_json_parser_free);
}

/*!
 * \brief Creates a spa json array parser. The \a json object must be valid for
 * the entire life-cycle of the returned parser.
 *
 * \ingroup wpspajson
 * \param json the spa json array to parse
 * \returns (transfer full): The new spa json parser
 */
WpSpaJsonParser *
wp_spa_json_parser_new_array (WpSpaJson *json)
{
  WpSpaJsonParser *self;

  g_return_val_if_fail (wp_spa_json_is_array (json), NULL);

  self = g_rc_box_new0 (WpSpaJsonParser);
  self->json = json;
  self->data[0] = *json->json;
  spa_json_enter_array (&self->data[0], &self->data[1]);
  self->pos = &self->data[1];
  return self;
}

/*!
 * \brief Creates a spa json object parser. The \a json object must be valid for
 * the entire life-cycle of the returned parser.
 *
 * \ingroup wpspajson
 * \param json the spa json object to parse
 * \returns (transfer full): The new spa json parser
 */
WpSpaJsonParser *
wp_spa_json_parser_new_object (WpSpaJson *json)
{
  WpSpaJsonParser *self;

  g_return_val_if_fail (wp_spa_json_is_object (json), NULL);

  self = g_rc_box_new0 (WpSpaJsonParser);
  self->json = json;
  self->data[0] = *json->json;
  spa_json_enter_object (&self->data[0], &self->data[1]);
  self->pos = &self->data[1];
  return self;
}

/*!
 * \brief Creates a new spa json parser for undefined type of data. The \a json
 * object must be valid for the entire life-cycle of the returned parser.
 *
 * This function allows creating a parser object for any type of spa json and is
 * mostly useful to parse non-standard JSON data that should be treated as if it
 * were an object or array, but does not start with a '{' or '[' character. Such
 * data can be for instance a comma-separated list of single values (array) or
 * key-value pairs (object). Such data is also the main configuration file,
 * which is an object but doesn't start with a '{' character.
 *
 * \note If the data is an array or object, the parser will not enter it and the
 * only token it will be able to parse is the same \a json object that is passed
 * in as an argument. Use wp_spa_json_parser_new_array() or
 * wp_spa_json_parser_new_object() to parse arrays or objects.
 *
 * \ingroup wpspajson
 * \param json the spa json to parse
 * \returns (transfer full): The new spa json parser
 * \since 0.5.0
 */
WpSpaJsonParser *
wp_spa_json_parser_new_undefined (WpSpaJson *json)
{
  WpSpaJsonParser *self;

  self = g_rc_box_new0 (WpSpaJsonParser);
  self->json = json;
  self->data[0] = *json->json;
  self->pos = &self->data[0];
  return self;
}

static int
check_nested_size (struct spa_json *parent, const gchar *data, int size)
{
  const gchar *nested_data;
  int nested_size;
  struct spa_json nested[2];

  /* only arrays and objects are considered nested data */
  if (!spa_json_is_array (data, size) && !spa_json_is_object (data, size))
    return 0;

  /* enter */
  nested[0] = *parent;
  spa_json_enter (&nested[0], &nested[1]);

  /* recursively advance */
  while ((nested_size = spa_json_next (&nested[1], &nested_data)) > 0) {
    if (check_nested_size (&nested[1], nested_data, nested_size) < 0)
      return -1;
  }
  if (nested_size < 0)
    return -1;

  /* advance one more time to reach end of nested data */
  if (spa_json_next (&nested[1], &nested_data) < 0)
    return -1;

  return nested_data - data;
}

static gboolean
wp_spa_json_parser_advance (WpSpaJsonParser *self)
{
  const gchar *data = NULL;
  int size, nested_size;

  if (!self->pos)
    return FALSE;

  /* advance */
  size = spa_json_next (self->pos, &data);
  if (size <= 0)
    return FALSE;
  g_return_val_if_fail (data != NULL, FALSE);

  /* if array or object, add the nested size */
  nested_size = check_nested_size (self->pos, data, size);
  if (nested_size < 0)
    return FALSE;
  size += nested_size;

  /* update current */
  spa_json_init (&self->curr, data, size);
  return TRUE;
}

/*!
 * \brief Gets the null value from a spa json parser
 *
 * \ingroup wpspajson
 * \param self the spa json parser object
 * \returns TRUE if the null value is present, FALSE otherwise
 */
gboolean
wp_spa_json_parser_get_null (WpSpaJsonParser *self)
{
  return wp_spa_json_parser_advance (self) &&
      spa_json_is_null (self->curr.cur, self->curr.end - self->curr.cur);
}

/*!
 * \brief Gets the boolean value from a spa json parser
 *
 * \ingroup wpspajson
 * \param self the spa json parser object
 * \param value (out): the boolean value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parser_get_boolean (WpSpaJsonParser *self, gboolean *value)
{
  return wp_spa_json_parser_advance (self) &&
      wp_spa_json_parse_boolean_internal (self->curr.cur,
          self->curr.end - self->curr.cur, value);
}

/*!
 * \brief Gets the int value from a spa json parser object
 *
 * \ingroup wpspajson
 * \param self the spa json parser object
 * \param value (out): the int value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parser_get_int (WpSpaJsonParser *self, gint *value)
{
  return wp_spa_json_parser_advance (self) &&
      spa_json_parse_int (self->curr.cur,
          self->curr.end - self->curr.cur, value) >= 0;
}

/*!
 * \brief Gets the float value from a spa json parser object
 *
 * \ingroup wpspajson
 * \param self the spa json parser object
 * \param value (out): the float value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parser_get_float (WpSpaJsonParser *self, float *value)
{
  return wp_spa_json_parser_advance (self) &&
      spa_json_parse_float (self->curr.cur,
          self->curr.end - self->curr.cur, value) >= 0;
}

/*!
 * \brief Gets the string value from a spa json parser object
 *
 * \ingroup wpspajson
 * \param self the spa json parser object
 * \returns (transfer full): The newly allocated parsed string
 */
gchar *
wp_spa_json_parser_get_string (WpSpaJsonParser *self)
{
  return wp_spa_json_parser_advance (self) ?
      wp_spa_json_parse_string_internal (self->curr.cur,
          self->curr.end - self->curr.cur) : NULL;
}

/*!
 * \brief Gets the spa json value from a spa json parser object
 *
 * \note the returned spa json object references the original data instead
 * of copying it, therefore the original data must be valid for the entire
 * life-cycle of the returned object
 *
 * \ingroup wpspajson
 * \param self the spa json parser object
 * \returns (transfer full): The spa json value or NULL if it could not be
 * obtained
 */
WpSpaJson *
wp_spa_json_parser_get_json (WpSpaJsonParser *self)
{
  return wp_spa_json_parser_advance (self) ?
      wp_spa_json_new_wrap_stringn (self->curr.cur,
          self->curr.end - self->curr.cur) : NULL;
}

gboolean
wp_spa_json_parser_get_value (WpSpaJsonParser *self, const gchar *fmt,
    va_list args)
{
  if (wp_spa_json_parser_advance (self)) {
    wp_spa_json_parse_value (self->curr.cur, self->curr.end - self->curr.cur,
        fmt, args);
    return TRUE;
  }
  return FALSE;
}

/*!
 * \brief Gets the values from a spa json parser object
 *
 * \ingroup wpspajson
 * \param self the spa json parser object
 * \param ... (out): a list of values to get, followed by NULL
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parser_get (WpSpaJsonParser *self, ...)
{
  gboolean res;
  va_list args;
  va_start (args, self);
  res = wp_spa_json_parser_get_valist (self, args);
  va_end (args);
  return res;
}

/*!
 * \brief This is the `va_list` version of wp_spa_json_parser_get()
 *
 * \ingroup wpspajson
 * \param self the spa json parser object
 * \param args the variable arguments passed to wp_spa_json_parser_get()
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_json_parser_get_valist (WpSpaJsonParser *self, va_list args)
{
  do {
    const char *format = NULL;

    /* parse property key if object */
    if (self->json->data[0] == '{') {
      gchar **key = va_arg(args, gchar **);
      if (!key)
        return TRUE;
      *key = wp_spa_json_parser_get_string (self);
      if (!(*key))
        return FALSE;
    }

    /* parse format */
    format = va_arg(args, const gchar *);
    if (!format)
      return TRUE;

    /* advance */
    if (!wp_spa_json_parser_advance (self))
      return FALSE;

    /* parse value */
    wp_spa_json_parse_value (self->curr.cur, self->curr.end - self->curr.cur,
        format, args);
  } while (TRUE);

  return FALSE;
}

void
wp_spa_json_parser_end (WpSpaJsonParser *self)
{
  self->pos = NULL;
}

struct _WpSpaJsonIterator
{
  WpSpaJson *json;
  WpSpaJsonParser *parser;
};
typedef struct _WpSpaJsonIterator WpSpaJsonIterator;

static void
wp_spa_json_iterator_reset (WpIterator *iterator)
{
  WpSpaJsonIterator *self = wp_iterator_get_user_data (iterator);
  g_clear_pointer (&self->parser, wp_spa_json_parser_unref);
}

static gboolean
wp_spa_json_iterator_next (WpIterator *iterator, GValue *item)
{
  WpSpaJsonIterator *self = wp_iterator_get_user_data (iterator);

  /* init iterator if first time */
  if (!self->parser) {
    switch (self->json->json->cur[0]) {
      case '[':  /* array */
        self->parser = wp_spa_json_parser_new_array (self->json);
        break;
      case '{':  /* object */
        self->parser = wp_spa_json_parser_new_object (self->json);
        break;
      default:
        return FALSE;
    }
  }

  /* advance */
  if (!wp_spa_json_parser_advance (self->parser))
    return FALSE;

  if (item) {
    g_value_init (item, WP_TYPE_SPA_JSON);
    g_value_take_boxed (item, wp_spa_json_new_wrap (&self->parser->curr));
  }

  return TRUE;
}

static void
wp_spa_json_iterator_finalize (WpIterator *iterator)
{
  WpSpaJsonIterator *self = wp_iterator_get_user_data (iterator);
  g_clear_pointer (&self->parser, wp_spa_json_parser_unref);
  g_clear_pointer (&self->json, wp_spa_json_unref);
}

/*!
 * \brief Creates a new iterator for a spa json object.
 *
 * \ingroup wpspajson
 * \param self the spa json object
 * \returns (transfer full): the new spa json iterator
 */
WpIterator *
wp_spa_json_new_iterator (WpSpaJson *self)
{
  static const WpIteratorMethods methods = {
    .version = WP_ITERATOR_METHODS_VERSION,
    .reset = wp_spa_json_iterator_reset,
    .next = wp_spa_json_iterator_next,
    .fold = NULL,
    .foreach = NULL,
    .finalize = wp_spa_json_iterator_finalize
  };
  WpIterator *it = wp_iterator_new (&methods, sizeof (WpSpaJsonIterator));
  WpSpaJsonIterator *jit = wp_iterator_get_user_data (it);

  jit->json = wp_spa_json_ref (self);
  jit->parser = NULL;

  return it;
}
 070701000000BF000081A400000000000000000000000165F863040000199F000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/spa-json.h   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_SPA_JSON_H__
#define __WIREPLUMBER_SPA_JSON_H__

#include <gio/gio.h>

#include "defs.h"
#include "iterator.h"

G_BEGIN_DECLS

struct spa_json;

/*!
 * \brief The WpSpaJson GType
 * \ingroup wpspajson
 */
#define WP_TYPE_SPA_JSON (wp_spa_json_get_type ())
WP_API
GType wp_spa_json_get_type (void);

typedef struct _WpSpaJson WpSpaJson;

WP_API
WpSpaJson *wp_spa_json_ref (WpSpaJson *self);

WP_API
void wp_spa_json_unref (WpSpaJson *self);

WP_API
WpSpaJson * wp_spa_json_new_from_string (const gchar *json_str);

WP_API
WpSpaJson * wp_spa_json_new_from_stringn (const gchar *json_str, size_t len);

WP_API
WpSpaJson * wp_spa_json_new_wrap_string (const gchar *json_str);

WP_API
WpSpaJson * wp_spa_json_new_wrap_stringn (const gchar *json_str, size_t len);

WP_API
WpSpaJson * wp_spa_json_new_wrap (struct spa_json *json);

WP_API
const struct spa_json * wp_spa_json_get_spa_json (const WpSpaJson *self);

WP_API
const gchar * wp_spa_json_get_data (const WpSpaJson *self);

WP_API
size_t wp_spa_json_get_size (const WpSpaJson *self);

WP_API
gchar * wp_spa_json_to_string (const WpSpaJson *self);

WP_API
WpSpaJson *wp_spa_json_copy (WpSpaJson *other);

WP_API
gboolean wp_spa_json_is_unique_owner (WpSpaJson *self);

WP_API
WpSpaJson *wp_spa_json_ensure_unique_owner (WpSpaJson *self);

WP_API
WpSpaJson *wp_spa_json_new_null (void);

WP_API
WpSpaJson *wp_spa_json_new_boolean (gboolean value);

WP_API
WpSpaJson *wp_spa_json_new_int (gint value);

WP_API
WpSpaJson *wp_spa_json_new_float (float value);

WP_API
WpSpaJson *wp_spa_json_new_string (const gchar *value);

WP_API
WpSpaJson *wp_spa_json_new_array (const gchar *format, ...)
    G_GNUC_NULL_TERMINATED;

WP_API
WpSpaJson *wp_spa_json_new_array_valist (const gchar *format, va_list args);

WP_API
WpSpaJson *wp_spa_json_new_object (const gchar *key, const gchar *format, ...)
    G_GNUC_NULL_TERMINATED;

WP_API
WpSpaJson *wp_spa_json_new_object_valist (const gchar *key, const gchar *format,
    va_list args);

WP_API
gboolean wp_spa_json_is_null (WpSpaJson *self);

WP_API
gboolean wp_spa_json_is_boolean (WpSpaJson *self);

WP_API
gboolean wp_spa_json_is_int (WpSpaJson *self);

WP_API
gboolean wp_spa_json_is_float (WpSpaJson *self);

WP_API
gboolean wp_spa_json_is_string (WpSpaJson *self);

WP_API
gboolean wp_spa_json_is_array (WpSpaJson *self);

WP_API
gboolean wp_spa_json_is_container (WpSpaJson *self);

WP_API
gboolean wp_spa_json_is_object (WpSpaJson *self);

WP_API
gboolean wp_spa_json_parse_boolean (WpSpaJson *self, gboolean *value);

WP_API
gboolean wp_spa_json_parse_int (WpSpaJson *self, gint *value);

WP_API
gboolean wp_spa_json_parse_float (WpSpaJson *self, float *value);

WP_API
gchar *wp_spa_json_parse_string (WpSpaJson *self);

WP_API
gboolean wp_spa_json_parse_array (WpSpaJson *self, ...) G_GNUC_NULL_TERMINATED;

WP_API
gboolean wp_spa_json_parse_array_valist (WpSpaJson *self, va_list args);

WP_API
gboolean wp_spa_json_parse_object (WpSpaJson *self, ...) G_GNUC_NULL_TERMINATED;

WP_API
gboolean wp_spa_json_parse_object_valist (WpSpaJson *self, va_list args);

WP_API
gboolean wp_spa_json_object_get (WpSpaJson *self, ...) G_GNUC_NULL_TERMINATED;

WP_API
gboolean wp_spa_json_object_get_valist (WpSpaJson *self, va_list args);

WP_API
WpIterator *wp_spa_json_new_iterator (WpSpaJson *self);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSpaJson, wp_spa_json_unref)

/*!
 * \brief The WpSpaJsonBuilder GType
 * \ingroup wpspajson
 */
#define WP_TYPE_SPA_JSON_BUILDER (wp_spa_json_builder_get_type ())
WP_API
GType wp_spa_json_builder_get_type (void);

typedef struct _WpSpaJsonBuilder WpSpaJsonBuilder;

WP_API
WpSpaJsonBuilder *wp_spa_json_builder_ref (WpSpaJsonBuilder *self);

WP_API
void wp_spa_json_builder_unref (WpSpaJsonBuilder *self);

WP_API
WpSpaJsonBuilder *wp_spa_json_builder_new_array (void);

WP_API
WpSpaJsonBuilder *wp_spa_json_builder_new_object (void);

WP_API
void wp_spa_json_builder_add_property (WpSpaJsonBuilder *self, const gchar *key);

WP_API
void wp_spa_json_builder_add_null (WpSpaJsonBuilder *self);

WP_API
void wp_spa_json_builder_add_boolean (WpSpaJsonBuilder *self, gboolean value);

WP_API
void wp_spa_json_builder_add_int (WpSpaJsonBuilder *self, gint value);

WP_API
void wp_spa_json_builder_add_float (WpSpaJsonBuilder *self, float value);

WP_API
void wp_spa_json_builder_add_string (WpSpaJsonBuilder *self, const gchar *value);

WP_API
void wp_spa_json_builder_add_json (WpSpaJsonBuilder *self, WpSpaJson *json);

WP_API
void wp_spa_json_builder_add_from_string (WpSpaJsonBuilder *self,
    const gchar *json_str);

WP_API
void wp_spa_json_builder_add_from_stringn (WpSpaJsonBuilder *self,
    const gchar *json_str, size_t len);

WP_API
void wp_spa_json_builder_add (WpSpaJsonBuilder *self, ...)
    G_GNUC_NULL_TERMINATED;

WP_API
void wp_spa_json_builder_add_valist (WpSpaJsonBuilder *self, va_list args);

WP_API
WpSpaJson *wp_spa_json_builder_end (WpSpaJsonBuilder *self);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSpaJsonBuilder, wp_spa_json_builder_unref)

/*!
 * \brief The WpSpaJsonParser GType
 * \ingroup wpspajson
 */
#define WP_TYPE_SPA_JSON_PARSER (wp_spa_json_parser_get_type ())
WP_API
GType wp_spa_json_parser_get_type (void);

typedef struct _WpSpaJsonParser WpSpaJsonParser;

WP_API
WpSpaJsonParser *wp_spa_json_parser_ref (WpSpaJsonParser *self);

WP_API
void wp_spa_json_parser_unref (WpSpaJsonParser *self);

WP_API
WpSpaJsonParser *wp_spa_json_parser_new_array (WpSpaJson *json);

WP_API
WpSpaJsonParser *wp_spa_json_parser_new_object (WpSpaJson *json);

WP_API
WpSpaJsonParser *wp_spa_json_parser_new_undefined (WpSpaJson *json);

WP_API
gboolean wp_spa_json_parser_get_null (WpSpaJsonParser *self);

WP_API
gboolean wp_spa_json_parser_get_boolean (WpSpaJsonParser *self,
    gboolean *value);

WP_API
gboolean wp_spa_json_parser_get_int (WpSpaJsonParser *self, gint *value);

WP_API
gboolean wp_spa_json_parser_get_float (WpSpaJsonParser *self, float *value);

WP_API
gchar *wp_spa_json_parser_get_string (WpSpaJsonParser *self);

WP_API
WpSpaJson *wp_spa_json_parser_get_json (WpSpaJsonParser *self);

WP_API
gboolean wp_spa_json_parser_get (WpSpaJsonParser *self, ...)
    G_GNUC_NULL_TERMINATED;

WP_API
gboolean wp_spa_json_parser_get_valist (WpSpaJsonParser *self, va_list args);

WP_API
void wp_spa_json_parser_end (WpSpaJsonParser *self);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSpaJsonParser, wp_spa_json_parser_unref)

G_END_DECLS

#endif
 070701000000C0000081A400000000000000000000000165F8630400014FDC000000000000000000000000000000000000002300000000wireplumber-0.5.0/lib/wp/spa-pod.c    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "spa-pod.h"
#include "spa-type.h"
#include "log.h"

#include <spa/utils/type-info.h>
#include <spa/pod/builder.h>
#include <spa/pod/parser.h>
#include <spa/pod/filter.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-spa-pod")

#define WP_SPA_POD_BUILDER_REALLOC_STEP_SIZE 64
#define WP_SPA_POD_ID_PROPERTY_NAME_MAX 16

/*! \defgroup wpspapod WpSpaPod */
/*!
 * \struct WpSpaPod
 */
/*!
 * \struct WpSpaPodBuilder
 */
/*!
 * \struct WpSpaPodParser
 */

enum {
  FLAG_NO_OWNERSHIP = (1 << 0),
  FLAG_CONSTANT = (1 << 1)
};

typedef enum {
  WP_SPA_POD_REGULAR = 0,
  WP_SPA_POD_PROPERTY,
  WP_SPA_POD_CONTROL,
} WpSpaPodType;

struct _WpSpaPod
{
  grefcount ref;
  guint32 flags;

  /* The pipewire spa pod API does not have a type for Property and Control,
   * so we create our own and separate them with their data from the regular
   * spa pod types */
  WpSpaPodType type;

  /* Pod */
  union {
    struct spa_pod pod_none;
    struct spa_pod_bool pod_bool;
    struct spa_pod_id pod_id;
    struct spa_pod_int pod_int;
    struct spa_pod_long pod_long;
    struct spa_pod_float pod_float;
    struct spa_pod_double pod_double;
    struct spa_pod_pointer pod_pointer;
    struct spa_pod_fd pod_fd;
    struct spa_pod_rectangle pod_rectangle;
    struct spa_pod_fraction pod_fraction;
    struct wp_property_data {
      WpSpaIdTable table;
      guint32 key;
      guint32 flags;
      gchar id_key_name[WP_SPA_POD_ID_PROPERTY_NAME_MAX];
    } data_property;         /* Only used for property pods */
    struct wp_control_data {
      guint32 offset;
      enum spa_control_type type;
    } data_control;          /* Only used for control pods */
  } static_pod;              /* Only used for statically allocated pods */
  WpSpaPodBuilder *builder;  /* Only used for dynamically allocated pods */
  struct spa_pod *pod;
};

G_DEFINE_BOXED_TYPE (WpSpaPod, wp_spa_pod, wp_spa_pod_ref, wp_spa_pod_unref)

struct _WpSpaPodBuilder
{
  struct spa_pod_builder builder;
  struct spa_pod_frame frame;
  WpSpaType type;
  size_t size;
  guint8 *buf;
};

G_DEFINE_BOXED_TYPE (WpSpaPodBuilder, wp_spa_pod_builder,
    wp_spa_pod_builder_ref, wp_spa_pod_builder_unref)

struct _WpSpaPodParser
{
  struct spa_pod_parser parser;
  struct spa_pod_frame frame;
  WpSpaType type;
  WpSpaPod *pod;
};

G_DEFINE_BOXED_TYPE (WpSpaPodParser, wp_spa_pod_parser,
    wp_spa_pod_parser_ref, wp_spa_pod_parser_unref)

static int
wp_spa_pod_builder_overflow (gpointer data, uint32_t size)
{
  WpSpaPodBuilder *self = data;
  const uint32_t next_size = self->size + WP_SPA_POD_BUILDER_REALLOC_STEP_SIZE;
  const uint32_t new_size = size > next_size ? size : next_size;
  self->buf = g_realloc (self->buf, new_size);
  self->builder.data = self->buf;
  self->builder.size = new_size;
  self->size = new_size;
  return 0;
}

static const struct spa_pod_builder_callbacks builder_callbacks = {
  SPA_VERSION_POD_BUILDER_CALLBACKS,
  .overflow = wp_spa_pod_builder_overflow
};

static WpSpaPodBuilder *
wp_spa_pod_builder_new (size_t size, WpSpaType type)
{
  WpSpaPodBuilder *self = g_rc_box_new0 (WpSpaPodBuilder);
  self->size = size;
  self->buf = g_new0 (guint8, self->size);
  self->builder = SPA_POD_BUILDER_INIT (self->buf, self->size);
  self->type = type;

  spa_pod_builder_set_callbacks (&self->builder, &builder_callbacks, self);

  return self;
}

/*!
 * \brief Increases the reference count of a spa pod object
 * \ingroup wpspapod
 * \param self a spa pod object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSpaPod *
wp_spa_pod_ref (WpSpaPod *self)
{
  g_ref_count_inc (&self->ref);
  return self;
}

static void
wp_spa_pod_free (WpSpaPod *self)
{
  g_clear_pointer (&self->builder, wp_spa_pod_builder_unref);
  self->pod = NULL;
  g_slice_free (WpSpaPod, self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 * \ingroup wpspapod
 * \param self (transfer full): a spa pod object
 */
void
wp_spa_pod_unref (WpSpaPod *self)
{
  if (g_ref_count_dec (&self->ref))
    wp_spa_pod_free (self);
}

static WpSpaPod *
wp_spa_pod_new (const struct spa_pod *pod, WpSpaPodType type, guint32 flags)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->flags = flags;
  self->type = type;

  /* Copy the reference if no ownership, otherwise copy the pod */
  if (self->flags & FLAG_NO_OWNERSHIP) {
    self->pod = (struct spa_pod *)pod;
  } else {
    self->builder = wp_spa_pod_builder_new (
      SPA_ROUND_UP_N (sizeof (*pod) + pod->size, 8), pod->type);
    self->pod = self->builder->builder.data;
    spa_pod_builder_primitive (&self->builder->builder, pod);
  }

  /* Set the prop table if it is an object */
  if (pod->type == SPA_TYPE_Object) {
    self->static_pod.data_property.table =
        wp_spa_type_get_values_table (((struct spa_pod_object *) pod)->body.type);
  }

  return self;
}

/*!
 * \brief Constructs a new WpSpaPod that wraps the given `spa_pod`.
 *
 * \ingroup wpspapod
 * \param pod a spa_pod
 * \returns a new WpSpaPod that references the data in \a pod. \a pod is not
 *   copied, so it needs to stay alive. The returned WpSpaPod can be modified
 *   by using the setter functions, in which case \a pod will be modified
 *   underneath.
 */
WpSpaPod *
wp_spa_pod_new_wrap (struct spa_pod *pod)
{
  return wp_spa_pod_new (pod, WP_SPA_POD_REGULAR, FLAG_NO_OWNERSHIP);
}

/*!
 * \brief Constructs a new immutable WpSpaPod that wraps the given `spa_pod`.
 *
 * \ingroup wpspapod
 * \param pod a constant spa_pod
 * \returns a new WpSpaPod that references the data in \a pod. \a pod is not
 *   copied, so it needs to stay alive. The returned WpSpaPod cannot be
 *   modified, unless it's copied first.
 */
WpSpaPod *
wp_spa_pod_new_wrap_const (const struct spa_pod *pod)
{
  return wp_spa_pod_new (pod, WP_SPA_POD_REGULAR,
      FLAG_NO_OWNERSHIP | FLAG_CONSTANT);
}

static WpSpaPod *
wp_spa_pod_new_property_wrap (WpSpaIdTable table, guint32 key, guint32 flags,
    struct spa_pod *pod)
{
  WpSpaPod *self = wp_spa_pod_new (pod, WP_SPA_POD_PROPERTY, FLAG_NO_OWNERSHIP);
  self->static_pod.data_property.table = table;
  self->static_pod.data_property.key = key;
  self->static_pod.data_property.flags = flags;
  return self;
}

static WpSpaPod *
wp_spa_pod_new_control_wrap (guint32 offset, enum spa_control_type type,
    struct spa_pod *pod)
{
  WpSpaPod *self = wp_spa_pod_new (pod, WP_SPA_POD_CONTROL, FLAG_NO_OWNERSHIP);
  self->static_pod.data_control.offset = offset;
  self->static_pod.data_control.type = type;
  return self;
}

#if 0
/* there is no use for these _const variants, but let's keep them just in case */

static WpSpaPod *
wp_spa_pod_new_property_wrap_const (WpSpaIdTable table, guint32 key,
    guint32 flags, const struct spa_pod *pod)
{
  WpSpaPod *self = wp_spa_pod_new (pod, WP_SPA_POD_PROPERTY,
      FLAG_NO_OWNERSHIP | FLAG_CONSTANT);
  self->static_pod.data_property.table = table;
  self->static_pod.data_property.key = key;
  self->static_pod.data_property.flags = flags;
  return self;
}

static WpSpaPod *
wp_spa_pod_new_control_wrap_const (guint32 offset, enum spa_control_type type,
    const struct spa_pod *pod)
{
  WpSpaPod *self = wp_spa_pod_new (pod, WP_SPA_POD_CONTROL,
      FLAG_NO_OWNERSHIP | FLAG_CONSTANT);
  self->static_pod.data_control.offset = offset;
  self->static_pod.data_control.type = type;
  return self;
}
#endif

static WpSpaPod *
wp_spa_pod_new_wrap_copy (const struct spa_pod *pod)
{
  return wp_spa_pod_new (pod, WP_SPA_POD_REGULAR, 0);
}

static WpSpaPod *
wp_spa_pod_new_property_wrap_copy (WpSpaIdTable table, guint32 key,
    guint32 flags, const struct spa_pod *pod)
{
  WpSpaPod *self = wp_spa_pod_new (pod, WP_SPA_POD_PROPERTY, 0);
  self->static_pod.data_property.table = table;
  self->static_pod.data_property.key = key;
  self->static_pod.data_property.flags = flags;
  return self;
}

static WpSpaPod *
wp_spa_pod_new_control_wrap_copy (guint32 offset, enum spa_control_type type,
    const struct spa_pod *pod)
{
  WpSpaPod *self = wp_spa_pod_new (pod, WP_SPA_POD_CONTROL, 0);
  self->static_pod.data_control.offset = offset;
  self->static_pod.data_control.type = type;
  return self;
}

/*!
 * \brief Converts a WpSpaPod pointer to a `struct spa_pod` one, for use with
 * native pipewire & spa functions. The returned pointer is owned by WpSpaPod
 * and may not be modified or freed.
 *
 * \ingroup wpspapod
 * \param self a spa pod object
 * \returns a const pointer to the underlying spa_pod structure
 */
const struct spa_pod *
wp_spa_pod_get_spa_pod (const WpSpaPod *self)
{
  return self->pod;
}

/*!
 * \brief Gets the SPA type of the spa pod.
 *
 * If the pod is an object or pointer, this will return the derived
 * object/pointer type directly.
 * If the pod is an object property or a control, this will return the type
 * of the contained value.
 *
 * \ingroup wpspapod
 * \param self a spa pod
 * \returns (transfer none): the type of the spa pod
 */
WpSpaType
wp_spa_pod_get_spa_type (WpSpaPod *self)
{
  g_return_val_if_fail (self != NULL, WP_SPA_TYPE_INVALID);

  if (wp_spa_pod_is_object (self) || wp_spa_pod_is_pointer (self))
    return SPA_POD_OBJECT_TYPE (self->pod);
  else
    return SPA_POD_TYPE (self->pod);
}

/*!
 * \brief If the pod is a Choice, this gets the choice type
 * (Range, Step, Enum, ...)
 *
 * \ingroup wpspapod
 * \param self a choice pod
 * \returns the choice type of the choice pod
 */
WpSpaIdValue
wp_spa_pod_get_choice_type (WpSpaPod *self)
{
  g_return_val_if_fail (wp_spa_pod_is_choice (self), NULL);
  return wp_spa_id_value_from_number (
      SPA_TYPE_INFO_Choice, SPA_POD_CHOICE_TYPE (self->pod));
}

/*!
 * \brief Copies a spa pod object
 *
 * \ingroup wpspapod
 * \param other a spa pod object
 * \returns (transfer full): The newly copied spa pod
 */
WpSpaPod *
wp_spa_pod_copy (WpSpaPod *other)
{
  g_return_val_if_fail (other, NULL);
  switch (other->type) {
  case WP_SPA_POD_PROPERTY:
    return wp_spa_pod_new_property_wrap_copy (
        other->static_pod.data_property.table,
        other->static_pod.data_property.key,
        other->static_pod.data_property.flags, other->pod);
  case WP_SPA_POD_CONTROL:
    return wp_spa_pod_new_control_wrap_copy (
        other->static_pod.data_control.offset,
        other->static_pod.data_control.type, other->pod);
  case WP_SPA_POD_REGULAR:
  default:
    break;
  }
  return wp_spa_pod_new_wrap_copy (other->pod);
}

/*!
 * \brief Checks if the pod is the unique owner of its data or not
 *
 * \ingroup wpspapod
 * \param self a spa pod object
 * \returns TRUE if the pod owns the data, FALSE otherwise.
 */
gboolean
wp_spa_pod_is_unique_owner (WpSpaPod *self)
{
  return g_ref_count_compare (&self->ref, 1) &&
      !(self->flags & FLAG_NO_OWNERSHIP);
}

/*!
 * \brief If \a self is not uniquely owned already, then it is unrefed and a
 * copy of it is returned instead. You should always consider \a self as unsafe
 * to use after this call and you should use the returned object instead.
 *
 * \ingroup wpspapod
 * \param self (transfer full): a spa pod object
 * \returns (transfer full): the uniquely owned spa pod object which may or may
 * not be the same as \a self.
 */
WpSpaPod *
wp_spa_pod_ensure_unique_owner (WpSpaPod *self)
{
  WpSpaPod *copy = NULL;

  if (wp_spa_pod_is_unique_owner (self))
    return self;

  copy = wp_spa_pod_copy (self);
  wp_spa_pod_unref (self);
  return copy;
}

/*!
 * \brief Creates a spa pod of type None
 * \ingroup wpspapod
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_none (void)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_none = SPA_POD_INIT_None();
  self->pod = &self->static_pod.pod_none;
  return self;
}

/*!
 * \brief Creates a spa pod of type boolean
 *
 * \ingroup wpspapod
 * \param value the boolean value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_boolean (gboolean value)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_bool = SPA_POD_INIT_Bool (value ? true : false);
  self->pod = &self->static_pod.pod_bool.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type Id
 *
 * \ingroup wpspapod
 * \param value the Id value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_id (guint32 value)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_id = SPA_POD_INIT_Id (value);
  self->pod = &self->static_pod.pod_id.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type int
 *
 * \ingroup wpspapod
 * \param value the int value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_int (gint32 value)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_int = SPA_POD_INIT_Int (value);
  self->pod = &self->static_pod.pod_int.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type long
 *
 * \ingroup wpspapod
 * \param value the long value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_long (gint64 value)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_long = SPA_POD_INIT_Long (value);
  self->pod = &self->static_pod.pod_long.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type float
 *
 * \ingroup wpspapod
 * \param value the float value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_float (float value)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_float = SPA_POD_INIT_Float (value);
  self->pod = &self->static_pod.pod_float.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type double
 *
 * \ingroup wpspapod
 * \param value the double value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_double (double value)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_double = SPA_POD_INIT_Double (value);
  self->pod = &self->static_pod.pod_double.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type string
 *
 * \ingroup wpspapod
 * \param value the string value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_string (const char *value)
{
  const uint32_t len = value ? strlen (value) : 0;
  const char *str = value ? value : "";
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;

  struct spa_pod_string p = SPA_POD_INIT_String (len + 1);
  self->builder = wp_spa_pod_builder_new (
      SPA_ROUND_UP_N (sizeof (p) + len + 1, 8), SPA_TYPE_String);

  self->pod = self->builder->builder.data;

  spa_pod_builder_raw (&self->builder->builder, &p, sizeof(p));
  spa_pod_builder_write_string (&self->builder->builder, str, len);
  return self;
}

/*!
 * \brief Creates a spa pod of type bytes
 *
 * \ingroup wpspapod
 * \param value the bytes value
 * \param len the length of the bytes value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_bytes (gconstpointer value, guint32 len)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  const struct spa_pod_bytes p = SPA_POD_INIT_Bytes (len);
  self->builder = wp_spa_pod_builder_new (
      SPA_ROUND_UP_N (sizeof (p) + p.pod.size, 8),
      SPA_TYPE_Bytes);

  self->pod = self->builder->builder.data;

  spa_pod_builder_raw (&self->builder->builder, &p, sizeof(p));
  spa_pod_builder_raw_padded (&self->builder->builder, value, len);
  return self;
}

/*!
 * \brief Creates a spa pod of type pointer
 *
 * \ingroup wpspapod
 * \param type_name the name of the type of the pointer
 * \param value the pointer value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_pointer (const char *type_name, gconstpointer value)
{
  WpSpaType type = wp_spa_type_from_name (type_name);
  g_return_val_if_fail (type != WP_SPA_TYPE_INVALID, NULL);

  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_pointer = SPA_POD_INIT_Pointer (type, value);
  self->pod = &self->static_pod.pod_pointer.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type Fd
 *
 * \ingroup wpspapod
 * \param value the Fd value
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_fd (gint64 value)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_fd = SPA_POD_INIT_Fd (value);
  self->pod = &self->static_pod.pod_fd.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type rectangle
 *
 * \ingroup wpspapod
 * \param width the width value of the rectangle
 * \param height the height value of the rectangle
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_rectangle (guint32 width, guint32 height)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_rectangle =
      SPA_POD_INIT_Rectangle (SPA_RECTANGLE (width, height));
  self->pod = &self->static_pod.pod_rectangle.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type fraction
 *
 * \ingroup wpspapod
 * \param num the numerator value of the fraction
 * \param denom the denominator value of the fraction
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_fraction (guint32 num, guint32 denom)
{
  WpSpaPod *self = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&self->ref);
  self->type = WP_SPA_POD_REGULAR;
  self->static_pod.pod_fraction =
      SPA_POD_INIT_Fraction (SPA_FRACTION (num, denom));
  self->pod = &self->static_pod.pod_fraction.pod;
  return self;
}

/*!
 * \brief Creates a spa pod of type choice
 *
 * \ingroup wpspapod
 * \param choice_type the name of the choice type ("Range", "Step", ...),
 * \param ... a list of choice values, followed by NULL
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_choice (const char *choice_type, ...)
{
  WpSpaPod *self;
  va_list args;

  va_start (args, choice_type);
  self = wp_spa_pod_new_choice_valist (choice_type, args);
  va_end (args);

  return self;
}

/*!
 * \brief This is the `va_list` version of wp_spa_pod_new_choice()
 *
 * \ingroup wpspapod
 * \param choice_type the name of the choice type ("Range", "Step", ...)
 * \param args the variable arguments passed to wp_spa_pod_new_choice()
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_choice_valist (const char *choice_type, va_list args)
{
  g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_choice (choice_type);
  wp_spa_pod_builder_add_valist (b, args);
  return wp_spa_pod_builder_end (b);
}

/*!
 * \brief Creates a spa pod of type object
 *
 * \ingroup wpspapod
 * \param type_name the type name of the object type
 * \param id_name the id name of the object,
 * \param ... a list of object properties with their values, followed by NULL
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_object (const char *type_name, const char *id_name, ...)
{
  WpSpaPod *self;
  va_list args;

  va_start (args, id_name);
  self = wp_spa_pod_new_object_valist (type_name, id_name, args);
  va_end (args);

  return self;
}

/*!
 * \brief This is the `va_list` version of wp_spa_pod_new_object()
 *
 * \ingroup wpspapod
 * \param type_name the type name of the object type
 * \param id_name the id name of the object
 * \param args the variable arguments passed to wp_spa_pod_new_object()
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_object_valist (const char *type_name, const char *id_name,
    va_list args)
{
  g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_object (type_name,
      id_name);
  wp_spa_pod_builder_add_valist (b, args);
  return wp_spa_pod_builder_end (b);
}

/*!
 * \brief Creates a spa pod of type sequence
 *
 * \ingroup wpspapod
 * \param unit the unit of the sequence
 * \param ... a list of sequence controls with their values, followed by NULL
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_sequence (guint unit, ...)
{
  WpSpaPod *self;
  va_list args;

  va_start(args, unit);
  self = wp_spa_pod_new_sequence_valist (unit, args);
  va_end(args);

  return self;
}

/*!
 * \brief This is the `va_list` version of wp_spa_pod_new_sequence()
 *
 * \ingroup wpspapod
 * \param unit the unit of the sequence
 * \param args the variable arguments passed to wp_spa_pod_new_sequence()
 * \returns (transfer full): The new spa pod
 */
WpSpaPod *
wp_spa_pod_new_sequence_valist (guint unit, va_list args)
{
  g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_sequence (unit);
  wp_spa_pod_builder_add_valist (b, args);
  return wp_spa_pod_builder_end (b);
}

/*!
 * \brief Checks wether the spa pod is of type none or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type none, FALSE otherwise
 */
gboolean
wp_spa_pod_is_none (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_none (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type boolean or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type boolean, FALSE otherwise
 */
gboolean
wp_spa_pod_is_boolean (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_bool (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type Id or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type Id, FALSE otherwise
 */
gboolean
wp_spa_pod_is_id (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_id (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type int or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type int, FALSE otherwise
 */
gboolean
wp_spa_pod_is_int (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_int (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type long or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type long, FALSE otherwise
 */
gboolean
wp_spa_pod_is_long (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_long (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type float or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type float, FALSE otherwise
 */
gboolean
wp_spa_pod_is_float (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_float (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type double or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type double, FALSE otherwise
 */
gboolean
wp_spa_pod_is_double (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_double (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type string or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type string, FALSE otherwise
 */
gboolean
wp_spa_pod_is_string (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_string (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type bytes or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type bytes, FALSE otherwise
 */
gboolean
wp_spa_pod_is_bytes (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_bytes (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type pointer or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type pointer, FALSE otherwise
 */
gboolean
wp_spa_pod_is_pointer (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_pointer (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type Fd or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type Fd, FALSE otherwise
 */
gboolean
wp_spa_pod_is_fd (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_fd (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type rectangle or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type rectangle, FALSE otherwise
 */
gboolean
wp_spa_pod_is_rectangle (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_rectangle (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type fraction or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type fraction, FALSE otherwise
 */
gboolean
wp_spa_pod_is_fraction (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_fraction (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type array or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type array, FALSE otherwise
 */
gboolean
wp_spa_pod_is_array (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_array (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type choice or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type choice, FALSE otherwise
 */
gboolean
wp_spa_pod_is_choice (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_choice (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type object or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type object, FALSE otherwise
 */
gboolean
wp_spa_pod_is_object (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_object (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type struct or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type struct, FALSE otherwise
 */
gboolean
wp_spa_pod_is_struct (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_struct (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type sequence or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type sequence, FALSE otherwise
 */
gboolean
wp_spa_pod_is_sequence (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_REGULAR && spa_pod_is_sequence (self->pod);
}

/*!
 * \brief Checks wether the spa pod is of type property or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type property, FALSE otherwise
 */
gboolean
wp_spa_pod_is_property (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_PROPERTY;
}

/*!
 * \brief Checks wether the spa pod is of type control or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \returns TRUE if it is of type control, FALSE otherwise
 */
gboolean
wp_spa_pod_is_control (WpSpaPod *self)
{
  return self->type == WP_SPA_POD_CONTROL;
}

/*!
 * \brief Gets the boolean value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the boolean value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_boolean (WpSpaPod *self, gboolean *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);
  bool v = FALSE;
  const int res = spa_pod_get_bool (self->pod, &v);
  *value = v ? TRUE : FALSE;
  return res >= 0;
}

/*!
 * \brief Gets the Id value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the Id value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_id (WpSpaPod *self, guint32 *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);
  uint32_t v = 0;
  const int res = spa_pod_get_id (self->pod, &v);
  *value = v;
  return res >= 0;
}

/*!
 * \brief Gets the int value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the int value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_int (WpSpaPod *self, gint32 *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);
  return spa_pod_get_int (self->pod, value) >= 0;
}

/*!
 * \brief Gets the long value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the long value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_long (WpSpaPod *self, gint64 *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);
  return spa_pod_get_long (self->pod, value) >= 0;
}

/*!
 * \brief Gets the float value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the float value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_float (WpSpaPod *self, float *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);
  return spa_pod_get_float (self->pod, value) >= 0;
}

/*!
 * \brief Gets the double value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the double value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_double (WpSpaPod *self, double *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);
  return spa_pod_get_double (self->pod, value) >= 0;
}

/*!
 * \brief Gets the string value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the string value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_string (WpSpaPod *self, const char **value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);
  return spa_pod_get_string (self->pod, value) >= 0;
}

/*!
 * \brief Gets the bytes value and its len of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the bytes value
 * \param len (out): the length of the bytes value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_bytes (WpSpaPod *self, gconstpointer *value, guint32 *len)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);
  g_return_val_if_fail (len, FALSE);
  return spa_pod_get_bytes (self->pod, value, len) >= 0;
}

/*!
 * \brief Gets the pointer value and its type name of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the pointer value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_pointer (WpSpaPod *self, gconstpointer *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);

  guint32 type = 0;
  return spa_pod_get_pointer (self->pod, &type, value) >= 0;
}

/*!
 * \brief Gets the Fd value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value (out): the Fd value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_fd (WpSpaPod *self, gint64 *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);
  return spa_pod_get_fd (self->pod, value) >= 0;
}

/*!
 * \brief Gets the rectangle's width and height value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param width (out): the rectangle's width value
 * \param height (out): the rectangle's height value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_rectangle (WpSpaPod *self, guint32 *width, guint32 *height)
{
  g_return_val_if_fail (self, FALSE);
  struct spa_rectangle rectangle = { 0, };
  const gboolean res = spa_pod_get_rectangle (self->pod, &rectangle) >= 0;
  if (width)
    *width = rectangle.width;
  if (height)
    *height = rectangle.height;
  return res;
}

/*!
 * \brief Gets the fractions's numerator and denominator value of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param num (out): the fractions's numerator value
 * \param denom (out): the fractions's denominator value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_fraction (WpSpaPod *self, guint32 *num, guint32 *denom)
{
  g_return_val_if_fail (self, FALSE);
  struct spa_fraction fraction = { 0, };
  const gboolean res = spa_pod_get_fraction (self->pod, &fraction) >= 0;
  if (num)
    *num = fraction.num;
  if (denom)
    *denom = fraction.denom;
  return res;
}

/*!
 * \brief Sets a boolean value in the spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value the boolean value
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_boolean (WpSpaPod *self, gboolean value)
{
  g_return_val_if_fail (wp_spa_pod_is_boolean (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  ((struct spa_pod_bool *)self->pod)->value = value ? true : false;
  return TRUE;
}

/*!
 * \brief Sets an Id value in the spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value the Id value
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_id (WpSpaPod *self, guint32 value)
{
  g_return_val_if_fail (wp_spa_pod_is_id (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  ((struct spa_pod_id *)self->pod)->value = value;
  return TRUE;
}

/*!
 * \brief Sets an int value in the spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value the int value
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_int (WpSpaPod *self, gint32 value)
{
  g_return_val_if_fail (wp_spa_pod_is_int (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  ((struct spa_pod_int *)self->pod)->value = value;
  return TRUE;
}

/*!
 * \brief Sets a long value in the spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value the long value
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_long (WpSpaPod *self, gint64 value)
{
  g_return_val_if_fail (wp_spa_pod_is_long (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  ((struct spa_pod_long *)self->pod)->value = value;
  return TRUE;
}

/*!
 * \brief Sets a float value in the spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value the float value
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_float (WpSpaPod *self, float value)
{
  g_return_val_if_fail (wp_spa_pod_is_float (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  ((struct spa_pod_float *)self->pod)->value = value;
  return TRUE;
}

/*!
 * \brief Sets a double value in the spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value the double value
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_double (WpSpaPod *self, double value)
{
  g_return_val_if_fail (wp_spa_pod_is_double (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  ((struct spa_pod_double *)self->pod)->value = value;
  return TRUE;
}

/*!
 * \brief Sets a pointer value with its type name in the spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param type_name the name of the type of the pointer
 * \param value the pointer value
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_pointer (WpSpaPod *self, const char *type_name,
    gconstpointer value)
{
  WpSpaType type = wp_spa_type_from_name (type_name);

  g_return_val_if_fail (wp_spa_pod_is_pointer (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  g_return_val_if_fail (type != WP_SPA_TYPE_INVALID, FALSE);

  ((struct spa_pod_pointer *)self->pod)->body.type = type;
  ((struct spa_pod_pointer *)self->pod)->body.value = value;
  return TRUE;
}

/*!
 * \brief Sets a Fd value in the spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param value the Fd value
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_fd (WpSpaPod *self, gint64 value)
{
  g_return_val_if_fail (wp_spa_pod_is_fd (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  ((struct spa_pod_fd *)self->pod)->value = value;
  return TRUE;
}

/*!
 * \brief Sets the width and height values of a rectangle in the spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param width the width value of the rectangle
 * \param height the height value of the rectangle
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_rectangle (WpSpaPod *self, guint32 width, guint32 height)
{
  g_return_val_if_fail (wp_spa_pod_is_rectangle (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  ((struct spa_pod_rectangle *)self->pod)->value.width = width;
  ((struct spa_pod_rectangle *)self->pod)->value.height = height;
  return TRUE;
}

/*!
 * \brief Sets the numerator and denominator values of a fraction in the
 * spa pod object.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param num the numerator value of the farction
 * \param denom the denominator value of the fraction
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_fraction (WpSpaPod *self, guint32 num, guint32 denom)
{
  g_return_val_if_fail (wp_spa_pod_is_fraction (self), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);
  ((struct spa_pod_fraction *)self->pod)->value.num = num;
  ((struct spa_pod_fraction *)self->pod)->value.denom = denom;
  return TRUE;
}

/*!
 * \brief Sets the value of a spa pod object in the current spa pod object.
 * The spa pod objects must be of the same value.
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param pod the pod with the value to be set
 * \returns TRUE if the value could be set, FALSE othewrise.
 */
gboolean
wp_spa_pod_set_pod (WpSpaPod *self, WpSpaPod *pod)
{
  g_return_val_if_fail (self->type == pod->type, FALSE);
  g_return_val_if_fail (SPA_POD_TYPE (self->pod) == SPA_POD_TYPE (pod->pod), FALSE);
  g_return_val_if_fail (!(self->flags & FLAG_CONSTANT), FALSE);

  switch (SPA_POD_TYPE (self->pod)) {
  case SPA_TYPE_None:
    break;
  case SPA_TYPE_Bool:
    ((struct spa_pod_bool *)self->pod)->value = ((struct spa_pod_bool *)pod->pod)->value;
    break;
  case SPA_TYPE_Id:
    ((struct spa_pod_id *)self->pod)->value = ((struct spa_pod_id *)pod->pod)->value;
    break;
  case SPA_TYPE_Int:
    ((struct spa_pod_int *)self->pod)->value = ((struct spa_pod_int *)pod->pod)->value;
    break;
  case SPA_TYPE_Long:
    ((struct spa_pod_long *)self->pod)->value = ((struct spa_pod_long *)pod->pod)->value;
    break;
  case SPA_TYPE_Float:
    ((struct spa_pod_float *)self->pod)->value = ((struct spa_pod_float *)pod->pod)->value;
    break;
  case SPA_TYPE_Double:
    ((struct spa_pod_double *)self->pod)->value = ((struct spa_pod_double *)pod->pod)->value;
    break;
  case SPA_TYPE_Pointer:
    ((struct spa_pod_pointer *)self->pod)->body.type = ((struct spa_pod_pointer *)pod->pod)->body.type;
    ((struct spa_pod_pointer *)self->pod)->body.value = ((struct spa_pod_pointer *)pod->pod)->body.value;
    break;
  case SPA_TYPE_Fd:
    ((struct spa_pod_fd *)self->pod)->value = ((struct spa_pod_fd *)pod->pod)->value;
    break;
  case SPA_TYPE_Rectangle:
    ((struct spa_pod_rectangle *)self->pod)->value.width = ((struct spa_pod_rectangle *)pod->pod)->value.width;
    ((struct spa_pod_rectangle *)self->pod)->value.height = ((struct spa_pod_rectangle *)pod->pod)->value.height;
    break;
  case SPA_TYPE_Fraction:
    ((struct spa_pod_fraction *)self->pod)->value.num = ((struct spa_pod_fraction *)pod->pod)->value.num;
    ((struct spa_pod_fraction *)self->pod)->value.denom = ((struct spa_pod_fraction *)pod->pod)->value.denom;
    break;
  default:
    g_return_val_if_fail (self->pod->size >= pod->pod->size, FALSE);
    memcpy (SPA_MEMBER (self->pod, sizeof (struct spa_pod), void),
        SPA_MEMBER (pod->pod, sizeof (struct spa_pod), void),
        SPA_MIN (self->pod->size, pod->pod->size));
    self->pod->type = pod->pod->type;
    self->pod->size = pod->pod->size;
    break;
  }

  switch (self->type) {
  case WP_SPA_POD_PROPERTY:
    self->static_pod.data_property.table = pod->static_pod.data_property.table;
    self->static_pod.data_property.key = pod->static_pod.data_property.key;
    self->static_pod.data_property.flags = pod->static_pod.data_property.flags;
    break;
  case WP_SPA_POD_CONTROL:
    self->static_pod.data_control.offset = pod->static_pod.data_control.offset;
    self->static_pod.data_control.type = pod->static_pod.data_control.type;
    break;
  case WP_SPA_POD_REGULAR:
  default:
    break;
  }

  return TRUE;
}

/*!
 * \brief Checks whether two spa pod objects have the same value or not
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param pod the pod with the value to be compared with
 * \returns TRUE if both spa pod objects have the same values, FALSE othewrise.
 */
gboolean
wp_spa_pod_equal (WpSpaPod *self, WpSpaPod *pod)
{
  g_return_val_if_fail (self != NULL, FALSE);
  g_return_val_if_fail (pod != NULL, FALSE);

  if (self->type != pod->type)
    return FALSE;
  if (SPA_POD_TYPE (self->pod) != SPA_POD_TYPE (pod->pod))
    return FALSE;

  switch (SPA_POD_TYPE (self->pod)) {
  case SPA_TYPE_None:
    break;
  case SPA_TYPE_Bool:
    if (((struct spa_pod_bool *)self->pod)->value != ((struct spa_pod_bool *)pod->pod)->value)
      return FALSE;
    break;
  case SPA_TYPE_Id:
    if (((struct spa_pod_id *)self->pod)->value != ((struct spa_pod_id *)pod->pod)->value)
      return FALSE;
    break;
  case SPA_TYPE_Int:
    if (((struct spa_pod_int *)self->pod)->value != ((struct spa_pod_int *)pod->pod)->value)
      return FALSE;
    break;
  case SPA_TYPE_Long:
    if (((struct spa_pod_long *)self->pod)->value != ((struct spa_pod_long *)pod->pod)->value)
      return FALSE;
    break;
  case SPA_TYPE_Float:
    if (((struct spa_pod_float *)self->pod)->value != ((struct spa_pod_float *)pod->pod)->value)
      return FALSE;
    break;
  case SPA_TYPE_Double:
    if (((struct spa_pod_double *)self->pod)->value != ((struct spa_pod_double *)pod->pod)->value)
      return FALSE;
    break;
  case SPA_TYPE_Pointer:
    if (((struct spa_pod_pointer *)self->pod)->body.type != ((struct spa_pod_pointer *)pod->pod)->body.type ||
        ((struct spa_pod_pointer *)self->pod)->body.value != ((struct spa_pod_pointer *)pod->pod)->body.value)
      return FALSE;
    break;
  case SPA_TYPE_Fd:
    if (((struct spa_pod_fd *)self->pod)->value != ((struct spa_pod_fd *)pod->pod)->value)
      return FALSE;
    break;
  case SPA_TYPE_Rectangle:
    if (((struct spa_pod_rectangle *)self->pod)->value.width != ((struct spa_pod_rectangle *)pod->pod)->value.width ||
        ((struct spa_pod_rectangle *)self->pod)->value.height != ((struct spa_pod_rectangle *)pod->pod)->value.height)
      return FALSE;
    break;
  case SPA_TYPE_Fraction:
    if (((struct spa_pod_fraction *)self->pod)->value.num != ((struct spa_pod_fraction *)pod->pod)->value.num ||
        ((struct spa_pod_fraction *)self->pod)->value.denom != ((struct spa_pod_fraction *)pod->pod)->value.denom)
      return FALSE;
    break;
  default:
    if (self->pod->size != pod->pod->size ||
        memcmp (SPA_MEMBER (self->pod, sizeof (struct spa_pod), void),
            SPA_MEMBER (pod->pod, sizeof (struct spa_pod), void),
            self->pod->size) != 0)
      return FALSE;
    break;
  }

  switch (self->type) {
  case WP_SPA_POD_PROPERTY:
    if (self->static_pod.data_property.table != pod->static_pod.data_property.table ||
        self->static_pod.data_property.key != pod->static_pod.data_property.key ||
        self->static_pod.data_property.flags != pod->static_pod.data_property.flags)
      return FALSE;
    break;
  case WP_SPA_POD_CONTROL:
    if (self->static_pod.data_control.offset != pod->static_pod.data_control.offset ||
        self->static_pod.data_control.type != pod->static_pod.data_control.type)
      return FALSE;
    break;
  case WP_SPA_POD_REGULAR:
  default:
    break;
  }

  return TRUE;
}

/*!
 * \brief Gets the object properties values of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param id_name (out): the id name of the object,
 * \param ... (out): the list of the object properties values, followed by NULL
 * \returns TRUE if the object properties values were obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_object (WpSpaPod *self, const char **id_name, ...)
{
  va_list args;
  gboolean res;
  va_start (args, id_name);
  res = wp_spa_pod_get_object_valist (self, id_name, args);
  va_end (args);
  return res;
}

/*!
 * \brief This is the `va_list` version of wp_spa_pod_get_object()
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param id_name (out): the id name of the object
 * \param args (out): the variable arguments passed to wp_spa_pod_get_object()
 * \returns TRUE if the object properties values were obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_object_valist (WpSpaPod *self, const char **id_name, va_list args)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (wp_spa_pod_is_object (self), FALSE);
  g_autoptr (WpSpaPodParser) p = wp_spa_pod_parser_new_object (self, id_name);
  const gboolean res = wp_spa_pod_parser_get_valist (p, args);
  wp_spa_pod_parser_end (p);
  return res;
}

/*!
 * \brief Gets the struct's values of a spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param ... (out): the list of the struct values, followed by NULL
 * \returns TRUE if the struct values were obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_struct (WpSpaPod *self, ...)
{
  va_list args;
  gboolean res;
  va_start (args, self);
  res = wp_spa_pod_get_struct_valist (self, args);
  va_end (args);
  return res;
}

/*!
 * \brief This is the `va_list` version of wp_spa_pod_get_struct()
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param args (out): the variable arguments passed to wp_spa_pod_get_struct()
 * \returns TRUE if the struct values were obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_struct_valist (WpSpaPod *self, va_list args)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (wp_spa_pod_is_struct (self), FALSE);
  g_autoptr (WpSpaPodParser) p = wp_spa_pod_parser_new_struct (self);
  const gboolean res = wp_spa_pod_parser_get_valist (p, args);
  wp_spa_pod_parser_end (p);
  return res;
}

/*!
 * \brief Gets the name, flags and spa pod value of a spa pod property
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param key (out) (optional): the name of the property
 * \param value (out) (optional): the spa pod value of the property
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_property (WpSpaPod *self, const char **key,
    WpSpaPod **value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (wp_spa_pod_is_property (self), FALSE);

  if (key) {
    WpSpaIdValue key_val = wp_spa_id_table_find_value (
        self->static_pod.data_property.table,
        self->static_pod.data_property.key);
    if (key_val) {
      *key = wp_spa_id_value_short_name (key_val);
    } else {
      g_snprintf (self->static_pod.data_property.id_key_name,
          WP_SPA_POD_ID_PROPERTY_NAME_MAX, "id-%08x",
          self->static_pod.data_property.key);
      *key = self->static_pod.data_property.id_key_name;
    }
  }
  if (value)
    *value = wp_spa_pod_new_wrap (self->pod);

  return TRUE;
}

/*!
 * \brief Gets the offset, type name and spa pod value of a spa pod control
 *
 * \ingroup wpspapod
 * \param self the spa pod object
 * \param offset (out) (optional): the offset of the control
 * \param ctl_type (out) (optional): the control type (Properties, Midi, ...)
 * \param value (out) (optional): the spa pod value of the control
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_get_control (WpSpaPod *self, guint32 *offset, const char **ctl_type,
    WpSpaPod **value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (wp_spa_pod_is_control (self), FALSE);

  if (offset)
    *offset = self->static_pod.data_control.offset;
  if (ctl_type) {
    WpSpaIdValue type_val = wp_spa_id_value_from_number (
        SPA_TYPE_INFO_Control, self->static_pod.data_control.type);
    g_return_val_if_fail (type_val != NULL, FALSE);
    *ctl_type = wp_spa_id_value_short_name (type_val);
  }
  if (value)
    *value = wp_spa_pod_new_wrap (self->pod);

  return TRUE;
}

/*!
 * \brief Gets the child of a spa pod choice object
 *
 * \ingroup wpspapod
 * \param self a spa pod choice object
 * \returns (transfer full): the child of the spa pod choice object
 */
WpSpaPod *
wp_spa_pod_get_choice_child (WpSpaPod *self)
{
  g_return_val_if_fail (wp_spa_pod_is_choice (self), NULL);
  return wp_spa_pod_new_wrap (SPA_POD_CHOICE_CHILD (self->pod));
}

/*!
 * \brief Gets the child of a spa pod array object
 *
 * \ingroup wpspapod
 * \param self a spa pod choice object
 * \returns (transfer full): the child of the spa pod array object
 */
WpSpaPod *
wp_spa_pod_get_array_child (WpSpaPod *self)
{
  g_return_val_if_fail (wp_spa_pod_is_array (self), NULL);
  return wp_spa_pod_new_wrap (SPA_POD_ARRAY_CHILD (self->pod));
}

/*!
 * \brief Fixates choices in an object pod so that they only have one value
 *
 * \ingroup wpspapod
 * \param self a spa pod
 * \returns TRUE if the pod was an object and it went through the fixation
 *   procedure, FALSE otherwise
 */
gboolean
wp_spa_pod_fixate (WpSpaPod *self)
{
  g_return_val_if_fail (self, FALSE);

  if (wp_spa_pod_is_object (self))
    return spa_pod_object_fixate ((struct spa_pod_object *) self->pod) == 0;
  return FALSE;
}

/*!
 * \brief Returns the intersection between \a self and \a filter
 *
 * This is typically used to intersect pods that describe formats, in order to
 * find a common format that is accceptable by both sides. For that purpose,
 * this is not exactly an intersection with its mathematical meaning.
 * Object properties can be thought of as format constraints. When one side does
 * not specify a specific property, it is considered to accept any value for it,
 * so the value of this property from the other side is added in the result.
 *
 * Both input pods are left unmodified after this function call.
 *
 * If NULL is passed in the \a filter, this function just copies \a self and
 * returns the copy.
 *
 * \param self the first pod
 * \param filter (nullable): the second pod
 * \return (transfer full) (nullable): a new pod that contains the intersection
 *   between \a self and \a filter, or NULL if the intersection was not possible
 *   to make
 */
WpSpaPod *
wp_spa_pod_filter (WpSpaPod *self, WpSpaPod *filter)
{
  char buffer[1024];
  struct spa_pod_builder b = SPA_POD_BUILDER_INIT(&buffer, sizeof(buffer));
  struct spa_pod *result = NULL;

  g_return_val_if_fail (self, NULL);

  if (spa_pod_filter(&b, &result, self->pod, filter ? filter->pod : NULL) >= 0)
    return wp_spa_pod_new_wrap_copy (result);
  return NULL;
}

/*!
 * \brief Increases the reference count of a spa pod builder
 *
 * \ingroup wpspapod
 * \param self a spa pod builder object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSpaPodBuilder *
wp_spa_pod_builder_ref (WpSpaPodBuilder *self)
{
  return (WpSpaPodBuilder *) g_rc_box_acquire ((gpointer) self);
}

static void
wp_spa_pod_builder_free (WpSpaPodBuilder *self)
{
  g_clear_pointer (&self->buf, g_free);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 *
 * \ingroup wpspapod
 * \param self (transfer full): a spa pod builder object
 */
void
wp_spa_pod_builder_unref (WpSpaPodBuilder *self)
{
  g_rc_box_release_full (self, (GDestroyNotify) wp_spa_pod_builder_free);
}

/*!
 * \brief Creates a spa pod builder of type array
 * \ingroup wpspapod
 * \returns (transfer full): the new spa pod builder
 */
WpSpaPodBuilder *
wp_spa_pod_builder_new_array (void)
{
  WpSpaPodBuilder *self = wp_spa_pod_builder_new (
      WP_SPA_POD_BUILDER_REALLOC_STEP_SIZE, SPA_TYPE_Array);
  spa_pod_builder_push_array (&self->builder, &self->frame);
  return self;
}

/*!
 * \brief Creates a spa pod builder of type choice
 *
 * \ingroup wpspapod
 * \param choice_type the name of the choice type ("Range", "Step", ...)
 * \returns (transfer full): the new spa pod builder
 */
WpSpaPodBuilder *
wp_spa_pod_builder_new_choice (const char *choice_type)
{
  WpSpaPodBuilder *self = NULL;
  WpSpaIdValue type = wp_spa_id_value_from_short_name (
      SPA_TYPE_INFO_Choice, choice_type);
  g_return_val_if_fail (type != NULL, NULL);

  /* Construct the builder */
  self = wp_spa_pod_builder_new (WP_SPA_POD_BUILDER_REALLOC_STEP_SIZE,
      SPA_TYPE_Choice);

  /* Push the array */
  spa_pod_builder_push_choice (&self->builder, &self->frame,
      wp_spa_id_value_number (type), 0);

  return self;
}

/*!
 * \brief Creates a spa pod builder of type object
 *
 * \ingroup wpspapod
 * \param type_name the type name of the object type
 * \param id_name the Id name of the object
 * \returns (transfer full): the new spa pod builder
 */
WpSpaPodBuilder *
wp_spa_pod_builder_new_object (const char *type_name, const char *id_name)
{
  WpSpaPodBuilder *self = NULL;
  WpSpaType type;
  WpSpaIdTable table;
  WpSpaIdValue id;

  /* Find the type */
  type = wp_spa_type_from_name (type_name);
  g_return_val_if_fail (wp_spa_type_is_object (type), NULL);

  /* Find the id */
  table = wp_spa_type_get_object_id_values_table (type);
  g_return_val_if_fail (table != NULL, NULL);

  id = wp_spa_id_table_find_value_from_short_name (table, id_name);
  g_return_val_if_fail (id != NULL, NULL);

  /* Construct the builder */
  self = wp_spa_pod_builder_new (WP_SPA_POD_BUILDER_REALLOC_STEP_SIZE, type);

  /* Push the object */
  spa_pod_builder_push_object (&self->builder, &self->frame, type,
      wp_spa_id_value_number (id));

  return self;
}

/*!
 * \brief Creates a spa pod builder of type struct
 *
 * \ingroup wpspapod
 * \returns (transfer full): the new spa pod builder
 */
WpSpaPodBuilder *
wp_spa_pod_builder_new_struct (void)
{
  WpSpaPodBuilder *self = NULL;
  self = wp_spa_pod_builder_new (WP_SPA_POD_BUILDER_REALLOC_STEP_SIZE,
      SPA_TYPE_Struct);
  spa_pod_builder_push_struct (&self->builder, &self->frame);
  return self;
}

/*!
 * \brief Creates a spa pod builder of type sequence
 *
 * \ingroup wpspapod
 * \returns (transfer full): the new spa pod builder
 */
WpSpaPodBuilder *
wp_spa_pod_builder_new_sequence (guint unit)
{
  WpSpaPodBuilder *self = NULL;
  self = wp_spa_pod_builder_new (WP_SPA_POD_BUILDER_REALLOC_STEP_SIZE,
      SPA_TYPE_Sequence);
  spa_pod_builder_push_sequence (&self->builder, &self->frame, unit);
  return self;
}

/*!
 * \brief Adds a none value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 */

void
wp_spa_pod_builder_add_none (WpSpaPodBuilder *self)
{
  spa_pod_builder_none (&self->builder);
}

/*!
 * \brief Adds a boolean value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param value the boolean value
 */
void
wp_spa_pod_builder_add_boolean (WpSpaPodBuilder *self, gboolean value)
{
  spa_pod_builder_bool (&self->builder, value ? true : false);
}

/*!
 * \brief Adds a Id value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param value the Id value
 */
void
wp_spa_pod_builder_add_id (WpSpaPodBuilder *self, guint32 value)
{
  spa_pod_builder_id (&self->builder, value);
}

/*!
 * \brief Adds a int value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param value the int value
 */
void
wp_spa_pod_builder_add_int (WpSpaPodBuilder *self, gint32 value)
{
  spa_pod_builder_int (&self->builder, value);
}

/*!
 * \brief Adds a long value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param value the long value
 */
void
wp_spa_pod_builder_add_long (WpSpaPodBuilder *self, gint64 value)
{
  spa_pod_builder_long (&self->builder, value);
}

/*!
 * \brief Adds a float value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param value the float value
 */
void
wp_spa_pod_builder_add_float (WpSpaPodBuilder *self, float value)
{
  spa_pod_builder_float (&self->builder, value);
}

/*!
 * \brief Adds a double value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param value the double value
 */
void
wp_spa_pod_builder_add_double (WpSpaPodBuilder *self, double value)
{
  spa_pod_builder_double (&self->builder, value);
}

/*!
 * \brief Adds a string value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param value the string value
 */
void
wp_spa_pod_builder_add_string (WpSpaPodBuilder *self, const char *value)
{
  spa_pod_builder_string (&self->builder, value);
}

/*!
 * \brief Adds a bytes value with its length into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param value the bytes value
 * \param len the length of the bytes value
 */
void
wp_spa_pod_builder_add_bytes (WpSpaPodBuilder *self, gconstpointer value,
    guint32 len)
{
  spa_pod_builder_bytes (&self->builder, value, len);
}

/*!
 * \brief Adds a pointer value with its type name into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param type_name the type name that the pointer points to
 * \param value the pointer vaue
 */
void
wp_spa_pod_builder_add_pointer (WpSpaPodBuilder *self, const char *type_name,
    gconstpointer value)
{
  WpSpaType type = wp_spa_type_from_name (type_name);
  g_return_if_fail (wp_spa_type_parent (type) == SPA_TYPE_Pointer);
  spa_pod_builder_pointer (&self->builder, type, value);
}

/*!
 * \brief Adds a Fd value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param value the Fd value
 */
void
wp_spa_pod_builder_add_fd (WpSpaPodBuilder *self, gint64 value)
{
  spa_pod_builder_fd (&self->builder, value);
}

/*!
 * \brief Adds the width and height values of a rectangle into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param width the width value of the rectangle
 * \param height the height value of the rectangle
 */
void
wp_spa_pod_builder_add_rectangle (WpSpaPodBuilder *self, guint32 width,
    guint32 height)
{
  spa_pod_builder_rectangle (&self->builder, width, height);
}

/*!
 * \brief Adds the numerator and denominator values of a fraction into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param num the numerator value of the fraction
 * \param denom the denominator value of the fraction
 */
void
wp_spa_pod_builder_add_fraction (WpSpaPodBuilder *self, guint32 num,
    guint32 denom)
{
  spa_pod_builder_fraction (&self->builder, num, denom);
}

/*!
 * \brief Adds a pod value into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param pod the pod value
 */
void
wp_spa_pod_builder_add_pod (WpSpaPodBuilder *self, WpSpaPod *pod)
{
  spa_pod_builder_primitive (&self->builder, pod->pod);
}

/*!
 * \brief Adds a property into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param key the name of the property
 */
void
wp_spa_pod_builder_add_property (WpSpaPodBuilder *self, const char *key)
{
  guint key_id;
  if (g_str_has_prefix (key, "id-")) {
    g_return_if_fail (sscanf (key, "id-%08x", &key_id) == 1);
  } else {
    WpSpaIdTable table = wp_spa_type_get_values_table (self->type);
    WpSpaIdValue id = wp_spa_id_table_find_value_from_short_name (table, key);
    g_return_if_fail (id != NULL);
    key_id = wp_spa_id_value_number (id);
  }
  spa_pod_builder_prop (&self->builder, key_id, 0);
}

/*!
 * \brief Adds a property into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param id the id of the property
 */
void
wp_spa_pod_builder_add_property_id (WpSpaPodBuilder *self, guint32 id)
{
  spa_pod_builder_prop (&self->builder, id, 0);
}

/*!
 * \brief Adds a control into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param offset the offset of the control
 * \param ctl_type the type name of the control
 */
void
wp_spa_pod_builder_add_control (WpSpaPodBuilder *self, guint32 offset,
    const char *ctl_type)
{
  WpSpaIdValue id = wp_spa_id_value_from_short_name (
      SPA_TYPE_INFO_Control, ctl_type);
  g_return_if_fail (id != NULL);
  spa_pod_builder_control (&self->builder, offset, wp_spa_id_value_number (id));
}

/*!
 * \brief Adds a list of values into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param ... a list of additional values, followed by NULL
 */
void
wp_spa_pod_builder_add (WpSpaPodBuilder *self, ...)
{
  va_list args;
  va_start (args, self);
  wp_spa_pod_builder_add_valist (self, args);
  va_end (args);
}

/*!
 * \brief Adds a list of values into the builder
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \param args the variable arguments passed to wp_spa_pod_builder_add()
 */
void
wp_spa_pod_builder_add_valist (WpSpaPodBuilder *self, va_list args)
{
  WpSpaIdTable table = wp_spa_type_get_values_table (self->type);

  do {
    WpSpaIdValue key = NULL;
    const char *format;
    int n_values = 1;
    struct spa_pod_frame f;
    gboolean choice;

    if (wp_spa_type_is_object (self->type)) {
      guint key_id;
      const char *key_name = va_arg(args, const char *);
      if (!key_name)
        return;
      if (g_str_has_prefix (key_name, "id-")) {
        g_return_if_fail (sscanf (key_name, "id-%08x", &key_id) == 1);
      } else {
        key = wp_spa_id_table_find_value_from_short_name (table, key_name);
        g_return_if_fail (key != NULL);
        key_id = wp_spa_id_value_number (key);
      }
      spa_pod_builder_prop (&self->builder, key_id, 0);
    }
    else if (self->type == SPA_TYPE_Sequence) {
      guint32 offset = va_arg(args, uint32_t);
      if (offset == 0)
        return;
      const char *control_name = va_arg(args, const char *);
      if (!control_name)
        return;
      WpSpaIdValue type = wp_spa_id_value_from_short_name (
          SPA_TYPE_INFO_Control, control_name);
      g_return_if_fail (type != NULL);

      spa_pod_builder_control (&self->builder, offset,
          wp_spa_id_value_number (type));
    }

    if ((format = va_arg(args, const char *)) == NULL)
      break;

    choice = *format == '?';
    if (choice) {
      uint32_t type = spa_choice_from_id (*++format);
      if (*format != '\0')
        format++;
      spa_pod_builder_push_choice (&self->builder, &f, type, 0);
      n_values = va_arg(args, int);
    }
    while (n_values-- > 0) {
      switch (*format) {
      case 'P':  /* Pod */
      case 'V':  /* Choice */
      case 'O':  /* Object */
      case 'T': { /* Struct */
        WpSpaPod *arg = va_arg(args, WpSpaPod *);
        if (arg == NULL)
          spa_pod_builder_none (&self->builder);
        else
          spa_pod_builder_primitive (&self->builder, arg->pod);
        break;
      }
      case 'K': { /* Id as string - WirePlumber extension */
        const char * id = va_arg(args, const char *);
        if (key) {
          WpSpaIdTable id_table = NULL;
          wp_spa_id_value_get_value_type (key, &id_table);
          WpSpaIdValue id_val =
              wp_spa_id_table_find_value_from_short_name (id_table, id);
          spa_pod_builder_id (&self->builder, wp_spa_id_value_number (id_val));
        }
        break;
      }
      case 'b':
        spa_pod_builder_bool(&self->builder,
            va_arg(args, gboolean) ? true : false);
        break;
      default:
        SPA_POD_BUILDER_COLLECT(&self->builder, *format, args);
        break;
      }
    }

    if (choice)
      spa_pod_builder_pop (&self->builder, &f);

  } while (TRUE);
}

/*!
 * \brief Ends the builder process and returns the constructed spa pod object
 *
 * \ingroup wpspapod
 * \param self the spa pod builder object
 * \returns (transfer full): the constructed spa pod object
 */
WpSpaPod *
wp_spa_pod_builder_end (WpSpaPodBuilder *self)
{
  WpSpaPod *ret = NULL;

  /* Construct the pod */
  ret = g_slice_new0 (WpSpaPod);
  g_ref_count_init (&ret->ref);
  ret->type = WP_SPA_POD_REGULAR;
  ret->pod = spa_pod_builder_pop (&self->builder, &self->frame);
  ret->builder = wp_spa_pod_builder_ref (self);

  /* Also copy the specific object type if it is an object */
  if (spa_pod_is_object (ret->pod))
    ret->static_pod.data_property.table =
        wp_spa_type_get_values_table (ret->builder->type);

  return ret;
}

/*!
 * \brief Increases the reference count of a spa pod parser
 * \ingroup wpspapod
 * \param self a spa pod sparser object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSpaPodParser *
wp_spa_pod_parser_ref (WpSpaPodParser *self)
{
  return (WpSpaPodParser *) g_rc_box_acquire ((gpointer) self);
}

static void
wp_spa_pod_parser_free (WpSpaPodParser *self)
{
  self->pod = NULL;
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 *
 * \ingroup wpspapod
 * \param self (transfer full): a spa pod parser object
 */
void
wp_spa_pod_parser_unref (WpSpaPodParser *self)
{
  g_rc_box_release_full (self, (GDestroyNotify) wp_spa_pod_parser_free);
}

static WpSpaPodParser *
wp_spa_pod_parser_new (WpSpaPod *pod, guint32 type)
{
  WpSpaPodParser *self = g_rc_box_new0 (WpSpaPodParser);
  self->type = type;
  self->pod = pod;
  spa_pod_parser_pod (&self->parser, self->pod->pod);
  return self;
}

/*!
 * \brief Creates an object spa pod parser. The \a pod object must be valid for
 * the entire life-cycle of the returned parser.
 *
 * \ingroup wpspapod
 * \param pod the object spa pod to parse
 * \param id_name the Id name of the object
 * \returns (transfer full): The new spa pod parser
 */
WpSpaPodParser *
wp_spa_pod_parser_new_object (WpSpaPod *pod, const char **id_name)
{
  WpSpaPodParser *self = NULL;
  WpSpaType type = wp_spa_pod_get_spa_type (pod);
  guint32 id = SPA_ID_INVALID;

  g_return_val_if_fail (wp_spa_pod_is_object (pod), NULL);

  self = wp_spa_pod_parser_new (pod, type);
  spa_pod_parser_push_object (&self->parser, &self->frame, type, &id);
  if (id_name) {
    WpSpaIdTable table = wp_spa_type_get_object_id_values_table (type);
    *id_name = wp_spa_id_value_short_name (
        wp_spa_id_table_find_value (table, id));
  }
  return self;
}

/*!
 * \brief Creates an struct spa pod parser. The \a pod object must be valid for
 * the entire life-cycle of the returned parser.
 *
 * \ingroup wpspapod
 * \param pod the struct spa pod to parse
 * \returns (transfer full): The new spa pod parser
 */
WpSpaPodParser *
wp_spa_pod_parser_new_struct (WpSpaPod *pod)
{
  WpSpaPodParser *self = NULL;

  g_return_val_if_fail (wp_spa_pod_is_struct (pod), NULL);

  self = wp_spa_pod_parser_new (pod, SPA_TYPE_Struct);
  spa_pod_parser_push_struct (&self->parser, &self->frame);
  return self;
}

/*!
 * \brief Gets the boolean value from a spa pod parser
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the boolean value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_boolean (WpSpaPodParser *self, gboolean *value)
{
  g_return_val_if_fail (value, FALSE);
  bool v = FALSE;
  gboolean res = spa_pod_parser_get_bool (&self->parser, &v) >= 0;
  *value = v ? TRUE : FALSE;
  return res;
}

/*!
 * \brief Gets the Id value from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the Id value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_id (WpSpaPodParser *self, guint32 *value)
{
  g_return_val_if_fail (value, FALSE);
  return spa_pod_parser_get_id (&self->parser, value) >= 0;
}

/*!
 * \brief Gets the int value from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the int value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_int (WpSpaPodParser *self, gint32 *value)
{
  g_return_val_if_fail (value, FALSE);
  return spa_pod_parser_get_int (&self->parser, value) >= 0;
}

/*!
 * \brief Gets the long value from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the long value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_long (WpSpaPodParser *self, gint64 *value)
{
  g_return_val_if_fail (value, FALSE);
  return spa_pod_parser_get_long (&self->parser, value) >= 0;
}

/*!
 * \brief Gets the float value from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the float value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_float (WpSpaPodParser *self, float *value)
{
  g_return_val_if_fail (value, FALSE);
  return spa_pod_parser_get_float (&self->parser, value) >= 0;
}

/*!
 * \brief Gets the double value from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the double value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_double (WpSpaPodParser *self, double *value)
{
  g_return_val_if_fail (value, FALSE);
  return spa_pod_parser_get_double (&self->parser, value) >= 0;
}

/*!
 * \brief Gets the string value from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the string value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_string (WpSpaPodParser *self, const char **value)
{
  g_return_val_if_fail (value, FALSE);
  return spa_pod_parser_get_string (&self->parser, value) >= 0;
}

/*!
 * \brief Gets the bytes value and its length from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the bytes value
 * \param len (out): the length of the bytes value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_bytes (WpSpaPodParser *self, gconstpointer *value,
    guint32 *len)
{
  return spa_pod_parser_get_bytes (&self->parser, value, len) >= 0;
}

/*!
 * \brief Gets the pointer value and its type name from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the pointer value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_pointer (WpSpaPodParser *self, gconstpointer *value)
{
  g_return_val_if_fail (value, FALSE);
  guint32 type = 0;
  return spa_pod_parser_get_pointer (&self->parser, &type, value) >= 0;
}

/*!
 * \brief Gets the Fd value from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param value (out): the Fd value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_fd (WpSpaPodParser *self, gint64 *value)
{
  g_return_val_if_fail (value, FALSE);
  return spa_pod_parser_get_fd (&self->parser, value) >= 0;
}

/*!
 * \brief Gets the rectangle's width and height value from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param width (out): the rectangle's width value
 * \param height (out): the rectangle's height value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_rectangle (WpSpaPodParser *self, guint32 *width,
    guint32 *height)
{
  struct spa_rectangle r = { 0, };
  gboolean res = spa_pod_parser_get_rectangle (&self->parser, &r) >= 0;
  if (width)
    *width = r.width;
  if (height)
    *height = r.height;
  return res;
}

/*!
 * \brief Gets the fractions's numerator and denominator value from a spa pod
 * parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param num (out): the fractions's numerator value
 * \param denom (out): the fractions's denominator value
 * \returns TRUE if the value was obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_fraction (WpSpaPodParser *self, guint32 *num,
    guint32 *denom)
{
  struct spa_fraction f = { 0, };
  gboolean res = spa_pod_parser_get_fraction (&self->parser, &f) >= 0;
  if (num)
    *num = f.num;
  if (denom)
    *denom = f.denom;
  return res;
}

/*!
 * \brief Gets the spa pod value from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \returns (transfer full): The spa pod value or NULL if it could not be
 * obtained
 */
WpSpaPod *
wp_spa_pod_parser_get_pod (WpSpaPodParser *self)
{
  struct spa_pod *p = NULL;
  gboolean res = spa_pod_parser_get_pod (&self->parser, &p) >= 0;
  if (!res || !p)
    return NULL;

  return wp_spa_pod_new_wrap (p);
}

/*!
 * \brief Gets a list of values from a spa pod parser object
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param ... (out): a list of values to get, followed by NULL
 * \returns TRUE if the values were obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get (WpSpaPodParser *self, ...)
{
  gboolean res;
  va_list args;

  va_start (args, self);
  res = wp_spa_pod_parser_get_valist (self, args);
  va_end (args);

  return res;
}

static inline gboolean
wp_spa_pod_parser_can_collect (const struct spa_pod *pod, char format)
{
  format = (format == 'K') ? 'I' : format;
  return spa_pod_parser_can_collect (pod, format);
}

/*!
 * \brief This is the `va_list` version of wp_spa_pod_parser_get()
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 * \param args the variable arguments passed to wp_spa_pod_parser_get()
 * \returns TRUE if the values were obtained, FALSE otherwise
 */
gboolean
wp_spa_pod_parser_get_valist (WpSpaPodParser *self, va_list args)
{
  const struct spa_pod_prop *prop = NULL;
  WpSpaIdTable table = wp_spa_type_get_values_table (self->type);

  do {
    WpSpaIdValue key = NULL;
    bool optional;
    const struct spa_pod *pod = NULL;
    const char *format;

    if (wp_spa_type_is_object (self->type)) {
      guint key_id;
      const struct spa_pod_object *object;
      const char *key_name = va_arg(args, const char *);
      if (!key_name)
        break;

      if (g_str_has_prefix (key_name, "id-")) {
        if (sscanf (key_name, "id-%08x", &key_id) != 1)
          return FALSE;
      } else {
        key = wp_spa_id_table_find_value_from_short_name (table, key_name);
        g_return_val_if_fail (key != NULL, FALSE);
        key_id = wp_spa_id_value_number (key);
      }

      object = (const struct spa_pod_object *)spa_pod_parser_frame
            (&self->parser, &self->frame);
      prop = spa_pod_object_find_prop (object, prop, key_id);
      pod = prop ? &prop->value : NULL;
    }

    if ((format = va_arg(args, char *)) == NULL)
      break;

    if (self->type == SPA_TYPE_Struct)
      pod = spa_pod_parser_next (&self->parser);

    if ((optional = (*format == '?')))
      format++;

    if (!pod || !wp_spa_pod_parser_can_collect (pod, *format)) {
      if (!optional)
        return FALSE;

      SPA_POD_PARSER_SKIP (*format, args);
    } else {
      if (pod->type == SPA_TYPE_Choice && *format != 'V' &&
          SPA_POD_CHOICE_TYPE(pod) == SPA_CHOICE_None)
        pod = SPA_POD_CHOICE_CHILD(pod);

      switch (*format) {
      case 'P':  /* Pod */
      case 'V':  /* Choice */
      case 'O':  /* Object */
      case 'T':  /* Struct */
        *va_arg(args, WpSpaPod**) = wp_spa_pod_new_wrap_copy (pod);
        break;
      case 'K': { /* Id as string - WirePlumber extension */
        const char ** idstr = va_arg(args, const char **);
        uint32_t id = SPA_POD_VALUE(struct spa_pod_id, pod);
        if (key) {
          WpSpaIdTable id_table = NULL;
          wp_spa_id_value_get_value_type (key, &id_table);
          WpSpaIdValue id_val = wp_spa_id_table_find_value (id_table, id);
          *idstr = wp_spa_id_value_short_name (id_val);
        }
        break;
      }
      case 'b':
        *va_arg(args, gboolean*) =
            SPA_POD_VALUE(struct spa_pod_bool, pod) ? TRUE : FALSE;
        break;
      default:
        SPA_POD_PARSER_COLLECT (pod, *format, args);
        break;
      }
    }
  } while (TRUE);

  return TRUE;
}

/*!
 * \brief Ends the parser process
 *
 * \ingroup wpspapod
 * \param self the spa pod parser object
 */
void
wp_spa_pod_parser_end (WpSpaPodParser *self)
{
  spa_pod_parser_pop (&self->parser, &self->frame);
}

struct _WpSpaPodIterator
{
  WpSpaPod *pod;
  union {
    gpointer value;                   /* Array and Choice */
    struct spa_pod *pod;              /* Struct */
    struct spa_pod_prop *prop;        /* Object */
    struct spa_pod_control *control;  /* Sequence */
  } curr;
};
typedef struct _WpSpaPodIterator WpSpaPodIterator;

static gboolean
wp_spa_pod_iterator_next_choice (WpSpaPodIterator *self, GValue *item)
{
  const struct spa_pod_choice *pod_choice =
      (const struct spa_pod_choice *) self->pod->pod;

  if (!self->curr.value)
    self->curr.value = SPA_MEMBER (&pod_choice->body, sizeof(struct spa_pod_choice_body), void);
  else
    self->curr.value = SPA_MEMBER (self->curr.value, pod_choice->body.child.size, void);

  if (self->curr.value >= SPA_MEMBER(&pod_choice->body, SPA_POD_BODY_SIZE (pod_choice), void))
    return FALSE;

  if (item) {
    g_value_init (item, G_TYPE_POINTER);
    g_value_set_pointer (item, self->curr.value);
  }
  return TRUE;
}

static gboolean
wp_spa_pod_iterator_next_array (WpSpaPodIterator *self, GValue *item)
{
  const struct spa_pod_array *pod_arr =
      (const struct spa_pod_array *) self->pod->pod;

  if (!self->curr.value)
    self->curr.value = SPA_MEMBER (&pod_arr->body, sizeof(struct spa_pod_array_body), void);
  else
    self->curr.value = SPA_MEMBER (self->curr.value, pod_arr->body.child.size, void);

  if (self->curr.value >= SPA_MEMBER(&pod_arr->body, SPA_POD_BODY_SIZE (pod_arr), void))
    return FALSE;

  if (item) {
    g_value_init (item, G_TYPE_POINTER);
    g_value_set_pointer (item, self->curr.value);
  }
  return TRUE;
}

static gboolean
wp_spa_pod_iterator_next_object (WpSpaPodIterator *self, GValue *item)
{
  const struct spa_pod_object *pod_obj =
      (const struct spa_pod_object *) self->pod->pod;

  if (!self->curr.prop)
    self->curr.prop = spa_pod_prop_first (&pod_obj->body);
  else
    self->curr.prop = spa_pod_prop_next (self->curr.prop);

  if (!spa_pod_prop_is_inside (&pod_obj->body, SPA_POD_BODY_SIZE (pod_obj),
      self->curr.prop))
    return FALSE;

  if (item) {
    g_value_init (item, WP_TYPE_SPA_POD);
    g_value_take_boxed (item, wp_spa_pod_new_property_wrap (
        self->pod->static_pod.data_property.table, self->curr.prop->key,
        self->curr.prop->flags, &self->curr.prop->value));
  }
  return TRUE;
}

static gboolean
wp_spa_pod_iterator_next_struct (WpSpaPodIterator *self, GValue *item)
{
  if (!self->curr.pod)
    self->curr.pod = SPA_POD_BODY (self->pod->pod);
  else
    self->curr.pod = spa_pod_next (self->curr.pod);

  if (!spa_pod_is_inside (SPA_POD_BODY (self->pod->pod),
      SPA_POD_BODY_SIZE (self->pod->pod), self->curr.pod))
    return FALSE;

  if (item) {
    g_value_init (item, WP_TYPE_SPA_POD);
    g_value_take_boxed (item, wp_spa_pod_new_wrap (self->curr.pod));
  }
  return TRUE;
}

static gboolean
wp_spa_pod_iterator_next_sequence (WpSpaPodIterator *self, GValue *item)
{
  const struct spa_pod_sequence *pod_seq =
      (const struct spa_pod_sequence *) self->pod->pod;

  if (!self->curr.control)
    self->curr.control = spa_pod_control_first (&pod_seq->body);
  else
    self->curr.control = spa_pod_control_next (self->curr.control);

  if (!spa_pod_control_is_inside (&pod_seq->body, SPA_POD_BODY_SIZE (pod_seq),
      self->curr.control))
    return FALSE;

  if (item) {
    g_value_init (item, WP_TYPE_SPA_POD);
    g_value_take_boxed (item, wp_spa_pod_new_control_wrap (
        self->curr.control->offset, self->curr.control->type,
        &self->curr.control->value));
  }
  return TRUE;
}


static void
wp_spa_pod_iterator_reset (WpIterator *iterator)
{
  WpSpaPodIterator *self = wp_iterator_get_user_data (iterator);
  self->curr.value = NULL;
  self->curr.pod = NULL;
  self->curr.prop = NULL;
  self->curr.control = NULL;
}

static gboolean
wp_spa_pod_iterator_next (WpIterator *iterator, GValue *item)
{
  WpSpaPodIterator *self = wp_iterator_get_user_data (iterator);

  switch (self->pod->pod->type) {
  case SPA_TYPE_Choice:
    return wp_spa_pod_iterator_next_choice (self, item);
  case SPA_TYPE_Array:
    return wp_spa_pod_iterator_next_array (self, item);
  case SPA_TYPE_Object:
    return wp_spa_pod_iterator_next_object (self, item);
  case SPA_TYPE_Struct:
    return wp_spa_pod_iterator_next_struct (self, item);
  case SPA_TYPE_Sequence:
    return wp_spa_pod_iterator_next_sequence (self, item);
  default:
    break;
  }

  return FALSE;
}

static gboolean
wp_spa_pod_iterator_fold (WpIterator *iterator, WpIteratorFoldFunc func,
    GValue *ret, gpointer data)
{
  WpSpaPodIterator *self = wp_iterator_get_user_data (iterator);

  wp_iterator_reset (iterator);

  switch (self->pod->pod->type) {
  case SPA_TYPE_Choice:
  {
    const struct spa_pod_choice *pod_choice =
        (const struct spa_pod_choice *) self->pod->pod;
    gpointer p = NULL;
    SPA_POD_CHOICE_FOREACH (pod_choice, p) {
      GValue v = G_VALUE_INIT;
      g_value_init (&v, G_TYPE_POINTER);
      g_value_set_pointer (&v, p);
      const gboolean res = func (&v, ret, data);
      g_value_unset (&v);
      if (!res)
        return FALSE;
    }
    break;
  }
  case SPA_TYPE_Array:
  {
    const struct spa_pod_array *pod_arr =
        (const struct spa_pod_array *) self->pod->pod;
    gpointer p = NULL;
    SPA_POD_ARRAY_FOREACH (pod_arr, p) {
      GValue v = G_VALUE_INIT;
      g_value_init (&v, G_TYPE_POINTER);
      g_value_set_pointer (&v, p);
      const gboolean res = func (&v, ret, data);
      g_value_unset (&v);
      if (!res)
        return FALSE;
    }
    break;
  }
  case SPA_TYPE_Object:
  {
    const struct spa_pod_object *pod_obj =
        (const struct spa_pod_object *) self->pod->pod;
    struct spa_pod_prop *p = NULL;
    SPA_POD_OBJECT_FOREACH (pod_obj, p) {
      GValue v = G_VALUE_INIT;
      g_value_init (&v, WP_TYPE_SPA_POD);
      g_value_take_boxed (&v, wp_spa_pod_new_property_wrap (
          self->pod->static_pod.data_property.table, p->key, p->flags,
          &p->value));
      const gboolean res = func (&v, ret, data);
      g_value_unset (&v);
      if (!res)
        return FALSE;
    }
    break;
  }
  case SPA_TYPE_Struct:
  {
    struct spa_pod *p = NULL;
    SPA_POD_STRUCT_FOREACH (self->pod->pod, p) {
      GValue v = G_VALUE_INIT;
      g_value_init (&v, WP_TYPE_SPA_POD);
      g_value_take_boxed (&v, wp_spa_pod_new_wrap (p));
      const gboolean res = func (&v, ret, data);
      g_value_unset (&v);
      if (!res)
        return FALSE;
    }
    break;
  }
  case SPA_TYPE_Sequence:
  {
    const struct spa_pod_sequence *pod_seq =
        (const struct spa_pod_sequence *) self->pod->pod;
    struct spa_pod_control *p = NULL;
    SPA_POD_SEQUENCE_FOREACH (pod_seq, p) {
      GValue v = G_VALUE_INIT;
      g_value_init (&v, WP_TYPE_SPA_POD);
      g_value_take_boxed (&v, wp_spa_pod_new_control_wrap (p->offset, p->type,
          &p->value));
      const gboolean res = func (&v, ret, data);
      g_value_unset (&v);
      if (!res)
        return FALSE;
    }
    break;
  }
  default:
    return FALSE;
  }

  return TRUE;
}

static void
wp_spa_pod_iterator_finalize (WpIterator *iterator)
{
  WpSpaPodIterator *self = wp_iterator_get_user_data (iterator);
  g_clear_pointer (&self->pod, wp_spa_pod_unref);
}

/*!
 * \brief Creates a new iterator for a spa pod object.
 *
 * \ingroup wpspapod
 * \param pod a spa pod object
 * \returns (transfer full): the new spa pod iterator
 */
WpIterator *
wp_spa_pod_new_iterator (WpSpaPod *pod)
{
  static const WpIteratorMethods methods = {
    .version = WP_ITERATOR_METHODS_VERSION,
    .reset = wp_spa_pod_iterator_reset,
    .next = wp_spa_pod_iterator_next,
    .fold = wp_spa_pod_iterator_fold,
    .foreach = NULL,
    .finalize = wp_spa_pod_iterator_finalize
  };
  WpIterator *it = wp_iterator_new (&methods, sizeof (WpSpaPodIterator));
  WpSpaPodIterator *self = wp_iterator_get_user_data (it);

  self->pod = wp_spa_pod_ref (pod);

  return it;
}
070701000000C1000081A400000000000000000000000165F8630400002B21000000000000000000000000000000000000002300000000wireplumber-0.5.0/lib/wp/spa-pod.h    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_SPA_POD_H__
#define __WIREPLUMBER_SPA_POD_H__

#include <gio/gio.h>

#include "defs.h"
#include "iterator.h"
#include "spa-type.h"

G_BEGIN_DECLS

struct spa_pod;

/*!
 * \brief The WpSpaPod GType
 * \ingroup wpspapod
 */
#define WP_TYPE_SPA_POD (wp_spa_pod_get_type ())
WP_API
GType wp_spa_pod_get_type (void);

typedef struct _WpSpaPod WpSpaPod;

WP_API
WpSpaPod *wp_spa_pod_ref (WpSpaPod *self);

WP_API
void wp_spa_pod_unref (WpSpaPod *self);

WP_API
WpSpaPod * wp_spa_pod_new_wrap (struct spa_pod *pod);

WP_API
WpSpaPod * wp_spa_pod_new_wrap_const (const struct spa_pod *pod);

WP_API
const struct spa_pod * wp_spa_pod_get_spa_pod (const WpSpaPod *self);

WP_API
WpSpaType wp_spa_pod_get_spa_type (WpSpaPod *self);

WP_API
WpSpaIdValue wp_spa_pod_get_choice_type (WpSpaPod *self);

WP_API
WpSpaPod *wp_spa_pod_copy (WpSpaPod *other);

WP_API
gboolean wp_spa_pod_is_unique_owner (WpSpaPod *self);

WP_API
WpSpaPod *wp_spa_pod_ensure_unique_owner (WpSpaPod *self);

WP_API
WpSpaPod *wp_spa_pod_new_none (void);

WP_API
WpSpaPod *wp_spa_pod_new_boolean (gboolean value);

WP_API
WpSpaPod *wp_spa_pod_new_id (guint32 value);

WP_API
WpSpaPod *wp_spa_pod_new_int (gint32 value);

WP_API
WpSpaPod *wp_spa_pod_new_long (gint64 value);

WP_API
WpSpaPod *wp_spa_pod_new_float (float value);

WP_API
WpSpaPod *wp_spa_pod_new_double (double value);

WP_API
WpSpaPod *wp_spa_pod_new_string (const char *value);

WP_API
WpSpaPod *wp_spa_pod_new_bytes (gconstpointer value, guint32 len);

WP_API
WpSpaPod *wp_spa_pod_new_pointer (const char *type_name, gconstpointer value);

WP_API
WpSpaPod *wp_spa_pod_new_fd (gint64 value);

WP_API
WpSpaPod *wp_spa_pod_new_rectangle (guint32 width, guint32 height);

WP_API
WpSpaPod *wp_spa_pod_new_fraction (guint32 num, guint32 denom);

WP_API
WpSpaPod *wp_spa_pod_new_choice (const char *choice_type, ...)
    G_GNUC_NULL_TERMINATED;

WP_API
WpSpaPod *wp_spa_pod_new_choice_valist (const char *choice_type, va_list args);

WP_API
WpSpaPod *wp_spa_pod_new_object (const char *type_name, const char *id_name,
    ...) G_GNUC_NULL_TERMINATED;

WP_API
WpSpaPod *wp_spa_pod_new_object_valist (const char *type_name,
    const char *id_name, va_list args);

WP_API
WpSpaPod *wp_spa_pod_new_sequence (guint unit, ...) G_GNUC_NULL_TERMINATED;

WP_API
WpSpaPod *wp_spa_pod_new_sequence_valist (guint unit, va_list args);

WP_API
gboolean wp_spa_pod_is_none (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_boolean (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_id (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_int (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_long (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_float (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_double (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_string (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_bytes (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_pointer (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_fd (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_rectangle (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_fraction (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_array (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_choice (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_object (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_struct (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_sequence (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_property (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_is_control (WpSpaPod *self);

WP_API
gboolean wp_spa_pod_get_boolean (WpSpaPod *self, gboolean *value);

WP_API
gboolean wp_spa_pod_get_id (WpSpaPod *self, guint32 *value);

WP_API
gboolean wp_spa_pod_get_int (WpSpaPod *self, gint32 *value);

WP_API
gboolean wp_spa_pod_get_long (WpSpaPod *self, gint64 *value);

WP_API
gboolean wp_spa_pod_get_float (WpSpaPod *self, float *value);

WP_API
gboolean wp_spa_pod_get_double (WpSpaPod *self, double *value);

WP_API
gboolean wp_spa_pod_get_string (WpSpaPod *self, const char **value);

WP_API
gboolean wp_spa_pod_get_bytes (WpSpaPod *self, gconstpointer *value,
    guint32 *len);

WP_API
gboolean wp_spa_pod_get_pointer (WpSpaPod *self, gconstpointer *value);

WP_API
gboolean wp_spa_pod_get_fd (WpSpaPod *self, gint64 *value);

WP_API
gboolean wp_spa_pod_get_rectangle (WpSpaPod *self, guint32 *width,
    guint32 *height);

WP_API
gboolean wp_spa_pod_get_fraction (WpSpaPod *self, guint32 *num, guint32 *denom);

WP_API
gboolean wp_spa_pod_set_boolean (WpSpaPod *self, gboolean value);

WP_API
gboolean wp_spa_pod_set_id (WpSpaPod *self, guint32 value);

WP_API
gboolean wp_spa_pod_set_int (WpSpaPod *self, gint32 value);

WP_API
gboolean wp_spa_pod_set_long (WpSpaPod *self, gint64 value);

WP_API
gboolean wp_spa_pod_set_float (WpSpaPod *self, float value);

WP_API
gboolean wp_spa_pod_set_double (WpSpaPod *self, double value);

WP_API
gboolean wp_spa_pod_set_pointer (WpSpaPod *self, const char *type_name,
   gconstpointer value);

WP_API
gboolean wp_spa_pod_set_fd (WpSpaPod *self, gint64 value);

WP_API
gboolean wp_spa_pod_set_rectangle (WpSpaPod *self, guint32 width,
    guint32 height);

WP_API
gboolean wp_spa_pod_set_fraction (WpSpaPod *self, guint32 num, guint32 denom);

WP_API
gboolean wp_spa_pod_set_pod (WpSpaPod *self, WpSpaPod *pod);

WP_API
gboolean wp_spa_pod_equal (WpSpaPod *self, WpSpaPod *pod);

WP_API
gboolean wp_spa_pod_get_object (WpSpaPod *self,
    const char **id_name, ...) G_GNUC_NULL_TERMINATED;

WP_API
gboolean wp_spa_pod_get_object_valist (WpSpaPod *self,
    const char **id_name, va_list args);

WP_API
gboolean wp_spa_pod_get_struct (WpSpaPod *self, ...) G_GNUC_NULL_TERMINATED;

WP_API
gboolean wp_spa_pod_get_struct_valist (WpSpaPod *self, va_list args);

WP_API
gboolean wp_spa_pod_get_property (WpSpaPod *self, const char **key,
    WpSpaPod **value);

WP_API
gboolean wp_spa_pod_get_control (WpSpaPod *self, guint32 *offset,
    const char **ctl_type, WpSpaPod **value);

WP_API
WpSpaPod *wp_spa_pod_get_choice_child (WpSpaPod *self);

WP_API
WpSpaPod *wp_spa_pod_get_array_child (WpSpaPod *self);

WP_API
WpIterator *wp_spa_pod_new_iterator (WpSpaPod *pod);

WP_API
gboolean wp_spa_pod_fixate (WpSpaPod *self);

WP_API
WpSpaPod *wp_spa_pod_filter (WpSpaPod *self, WpSpaPod *filter);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSpaPod, wp_spa_pod_unref)


/*!
 * \brief The WpSpaPodBuilder GType
 * \ingroup wpspapod
 */
#define WP_TYPE_SPA_POD_BUILDER (wp_spa_pod_builder_get_type ())
WP_API
GType wp_spa_pod_builder_get_type (void);

typedef struct _WpSpaPodBuilder WpSpaPodBuilder;

WP_API
WpSpaPodBuilder *wp_spa_pod_builder_ref (WpSpaPodBuilder *self);

WP_API
void wp_spa_pod_builder_unref (WpSpaPodBuilder *self);

WP_API
WpSpaPodBuilder *wp_spa_pod_builder_new_array (void);

WP_API
WpSpaPodBuilder *wp_spa_pod_builder_new_choice (const char *choice_type);

WP_API
WpSpaPodBuilder *wp_spa_pod_builder_new_object (const char *type_name,
    const char *id_name);

WP_API
WpSpaPodBuilder *wp_spa_pod_builder_new_struct (void);

WP_API
WpSpaPodBuilder *wp_spa_pod_builder_new_sequence (guint unit);

WP_API
void wp_spa_pod_builder_add_none (WpSpaPodBuilder *self);

WP_API
void wp_spa_pod_builder_add_boolean (WpSpaPodBuilder *self, gboolean value);

WP_API
void wp_spa_pod_builder_add_id (WpSpaPodBuilder *self, guint32 value);

WP_API
void wp_spa_pod_builder_add_int (WpSpaPodBuilder *self, gint32 value);

WP_API
void wp_spa_pod_builder_add_long (WpSpaPodBuilder *self, gint64 value);

WP_API
void wp_spa_pod_builder_add_float (WpSpaPodBuilder *self, float value);

WP_API
void wp_spa_pod_builder_add_double (WpSpaPodBuilder *self, double value);

WP_API
void wp_spa_pod_builder_add_string (WpSpaPodBuilder *self, const char *value);

WP_API
void wp_spa_pod_builder_add_bytes (WpSpaPodBuilder *self, gconstpointer value,
    guint32 len);

WP_API
void wp_spa_pod_builder_add_pointer (WpSpaPodBuilder *self,
    const char *type_name, gconstpointer value);

WP_API
void wp_spa_pod_builder_add_fd (WpSpaPodBuilder *self, gint64 value);

WP_API
void wp_spa_pod_builder_add_rectangle (WpSpaPodBuilder *self, guint32 width,
    guint32 height);

WP_API
void wp_spa_pod_builder_add_fraction (WpSpaPodBuilder *self, guint32 num,
    guint32 denom);

WP_API
void wp_spa_pod_builder_add_pod (WpSpaPodBuilder *self, WpSpaPod *pod);

WP_API
void wp_spa_pod_builder_add_property (WpSpaPodBuilder *self, const char *key);

WP_API
void wp_spa_pod_builder_add_property_id (WpSpaPodBuilder *self, guint32 id);

WP_API
void wp_spa_pod_builder_add_control (WpSpaPodBuilder *self, guint32 offset,
    const char *ctl_type);

WP_API
void wp_spa_pod_builder_add (WpSpaPodBuilder *self, ...) G_GNUC_NULL_TERMINATED;

WP_API
void wp_spa_pod_builder_add_valist (WpSpaPodBuilder *self, va_list args);

WP_API
WpSpaPod *wp_spa_pod_builder_end (WpSpaPodBuilder *self);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSpaPodBuilder, wp_spa_pod_builder_unref)


/*!
 * \brief The WpSpaPodParser GType
 * \ingroup wpspapod
 */
#define WP_TYPE_SPA_POD_PARSER (wp_spa_pod_parser_get_type ())
WP_API
GType wp_spa_pod_parser_get_type (void);

typedef struct _WpSpaPodParser WpSpaPodParser;

WP_API
WpSpaPodParser *wp_spa_pod_parser_ref (WpSpaPodParser *self);

WP_API
void wp_spa_pod_parser_unref (WpSpaPodParser *self);

WP_API
WpSpaPodParser *wp_spa_pod_parser_new_object (WpSpaPod *pod,
    const char **id_name);

WP_API
WpSpaPodParser *wp_spa_pod_parser_new_struct (WpSpaPod *pod);

WP_API
gboolean wp_spa_pod_parser_get_boolean (WpSpaPodParser *self, gboolean *value);

WP_API
gboolean wp_spa_pod_parser_get_id (WpSpaPodParser *self, guint32 *value);

WP_API
gboolean wp_spa_pod_parser_get_int (WpSpaPodParser *self, gint32 *value);

WP_API
gboolean wp_spa_pod_parser_get_long (WpSpaPodParser *self, gint64 *value);

WP_API
gboolean wp_spa_pod_parser_get_float (WpSpaPodParser *self, float *value);

WP_API
gboolean wp_spa_pod_parser_get_double (WpSpaPodParser *self, double *value);

WP_API
gboolean wp_spa_pod_parser_get_string (WpSpaPodParser *self,
    const char **value);

WP_API
gboolean wp_spa_pod_parser_get_bytes (WpSpaPodParser *self,
    gconstpointer *value, guint32 *len);

WP_API
gboolean wp_spa_pod_parser_get_pointer (WpSpaPodParser *self,
    gconstpointer *value);

WP_API
gboolean wp_spa_pod_parser_get_fd (WpSpaPodParser *self, gint64 *value);

WP_API
gboolean wp_spa_pod_parser_get_rectangle (WpSpaPodParser *self, guint32 *width,
    guint32 *height);

WP_API
gboolean wp_spa_pod_parser_get_fraction (WpSpaPodParser *self, guint32 *num,
    guint32 *denom);

WP_API
WpSpaPod *wp_spa_pod_parser_get_pod (WpSpaPodParser *self);

WP_API
gboolean wp_spa_pod_parser_get (WpSpaPodParser *self, ...)
    G_GNUC_NULL_TERMINATED;

WP_API
gboolean wp_spa_pod_parser_get_valist (WpSpaPodParser *self,
    va_list args);

WP_API
void wp_spa_pod_parser_end (WpSpaPodParser *self);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpSpaPodParser, wp_spa_pod_parser_unref)


G_END_DECLS

#endif
   070701000000C2000081A400000000000000000000000165F86304000056D8000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/spa-type.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "spa-type.h"
#include "log.h"

#include <spa/utils/type-info.h>
#include <spa/debug/types.h>
#include <pipewire/pipewire.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-spa-type")

/*! \defgroup wpspatype WpSpaType
 *
 * Spa has a type system that is represented by a set of arrays that contain
 * `spa_type_info` structures. This type system is simple, yet complex to
 * work with for a couple of reasons.
 *
 * WirePlumber uses this API to access the spa type system, which makes some
 * things easier to understand and work with. The main benefit of using this
 * API is that it makes it easy to work with string representations of the
 * types, allowing easier access from script bindings.
 *
 * \b Type \b hierarchy
 *
 * On the top level, there is a list of types like Int, Bool, String, Id, Object.
 * These are called fundamental types (terms borrowed from GType).
 * Fundamental types can be derived and therefore we can have other types
 * that represent specific objects, for instance.
 *
 * Enum and flag types are all represented with `SPA_TYPE_Id`. These types
 * may have a list of possible values that one can select from (enums)
 * or combine (flags). These values are accessed with the WpSpaIdTable API.
 *
 * Object types can have fields. All objects always have a special "id" field,
 * which is an enum. Its possible values can be given by
 * wp_spa_type_get_object_id_values_table(). Optionally, objects can also have
 * other object-specific fields, which can be accessed with
 * wp_spa_type_get_values_table().
 *
 * Every object field or enum value is represented by a WpSpaIdValue. In the
 * case of object fields, each field can be of a specific type, which is
 * returned by wp_spa_id_value_get_value_type().
 */

static const WpSpaType SPA_TYPE_VENDOR_WirePlumber = 0x03000000;
static GArray *extra_types = NULL;
static GArray *extra_id_tables = NULL;

typedef struct {
  const char *name;
  const struct spa_type_info *values;
} WpSpaIdTableInfo;

static const WpSpaIdTableInfo static_id_tables[] = {
  { SPA_TYPE_INFO_Choice, spa_type_choice },
  { SPA_TYPE_INFO_Direction, spa_type_direction },
  { SPA_TYPE_INFO_ParamId, spa_type_param },
  { SPA_TYPE_INFO_MediaType, spa_type_media_type },
  { SPA_TYPE_INFO_MediaSubtype, spa_type_media_subtype },
  { SPA_TYPE_INFO_ParamAvailability, spa_type_param_availability },
  { SPA_TYPE_INFO_ParamPortConfigMode, spa_type_param_port_config_mode },
  { SPA_TYPE_INFO_VideoFormat, spa_type_video_format },
  { SPA_TYPE_INFO_AudioFormat, spa_type_audio_format },
  { SPA_TYPE_INFO_AudioFlags, spa_type_audio_flags },
  { SPA_TYPE_INFO_AudioChannel, spa_type_audio_channel },
  { SPA_TYPE_INFO_AudioIEC958Codec, spa_type_audio_iec958_codec },
  { SPA_TYPE_INFO_IO, spa_type_io },
  { SPA_TYPE_INFO_Control, spa_type_control },
  { SPA_TYPE_INFO_Data, spa_type_data_type },
  { SPA_TYPE_INFO_Meta, spa_type_meta_type },
  { SPA_TYPE_INFO_DeviceEventId, spa_type_device_event_id },
  { SPA_TYPE_INFO_NodeEvent, spa_type_node_event_id },
  { SPA_TYPE_INFO_NodeCommand, spa_type_node_command_id },
  { NULL, NULL }
};

GType wp_spa_type_get_type (void)
{
  static gsize id__volatile = 0;
  if (g_once_init_enter (&id__volatile)) {
    GType id = g_type_register_static_simple (
        G_TYPE_UINT, g_intern_static_string ("WpSpaType"),
        0, NULL, 0, NULL, 0);
    g_once_init_leave (&id__volatile, id);
  }
  return id__volatile;
}

G_DEFINE_POINTER_TYPE (WpSpaIdTable, wp_spa_id_table)

G_DEFINE_POINTER_TYPE (WpSpaIdValue, wp_spa_id_value)


static const struct spa_type_info *
wp_spa_type_info_find_by_type (WpSpaType type)
{
  const struct spa_type_info *info;

  g_return_val_if_fail (type != WP_SPA_TYPE_INVALID, NULL);
  g_return_val_if_fail (type != 0, NULL);

  if (extra_types)
    info = spa_debug_type_find (
        (const struct spa_type_info *) extra_types->data, type);
  else
    info = spa_debug_type_find (SPA_TYPE_ROOT, type);

  return info;
}

/* similar to spa_debug_type_find() and unlike spa_debug_type_find_type(),
   which steps into id values / object fields */
static const struct spa_type_info *
_spa_type_find_by_name (const struct spa_type_info * info, const char * name)
{
  const struct spa_type_info * res;

  while (info->name) {
    if (info->type == SPA_ID_INVALID) {
      if (info->values && (res = _spa_type_find_by_name (info->values, name)))
        return res;
    }
    if (strcmp (info->name, name) == 0)
      return info;
    info++;
  }
  return NULL;
}

static const struct spa_type_info *
wp_spa_type_info_find_by_name (const gchar *name)
{
  const struct spa_type_info *info = NULL;

  g_return_val_if_fail (name != NULL, NULL);

  if (extra_types)
    info = _spa_type_find_by_name (
        (const struct spa_type_info *) extra_types->data, name);
  else
    info = _spa_type_find_by_name (SPA_TYPE_ROOT, name);

  return info;
}

/*!
 * \brief Looks up the type id from a given type name
 *
 * \ingroup wpspatype
 * \param name the name to look up
 * \returns (transfer none): the corresponding type id or WP_SPA_TYPE_INVALID
 *   if not found
 */
WpSpaType
wp_spa_type_from_name (const gchar *name)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_name (name);
  return info ? info->type : WP_SPA_TYPE_INVALID;
}

/*!
 * \brief Gets the parent type of an SPA type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns (transfer none): the direct parent type of the given \a type; if the
 *   type is fundamental (i.e. has no parent), the returned type is the same
 *   as \a type
 */
WpSpaType
wp_spa_type_parent (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? info->parent : WP_SPA_TYPE_INVALID;
}

/*!
 * \brief Gets the name of an SPA type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns the complete name of the given \a type or NULL if \a type is invalid
 */
const gchar *
wp_spa_type_name (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? info->name : NULL;
}

/*!
 * \brief Checks if an SPA type is a fundamental type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns TRUE if the \a type has no parent, FALSE otherwise
 */
gboolean
wp_spa_type_is_fundamental (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? (info->type == info->parent) : FALSE;
}

/*!
 * \brief Checks if an SPA type is an Id type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns TRUE if the \a type is a SPA_TYPE_Id, FALSE otherwise
 */
gboolean
wp_spa_type_is_id (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? (info->parent == SPA_TYPE_Id) : FALSE;
}

/*!
 * \brief Checks if an SPA type is an Object type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns TRUE if the \a type is a SPA_TYPE_Object, FALSE otherwise
 */
gboolean
wp_spa_type_is_object (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? (info->parent == SPA_TYPE_Object) : FALSE;
}

/*!
 * \brief Gets the table with the values that can be stored in the special "id"
 *   field of an object of the given \a type
 *
 * Object pods (see WpSpaPod) always have a special "id" field along with
 * other fields that can be defined. This "id" field can only store values
 * of a specific `SPA_TYPE_Id` type. This function returns the table that
 * contains the possible values for that field.
 *
 * \ingroup wpspatype
 * \param type the type id of an object type
 * \returns the table with the values that can be stored in the special "id"
 *   field of an object of the given \a type
 */
WpSpaIdTable
wp_spa_type_get_object_id_values_table (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);

  g_return_val_if_fail (info != NULL, NULL);
  g_return_val_if_fail (info->parent == SPA_TYPE_Object, NULL);
  g_return_val_if_fail (info->values != NULL, NULL);
  g_return_val_if_fail (info->values->name != NULL, NULL);
  g_return_val_if_fail (info->values->parent == SPA_TYPE_Id, NULL);

  return info->values->values;
}

/*!
 * \brief Gets the values table of an SPA type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns the associated WpSpaIdTable that contains possible
 *   values or object fields for this type, or NULL
 */
WpSpaIdTable
wp_spa_type_get_values_table (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);

  g_return_val_if_fail (info != NULL, NULL);
  return info->values;
}

struct spa_type_info_iterator_data
{
  const struct spa_type_info *base;
  const struct spa_type_info *cur;
};

static void
spa_type_info_iterator_reset (WpIterator *it)
{
  struct spa_type_info_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->cur = it_data->base;
}

static gboolean
spa_type_info_iterator_next (WpIterator *it, GValue *item)
{
  struct spa_type_info_iterator_data *it_data = wp_iterator_get_user_data (it);

  if (it_data->cur->name) {
    g_value_init (item, WP_TYPE_SPA_ID_VALUE);
    g_value_set_pointer (item, (gpointer) it_data->cur);
    it_data->cur++;
    return TRUE;
  }
  return FALSE;
}

static gboolean
spa_type_info_iterator_fold (WpIterator *it, WpIteratorFoldFunc func,
    GValue *ret, gpointer data)
{
  struct spa_type_info_iterator_data *it_data = wp_iterator_get_user_data (it);
  const struct spa_type_info *cur, *base;

  cur = base = it_data->base;

  while (cur->name) {
    g_auto (GValue) item = G_VALUE_INIT;
    g_value_init (&item, WP_TYPE_SPA_ID_VALUE);
    g_value_set_pointer (&item, (gpointer) cur);
    if (!func (&item, ret, data))
      return FALSE;
    cur++;
  }
  return TRUE;
}

static const WpIteratorMethods spa_type_info_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = spa_type_info_iterator_reset,
  .next = spa_type_info_iterator_next,
  .fold = spa_type_info_iterator_fold,
};

/*!
 * \brief Finds a WpSpaIdTable given its name.
 *
 * This name can either be the full type name of an object type,
 * or the name of an enum (which is \b not(!!) a type).
 * For example, "Spa:Pod:Object:Param:Format" and "Spa:Enum:ParamId" are
 * both valid table names.
 *
 * \ingroup wpspatype
 * \param name the full name of an id table
 * \returns (nullable): the associated table, or NULL
 */
WpSpaIdTable
wp_spa_id_table_from_name (const gchar *name)
{
  g_return_val_if_fail (name != NULL, NULL);
  const WpSpaIdTableInfo *info = NULL;

  /* first look in dynamic id tables */
  if (extra_id_tables) {
    info = (const WpSpaIdTableInfo *) extra_id_tables->data;
    while (info && info->name) {
      if (strcmp (info->name, name) == 0)
        return info->values;
      info++;
    }
  }

  /* then look at the well-known static ones */
  info = static_id_tables;
  while (info && info->name) {
    if (strcmp (info->name, name) == 0)
      return info->values;
    info++;
  }

  /* then look into types, hoping to find an object type */
  const struct spa_type_info *tinfo = wp_spa_type_info_find_by_name (name);
  return tinfo ? tinfo->values : NULL;
}

/*!
 * \brief This function returns an iterator that allows you to iterate
 * through the values associated with this table.
 *
 * The items in the iterator are of type WpSpaIdValue.
 *
 * \ingroup wpspatype
 * \param table the id table
 * \returns a WpIterator that iterates over WpSpaIdValue items
 */
WpIterator *
wp_spa_id_table_new_iterator (WpSpaIdTable table)
{
  g_return_val_if_fail (table != NULL, NULL);

  WpIterator *it = wp_iterator_new (&spa_type_info_iterator_methods,
      sizeof (struct spa_type_info_iterator_data));
  struct spa_type_info_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->base = (const struct spa_type_info *) table;
  it_data->cur = it_data->base;
  return it;
}

/*!
 * \brief Finds a value in an SPA Id table
 *
 * \ingroup wpspatype
 * \param table the id table
 * \param value a numeric value that is contained in the table
 * \returns (nullable): the WpSpaIdValue associated with \a value, or NULL
 */
WpSpaIdValue
wp_spa_id_table_find_value (WpSpaIdTable table, guint value)
{
  g_return_val_if_fail (table != NULL, NULL);

  const struct spa_type_info *info = table;
  while (info && info->name) {
    if (info->type == value)
      return info;
    info++;
  }
  return NULL;
}

/*!
 * \brief Finds a named value in an SPA Id table
 *
 * \ingroup wpspatype
 * \param table the id table
 * \param name the full name of a value that is contained in the table
 * \returns (nullable): the WpSpaIdValue associated with \a name, or NULL
 */
WpSpaIdValue
wp_spa_id_table_find_value_from_name (WpSpaIdTable table, const gchar * name)
{
  g_return_val_if_fail (table != NULL, NULL);

  const struct spa_type_info *info = table;
  while (info && info->name) {
    if (!strcmp (info->name, name))
      return info;
    info++;
  }
  return NULL;
}

/*!
 * \brief Finds a short named value in an SPA Id table
 *
 * \ingroup wpspatype
 * \param table the id table
 * \param short_name the short name of a value that is contained in the table
 * \returns (nullable): the WpSpaIdValue associated with \a short_name, or NULL
 */
WpSpaIdValue
wp_spa_id_table_find_value_from_short_name (WpSpaIdTable table,
    const gchar * short_name)
{
  g_return_val_if_fail (table != NULL, NULL);

  const struct spa_type_info *info = table;
  while (info && info->name) {
    if (!strcmp (spa_debug_type_short_name (info->name), short_name))
      return info;
    info++;
  }
  return NULL;
}

static WpSpaIdTable
wp_spa_id_name_find_id_table (const gchar * name)
{
  WpSpaIdTable table = NULL;
  g_autofree gchar *parent_name = g_strdup (name);
  gchar *h;

  if ((h = strrchr(parent_name, ':')) != NULL) {
    /* chop the enum name to get the type, ex:
       Spa:Enum:Direction:Input -> Spa:Enum:Direction */
    *h = '\0';
    table = wp_spa_id_table_from_name (parent_name);

    /* in some cases, the parent name is one layer further up, ex:
       Spa:Pod:Object:Param:Format:Audio:rate -> Spa:Pod:Object:Param:Format */
    if (!table && (h = strrchr(parent_name, ':')) != NULL) {
      *h = '\0';
      table = wp_spa_id_table_from_name (parent_name);
    }
  }
  return table;
}

/*!
 * \brief Looks up an id value (enum, flag or object field) directly from its
 * full name.
 *
 * For instance, "Spa:Enum:Direction:Input" will resolve to the
 * id value that represents "Input" in the "Spa:Enum:Direction" enum.
 *
 * \ingroup wpspatype
 * \param name the full name of an id value
 * \returns the id value for \a name, or NULL if no such id value was found
 */
WpSpaIdValue
wp_spa_id_value_from_name (const gchar * name)
{
  g_return_val_if_fail (name != NULL, NULL);

  WpSpaIdTable table = wp_spa_id_name_find_id_table (name);
  return wp_spa_id_table_find_value_from_name (table, name);
}

/*!
 * \brief Looks up an id value given its container \a table_name and its
 * \a short_name
 *
 * \ingroup wpspatype
 * \param table_name the name of the WpSpaIdTable to look up the value in
 * \param short_name the short name of the value to look up
 * \returns the id value or NULL if it was not found
 */
WpSpaIdValue
wp_spa_id_value_from_short_name (const gchar * table_name,
    const gchar * short_name)
{
  g_return_val_if_fail (table_name != NULL, NULL);
  g_return_val_if_fail (short_name != NULL, NULL);

  WpSpaIdTable table = wp_spa_id_table_from_name (table_name);
  return wp_spa_id_table_find_value_from_short_name (table, short_name);
}

/*!
 * \brief Looks up an id value given its container \a table_name and its numeric
 * representation, \a id
 *
 * \ingroup wpspatype
 * \param table_name the name of the WpSpaIdTable to look up the value in
 * \param id the numeric representation of the value to look up
 * \returns the id value or NULL if it was not found
 */
WpSpaIdValue
wp_spa_id_value_from_number (const gchar * table_name, guint id)
{
  g_return_val_if_fail (table_name != NULL, NULL);

  WpSpaIdTable table = wp_spa_id_table_from_name (table_name);
  return wp_spa_id_table_find_value (table, id);
}

/*!
 * \brief Gets the numeric value of an id value
 *
 * \ingroup wpspatype
 * \param id an id value
 * \returns the numeric representation of this id value
 */
guint
wp_spa_id_value_number (WpSpaIdValue id)
{
  g_return_val_if_fail (id != NULL, -1);

  const struct spa_type_info *info = id;
  return info->type;
}

/*!
 * \brief Gets the name of an id value
 *
 * \ingroup wpspatype
 * \param id an id value
 * \returns the full name of this id value
 */
const gchar *
wp_spa_id_value_name (WpSpaIdValue id)
{
  g_return_val_if_fail (id != NULL, NULL);

  const struct spa_type_info *info = id;
  return info->name;
}

/*!
 * \brief Gets the short name of an id value
 *
 * \ingroup wpspatype
 * \param id an id value
 * \returns the short name of this id value
 */
const gchar *
wp_spa_id_value_short_name (WpSpaIdValue id)
{
  g_return_val_if_fail (id != NULL, NULL);

  const struct spa_type_info *info = id;
  return spa_debug_type_short_name (info->name);
}

/*!
 * \brief Returns the value type associated with this WpSpaIdValue.
 *
 * This information is useful when \a id represents an object field,
 * which can take a value of an arbitrary type.
 *
 * When the returned type is (or is derived from) `SPA_TYPE_Id` or
 * `SPA_TYPE_Object`, \a table is set to point to the WpSpaIdTable
 * that contains the possible Id values / object fields.
 *
 * \ingroup wpspatype
 * \param id an id value
 * \param table (out) (optional): the associated WpSpaIdTable
 * \returns (transfer none): the value type associated with \a id
 */
WpSpaType
wp_spa_id_value_get_value_type (WpSpaIdValue id, WpSpaIdTable * table)
{
  g_return_val_if_fail (id != NULL, WP_SPA_TYPE_INVALID);

  const struct spa_type_info *info = id;

  if (table) {
    /* info->values has different semantics on Array types */
    if (info->values && info->parent != SPA_TYPE_Array) {
      *table = info->values;
    }
    /* derived object types normally don't have info->values directly set,
       so we need to look them up */
    else if (wp_spa_type_is_object (info->parent)) {
      WpSpaIdTable t = wp_spa_type_get_values_table (info->parent);
      if (t) *table = t;
    }
  }

  return info->parent;
}

/*!
 * \brief If the value type of \a id is `SPA_TYPE_Array`, this function
 * returns the type that is allowed to be contained inside the array.
 *
 * When the returned type is (or is derived from) `SPA_TYPE_Id` or
 * `SPA_TYPE_Object`, \a table is set to point to the WpSpaIdTable
 * that contains the possible Id values / object fields.
 *
 * \ingroup wpspatype
 * \param id an id value
 * \param table (out) (optional): the associated WpSpaIdTable
 * \returns (transfer none): the type that is allowed in the array, if \a id
 *   represents an object field that takes an array as value
 */
WpSpaType
wp_spa_id_value_array_get_item_type (WpSpaIdValue id, WpSpaIdTable * table)
{
  g_return_val_if_fail (id != NULL, WP_SPA_TYPE_INVALID);

  const struct spa_type_info *info = id;
  g_return_val_if_fail (info->parent == SPA_TYPE_Array, WP_SPA_TYPE_INVALID);

  return info->values ?
      wp_spa_id_value_get_value_type (info->values, table) :
      WP_SPA_TYPE_INVALID;
}

/*!
 * \brief Initializes the spa dynamic type registry.
 *
 * This allows registering new spa types at runtime. The spa type system
 * still works if this function is not called.
 *
 * Normally called by wp_init() when WP_INIT_SPA_TYPES is passed in its flags.
 *
 * \ingroup wpspatype
 */
void
wp_spa_dynamic_type_init (void)
{
  extra_types = g_array_new (TRUE, FALSE, sizeof (struct spa_type_info));
  extra_id_tables = g_array_new (TRUE, FALSE, sizeof (WpSpaIdTableInfo));

  /* init to chain up to spa types */
  struct spa_type_info info = {
      SPA_ID_INVALID, SPA_ID_INVALID, "spa_types", SPA_TYPE_ROOT
  };
  g_array_append_val (extra_types, info);
}

/*!
 * \brief Deinitializes the spa type registry.
 *
 * You do not need to ever call this, unless you want to free memory at the
 * end of the execution of a test, so that it doesn't show as leaked in
 * the memory profiler.
 *
 * \ingroup wpspatype
 */
void
wp_spa_dynamic_type_deinit (void)
{
  g_clear_pointer (&extra_types, g_array_unref);
  g_clear_pointer (&extra_id_tables, g_array_unref);
}

/*!
 * \brief Registers an additional type in the spa type system.
 *
 * This is useful to add a custom pod object type.
 *
 * Note that both \a name and \a values must be statically allocated, or
 * otherwise guaranteed to be kept in memory until wp_spa_dynamic_type_deinit()
 * is called. No memory copy is done by this function.
 *
 * \ingroup wpspatype
 * \param name the name of the type
 * \param parent the parent type
 * \param values an array of `spa_type_info` that contains the values of the type,
 *   used only for Object types
 * \returns (transfer none): the new type
 */
WpSpaType
wp_spa_dynamic_type_register (const gchar *name, WpSpaType parent,
    const struct spa_type_info * values)
{
  struct spa_type_info info;
  info.type = SPA_TYPE_VENDOR_WirePlumber + extra_types->len;
  info.name = name;
  info.parent = parent;
  info.values = values;
  g_array_append_val (extra_types, info);
  return info.type;
}

/*!
 * \brief Registers an additional WpSpaIdTable in the spa type system.
 *
 * This is useful to add custom enumeration types.
 *
 * Note that both \a name and \a values must be statically allocated, or
 * otherwise guaranteed to be kept in memory until wp_spa_dynamic_type_deinit()
 * is called. No memory copy is done by this function.
 *
 * \ingroup wpspatype
 * \param name the name of the id table
 * \param values an array of `spa_type_info` that contains the values of the table
 * \returns the new table
 */
WpSpaIdTable
wp_spa_dynamic_id_table_register (const gchar *name,
    const struct spa_type_info * values)
{
  WpSpaIdTableInfo info;
  info.name = name;
  info.values = values;
  g_array_append_val (extra_id_tables, info);
  return values;
}
070701000000C3000081A400000000000000000000000165F8630400000C20000000000000000000000000000000000000002400000000wireplumber-0.5.0/lib/wp/spa-type.h   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_SPA_TYPE_H__
#define __WIREPLUMBER_SPA_TYPE_H__

#include "defs.h"
#include "iterator.h"

G_BEGIN_DECLS

/*!
 * \ingroup wpspatype
 */
typedef guint32 WpSpaType;
/*!
 * \ingroup wpspatype
 */
typedef gconstpointer WpSpaIdTable;
/*!
 * \ingroup wpspatype
 */
typedef gconstpointer WpSpaIdValue;
struct spa_type_info;

/* WpSpaType */

/*!
 * \brief The WpSpaType GType
 * \ingroup wpspatype
 */
#define WP_TYPE_SPA_TYPE (wp_spa_type_get_type ())
WP_API
GType wp_spa_type_get_type (void);

/*!
 * \brief Type id representing an invalid SPA type
 * \ingroup wpspatype
 */
static const WpSpaType WP_SPA_TYPE_INVALID = 0xffffffff;

WP_API
WpSpaType wp_spa_type_from_name (const gchar *name);

WP_API
WpSpaType wp_spa_type_parent (WpSpaType type);

WP_API
const gchar * wp_spa_type_name (WpSpaType type);

WP_API
gboolean wp_spa_type_is_fundamental (WpSpaType type);

WP_API
gboolean wp_spa_type_is_id (WpSpaType type);

WP_API
gboolean wp_spa_type_is_object (WpSpaType type);

WP_API
WpSpaIdTable wp_spa_type_get_object_id_values_table (WpSpaType type);

WP_API
WpSpaIdTable wp_spa_type_get_values_table (WpSpaType type);

/*!
 * \brief The WpSpaIdTable GType
 * \ingroup wpspatype
 */
#define WP_TYPE_SPA_ID_TABLE (wp_spa_id_table_get_type ())
WP_API
GType wp_spa_id_table_get_type (void);

WP_API
WpSpaIdTable wp_spa_id_table_from_name (const gchar *name);

WP_API
WpIterator * wp_spa_id_table_new_iterator (WpSpaIdTable table);

WP_API
WpSpaIdValue wp_spa_id_table_find_value (WpSpaIdTable table, guint value);

WP_API
WpSpaIdValue wp_spa_id_table_find_value_from_name (WpSpaIdTable table,
    const gchar * name);

WP_API
WpSpaIdValue wp_spa_id_table_find_value_from_short_name (WpSpaIdTable table,
    const gchar * short_name);

/*!
 * \brief The WpSpaIdValue GType
 * \ingroup wpspatype
 */
#define WP_TYPE_SPA_ID_VALUE (wp_spa_id_value_get_type ())
WP_API
GType wp_spa_id_value_get_type (void);

WP_API
WpSpaIdValue wp_spa_id_value_from_name (const gchar * name);

WP_API
WpSpaIdValue wp_spa_id_value_from_short_name (const gchar * table_name,
    const gchar * short_name);

WP_API
WpSpaIdValue wp_spa_id_value_from_number (const gchar * table_name, guint id);

WP_API
guint wp_spa_id_value_number (WpSpaIdValue id);

WP_API
const gchar * wp_spa_id_value_name (WpSpaIdValue id);

WP_API
const gchar * wp_spa_id_value_short_name (WpSpaIdValue id);

WP_API
WpSpaType wp_spa_id_value_get_value_type (WpSpaIdValue id, WpSpaIdTable *table);

WP_API
WpSpaType wp_spa_id_value_array_get_item_type (WpSpaIdValue id,
    WpSpaIdTable *table);


/* Dynamic type registration */

WP_API
void wp_spa_dynamic_type_init (void);

WP_API
void wp_spa_dynamic_type_deinit (void);

WP_API
WpSpaType wp_spa_dynamic_type_register (const gchar *name, WpSpaType parent,
    const struct spa_type_info * values);

WP_API
WpSpaIdTable wp_spa_dynamic_id_table_register (const gchar *name,
    const struct spa_type_info * values);

G_END_DECLS

#endif
070701000000C4000081A400000000000000000000000165F8630400002D52000000000000000000000000000000000000002100000000wireplumber-0.5.0/lib/wp/state.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <stdio.h>
#include <errno.h>

#include "log.h"
#include "state.h"
#include "wp.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-state")

#define DEFAULT_TIMEOUT_MS 1000
#define ESCAPED_CHARACTER '\\'

static char *
escape_string (const gchar *str)
{
  char *res = NULL;
  size_t str_size, i, j;

  g_return_val_if_fail (str, NULL);
  str_size = strlen (str);
  g_return_val_if_fail (str_size > 0, NULL);

  res = g_malloc_n ((str_size * 2) + 1, sizeof(gchar));

  j = 0;
  for (i = 0; i < str_size; i++) {
    switch (str[i]) {
      case ESCAPED_CHARACTER:
        res[j++] = ESCAPED_CHARACTER;
        res[j++] = ESCAPED_CHARACTER;
        break;
      case ' ':
        res[j++] = ESCAPED_CHARACTER;
        res[j++] = 's';
        break;
      case '=':
        res[j++] = ESCAPED_CHARACTER;
        res[j++] = 'e';
        break;
      case '[':
        res[j++] = ESCAPED_CHARACTER;
        res[j++] = 'o';
        break;
      case ']':
        res[j++] = ESCAPED_CHARACTER;
        res[j++] = 'c';
        break;
      default:
        res[j++] = str[i];
        break;
    }
  }
  res[j++] = '\0';

  return res;
}

static char *
compress_string (const gchar *str)
{
  char *res = NULL;
  size_t str_size, i, j;

  g_return_val_if_fail (str, NULL);
  str_size = strlen (str);
  g_return_val_if_fail (str_size > 0, NULL);

  res = g_malloc_n (str_size + 1, sizeof(gchar));

  j = 0;
  for (i = 0; i < str_size - 1; i++) {
    if (str[i] == ESCAPED_CHARACTER) {
      switch (str[i + 1]) {
        case ESCAPED_CHARACTER:
          res[j++] = ESCAPED_CHARACTER;
          break;
        case 's':
          res[j++] = ' ';
          break;
        case 'e':
          res[j++] = '=';
          break;
        case 'o':
          res[j++] = '[';
          break;
        case 'c':
          res[j++] = ']';
          break;
        default:
          res[j++] = str[i];
          break;
      }
      i++;
    } else {
      res[j++] = str[i];
    }
  }
  if (i < str_size)
    res[j++] = str[i];
  res[j++] = '\0';

  return res;
}

/*! \defgroup wpstate WpState */
/*!
 * \struct WpState
 *
 * The WpState class saves and loads properties from a file
 *
 * \gproperties
 * \gproperty{name, gchar *, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The file name where the state will be stored.}
 */

enum {
  PROP_0,
  PROP_NAME,
  PROP_TIMEOUT,
};

struct _WpState
{
  GObject parent;

  /* Props */
  gchar *name;
  guint timeout;

  gchar *location;
  GSource *timeout_source;
  WpProperties *timeout_props;
};

G_DEFINE_TYPE (WpState, wp_state, G_TYPE_OBJECT)

/* Gets the full path to the WirePlumber XDG_STATE_HOME subdirectory */
static const gchar *
wp_get_xdg_state_dir (void)
{
  static gchar xdg_dir[PATH_MAX] = {0};
  if (xdg_dir[0] == '\0') {
    g_autofree gchar *path = NULL;
    g_autofree gchar *base = g_strdup (g_getenv ("XDG_STATE_HOME"));
    if (!base)
      base = g_build_filename (g_get_home_dir (), ".local", "state", NULL);

    path = g_build_filename (base, "wireplumber", NULL);
    (void) g_strlcpy (xdg_dir, path, sizeof (xdg_dir));
  }
  return xdg_dir;
}

static char *
get_new_location (const char *name)
{
  const gchar *path = wp_get_xdg_state_dir ();

  /* Create the directory if it doesn't exist */
  if (g_mkdir_with_parents (path, 0700) < 0)
    wp_warning ("failed to create directory %s: %s", path, g_strerror (errno));

  return g_build_filename (path, name, NULL);
}

static void
wp_state_ensure_location (WpState *self)
{
  if (!self->location)
    self->location = get_new_location (self->name);
  g_return_if_fail (self->location);
}

static void
wp_state_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpState *self = WP_STATE (object);

  switch (property_id) {
  case PROP_NAME:
    g_clear_pointer (&self->name, g_free);
    self->name = g_value_dup_string (value);
    break;
  case PROP_TIMEOUT:
    self->timeout = g_value_get_uint (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_state_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpState *self = WP_STATE (object);

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, self->name);
    break;
  case PROP_TIMEOUT:
    g_value_set_uint (value, self->timeout);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_state_finalize (GObject * object)
{
  WpState * self = WP_STATE (object);

  g_clear_pointer (&self->name, g_free);
  g_clear_pointer (&self->location, g_free);
  g_clear_pointer (&self->timeout_source, g_source_unref);
  g_clear_pointer (&self->timeout_props, wp_properties_unref);

  G_OBJECT_CLASS (wp_state_parent_class)->finalize (object);
}

static void
wp_state_init (WpState * self)
{
  self->timeout = DEFAULT_TIMEOUT_MS;
}

static void
wp_state_class_init (WpStateClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->finalize = wp_state_finalize;
  object_class->set_property = wp_state_set_property;
  object_class->get_property = wp_state_get_property;

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name",
          "The file name where the state will be stored", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_TIMEOUT,
      g_param_spec_uint ("timeout", "timeout",
          "The timeout in milliseconds to save the state", 0, G_MAXUINT,
          DEFAULT_TIMEOUT_MS, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Constructs a new state object
 * \ingroup wpstate
 * \param name the state name
 * \returns (transfer full): the new WpState
 */
WpState *
wp_state_new (const gchar *name)
{
  g_return_val_if_fail (name, NULL);
  return g_object_new (wp_state_get_type (),
      "name", name,
      NULL);
}

/*!
 * \brief Gets the name of a state object
 * \ingroup wpstate
 * \param self the state
 * \returns the name of this state
 */
const gchar *
wp_state_get_name (WpState *self)
{
  g_return_val_if_fail (WP_IS_STATE (self), NULL);

  return self->name;
}

/*!
 * \brief Gets the location of a state object
 * \ingroup wpstate
 * \param self the state
 * \returns the location of this state
 */
const gchar *
wp_state_get_location (WpState *self)
{
  g_return_val_if_fail (WP_IS_STATE (self), NULL);
  wp_state_ensure_location (self);

  return self->location;
}

/*!
 * \brief Clears the state removing its file
 * \ingroup wpstate
 * \param self the state
 */
void
wp_state_clear (WpState *self)
{
  g_return_if_fail (WP_IS_STATE (self));
  wp_state_ensure_location (self);
  if (remove (self->location) < 0)
    wp_warning ("failed to remove %s: %s", self->location, g_strerror (errno));
}

/*!
 * \brief Saves new properties in the state, overwriting all previous data.
 * \ingroup wpstate
 * \param self the state
 * \param props (transfer none): the properties to save
 * \param error (out)(optional): return location for a GError, or NULL
 * \returns TRUE if the properties could be saved, FALSE otherwise
 */
gboolean
wp_state_save (WpState *self, WpProperties *props, GError ** error)
{
  g_autoptr (GKeyFile) keyfile = g_key_file_new ();
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;
  GError *err = NULL;

  g_return_val_if_fail (WP_IS_STATE (self), FALSE);
  g_return_val_if_fail (props, FALSE);
  wp_state_ensure_location (self);

  wp_info_object (self, "saving state into %s", self->location);

  /* Set the properties */
  for (it = wp_properties_new_iterator (props);
      wp_iterator_next (it, &item);
      g_value_unset (&item)) {
    WpPropertiesItem *pi = g_value_get_boxed (&item);
    const gchar *key = wp_properties_item_get_key (pi);
    const gchar *val = wp_properties_item_get_value (pi);
    g_autofree gchar *escaped_key = escape_string (key);
    if (escaped_key)
      g_key_file_set_string (keyfile, self->name, escaped_key, val);
  }

  if (!g_key_file_save_to_file (keyfile, self->location, &err)) {
    g_propagate_prefixed_error (error, err, "could not save %s: ", self->name);
    return FALSE;
  }

  return TRUE;
}

static gboolean
timeout_save_state_callback (WpState *self)
{
  g_autoptr (GError) error = NULL;

  if (!wp_state_save (self, self->timeout_props, &error))
    wp_warning_object (self, "%s", error->message);

  g_clear_pointer (&self->timeout_source, g_source_unref);
  g_clear_pointer (&self->timeout_props, wp_properties_unref);

  return G_SOURCE_REMOVE;
}

/*!
 * \brief Saves new properties in the state, overwriting all previous data,
 *   after a timeout
 *
 * This is similar to wp_state_save() but it will save the state after a timeout
 * has elapsed. If the state is saved again before the timeout elapses, the
 * timeout is reset.
 *
 * This function is useful to avoid saving the state too often. When called
 * consecutively, it will save the state only once. Every time it is called,
 * it will cancel the previous timer and start a new one, resulting in timing
 * out only after the last call.
 *
 * \ingroup wpstate
 * \param self the state
 * \param core the core, used to add the timeout callback to the main loop
 * \param props (transfer none): the properties to save. This object will be
 *   referenced and kept alive until the timeout elapses, but not deep copied.
 * \since 0.5.0
 */
void
wp_state_save_after_timeout (WpState *self, WpCore *core, WpProperties *props)
{
  /* Clear the current timeout callback */
  if (self->timeout_source)
    g_source_destroy (self->timeout_source);
  g_clear_pointer (&self->timeout_source, g_source_unref);
  g_clear_pointer (&self->timeout_props, wp_properties_unref);

  self->timeout_props = wp_properties_ref (props);

  /* Add the timeout callback */
  wp_core_timeout_add_closure (core, &self->timeout_source, self->timeout,
      g_cclosure_new_object (G_CALLBACK (timeout_save_state_callback),
          G_OBJECT (self)));
}

/*!
 * \brief Loads the state data from the file system
 *
 * This function will never fail. If it cannot load the state, for any reason,
 * it will simply return an empty WpProperties, behaving as if there was no
 * previous state stored.
 *
 * \ingroup wpstate
 * \param self the state
 * \returns (transfer full): a new WpProperties containing the state data
 */
WpProperties *
wp_state_load (WpState *self)
{
  g_autoptr (GKeyFile) keyfile = g_key_file_new ();
  g_autoptr (WpProperties) props = wp_properties_new_empty ();
  gchar ** keys = NULL;

  g_return_val_if_fail (WP_IS_STATE (self), NULL);
  wp_state_ensure_location (self);

  /* Open */
  if (!g_key_file_load_from_file (keyfile, self->location,
      G_KEY_FILE_NONE, NULL))
    return g_steal_pointer (&props);

  /* Load all keys */
  keys = g_key_file_get_keys (keyfile, self->name, NULL, NULL);
  if (!keys)
    return g_steal_pointer (&props);

  for (guint i = 0; keys[i]; i++) {
    g_autofree gchar *compressed_key = NULL;
    const gchar *key = keys[i];
    g_autofree gchar *val = NULL;
    val = g_key_file_get_string (keyfile, self->name, key, NULL);
    if (!val)
      continue;
    compressed_key = compress_string (key);
    if (compressed_key)
      wp_properties_set (props, compressed_key, val);
  }

  g_strfreev (keys);

  return g_steal_pointer (&props);
}
  070701000000C5000081A400000000000000000000000165F86304000003A4000000000000000000000000000000000000002100000000wireplumber-0.5.0/lib/wp/state.h  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_STATE_H__
#define __WIREPLUMBER_STATE_H__

#include "properties.h"
#include "core.h"

G_BEGIN_DECLS

/* WpState */

/*!
 * \brief The WpState GType
 * \ingroup wpstate
 */
#define WP_TYPE_STATE (wp_state_get_type ())
WP_API
G_DECLARE_FINAL_TYPE (WpState, wp_state, WP, STATE, GObject)

WP_API
WpState * wp_state_new (const gchar *name);

WP_API
const gchar * wp_state_get_name (WpState *self);

WP_API
const gchar * wp_state_get_location (WpState *self);

WP_API
void wp_state_clear (WpState *self);

WP_API
gboolean wp_state_save (WpState *self, WpProperties *props, GError ** error);

WP_API
void wp_state_save_after_timeout (WpState *self, WpCore *core,
    WpProperties *props);

WP_API
WpProperties * wp_state_load (WpState *self);

G_END_DECLS

#endif
070701000000C6000081A400000000000000000000000165F86304000043AC000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/transition.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "transition.h"
#include "log.h"
#include "error.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-transition")

/*! \defgroup wptransition Transitions */
/*!
 * \struct WpTransition
 *
 * A transition is an asynchronous operation, like GTask, that contains an
 * internal state machine, where a series of 'steps' are executed in order to
 * complete the operation.
 *
 * For every step, _WpTransitionClass::get_next_step()
 * is called in order to determine the next step to execute. Afterwards,
 * _WpTransitionClass::execute_step() is called
 * to perform any actions necessary to complete this step. When execution
 * of the step is done, the operation's code must call wp_transition_advance()
 * in order to continue to the next step. If an error occurs, the operation's
 * code must call wp_transition_return_error() instead, in which case the
 * transition completes immediately and wp_transition_had_error() returns TRUE.
 *
 * Typically, every step will start an asynchronous operation. Although it is
 * possible, the WpTransition base class does not expect
 * _WpTransitionClass::execute_step() to call wp_transition_advance() directly.
 * Instead, it is expected that wp_transition_advance() will be called from
 * the callback that the step's asynchronous operation will call when it is
 * completed.
 *
 * \gproperties

 * \gproperty{completed, gboolean, G_PARAM_READABLE,
 *   Whether the transition has completed\, meaning its callback (if set)
 *   has been invoked. This can only happen after the final step has been
 *   reached or wp_transition_return_error() has been called.
 *   \n
 *   This property is guaranteed to change from FALSE to TRUE exactly once.
 *   \n
 *   The GObject \c notify signal for this change is emitted in the same context
 *   as the transition’s callback\, immediately after that callback is invoked.}
 */

typedef struct _WpTransitionPrivate WpTransitionPrivate;
struct _WpTransitionPrivate
{
  /* source obj & callback */
  GObject *source_object;
  GCancellable *cancellable;
  GClosure *closure;

  /* GAsyncResult tag */
  gpointer tag;

  /* task data */
  gpointer data;
  GDestroyNotify data_destroy;

  /* state machine */
  gboolean started;
  guint step;
  GError *error;
};

enum {
  PROP_0,
  PROP_COMPLETED,
};

static void wp_transition_async_result_init (GAsyncResultIface *iface);

G_DEFINE_ABSTRACT_TYPE_WITH_CODE (WpTransition, wp_transition, G_TYPE_OBJECT,
    G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_RESULT, wp_transition_async_result_init)
    G_ADD_PRIVATE (WpTransition))

static void
wp_transition_init (WpTransition * self)
{
}

static void
wp_transition_finalize (GObject * object)
{
  WpTransition * self = WP_TRANSITION (object);
  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);

  if (priv->data && priv->data_destroy)
    priv->data_destroy (priv->data);

  g_clear_error (&priv->error);
  g_clear_pointer (&priv->closure, g_closure_unref);
  g_clear_object (&priv->cancellable);
  g_clear_object (&priv->source_object);

  G_OBJECT_CLASS (wp_transition_parent_class)->finalize (object);
}

static void
wp_transition_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpTransition * self = WP_TRANSITION (object);

  switch (property_id) {
  case PROP_COMPLETED:
    g_value_set_boolean (value, wp_transition_get_completed (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_transition_class_init (WpTransitionClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->finalize = wp_transition_finalize;
  object_class->get_property = wp_transition_get_property;

  g_object_class_install_property (object_class, PROP_COMPLETED,
      g_param_spec_boolean ("completed", "completed",
          "Whether the transition has completed", FALSE,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

static GObject *
get_source_object (GAsyncResult * res)
{
  WpTransitionPrivate *priv =
      wp_transition_get_instance_private (WP_TRANSITION (res));
  return priv->source_object ? g_object_ref (priv->source_object) : NULL;
}

static void
wp_transition_async_result_init (GAsyncResultIface * iface)
{
  iface->get_source_object = get_source_object;
  iface->get_user_data =
      (gpointer (*)(GAsyncResult *)) wp_transition_get_data;
  iface->is_tagged =
      (gboolean (*)(GAsyncResult *, gpointer)) wp_transition_is_tagged;
}

/*!
 * \brief Creates a WpTransition acting on \a source_object.
 *
 * When the transition is done, \a callback will be invoked.
 *
 * The transition does not automatically start executing steps. You must
 * call wp_transition_advance() after creating it in order to start it.
 *
 * \note The transition is automatically unref'ed after the \a callback
 * has been executed. If you wish to keep an additional reference on it,
 * you need to ref it explicitly.
 *
 * \ingroup wptransition
 * \param type the GType of the WpTransition subclass to instantiate
 * \param source_object (nullable) (type GObject): the GObject that owns this
 *   task, or NULL
 * \param cancellable (nullable): optional GCancellable
 * \param callback (scope async): a GAsyncReadyCallback
 * \param callback_data (closure): user data passed to \a callback
 * \returns (transfer none): the new transition
 */
WpTransition *
wp_transition_new (GType type,
    gpointer source_object, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data)
{
  return wp_transition_new_closure (type, source_object, cancellable,
      g_cclosure_new (G_CALLBACK (callback), callback_data, NULL));
}

/*!
 * \brief Creates a WpTransition acting on \a source_object.
 * When the transition is done, \a closure will be invoked.
 *
 * The transition does not automatically start executing steps. You must
 * call wp_transition_advance() after creating it in order to start it.
 *
 * Note that the transition is automatically unref'ed after the \a closure
 * has been executed. If you wish to keep an additional reference on it,
 * you need to ref it explicitly.
 *
 * \ingroup wptransition
 * \param type the GType of the WpTransition subclass to instantiate
 * \param source_object (nullable) (type GObject): the GObject that owns this
 *   task, or NULL
 * \param cancellable (nullable): optional GCancellable
 * \param closure (nullable): a GAsyncReadyCallback wrapped in a GClosure
 * \returns (transfer none): the new transition
 */
WpTransition *
wp_transition_new_closure (GType type, gpointer source_object,
    GCancellable * cancellable, GClosure * closure)
{
  g_return_val_if_fail (g_type_is_a (type, WP_TYPE_TRANSITION), NULL);
  g_return_val_if_fail (G_IS_OBJECT (source_object), NULL);

  WpTransition *self = g_object_new (type, NULL);
  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);

  priv->source_object = source_object ? g_object_ref (source_object) : NULL;
  priv->cancellable = cancellable ? g_object_ref (cancellable) : NULL;

  if (closure) {
    priv->closure = g_closure_ref (closure);
    g_closure_sink (closure);
    if (G_CLOSURE_NEEDS_MARSHAL (closure))
      g_closure_set_marshal (closure, g_cclosure_marshal_VOID__OBJECT);
  }

  return self;
}

/*!
 * \brief Gets the source object from the transition.
 *
 * Like g_async_result_get_source_object(), but does not ref the object.
 *
 * \ingroup wptransition
 * \param self the transition
 * \returns (transfer none) (type GObject): the source object
 */
gpointer
wp_transition_get_source_object (WpTransition * self)
{
  g_return_val_if_fail (WP_IS_TRANSITION (self), NULL);

  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);
  return priv->source_object;
}

/*!
 * \brief Checks if \a self has the given \a tag (generally a function pointer
 * indicating the function \a self was created by).
 *
 * \ingroup wptransition
 * \param self the transition
 * \param tag a tag
 * \returns TRUE if \a self has the indicated \a tag , FALSE if not.
 */
gboolean
wp_transition_is_tagged (WpTransition * self, gpointer tag)
{
  g_return_val_if_fail (WP_IS_TRANSITION (self), FALSE);

  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);
  return (priv->tag == tag);
}

/*!
 * \brief Gets \a self 's source tag.
 * \see wp_transition_set_source_tag().
 * \ingroup wptransition
 * \param self the transition
 * \returns (transfer none): the transition's source tag
 */
gpointer
wp_transition_get_source_tag (WpTransition * self)
{
  g_return_val_if_fail (WP_IS_TRANSITION (self), NULL);

  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);
  return priv->tag;
}

/*!
 * \brief Sets \a self 's source tag.
 *
 * You can use this to tag a transition's return
 * value with a particular pointer (usually a pointer to the function doing
 * the tagging) and then later check it using wp_transition_get_source_tag()
 * (or g_async_result_is_tagged()) in the transition's "finish" function,
 * to figure out if the response came from a particular place.

 * \ingroup wptransition
 * \param self the transition
 * \param tag an opaque pointer indicating the source of this transition
 */
void
wp_transition_set_source_tag (WpTransition * self, gpointer tag)
{
  g_return_if_fail (WP_IS_TRANSITION (self));

  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);
  priv->tag = tag;
}

/*!
 * \brief Gets \a self 's data.
 * \see wp_transition_set_data().
 * \ingroup wptransition
 * \param self the transition
 * \returns (transfer none): the transition's data
 */
gpointer
wp_transition_get_data (WpTransition * self)
{
  g_return_val_if_fail (WP_IS_TRANSITION (self), NULL);

  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);
  return priv->data;
}

/*!
 * \brief Sets \a self 's data (freeing the existing data, if any). This can be an
 * arbitrary user structure that holds data associated with this transition.
 *
 * \ingroup wptransition
 * \param self the transition
 * \param data (nullable): transition-specific user data
 * \param data_destroy (nullable): GDestroyNotify for \a data
 */
void
wp_transition_set_data (WpTransition * self, gpointer data,
    GDestroyNotify data_destroy)
{
  g_return_if_fail (WP_IS_TRANSITION (self));

  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);
  if (priv->data && priv->data_destroy)
    priv->data_destroy (priv->data);
  priv->data = data;
  priv->data_destroy = data_destroy;
}

/*!
 * \brief Checks if the transition completed.
 * \ingroup wptransition
 * \param self the transition
 * \returns TRUE if the transition has completed (with or without an error),
 *   FALSE otherwise
 */
gboolean
wp_transition_get_completed (WpTransition * self)
{
  g_return_val_if_fail (WP_IS_TRANSITION (self), FALSE);

  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);
  return (priv->step == WP_TRANSITION_STEP_NONE && priv->started) ||
         priv->step == WP_TRANSITION_STEP_ERROR;
}

/*!
 * \brief Checks if the transition completed with an error.
 * \ingroup wptransition
 * \param self the transition
 * \returns TRUE if the transition completed with an error, FALSE otherwise
 */
gboolean
wp_transition_had_error (WpTransition * self)
{
  g_return_val_if_fail (WP_IS_TRANSITION (self), FALSE);

  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);
  return priv->step == WP_TRANSITION_STEP_ERROR;
}

static void
wp_transition_return (WpTransition * self, WpTransitionPrivate *priv)
{
  if (priv->closure) {
    GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT };
    g_value_init (&values[0], G_TYPE_OBJECT);
    g_value_init (&values[1], G_TYPE_OBJECT);
    g_value_set_object (&values[0], priv->source_object);
    g_value_set_object (&values[1], self);
    g_closure_invoke (priv->closure, NULL, 2, values, NULL);
    g_value_unset (&values[0]);
    g_value_unset (&values[1]);
  }

  g_object_notify (G_OBJECT (self), "completed");

  /* WARNING */
  g_object_unref (self);
}

/*!
 * \brief Advances the transition to the next step.
 *
 * This initially calls _WpTransitionClass::get_next_step()
 * in order to determine what the next step is.
 * If _WpTransitionClass::get_next_step() returns a step
 * different than the previous one, it calls
 * _WpTransitionClass::execute_step() to execute it.
 *
 * The very first time that _WpTransitionClass::get_next_step()
 * is called, its \a step parameter equals WP_TRANSITION_STEP_NONE.
 *
 * When _WpTransitionClass::get_next_step() returns
 * WP_TRANSITION_STEP_NONE this function completes the transition,
 * calling the transition's callback and then unref-ing the transition.
 *
 * When _WpTransitionClass::get_next_step() returns
 * WP_TRANSITION_STEP_ERROR, this function calls wp_transition_return_error(),
 * unless it has already been called directly by
 * _WpTransitionClass::get_next_step().
 *
 * In error conditions, _WpTransitionClass::execute_step()
 * is called once with \a step being WP_TRANSITION_STEP_ERROR, allowing the
 * implementation to rollback any changes or cancel underlying jobs, if necessary.
 *
 * \ingroup wptransition
 * \param self the transition
 */
void
wp_transition_advance (WpTransition * self)
{
  g_return_if_fail (WP_IS_TRANSITION (self));

  /* keep a reference to avoid issues when wp_transition_return_error() is
     called from within get_next_step() */
  g_autoptr (WpTransition) self_ref = g_object_ref (self);
  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);
  guint next_step;
  GError *error = NULL;

  priv->started = TRUE;

  if (g_cancellable_set_error_if_cancelled (priv->cancellable, &error)) {
    wp_transition_return_error (self, error);
    return;
  }

  /* find the next step */
  next_step = WP_TRANSITION_GET_CLASS (self)->get_next_step (self, priv->step);

  wp_trace_object (priv->source_object, "transition: %d -> %d", priv->step,
      next_step);

  if (next_step == WP_TRANSITION_STEP_ERROR) {
    /* return error if the callback didn't do it already */
    if (G_UNLIKELY (!priv->error)) {
        wp_transition_return_error (self, g_error_new (WP_DOMAIN_LIBRARY,
                WP_LIBRARY_ERROR_INVARIANT, "state machine error"));
    }
    return;
  }

  /* if we reached STEP_NONE again, that means we reached the next state */
  if (next_step == WP_TRANSITION_STEP_NONE) {
    /* complete the transition */
    priv->step = next_step;
    wp_transition_return (self, priv);
    return;
  }

  /* still at the same step, this means we are waiting for something */
  if (next_step == priv->step)
    return;

  wp_trace_object (priv->source_object, "transition: execute %d", next_step);

  /* execute the next step */
  priv->step = next_step;
  WP_TRANSITION_GET_CLASS (self)->execute_step (self, priv->step);
}

/*!
 * \brief Completes the transition with an error.
 *
 * This can be called anytime from within any virtual function or an async
 * job handler.
 *
 * \note In most cases this will also unref the transition, so it is
 * not safe to access it after this function has been called.
 *
 * \ingroup wptransition
 * \param self the transition
 * \param error (transfer full): a GError
 */
void
wp_transition_return_error (WpTransition * self, GError * error)
{
  g_return_if_fail (WP_IS_TRANSITION (self));

  WpTransitionPrivate *priv = wp_transition_get_instance_private (self);

  /* don't allow _return_error() to be called multiple times,
     as it is dangerous to recurse in execute_step() */
  if (G_UNLIKELY (priv->error)) {
    wp_warning_object (self, "transition bailing out multiple times; "
        "new error is: %s", error->message);
    return;
  }

  priv->step = WP_TRANSITION_STEP_ERROR;
  priv->error = error;

  /* allow the implementation to rollback changes */
  if (WP_TRANSITION_GET_CLASS (self)->execute_step)
    WP_TRANSITION_GET_CLASS (self)->execute_step (self, priv->step);

  wp_transition_return (self, priv);
}

/*!
 * \brief Returns the final return status of the transition and its error,
 * if there was one.
 *
 * This is meant to be called from within the GAsyncReadyCallback
 * that was specified in wp_transition_new().
 *
 * \ingroup wptransition
 * \param res a transition, as a GAsyncResult
 * \param error (out) (optional): a location to return the transition's error,
 *   if any
 * \returns TRUE if the transition completed successfully, FALSE if there
 *   was an error
 */
gboolean
wp_transition_finish (GAsyncResult * res, GError ** error)
{
  g_return_val_if_fail (WP_IS_TRANSITION (res), FALSE);

  WpTransitionPrivate *priv =
      wp_transition_get_instance_private (WP_TRANSITION (res));
  if (priv->error) {
    g_propagate_error (error, priv->error);
    priv->error = NULL;
  } else if (!priv->started) {
    priv->step = WP_TRANSITION_STEP_ERROR;
    g_propagate_error (error, g_error_new (WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_INVARIANT, "finished before starting"));
  }

  wp_trace_object (priv->source_object, "transition: finished %s",
      (priv->step == WP_TRANSITION_STEP_NONE) ? "ok" : "with error");

  return (priv->step == WP_TRANSITION_STEP_NONE);
}
070701000000C7000081A400000000000000000000000165F8630400000994000000000000000000000000000000000000002600000000wireplumber-0.5.0/lib/wp/transition.h /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_TRANSITION_H__
#define __WIREPLUMBER_TRANSITION_H__

#include <gio/gio.h>
#include "defs.h"

G_BEGIN_DECLS

/*!
 * \brief The WpTransition GType
 * \ingroup wptransition
 */
#define WP_TYPE_TRANSITION (wp_transition_get_type ())
WP_API
G_DECLARE_DERIVABLE_TYPE (WpTransition, wp_transition, WP, TRANSITION, GObject)

/*!
 * \brief Values for the \em steps of the implemented state machine
 * \ingroup wptransition
 */
typedef enum {
  /*! the initial and final step of the transition */
  WP_TRANSITION_STEP_NONE = 0,
  /*! returned by _WpTransitionClass::get_next_step() in case of an error */
  WP_TRANSITION_STEP_ERROR,
  /*! starting value for steps defined in subclasses */
  WP_TRANSITION_STEP_CUSTOM_START = 0x10
} WpTransitionStep;

struct _WpTransitionClass
{
  GObjectClass parent_class;

  /*! See wp_transition_advance() */
  guint (*get_next_step) (WpTransition * transition, guint step);
  /*! See wp_transition_advance() */
  void (*execute_step) (WpTransition * transition, guint step);

  /*< private >*/
  WP_PADDING(6)
};

WP_API
WpTransition * wp_transition_new (GType type,
    gpointer source_object, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data);

WP_API
WpTransition * wp_transition_new_closure (GType type,
    gpointer source_object, GCancellable * cancellable, GClosure * closure);

/* source object */

WP_API
gpointer wp_transition_get_source_object (WpTransition * self);

/* tag */

WP_API
gboolean wp_transition_is_tagged (WpTransition * self, gpointer tag);

WP_API
gpointer wp_transition_get_source_tag (WpTransition * self);

WP_API
void wp_transition_set_source_tag (WpTransition * self, gpointer tag);

/* task data */

WP_API
gpointer wp_transition_get_data (WpTransition * self);

WP_API
void wp_transition_set_data (WpTransition * self, gpointer data,
    GDestroyNotify data_destroy);

/* state machine */

WP_API
gboolean wp_transition_get_completed (WpTransition * self);

WP_API
gboolean wp_transition_had_error (WpTransition * self);

WP_API
void wp_transition_advance (WpTransition * self);

WP_API
void wp_transition_return_error (WpTransition * self, GError * error);

/* result */

WP_API
gboolean wp_transition_finish (GAsyncResult * res, GError ** error);

G_END_DECLS

#endif
070701000000C8000081A400000000000000000000000165F86304000006F8000000000000000000000000000000000000001E00000000wireplumber-0.5.0/lib/wp/wp.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "wp.h"
#include <pipewire/pipewire.h>
#include <libintl.h>
#include "wpbuildbasedirs.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp")

/*!
 * \defgroup wp Library Initialization
 * \{
 */

/*!
 * \brief Initializes WirePlumber and PipeWire underneath.
 *
 * \em flags can modify which parts are initialized, in cases where you want
 * to handle part of this initialization externally.
 *
 * \param flags initialization flags
 */
void
wp_init (WpInitFlags flags)
{
  /* Initialize the logging system */
  wp_log_init (flags);

  wp_info ("WirePlumber " WIREPLUMBER_VERSION " initializing");

  if (flags & WP_INIT_PIPEWIRE)
    pw_init (NULL, NULL);

  if (flags & WP_INIT_SPA_TYPES)
    wp_spa_dynamic_type_init ();

  bindtextdomain (GETTEXT_PACKAGE, BUILD_LOCALEDIR);
  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");

  /* ensure WpProxy subclasses are loaded, which is needed to be able
    to autodetect the GType of proxies created through wp_proxy_new_global() */
  g_type_ensure (WP_TYPE_CLIENT);
  g_type_ensure (WP_TYPE_DEVICE);
  g_type_ensure (WP_TYPE_LINK);
  g_type_ensure (WP_TYPE_METADATA);
  g_type_ensure (WP_TYPE_NODE);
  g_type_ensure (WP_TYPE_PORT);
  g_type_ensure (WP_TYPE_FACTORY);
}

/*!
 * \brief Gets the WirePlumber library version
 * \returns WirePlumber library version
 *
 * \since 0.4.12
 */
const char *
wp_get_library_version (void)
{
  return WIREPLUMBER_VERSION;
}

/*!
 * \brief Gets the WirePlumber library API version
 * \returns WirePlumber library API version
 *
 * \since 0.4.12
 */
const char *
wp_get_library_api_version (void)
{
  return WIREPLUMBER_API_VERSION;
}

/*! \} */
070701000000C9000081A400000000000000000000000165F86304000006ED000000000000000000000000000000000000001E00000000wireplumber-0.5.0/lib/wp/wp.h /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_WP_H__
#define __WIREPLUMBER_WP_H__

#include "base-dirs.h"
#include "client.h"
#include "component-loader.h"
#include "conf.h"
#include "core.h"
#include "device.h"
#include "error.h"
#include "event-dispatcher.h"
#include "event-hook.h"
#include "global-proxy.h"
#include "iterator.h"
#include "json-utils.h"
#include "link.h"
#include "log.h"
#include "metadata.h"
#include "module.h"
#include "node.h"
#include "object-interest.h"
#include "object-manager.h"
#include "object.h"
#include "plugin.h"
#include "port.h"
#include "properties.h"
#include "proxy.h"
#include "proxy-interfaces.h"
#include "session-item.h"
#include "si-factory.h"
#include "si-interfaces.h"
#include "spa-json.h"
#include "spa-pod.h"
#include "spa-type.h"
#include "state.h"
#include "transition.h"
#include "wpenums.h"
#include "wpversion.h"
#include "factory.h"
#include "settings.h"

G_BEGIN_DECLS

/*!
 * \ingroup wp
 * Flags for wp_init()
 */
typedef enum {
  /*! Initialize PipeWire by calling pw_init() */
  WP_INIT_PIPEWIRE = (1<<0),
  /*! Initialize support for dynamic spa types.
   * See wp_spa_dynamic_type_init() */
  WP_INIT_SPA_TYPES = (1<<1),
  /*! Override PipeWire's logging system with WirePlumber's one */
  WP_INIT_SET_PW_LOG = (1<<2),
  /*! Set wp_log_writer_default() as GLib's default log writer function */
  WP_INIT_SET_GLIB_LOG = (1<<3),
  /*! Initialize all of the above */
  WP_INIT_ALL = 0xf,
} WpInitFlags;

WP_API
void wp_init (WpInitFlags flags);

WP_API
const char * wp_get_library_version (void);

WP_API
const char * wp_get_library_api_version (void);

G_END_DECLS

#endif
   070701000000CA000081A400000000000000000000000165F8630400000058000000000000000000000000000000000000002800000000wireplumber-0.5.0/lib/wp/wpversion.h.in   #define WIREPLUMBER_VERSION "@version@"
#define WIREPLUMBER_API_VERSION "@api_version@"
070701000000CB000081A400000000000000000000000165F8630400001655000000000000000000000000000000000000001E00000000wireplumber-0.5.0/meson.build project('wireplumber', ['c'],
  version : '0.5.0',
  license : 'MIT',
  meson_version : '>= 0.59.0',
  default_options : [
    'warning_level=1',
    'buildtype=debugoptimized',
  ]
)

wireplumber_api_version = '0.5'
wireplumber_so_version = '0'

wireplumber_headers_dir = get_option('includedir') / 'wireplumber-' + wireplumber_api_version / 'wp'

wireplumber_bin_dir = get_option('prefix') / get_option('bindir')
wireplumber_module_dir = get_option('prefix') / get_option('libdir') / 'wireplumber-' + wireplumber_api_version
wireplumber_data_dir = get_option('prefix') / get_option('datadir') / 'wireplumber'
wireplumber_config_dir = get_option('prefix') / get_option('sysconfdir') / 'wireplumber'
wireplumber_locale_dir = get_option('prefix') / get_option('localedir')
wireplumber_doc_dir = get_option('prefix') / get_option('datadir') / 'doc' / 'wireplumber'

cc = meson.get_compiler('c')

glib_req_version = '>= 2.68'
add_project_arguments([
    '-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_68',
    '-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_68',
  ], language: 'c'
)

add_project_arguments([
    '-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()),
  ], language: 'c'
)

build_modules = get_option('modules')
build_daemon = get_option('daemon')
if build_daemon and not build_modules
  error('\'modules\' option is required to be true when the \'daemon\' option is true')
endif
build_tools = get_option('tools')
if build_tools and not build_modules
  error('\'modules\' option is required to be true when the \'tools\' option is enabled')
endif

glib_dep = dependency('glib-2.0', version : glib_req_version)
gobject_dep = dependency('gobject-2.0', version : glib_req_version)
gmodule_dep = dependency('gmodule-2.0', version : glib_req_version)
gio_dep = dependency('gio-2.0', version : glib_req_version)
giounix_dep = dependency('gio-unix-2.0', version : glib_req_version)
spa_dep = dependency('libspa-0.2', version: '>= 0.2')
pipewire_dep = dependency('libpipewire-0.3', version: '>= 1.0.2')
mathlib = cc.find_library('m')
threads_dep = dependency('threads')
libintl_dep = dependency('intl')

if build_modules
  system_lua = get_option('system-lua')
  if system_lua
    if get_option('system-lua-version') != 'auto'
      lua_version_requested = get_option('system-lua-version')
      lua_dep = dependency('lua-' + lua_version_requested, required: false)
      if not lua_dep.found()
        lua_dep = dependency('lua' + lua_version_requested, required: false)
      endif

      if not lua_dep.found()
        error('Specified Lua version "' + lua_version_requested + '" not found')
      endif
    else
      lua_dep = dependency('lua-5.4', required: false)
      if not lua_dep.found()
        lua_dep = dependency('lua5.4', required: false)
      endif
      if not lua_dep.found()
        lua_dep = dependency('lua54', required: false)
      endif
      if not lua_dep.found()
        lua_dep = dependency('lua-5.3', required: false)
      endif
      if not lua_dep.found()
        lua_dep = dependency('lua5.3', required: false)
      endif
      if not lua_dep.found()
        lua_dep = dependency('lua53', required: false)
      endif
      if not lua_dep.found()
        lua_dep = dependency('lua', version: ['>=5.3.0'], required: false)
      endif
      if not lua_dep.found()
        error ('Could not find lua. Lua version 5.4 or 5.3 required')
      endif
    endif
  else
    lua_proj = subproject('lua', default_options: ['default_library=static'])
    lua_dep = lua_proj.get_variable('lua_dep')
  endif
  summary({'Lua version': lua_dep.version() + (system_lua ? ' (system)' : ' (built-in)')})
endif

if build_modules
  systemd = dependency('systemd', required: get_option('systemd'))
  libsystemd_dep = dependency('libsystemd',required: get_option('systemd'))
  libelogind_dep = dependency('libelogind', required: get_option('elogind'))
  summary({'systemd conf data': systemd.found(),
          'libsystemd': libsystemd_dep.found(),
          'libelogind': libelogind_dep.found()}, bool_yn: true)
endif

gnome = import('gnome')
pkgconfig = import('pkgconfig')
fs = import('fs')

wp_lib_include_dir = include_directories('lib')

common_flags = [
  '-fvisibility=hidden',
  '-Wsuggest-attribute=format',
  '-Wsign-compare',
  '-Wpointer-arith',
  '-Wpointer-sign',
  '-Wformat',
  '-Wformat-security',
  '-Wimplicit-fallthrough',
  '-Wmissing-braces',
  '-Wtype-limits',
  '-Wvariadic-macros',
  '-Wno-missing-field-initializers',
  '-Wno-unused-parameter',
  '-Wno-pedantic',
  '-Wold-style-declaration',
  '-Wunused-result',
]
add_project_arguments(cc.get_supported_arguments(common_flags), language: 'c')

common_args = [
  '-D_GNU_SOURCE',
  '-DG_LOG_USE_STRUCTURED',
  '-DWP_USE_LOCAL_LOG_TOPIC_IN_G_LOG',
]
add_project_arguments(common_args, language: 'c')

subdir('lib')
if build_modules
  subdir('modules')
endif
subdir('src')
subdir('po')
subdir('docs')

if get_option('tests')
  subdir('tests')
endif

builddir = meson.project_build_root()
srcdir = meson.project_source_root()

conf_uninstalled = configuration_data()
conf_uninstalled.set('MESON', '')
conf_uninstalled.set('MESON_SOURCE_ROOT', srcdir)
conf_uninstalled.set('MESON_BUILD_ROOT', builddir)

wp_uninstalled = configure_file(
  input : 'wp-uninstalled.sh',
  output : 'wp-uninstalled.sh.in',
  configuration : conf_uninstalled,
)

wireplumber_uninstalled = custom_target('wp-uninstalled',
  output : 'wp-uninstalled.sh',
  input : wp_uninstalled,
  build_by_default : true,
  command : ['cp', '@INPUT@', '@OUTPUT@'],
)

devenv = environment({
  'WIREPLUMBER_MODULE_DIR': builddir / 'modules',
  'WIREPLUMBER_CONFIG_DIR': srcdir / 'src' / 'config',
  'WIREPLUMBER_DATA_DIR': srcdir / 'src',
})

meson.add_devenv(devenv)
   070701000000CC000081A400000000000000000000000165F8630400000737000000000000000000000000000000000000002400000000wireplumber-0.5.0/meson_options.txt   option('introspection', type : 'feature', value : 'auto',
       description : 'Generate gobject-introspection bindings')
option('doc', type : 'feature', value : 'auto',
       description: 'Enable documentation.')
option('modules', type : 'boolean', value: true,
       description : 'Build modules')
option('daemon', type : 'boolean', value: true,
       description : 'Build session manager daemon')
option('tools', type : 'boolean', value: true,
       description : 'Build CLI tools')
option('system-lua', type : 'boolean', value : false,
       description : 'Use lua from the system instead of the bundled one')
option('system-lua-version',
       type: 'string', value : 'auto',
       description: 'The system lua version to use or "auto" for auto-detection')
option('elogind',
	type: 'feature', value : 'auto',
	description: 'Enable elogind integration')
option('systemd',
       type: 'feature', value: 'auto',
       description: 'Enable installing systemd units & logind integration')
option('systemd-system-service',
       type : 'boolean', value : false,
       description: 'Install systemd system service file')
option('systemd-user-service',
       type : 'boolean', value : true,
       description: 'Install systemd user service file')
option('systemd-system-unit-dir',
       type : 'string',
       description : 'Directory for system systemd units')
option('systemd-user-unit-dir',
       type : 'string',
       description : 'Directory for user systemd units')
option('glib-supp', type : 'string', value : '',
       description: 'The glib.supp valgrind suppressions file to be used when running valgrind')
option('tests', type : 'boolean', value : true,
       description : 'Build the test suite')
option('dbus-tests', type : 'boolean', value : true,
       description: 'Enable running tests that need a dbus-daemon')
 070701000000CD000081A400000000000000000000000165F86304000334FD000000000000000000000000000000000000002300000000wireplumber-0.5.0/missing-profiles    [
  {
    "id": 0,
    "type": "PipeWire:Interface:Core",
    "version": 4,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "cookie": 6620524,
      "user-name": "antonio",
      "host-name": "weatherwax",
      "version": "1.0.4",
      "name": "pipewire-0",
      "change-mask": [ "props" ],
      "props": {
        "clock.power-of-two-quantum": true,
        "config.name": "pipewire.conf",
        "core.daemon": true,
        "core.name": "pipewire-0",
        "cpu.max-align": 64,
        "default.clock.allowed-rates": "[ 44100 ]",
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 44100,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 16,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.access": true,
        "module.jackdbus-detect": true,
        "module.x11.bell": true,
        "object.id": 0,
        "object.serial": 0,
        "settings.check-quantum": false,
        "settings.check-rate": false
      }
    }
  },
  {
    "id": 1,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-rt",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-rt.so",
      "args": "{\n            nice.level    = -11\n            rt.prio       = 88\n            #rt.time.soft = -1\n            #rt.time.hard = -1\n            #uclamp.min = 0\n            #uclamp.max = 1024\n        }",
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Use realtime thread scheduling, falling back to RTKit",
        "module.name": "libpipewire-module-rt",
        "module.usage": "( nice.level=<priority: default 20(don't change)> ) ( rt.prio=<priority: default 83> ) ( rt.time.soft=<in usec: default -1> ) ( rt.time.hard=<in usec: default -1> ) ( rlimits.enabled=<default true> ) ( rtportal.enabled=<default true> ) ( rtkit.enabled=<default true> ) ( uclamp.min=<default 0> ) ( uclamp.max=<default 1024> )",
        "module.version": "1.0.4",
        "nice.level": -11,
        "object.id": 1,
        "object.serial": 1,
        "rt.prio": 88
      }
    }
  },
  {
    "id": 2,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-protocol-native",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-protocol-native.so",
      "args": "{\n            # List of server Unix sockets, and optionally permissions\n            #sockets = [ { name = \"pipewire-0\" }, { name = \"pipewire-0-manager\" } ]\n        }",
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Native protocol using unix sockets",
        "module.name": "libpipewire-module-protocol-native",
        "module.version": "1.0.4",
        "object.id": 2,
        "object.serial": 2
      }
    }
  },
  {
    "id": 4,
    "type": "PipeWire:Interface:Profiler",
    "version": 3,
    "permissions": [ "r" ],
    "props": {
      "object.serial": 4
    }
  },
  {
    "id": 3,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-profiler",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-profiler.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Generate Profiling data",
        "module.name": "libpipewire-module-profiler",
        "module.version": "1.0.4",
        "object.id": 3,
        "object.serial": 3
      }
    }
  },
  {
    "id": 5,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-metadata",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-metadata.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Allow clients to create metadata store",
        "module.name": "libpipewire-module-metadata",
        "module.version": "1.0.4",
        "object.id": 5,
        "object.serial": 5
      }
    }
  },
  {
    "id": 6,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "metadata",
      "type": "PipeWire:Interface:Metadata",
      "version": 3,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "metadata",
        "factory.type.name": "PipeWire:Interface:Metadata",
        "factory.type.version": 3,
        "factory.usage": "(metadata.name = <name> ) (metadata.values = [    { ( id = <int> ) key = <string> ( type = <string> ) value = <json> }    ...  ] )",
        "module.id": 5,
        "object.id": 6,
        "object.serial": 6
      }
    }
  },
  {
    "id": 7,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-spa-device-factory",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-spa-device-factory.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Provide a factory to make SPA devices",
        "module.name": "libpipewire-module-spa-device-factory",
        "module.version": "1.0.4",
        "object.id": 7,
        "object.serial": 7
      }
    }
  },
  {
    "id": 8,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "spa-device-factory",
      "type": "PipeWire:Interface:Device",
      "version": 3,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "spa-device-factory",
        "factory.type.name": "PipeWire:Interface:Device",
        "factory.type.version": 3,
        "module.id": 7,
        "object.id": 8,
        "object.serial": 8
      }
    }
  },
  {
    "id": 9,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-spa-node-factory",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-spa-node-factory.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Provide a factory to make SPA nodes",
        "module.name": "libpipewire-module-spa-node-factory",
        "module.version": "1.0.4",
        "object.id": 9,
        "object.serial": 9
      }
    }
  },
  {
    "id": 10,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "spa-node-factory",
      "type": "PipeWire:Interface:Node",
      "version": 3,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "spa-node-factory",
        "factory.type.name": "PipeWire:Interface:Node",
        "factory.type.version": 3,
        "module.id": 9,
        "object.id": 10,
        "object.serial": 10
      }
    }
  },
  {
    "id": 11,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-client-node",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-client-node.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Allow clients to create and control remote nodes",
        "module.name": "libpipewire-module-client-node",
        "module.version": "1.0.4",
        "object.id": 11,
        "object.serial": 11
      }
    }
  },
  {
    "id": 12,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "client-node",
      "type": "PipeWire:Interface:ClientNode",
      "version": 5,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "client-node",
        "factory.type.name": "PipeWire:Interface:ClientNode",
        "factory.type.version": 5,
        "module.id": 11,
        "object.id": 12,
        "object.serial": 12
      }
    }
  },
  {
    "id": 13,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-client-device",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-client-device.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Allow clients to create and control remote devices",
        "module.name": "libpipewire-module-client-device",
        "module.version": "1.0.4",
        "object.id": 13,
        "object.serial": 13
      }
    }
  },
  {
    "id": 14,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "client-device",
      "type": "Spa:Pointer:Interface:Device",
      "version": 0,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "client-device",
        "factory.type.name": "Spa:Pointer:Interface:Device",
        "factory.type.version": 0,
        "factory.usage": "[device.name=<string>]",
        "module.id": 13,
        "object.id": 14,
        "object.serial": 14
      }
    }
  },
  {
    "id": 15,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-portal",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-portal.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.name": "libpipewire-module-portal",
        "object.id": 15,
        "object.serial": 15
      }
    }
  },
  {
    "id": 16,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-access",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-access.so",
      "args": "{\n            # Socket-specific access permissions\n            #access.socket = { pipewire-0 = \"default\", pipewire-0-manager = \"unrestricted\" }\n\n            # Deprecated legacy mode (not socket-based),\n            # for now enabled by default if access.socket is not specified\n            #access.legacy = true\n        }",
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Perform access check",
        "module.name": "libpipewire-module-access",
        "module.usage": "( access.socket={ <socket>=<access>, ... } ) ( access.legacy=true ) ",
        "module.version": "1.0.4",
        "object.id": 16,
        "object.serial": 16
      }
    }
  },
  {
    "id": 17,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-adapter",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-adapter.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Manage adapter nodes",
        "module.name": "libpipewire-module-adapter",
        "module.version": "1.0.4",
        "object.id": 17,
        "object.serial": 17
      }
    }
  },
  {
    "id": 18,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "adapter",
      "type": "PipeWire:Interface:Node",
      "version": 3,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "adapter",
        "factory.type.name": "PipeWire:Interface:Node",
        "factory.type.version": 3,
        "factory.usage": "factory.name=<factory-name> (library.name=<library-name>) node.name=<string> ",
        "module.id": 17,
        "object.id": 18,
        "object.serial": 18
      }
    }
  },
  {
    "id": 19,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-link-factory",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-link-factory.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.author": "Wim Taymans <wim.taymans@gmail.com>",
        "module.description": "Allow clients to create links",
        "module.name": "libpipewire-module-link-factory",
        "module.usage": "( allow.link.passive=<bool, default false> ) ",
        "module.version": "1.0.4",
        "object.id": 19,
        "object.serial": 19
      }
    }
  },
  {
    "id": 20,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "link-factory",
      "type": "PipeWire:Interface:Link",
      "version": 3,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "link-factory",
        "factory.type.name": "PipeWire:Interface:Link",
        "factory.type.version": 3,
        "factory.usage": "(link.output.node=<output-node>) (link.output.port=<output-port>) (link.input.node=<input-node>) (link.input.port=<input-port>) (object.linger=<bool>) (link.passive=<bool>)",
        "module.id": 19,
        "object.id": 20,
        "object.serial": 20
      }
    }
  },
  {
    "id": 21,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-session-manager",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-session-manager.so",
      "args": null,
      "change-mask": [ "props" ],
      "props": {
        "module.author": "George Kiagiadakis <george.kiagiadakis@collabora.com>",
        "module.description": "Implements objects for session management",
        "module.name": "libpipewire-module-session-manager",
        "module.version": "1.0.4",
        "object.id": 21,
        "object.serial": 21
      }
    }
  },
  {
    "id": 22,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "client-endpoint",
      "type": "PipeWire:Interface:ClientEndpoint",
      "version": 0,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "client-endpoint",
        "factory.type.name": "PipeWire:Interface:ClientEndpoint",
        "factory.type.version": 0,
        "module.id": 21,
        "object.id": 22,
        "object.serial": 22
      }
    }
  },
  {
    "id": 23,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "client-session",
      "type": "PipeWire:Interface:ClientSession",
      "version": 0,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "client-session",
        "factory.type.name": "PipeWire:Interface:ClientSession",
        "factory.type.version": 0,
        "module.id": 21,
        "object.id": 23,
        "object.serial": 23
      }
    }
  },
  {
    "id": 24,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "session",
      "type": "PipeWire:Interface:Session",
      "version": 0,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "session",
        "factory.type.name": "PipeWire:Interface:Session",
        "factory.type.version": 0,
        "module.id": 21,
        "object.id": 24,
        "object.serial": 24
      }
    }
  },
  {
    "id": 25,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "endpoint",
      "type": "PipeWire:Interface:Endpoint",
      "version": 0,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "endpoint",
        "factory.type.name": "PipeWire:Interface:Endpoint",
        "factory.type.version": 0,
        "module.id": 21,
        "object.id": 25,
        "object.serial": 25
      }
    }
  },
  {
    "id": 26,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "endpoint-stream",
      "type": "PipeWire:Interface:EndpointStream",
      "version": 0,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "endpoint-stream",
        "factory.type.name": "PipeWire:Interface:EndpointStream",
        "factory.type.version": 0,
        "module.id": 21,
        "object.id": 26,
        "object.serial": 26
      }
    }
  },
  {
    "id": 27,
    "type": "PipeWire:Interface:Factory",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "endpoint-link",
      "type": "PipeWire:Interface:EndpointLink",
      "version": 0,
      "change-mask": [ "props" ],
      "props": {
        "factory.name": "endpoint-link",
        "factory.type.name": "PipeWire:Interface:EndpointLink",
        "factory.type.version": 0,
        "module.id": 21,
        "object.id": 27,
        "object.serial": 27
      }
    }
  },
  {
    "id": 28,
    "type": "PipeWire:Interface:Module",
    "version": 3,
    "permissions": [ "r", "m" ],
    "info": {
      "name": "libpipewire-module-jackdbus-detect",
      "filename": "/usr/lib64/pipewire-0.3/libpipewire-module-jackdbus-detect.so",
      "args": "{\n            #jack.library     = libjack.so.0\n            #jack.server      = null\n            #jack.client-name = PipeWire\n            #jack.connect     = true\n            #tunnel.mode      = duplex  # source|sink|duplex\n            source.props = {\n                #audio.channels = 2\n\t\t#midi.ports = 1\n                #audio.position = [ FL FR ]\n                # extra sink properties\n            }\n            sink.props = {\n                #audio.channels = 2\n\t\t#midi.ports = 1\n                #audio.position = [ FL FR ]\n                # extra sink properties\n            }\n        }",
      "change-mask": [ "props" ],
      "props": {
        "module.name": "libpipewire-module-jackdbus-detect",
        "object.id": 28,
        "object.serial": 29
      }
    }
  },
  {
    "id": 29,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 0,
      "max-output-ports": 0,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 0,
      "n-output-ports": 0,
      "state": "suspended",
      "error": null,
      "props": {
        "clock.id": "monotonic",
        "clock.name": "clock.system.monotonic",
        "clock.quantum-limit": 8192,
        "factory.id": 10,
        "factory.name": "support.node.driver",
        "node.driver": true,
        "node.group": "pipewire.dummy",
        "node.name": "Dummy-Driver",
        "object.id": 29,
        "object.serial": 30,
        "priority.driver": 20000
      },
      "params": {
      }
    }
  },
  {
    "id": 30,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 0,
      "max-output-ports": 0,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 0,
      "n-output-ports": 0,
      "state": "suspended",
      "error": null,
      "props": {
        "clock.id": "monotonic",
        "clock.name": "clock.system.monotonic",
        "clock.quantum-limit": 8192,
        "factory.id": 10,
        "factory.name": "support.node.driver",
        "node.driver": true,
        "node.freewheel": true,
        "node.group": "pipewire.freewheel",
        "node.name": "Freewheel-Driver",
        "object.id": 30,
        "object.serial": 31,
        "priority.driver": 19000
      },
      "params": {
      }
    }
  },
  {
    "id": 31,
    "type": "PipeWire:Interface:Metadata",
    "version": 3,
    "permissions": [ "r", "w", "x" ],
    "props": {
      "metadata.name": "settings",
      "object.serial": 32
    },
    "metadata": [
      { "subject": 0, "key": "log.level", "type": "", "value": 2 },
      { "subject": 0, "key": "clock.rate", "type": "", "value": 44100 },
      { "subject": 0, "key": "clock.allowed-rates", "type": "", "value": "[ 44100 ]" },
      { "subject": 0, "key": "clock.quantum", "type": "", "value": 1024 },
      { "subject": 0, "key": "clock.min-quantum", "type": "", "value": 32 },
      { "subject": 0, "key": "clock.max-quantum", "type": "", "value": 2048 },
      { "subject": 0, "key": "clock.force-quantum", "type": "", "value": 0 },
      { "subject": 0, "key": "clock.force-rate", "type": "", "value": 0 }
    ]
  },
  {
    "id": 32,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.language": "en_US.UTF-8",
        "application.name": "WirePlumber",
        "application.process.binary": "wireplumber",
        "application.process.host": "weatherwax",
        "application.process.id": 3785,
        "application.process.user": "antonio",
        "application.version": "0.5.0",
        "clock.power-of-two-quantum": true,
        "config.name": "null",
        "core.name": "pipewire-antonio-3785",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 32,
        "object.serial": 33,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 3785,
        "pipewire.sec.socket": "pipewire-0-manager",
        "pipewire.sec.uid": 1000,
        "remote.name": "[pipewire-0-manager,pipewire-0]",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "wireplumber.daemon": true,
        "wireplumber.profile": "main"
      }
    }
  },
  {
    "id": 33,
    "type": "PipeWire:Interface:Metadata",
    "version": 3,
    "permissions": [ "r", "w", "x" ],
    "props": {
      "client.id": 32,
      "factory.id": 6,
      "metadata.name": "schema-sm-settings",
      "module.id": 5,
      "object.serial": 34
    },
    "metadata": [
      { "subject": 0, "key": "bluetooth.use-persistent-storage", "type": "Spa:String:JSON", "value": { "description": "Whether to use persistent BT storage or not", "type": "bool", "default": true } },
      { "subject": 0, "key": "bluetooth.autoswitch-to-headset-profile", "type": "Spa:String:JSON", "value": { "description": "Whether to autoswitch to BT headset profile or not", "type": "bool", "default": true } },
      { "subject": 0, "key": "device.restore-profile", "type": "Spa:String:JSON", "value": { "description": "Whether to restore device profile or not", "type": "bool", "default": true } },
      { "subject": 0, "key": "device.restore-routes", "type": "Spa:String:JSON", "value": { "description": "Whether to restore device routes or not", "type": "bool", "default": true } },
      { "subject": 0, "key": "device.routes.default-sink-volume", "type": "Spa:String:JSON", "value": { "description": "The default volume for sink devices", "type": "float", "default": 0.064000, "min": 0.000000, "max": 1.000000 } },
      { "subject": 0, "key": "device.routes.default-source-volume", "type": "Spa:String:JSON", "value": { "description": "The default volume for source devices", "type": "float", "default": 1.000000, "min": 0.000000, "max": 1.000000 } },
      { "subject": 0, "key": "linking.allow-moving-streams", "type": "Spa:String:JSON", "value": { "description": "Whether to allow metadata to move streams at runtime or not", "type": "bool", "default": true } },
      { "subject": 0, "key": "linking.follow-default-target", "type": "Spa:String:JSON", "value": { "description": "Whether to allow streams follow the default device or not", "type": "bool", "default": true } },
      { "subject": 0, "key": "monitor.camera-discovery-timeout", "type": "Spa:String:JSON", "value": { "description": "The camera discovery timeout in milliseconds", "type": "int", "default": 100, "min": 0, "max": 60000 } },
      { "subject": 0, "key": "node.features.audio.no-dsp", "type": "Spa:String:JSON", "value": { "description": "Whether to never convert audio to F32 format or not", "type": "bool", "default": false } },
      { "subject": 0, "key": "node.features.audio.monitor-ports", "type": "Spa:String:JSON", "value": { "description": "Whether to enable monitor ports on audio nodes or not", "type": "bool", "default": true } },
      { "subject": 0, "key": "node.features.audio.control-port", "type": "Spa:String:JSON", "value": { "description": "Whether to enable control ports on audio nodes or not", "type": "bool", "default": false } },
      { "subject": 0, "key": "node.stream.restore-props", "type": "Spa:String:JSON", "value": { "description": "Whether to restore properties on stream nodes or not", "type": "bool", "default": true } },
      { "subject": 0, "key": "node.stream.restore-target", "type": "Spa:String:JSON", "value": { "description": "Whether to restore target on stream nodes or not", "type": "bool", "default": true } },
      { "subject": 0, "key": "node.stream.default-playback-volume", "type": "Spa:String:JSON", "value": { "description": "The default volume for playback nodes", "type": "float", "default": 1.000000, "min": 0.000000, "max": 1.000000 } },
      { "subject": 0, "key": "node.stream.default-capture-volume", "type": "Spa:String:JSON", "value": { "description": "The default volume for capture nodes", "type": "float", "default": 1.000000, "min": 0.000000, "max": 1.000000 } },
      { "subject": 0, "key": "node.filter.forward-format", "type": "Spa:String:JSON", "value": { "description": "Whether to forward format on filter nodes or not", "type": "bool", "default": false } },
      { "subject": 0, "key": "node.restore-default-targets", "type": "Spa:String:JSON", "value": { "description": "Whether to restore default targets or not", "type": "bool", "default": true } }
    ]
  },
  {
    "id": 35,
    "type": "PipeWire:Interface:Metadata",
    "version": 3,
    "permissions": [ "r", "w", "x" ],
    "props": {
      "client.id": 32,
      "factory.id": 6,
      "metadata.name": "sm-settings",
      "module.id": 5,
      "object.serial": 36
    },
    "metadata": [
      { "subject": 0, "key": "bluetooth.use-persistent-storage", "type": "Spa:String:JSON", "value": true },
      { "subject": 0, "key": "bluetooth.autoswitch-to-headset-profile", "type": "Spa:String:JSON", "value": true },
      { "subject": 0, "key": "device.restore-profile", "type": "Spa:String:JSON", "value": true },
      { "subject": 0, "key": "device.restore-routes", "type": "Spa:String:JSON", "value": true },
      { "subject": 0, "key": "device.routes.default-sink-volume", "type": "Spa:String:JSON", "value": 0.064000 },
      { "subject": 0, "key": "device.routes.default-source-volume", "type": "Spa:String:JSON", "value": 1.000000 },
      { "subject": 0, "key": "linking.allow-moving-streams", "type": "Spa:String:JSON", "value": true },
      { "subject": 0, "key": "linking.follow-default-target", "type": "Spa:String:JSON", "value": true },
      { "subject": 0, "key": "monitor.camera-discovery-timeout", "type": "Spa:String:JSON", "value": 100 },
      { "subject": 0, "key": "node.features.audio.no-dsp", "type": "Spa:String:JSON", "value": false },
      { "subject": 0, "key": "node.features.audio.monitor-ports", "type": "Spa:String:JSON", "value": true },
      { "subject": 0, "key": "node.features.audio.control-port", "type": "Spa:String:JSON", "value": false },
      { "subject": 0, "key": "node.stream.restore-props", "type": "Spa:String:JSON", "value": true },
      { "subject": 0, "key": "node.stream.restore-target", "type": "Spa:String:JSON", "value": true },
      { "subject": 0, "key": "node.stream.default-playback-volume", "type": "Spa:String:JSON", "value": 1.000000 },
      { "subject": 0, "key": "node.stream.default-capture-volume", "type": "Spa:String:JSON", "value": 1.000000 },
      { "subject": 0, "key": "node.filter.forward-format", "type": "Spa:String:JSON", "value": false },
      { "subject": 0, "key": "node.restore-default-targets", "type": "Spa:String:JSON", "value": true }
    ]
  },
  {
    "id": 37,
    "type": "PipeWire:Interface:Metadata",
    "version": 3,
    "permissions": [ "r", "w", "x" ],
    "props": {
      "client.id": 32,
      "factory.id": 6,
      "metadata.name": "default",
      "module.id": 5,
      "object.serial": 38
    },
    "metadata": [
      { "subject": 0, "key": "default.configured.audio.sink", "type": "Spa:String:JSON", "value": { "name": "alsa_output.usb-Logitech_Logitech_Wireless_Headset_C0288D58B0F1-00.analog-stereo" } },
      { "subject": 0, "key": "default.configured.audio.source", "type": "Spa:String:JSON", "value": { "name": "alsa_input.pci-0000_00_1f.3.analog-stereo" } },
      { "subject": 0, "key": "default.audio.sink", "type": "Spa:String:JSON", "value": { "name": "bluez_output.6C_D3_EE_5A_17_20.1" } },
      { "subject": 0, "key": "default.audio.source", "type": "Spa:String:JSON", "value": { "name": "alsa_input.pci-0000_00_1f.3.analog-stereo" } },
      { "subject": 0, "key": "default.video.source", "type": "Spa:String:JSON", "value": { "name": "v4l2_input.pci-0000_00_14.0-usb-0_3_1.0" } }
    ]
  },
  {
    "id": 40,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.language": "en_US.UTF-8",
        "application.name": "WirePlumber [export]",
        "application.process.binary": "wireplumber",
        "application.process.host": "weatherwax",
        "application.process.id": 3785,
        "application.process.user": "antonio",
        "application.version": "0.5.0",
        "clock.power-of-two-quantum": true,
        "config.name": "null",
        "core.name": "pipewire-antonio-3785",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 40,
        "object.serial": 41,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 3785,
        "pipewire.sec.socket": "pipewire-0-manager",
        "pipewire.sec.uid": 1000,
        "remote.name": "[pipewire-0-manager,pipewire-0]",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "wireplumber.daemon": true,
        "wireplumber.export-core": true,
        "wireplumber.profile": "main"
      }
    }
  },
  {
    "id": 41,
    "type": "PipeWire:Interface:Device",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props", "params" ],
      "props": {
        "alsa.card": 1,
        "alsa.card_name": "WD15 Dock",
        "alsa.driver_name": "snd_usb_audio",
        "alsa.long_card_name": "Dell-WD15-Dock",
        "api.acp.auto-port": false,
        "api.alsa.card": 1,
        "api.alsa.card.longname": "Dell-WD15-Dock",
        "api.alsa.card.name": "WD15 Dock",
        "api.alsa.path": "hw:1",
        "api.alsa.use-acp": true,
        "api.dbus.ReserveDevice1": "Audio1",
        "api.dbus.ReserveDevice1.Priority": -20,
        "client.id": 40,
        "device.api": "alsa",
        "device.bus": "usb",
        "device.bus-id": "usb-Generic_USB_Audio_200901010001-00",
        "device.bus-path": "pci-0000:00:14.0-usb-0:8.5:1.0",
        "device.description": "USB Audio",
        "device.enum.api": "udev",
        "device.icon-name": "audio-card-analog-usb",
        "device.name": "alsa_card.usb-Generic_USB_Audio_200901010001-00",
        "device.nick": "WD15 Dock",
        "device.plugged.usec": 8776684,
        "device.product.id": "0x4014",
        "device.product.name": "USB Audio",
        "device.profile-set": "dell-dock-tb16-usb-audio.conf",
        "device.serial": "Generic_USB_Audio_200901010001",
        "device.string": 1,
        "device.subsystem": "sound",
        "device.sysfs.path": "/devices/pci0000:00/0000:00:14.0/usb3/3-8/3-8.5/3-8.5:1.0/sound/card1",
        "device.vendor.id": "0x0bda",
        "device.vendor.name": "Realtek Semiconductor Corp.",
        "factory.id": 14,
        "media.class": "Audio/Device",
        "object.id": 41,
        "object.path": "alsa:pcm:1",
        "object.serial": 42
      },
      "params": {
        "EnumProfile": [
          {
            "index": 0,
            "name": "off",
            "description": "Off",
            "priority": 0,
            "available": "yes",
            "classes": [
              0
            ]
          },
          {
            "index": 1,
            "name": "HiFi",
            "description": "Default",
            "priority": 8000,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 2 ]
              ],
              [
                "Audio/Sink",
                2,
                "card.profile.devices",
                [ 0, 1 ]
              ]
            ]
          },
          {
            "index": 2,
            "name": "pro-audio",
            "description": "Pro Audio",
            "priority": 1,
            "available": "unknown",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 5 ]
              ],
              [
                "Audio/Sink",
                2,
                "card.profile.devices",
                [ 3, 4 ]
              ]
            ]
          }
        ],
        "Profile": [
          {
            "index": 0,
            "name": "off",
            "description": "Off",
            "priority": 0,
            "available": "yes",
            "classes": [
              0
            ],
            "save": false
          }
        ],
        "EnumRoute": [
          {
            "index": 0,
            "direction": "Output",
            "name": "[Out] Line",
            "description": "Line Out",
            "priority": 200,
            "available": "no",
            "info": [
              4,
              "port.type",
              "line",
              "port.availability-group",
              "Line Out",
              "device.icon_name",
              "audio-speakers",
              "card.profile.port",
              "0"
            ],
            "profiles": [ 1 ],
            "devices": [ 0 ]
          },
          {
            "index": 1,
            "direction": "Output",
            "name": "[Out] Headphones",
            "description": "Headphones",
            "priority": 100,
            "available": "no",
            "info": [
              4,
              "port.type",
              "headphones",
              "port.availability-group",
              "Headphone",
              "device.icon_name",
              "audio-headphones",
              "card.profile.port",
              "1"
            ],
            "profiles": [ 1 ],
            "devices": [ 1 ]
          },
          {
            "index": 2,
            "direction": "Input",
            "name": "[In] Headset",
            "description": "Headset Microphone",
            "priority": 100,
            "available": "no",
            "info": [
              4,
              "port.type",
              "headset",
              "port.availability-group",
              "Headset Mic",
              "device.icon_name",
              "audio-headset",
              "card.profile.port",
              "2"
            ],
            "profiles": [ 1 ],
            "devices": [ 2 ]
          }
        ],
        "Route": [
        ]
      }
    }
  },
  {
    "id": 42,
    "type": "PipeWire:Interface:Device",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props", "params" ],
      "props": {
        "alsa.card": 0,
        "alsa.card_name": "HDA Intel PCH",
        "alsa.driver_name": "snd_hda_intel",
        "alsa.long_card_name": "HDA Intel PCH at 0x6075298000 irq 204",
        "api.acp.auto-port": false,
        "api.alsa.card": 0,
        "api.alsa.card.longname": "HDA Intel PCH at 0x6075298000 irq 204",
        "api.alsa.card.name": "HDA Intel PCH",
        "api.alsa.path": "hw:0",
        "api.alsa.use-acp": true,
        "api.dbus.ReserveDevice1": "Audio0",
        "api.dbus.ReserveDevice1.Priority": -20,
        "client.id": 40,
        "device.api": "alsa",
        "device.bus": "pci",
        "device.bus-path": "pci-0000:00:1f.3",
        "device.description": "Built-in Audio",
        "device.enum.api": "udev",
        "device.form-factor": "internal",
        "device.icon-name": "audio-card-analog-pci",
        "device.name": "alsa_card.pci-0000_00_1f.3",
        "device.nick": "HDA Intel PCH",
        "device.plugged.usec": 6937024,
        "device.product.id": "0x43c8",
        "device.product.name": "Tiger Lake-H HD Audio Controller",
        "device.string": 0,
        "device.subsystem": "sound",
        "device.sysfs.path": "/devices/pci0000:00/0000:00:1f.3/sound/card0",
        "device.vendor.id": "0x8086",
        "device.vendor.name": "Intel Corporation",
        "factory.id": 14,
        "media.class": "Audio/Device",
        "object.id": 42,
        "object.path": "alsa:pcm:0",
        "object.serial": 43
      },
      "params": {
        "EnumProfile": [
          {
            "index": 0,
            "name": "off",
            "description": "Off",
            "priority": 0,
            "available": "yes",
            "classes": [
              0
            ]
          },
          {
            "index": 1,
            "name": "output:analog-stereo+input:analog-stereo",
            "description": "Analog Stereo Duplex",
            "priority": 6565,
            "available": "yes",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 7 ]
              ]
            ]
          },
          {
            "index": 2,
            "name": "output:analog-stereo",
            "description": "Analog Stereo Output",
            "priority": 6500,
            "available": "yes",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 7 ]
              ]
            ]
          },
          {
            "index": 3,
            "name": "output:hdmi-stereo+input:analog-stereo",
            "description": "Digital Stereo (HDMI) Output + Analog Stereo Input",
            "priority": 5965,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 8 ]
              ]
            ]
          },
          {
            "index": 4,
            "name": "output:hdmi-stereo",
            "description": "Digital Stereo (HDMI) Output",
            "priority": 5900,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 8 ]
              ]
            ]
          },
          {
            "index": 5,
            "name": "output:hdmi-stereo-extra1+input:analog-stereo",
            "description": "Digital Stereo (HDMI 2) Output + Analog Stereo Input",
            "priority": 5765,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 11 ]
              ]
            ]
          },
          {
            "index": 6,
            "name": "output:hdmi-stereo-extra2+input:analog-stereo",
            "description": "Digital Stereo (HDMI 3) Output + Analog Stereo Input",
            "priority": 5765,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 14 ]
              ]
            ]
          },
          {
            "index": 7,
            "name": "output:hdmi-stereo-extra3+input:analog-stereo",
            "description": "Digital Stereo (HDMI 4) Output + Analog Stereo Input",
            "priority": 5765,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 17 ]
              ]
            ]
          },
          {
            "index": 8,
            "name": "output:hdmi-stereo-extra1",
            "description": "Digital Stereo (HDMI 2) Output",
            "priority": 5700,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 11 ]
              ]
            ]
          },
          {
            "index": 9,
            "name": "output:hdmi-stereo-extra2",
            "description": "Digital Stereo (HDMI 3) Output",
            "priority": 5700,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 14 ]
              ]
            ]
          },
          {
            "index": 10,
            "name": "output:hdmi-stereo-extra3",
            "description": "Digital Stereo (HDMI 4) Output",
            "priority": 5700,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 17 ]
              ]
            ]
          },
          {
            "index": 11,
            "name": "output:hdmi-surround+input:analog-stereo",
            "description": "Digital Surround 5.1 (HDMI) Output + Analog Stereo Input",
            "priority": 865,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 9 ]
              ]
            ]
          },
          {
            "index": 12,
            "name": "output:hdmi-surround71+input:analog-stereo",
            "description": "Digital Surround 7.1 (HDMI) Output + Analog Stereo Input",
            "priority": 865,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 10 ]
              ]
            ]
          },
          {
            "index": 13,
            "name": "output:hdmi-surround",
            "description": "Digital Surround 5.1 (HDMI) Output",
            "priority": 800,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 9 ]
              ]
            ]
          },
          {
            "index": 14,
            "name": "output:hdmi-surround71",
            "description": "Digital Surround 7.1 (HDMI) Output",
            "priority": 800,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 10 ]
              ]
            ]
          },
          {
            "index": 15,
            "name": "output:hdmi-surround-extra1+input:analog-stereo",
            "description": "Digital Surround 5.1 (HDMI 2) Output + Analog Stereo Input",
            "priority": 665,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 12 ]
              ]
            ]
          },
          {
            "index": 16,
            "name": "output:hdmi-surround71-extra1+input:analog-stereo",
            "description": "Digital Surround 7.1 (HDMI 2) Output + Analog Stereo Input",
            "priority": 665,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 13 ]
              ]
            ]
          },
          {
            "index": 17,
            "name": "output:hdmi-surround-extra2+input:analog-stereo",
            "description": "Digital Surround 5.1 (HDMI 3) Output + Analog Stereo Input",
            "priority": 665,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 15 ]
              ]
            ]
          },
          {
            "index": 18,
            "name": "output:hdmi-surround71-extra2+input:analog-stereo",
            "description": "Digital Surround 7.1 (HDMI 3) Output + Analog Stereo Input",
            "priority": 665,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 16 ]
              ]
            ]
          },
          {
            "index": 19,
            "name": "output:hdmi-surround-extra3+input:analog-stereo",
            "description": "Digital Surround 5.1 (HDMI 4) Output + Analog Stereo Input",
            "priority": 665,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 18 ]
              ]
            ]
          },
          {
            "index": 20,
            "name": "output:hdmi-surround71-extra3+input:analog-stereo",
            "description": "Digital Surround 7.1 (HDMI 4) Output + Analog Stereo Input",
            "priority": 665,
            "available": "no",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 19 ]
              ]
            ]
          },
          {
            "index": 21,
            "name": "output:hdmi-surround-extra1",
            "description": "Digital Surround 5.1 (HDMI 2) Output",
            "priority": 600,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 12 ]
              ]
            ]
          },
          {
            "index": 22,
            "name": "output:hdmi-surround71-extra1",
            "description": "Digital Surround 7.1 (HDMI 2) Output",
            "priority": 600,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 13 ]
              ]
            ]
          },
          {
            "index": 23,
            "name": "output:hdmi-surround-extra2",
            "description": "Digital Surround 5.1 (HDMI 3) Output",
            "priority": 600,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 15 ]
              ]
            ]
          },
          {
            "index": 24,
            "name": "output:hdmi-surround71-extra2",
            "description": "Digital Surround 7.1 (HDMI 3) Output",
            "priority": 600,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 16 ]
              ]
            ]
          },
          {
            "index": 25,
            "name": "output:hdmi-surround-extra3",
            "description": "Digital Surround 5.1 (HDMI 4) Output",
            "priority": 600,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 18 ]
              ]
            ]
          },
          {
            "index": 26,
            "name": "output:hdmi-surround71-extra3",
            "description": "Digital Surround 7.1 (HDMI 4) Output",
            "priority": 600,
            "available": "no",
            "classes": [
              1,
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 19 ]
              ]
            ]
          },
          {
            "index": 27,
            "name": "input:analog-stereo",
            "description": "Analog Stereo Input",
            "priority": 65,
            "available": "yes",
            "classes": [
              1,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ]
            ]
          },
          {
            "index": 28,
            "name": "pro-audio",
            "description": "Pro Audio",
            "priority": 1,
            "available": "unknown",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 6 ]
              ],
              [
                "Audio/Sink",
                5,
                "card.profile.devices",
                [ 1, 2, 3, 4, 5 ]
              ]
            ]
          }
        ],
        "Profile": [
          {
            "index": 1,
            "name": "output:analog-stereo+input:analog-stereo",
            "description": "Analog Stereo Duplex",
            "priority": 6565,
            "available": "yes",
            "classes": [
              2,
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 7 ]
              ]
            ],
            "save": false
          }
        ],
        "EnumRoute": [
          {
            "index": 0,
            "direction": "Input",
            "name": "analog-input-internal-mic",
            "description": "Internal Microphone",
            "priority": 8900,
            "available": "unknown",
            "info": [
              4,
              "port.type",
              "mic",
              "port.availability-group",
              "Legacy 1",
              "device.icon_name",
              "audio-input-microphone",
              "card.profile.port",
              "0"
            ],
            "profiles": [ 27, 1, 3, 11, 12, 5, 15, 16, 6, 17, 18, 7, 19, 20 ],
            "devices": [ 0 ]
          },
          {
            "index": 1,
            "direction": "Input",
            "name": "analog-input-headphone-mic",
            "description": "Microphone",
            "priority": 8700,
            "available": "no",
            "info": [
              4,
              "port.type",
              "mic",
              "port.availability-group",
              "Legacy 2",
              "device.icon_name",
              "audio-input-microphone",
              "card.profile.port",
              "1"
            ],
            "profiles": [ 27, 1, 3, 11, 12, 5, 15, 16, 6, 17, 18, 7, 19, 20 ],
            "devices": [ 0 ]
          },
          {
            "index": 2,
            "direction": "Input",
            "name": "analog-input-headset-mic",
            "description": "Headset Microphone",
            "priority": 8800,
            "available": "no",
            "info": [
              4,
              "port.type",
              "headset",
              "port.availability-group",
              "Legacy 2",
              "device.icon_name",
              "audio-input-microphone",
              "card.profile.port",
              "2"
            ],
            "profiles": [ 27, 1, 3, 11, 12, 5, 15, 16, 6, 17, 18, 7, 19, 20 ],
            "devices": [ 0 ]
          },
          {
            "index": 3,
            "direction": "Output",
            "name": "analog-output-speaker",
            "description": "Speakers",
            "priority": 10000,
            "available": "unknown",
            "info": [
              4,
              "port.type",
              "speaker",
              "port.availability-group",
              "Legacy 3",
              "device.icon_name",
              "audio-speakers",
              "card.profile.port",
              "3"
            ],
            "profiles": [ 2, 1 ],
            "devices": [ 7 ]
          },
          {
            "index": 4,
            "direction": "Output",
            "name": "analog-output-headphones",
            "description": "Headphones",
            "priority": 9900,
            "available": "no",
            "info": [
              4,
              "port.type",
              "headphones",
              "port.availability-group",
              "Legacy 2",
              "device.icon_name",
              "audio-headphones",
              "card.profile.port",
              "4"
            ],
            "profiles": [ 2, 1 ],
            "devices": [ 7 ]
          },
          {
            "index": 5,
            "direction": "Output",
            "name": "hdmi-output-0",
            "description": "HDMI / DisplayPort",
            "priority": 5900,
            "available": "no",
            "info": [
              4,
              "port.type",
              "hdmi",
              "port.availability-group",
              "Legacy 4",
              "device.icon_name",
              "video-display",
              "card.profile.port",
              "5"
            ],
            "profiles": [ 4, 3, 13, 11, 14, 12 ],
            "devices": [ 8, 9, 10 ]
          },
          {
            "index": 6,
            "direction": "Output",
            "name": "hdmi-output-1",
            "description": "HDMI / DisplayPort 2",
            "priority": 5800,
            "available": "no",
            "info": [
              4,
              "port.type",
              "hdmi",
              "port.availability-group",
              "Legacy 5",
              "device.icon_name",
              "video-display",
              "card.profile.port",
              "6"
            ],
            "profiles": [ 8, 5, 21, 15, 22, 16 ],
            "devices": [ 11, 12, 13 ]
          },
          {
            "index": 7,
            "direction": "Output",
            "name": "hdmi-output-2",
            "description": "HDMI / DisplayPort 3",
            "priority": 5700,
            "available": "no",
            "info": [
              4,
              "port.type",
              "hdmi",
              "port.availability-group",
              "Legacy 6",
              "device.icon_name",
              "video-display",
              "card.profile.port",
              "7"
            ],
            "profiles": [ 9, 6, 23, 17, 24, 18 ],
            "devices": [ 14, 15, 16 ]
          },
          {
            "index": 8,
            "direction": "Output",
            "name": "hdmi-output-3",
            "description": "HDMI / DisplayPort 4",
            "priority": 5600,
            "available": "no",
            "info": [
              4,
              "port.type",
              "hdmi",
              "port.availability-group",
              "Legacy 7",
              "device.icon_name",
              "video-display",
              "card.profile.port",
              "8"
            ],
            "profiles": [ 10, 7, 25, 19, 26, 20 ],
            "devices": [ 17, 18, 19 ]
          }
        ],
        "Route": [
          {
            "index": 0,
            "direction": "Input",
            "name": "analog-input-internal-mic",
            "description": "Internal Microphone",
            "priority": 8900,
            "available": "unknown",
            "info": [
              6,
              "port.type",
              "mic",
              "port.availability-group",
              "Legacy 1",
              "device.icon_name",
              "audio-input-microphone",
              "card.profile.port",
              "0",
              "route.hw-mute",
              "true",
              "route.hw-volume",
              "true"
            ],
            "profiles": [ 27, 1, 3, 11, 12, 5, 15, 16, 6, 17, 18, 7, 19, 20 ],
            "device": 0,
            "props": {
              "mute": true,
              "channelVolumes": [ 0.103480, 0.103480 ],
              "volumeBase": 0.001000,
              "volumeStep": 0.000015,
              "channelMap": [ "FL", "FR" ],
              "softVolumes": [ 1.034633, 1.034633 ],
              "latencyOffsetNsec": 0
            },
            "devices": [ 0 ],
            "profile": 1,
            "save": true
          },
          {
            "index": 3,
            "direction": "Output",
            "name": "analog-output-speaker",
            "description": "Speakers",
            "priority": 10000,
            "available": "unknown",
            "info": [
              6,
              "port.type",
              "speaker",
              "port.availability-group",
              "Legacy 3",
              "device.icon_name",
              "audio-speakers",
              "card.profile.port",
              "3",
              "route.hw-mute",
              "true",
              "route.hw-volume",
              "true"
            ],
            "profiles": [ 2, 1 ],
            "device": 7,
            "props": {
              "mute": false,
              "channelVolumes": [ 0.125000, 0.125000 ],
              "volumeBase": 1.000000,
              "volumeStep": 0.000015,
              "channelMap": [ "FL", "FR" ],
              "softVolumes": [ 1.000000, 1.000000 ],
              "latencyOffsetNsec": 0
            },
            "devices": [ 7 ],
            "profile": 1,
            "save": true
          }
        ]
      }
    }
  },
  {
    "id": 43,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 256,
      "max-output-ports": 256,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 1,
      "n-output-ports": 1,
      "state": "suspended",
      "error": null,
      "props": {
        "client.id": 40,
        "clock.quantum-limit": 8192,
        "device.api": "alsa",
        "factory.id": 10,
        "factory.name": "api.alsa.seq.bridge",
        "media.class": "Midi/Bridge",
        "node.driver": true,
        "node.name": "Midi-Bridge",
        "object.id": 43,
        "object.serial": 44,
        "priority.driver": 1,
        "priority.session": 100
      },
      "params": {
        "PropInfo": [
          {
            "id": "device",
            "description": "The ALSA device",
            "type": "default"
          }
        ],
        "Props": [
          {
            "device": "default"
          }
        ],
        "IO": [
          {
            "id": "Clock",
            "size": 160
          },
          {
            "id": "Position",
            "size": 1688
          }
        ]
      }
    }
  },
  {
    "id": 44,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "input",
      "change-mask": [ "props", "params" ],
      "props": {
        "format.dsp": "8 bit raw midi",
        "node.id": 43,
        "object.id": 44,
        "object.path": "alsa:seq:default:client_14:playback_0",
        "object.serial": 45,
        "port.alias": "Midi Through:Midi Through Port-0",
        "port.direction": "in",
        "port.id": 0,
        "port.name": "Midi Through:(playback_0) Midi Through Port-0",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "application",
            "mediaSubtype": "control"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ]
      }
    }
  },
  {
    "id": 45,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "format.dsp": "8 bit raw midi",
        "node.id": 43,
        "object.id": 45,
        "object.path": "alsa:seq:default:client_14:capture_0",
        "object.serial": 46,
        "port.alias": "Midi Through:Midi Through Port-0",
        "port.direction": "out",
        "port.id": 0,
        "port.name": "Midi Through:(capture_0) Midi Through Port-0",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "application",
            "mediaSubtype": "control"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ]
      }
    }
  },
  {
    "id": 49,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 65,
      "max-output-ports": 0,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 2,
      "n-output-ports": 2,
      "state": "suspended",
      "error": null,
      "props": {
        "alsa.card": 0,
        "alsa.card_name": "HDA Intel PCH",
        "alsa.class": "generic",
        "alsa.device": 0,
        "alsa.driver_name": "snd_hda_intel",
        "alsa.id": "ALC3204 Analog",
        "alsa.long_card_name": "HDA Intel PCH at 0x6075298000 irq 204",
        "alsa.name": "ALC3204 Analog",
        "alsa.resolution_bits": 16,
        "alsa.subclass": "generic-mix",
        "alsa.subdevice": 0,
        "alsa.subdevice_name": "subdevice #0",
        "api.alsa.card.longname": "HDA Intel PCH at 0x6075298000 irq 204",
        "api.alsa.card.name": "HDA Intel PCH",
        "api.alsa.path": "front:0",
        "api.alsa.pcm.card": 0,
        "api.alsa.pcm.stream": "playback",
        "audio.adapt.follower": "",
        "audio.channels": 2,
        "audio.position": "FL,FR",
        "card.profile.device": 7,
        "client.id": 40,
        "clock.quantum-limit": 8192,
        "device.api": "alsa",
        "device.class": "sound",
        "device.id": 42,
        "device.profile.description": "Analog Stereo",
        "device.profile.name": "analog-stereo",
        "device.routes": 2,
        "factory.id": 18,
        "factory.mode": "merge",
        "factory.name": "api.alsa.pcm.sink",
        "library.name": "audioconvert/libspa-audioconvert",
        "media.class": "Audio/Sink",
        "node.description": "Built-in Audio Analog Stereo",
        "node.driver": true,
        "node.name": "alsa_output.pci-0000_00_1f.3.analog-stereo",
        "node.nick": "ALC3204 Analog",
        "node.pause-on-idle": false,
        "object.id": 49,
        "object.path": "alsa:pcm:0:front:0:playback",
        "object.serial": 50,
        "priority.driver": 1009,
        "priority.session": 1009
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "raw",
            "format": {
              "default": "S32LE",
              "alt1": "S32LE",
              "alt2": "S16LE"
            },
            "rate": { "default": 44100, "min": 44100, "max": 48000 },
            "channels": 2,
            "position": [ "FL", "FR" ]
          }
        ],
        "PropInfo": [
          {
            "id": "volume",
            "description": "Volume",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "mute",
            "description": "Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "channelVolumes",
            "description": "Channel Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "channelMap",
            "description": "Channel Map",
            "type": "",
            "container": "Array"
          },
          {
            "id": "monitorMute",
            "description": "Monitor Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "monitorVolumes",
            "description": "Monitor Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "softMute",
            "description": "Soft Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "softVolumes",
            "description": "Soft Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "name": "monitor.channel-volumes",
            "description": "Monitor channel volume",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.disable",
            "description": "Disable Channel mixing",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.min-volume",
            "description": "Minimum volume level",
            "type": { "default": 0.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.max-volume",
            "description": "Maximum volume level",
            "type": { "default": 10.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.normalize",
            "description": "Normalize Volumes",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.mix-lfe",
            "description": "Mix LFE into channels",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.upmix",
            "description": "Enable upmixing",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.lfe-cutoff",
            "description": "LFE cutoff frequency",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.fc-cutoff",
            "description": "FC cutoff frequency (Hz)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 48000.000000 },
            "params": true
          },
          {
            "name": "channelmix.rear-delay",
            "description": "Rear channels delay (ms)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.stereo-widen",
            "description": "Stereo widen",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1.000000 },
            "params": true
          },
          {
            "name": "channelmix.hilbert-taps",
            "description": "Taps for phase shift of rear",
            "type": { "default": 0, "min": 0, "max": 255 },
            "params": true
          },
          {
            "name": "channelmix.upmix-method",
            "description": "Upmix method to use",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "simple",
              "Simple upmixing",
              "psd",
              "Passive Surround Decoding"
            ]
          },
          {
            "id": "rate",
            "description": "Rate scaler",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "quality",
            "name": "resample.quality",
            "description": "Resample Quality",
            "type": { "default": 4, "min": 0, "max": 14 },
            "params": true
          },
          {
            "name": "resample.disable",
            "description": "Disable Resampling",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "dither.noise",
            "description": "Add noise bits",
            "type": { "default": 0, "min": 0, "max": 16 },
            "params": true
          },
          {
            "name": "dither.method",
            "description": "The dithering method",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "rectangular",
              "Rectangular dithering",
              "triangular",
              "Triangular dithering",
              "triangular-hf",
              "Sloped Triangular dithering",
              "wannamaker3",
              "Wannamaker 3 dithering",
              "shaped5",
              "Lipshitz 5 dithering"
            ]
          },
          {
            "name": "debug.wav-path",
            "description": "Path to WAV file",
            "type": "",
            "params": true
          },
          {
            "name": "channelmix.lock-volumes",
            "description": "Disable volume updates",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "id": "device",
            "name": "api.alsa.path",
            "description": "The ALSA device",
            "type": "front:0"
          },
          {
            "id": "deviceName",
            "description": "The ALSA device name",
            "type": ""
          },
          {
            "id": "cardName",
            "description": "The ALSA card name",
            "type": ""
          },
          {
            "id": "latencyOffsetNsec",
            "description": "Latency offset (ns)",
            "type": { "default": 0, "min": 0, "max": 2000000000 }
          },
          {
            "name": "audio.channels",
            "description": "Audio Channels",
            "type": 2,
            "params": true
          },
          {
            "name": "audio.rate",
            "description": "Audio Rate",
            "type": 0,
            "params": true
          },
          {
            "name": "audio.format",
            "description": "Audio Format",
            "type": "UNKNOWN",
            "params": true
          },
          {
            "name": "audio.position",
            "description": "Audio Position",
            "type": "[ FL, FR ]",
            "params": true
          },
          {
            "name": "audio.allowed-rates",
            "description": "Audio Allowed Rates",
            "type": "[  ]",
            "params": true
          },
          {
            "name": "api.alsa.period-size",
            "description": "Period Size",
            "type": { "default": 0, "min": 0, "max": 8192 },
            "params": true
          },
          {
            "name": "api.alsa.period-num",
            "description": "Number of Periods",
            "type": { "default": 0, "min": 0, "max": 1024 },
            "params": true
          },
          {
            "name": "api.alsa.headroom",
            "description": "Headroom",
            "type": { "default": 0, "min": 0, "max": 8192 },
            "params": true
          },
          {
            "name": "api.alsa.start-delay",
            "description": "Start Delay",
            "type": { "default": 0, "min": 0, "max": 8192 },
            "params": true
          },
          {
            "name": "api.alsa.disable-mmap",
            "description": "Disable MMAP",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "api.alsa.disable-batch",
            "description": "Disable Batch",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "api.alsa.disable-tsched",
            "description": "Disable timer based scheduling",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "api.alsa.use-chmap",
            "description": "Use the driver channelmap",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "api.alsa.multi-rate",
            "description": "Support multiple rates",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "api.alsa.htimestamp",
            "description": "Use hires timestamps",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "latency.internal.rate",
            "description": "Internal latency in samples",
            "type": { "default": 0, "min": 0, "max": 65536 },
            "params": true
          },
          {
            "name": "latency.internal.ns",
            "description": "Internal latency in nanoseconds",
            "type": { "default": 0, "min": 0, "max": 2000000000 },
            "params": true
          },
          {
            "name": "clock.name",
            "description": "The name of the clock",
            "type": "api.alsa.p-0",
            "params": true
          },
          {
            "name": "api.alsa.htimestamp.max-errors",
            "description": "Max errors before disabling htimestamp",
            "type": { "default": 64, "min": 0, "max": 2147483647 },
            "params": true
          }
        ],
        "Props": [
          {
            "volume": 1.000000,
            "mute": false,
            "channelVolumes": [ 0.125000, 0.125000 ],
            "channelMap": [ "FL", "FR" ],
            "softMute": false,
            "softVolumes": [ 1.000000, 1.000000 ],
            "monitorMute": false,
            "monitorVolumes": [ 1.000000, 1.000000 ],
            "params": [
              "monitor.channel-volumes",
              false,
              "channelmix.disable",
              false,
              "channelmix.min-volume",
              0.000000,
              "channelmix.max-volume",
              10.000000,
              "channelmix.normalize",
              false,
              "channelmix.mix-lfe",
              true,
              "channelmix.upmix",
              true,
              "channelmix.lfe-cutoff",
              0.000000,
              "channelmix.fc-cutoff",
              0.000000,
              "channelmix.rear-delay",
              0.000000,
              "channelmix.stereo-widen",
              0.000000,
              "channelmix.hilbert-taps",
              0,
              "channelmix.upmix-method",
              "none",
              "resample.quality",
              4,
              "resample.disable",
              false,
              "dither.noise",
              0,
              "dither.method",
              "none",
              "debug.wav-path",
              "",
              "channelmix.lock-volumes",
              false
            ]
          },
          {
            "device": "front:0",
            "deviceName": "",
            "cardName": "",
            "latencyOffsetNsec": 0,
            "params": [
              "audio.channels",
              2,
              "audio.rate",
              0,
              "audio.format",
              "UNKNOWN",
              "audio.position",
              "[ FL, FR ]",
              "audio.allowed-rates",
              "[  ]",
              "api.alsa.period-size",
              0,
              "api.alsa.period-num",
              0,
              "api.alsa.headroom",
              0,
              "api.alsa.start-delay",
              0,
              "api.alsa.disable-mmap",
              false,
              "api.alsa.disable-batch",
              false,
              "api.alsa.disable-tsched",
              false,
              "api.alsa.use-chmap",
              false,
              "api.alsa.multi-rate",
              true,
              "api.alsa.htimestamp",
              false,
              "api.alsa.htimestamp.max-errors",
              64,
              "latency.internal.rate",
              0,
              "latency.internal.ns",
              0,
              "clock.name",
              "api.alsa.p-0"
            ]
          }
        ],
        "Format": [ ],
        "EnumPortConfig": [
          {
            "direction": "Input",
            "mode": {
              "default": "none",
              "alt1": "none",
              "alt2": "dsp",
              "alt3": "convert"
            },
            "monitor": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "control": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          }
        ],
        "PortConfig": [
          {
            "direction": "Input",
            "mode": "dsp",
            "monitor": true,
            "control": false,
            "format": {
              "mediaType": "audio",
              "mediaSubtype": "raw",
              "format": "F32P",
              "channels": 2,
              "position": [ "FL", "FR" ]
            }
          }
        ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "ProcessLatency": [
          {
            "quantum": 0.000000,
            "rate": 0,
            "ns": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 50,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 0,
      "max-output-ports": 65,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 0,
      "n-output-ports": 2,
      "state": "suspended",
      "error": null,
      "props": {
        "alsa.card": 0,
        "alsa.card_name": "HDA Intel PCH",
        "alsa.class": "generic",
        "alsa.device": 0,
        "alsa.driver_name": "snd_hda_intel",
        "alsa.id": "ALC3204 Analog",
        "alsa.long_card_name": "HDA Intel PCH at 0x6075298000 irq 204",
        "alsa.name": "ALC3204 Analog",
        "alsa.resolution_bits": 16,
        "alsa.subclass": "generic-mix",
        "alsa.subdevice": 0,
        "alsa.subdevice_name": "subdevice #0",
        "api.alsa.card.longname": "HDA Intel PCH at 0x6075298000 irq 204",
        "api.alsa.card.name": "HDA Intel PCH",
        "api.alsa.path": "front:0",
        "api.alsa.pcm.card": 0,
        "api.alsa.pcm.stream": "capture",
        "audio.adapt.follower": "",
        "audio.channels": 2,
        "audio.position": "FL,FR",
        "card.profile.device": 0,
        "client.id": 40,
        "clock.quantum-limit": 8192,
        "device.api": "alsa",
        "device.class": "sound",
        "device.id": 42,
        "device.profile.description": "Analog Stereo",
        "device.profile.name": "analog-stereo",
        "device.routes": 3,
        "factory.id": 18,
        "factory.mode": "split",
        "factory.name": "api.alsa.pcm.source",
        "library.name": "audioconvert/libspa-audioconvert",
        "media.class": "Audio/Source",
        "node.description": "Built-in Audio Analog Stereo",
        "node.driver": true,
        "node.name": "alsa_input.pci-0000_00_1f.3.analog-stereo",
        "node.nick": "ALC3204 Analog",
        "node.pause-on-idle": false,
        "object.id": 50,
        "object.path": "alsa:pcm:0:front:0:capture",
        "object.serial": 51,
        "priority.driver": 2009,
        "priority.session": 2009
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "raw",
            "format": {
              "default": "S32LE",
              "alt1": "S32LE",
              "alt2": "S16LE"
            },
            "rate": { "default": 44100, "min": 44100, "max": 192000 },
            "channels": 2,
            "position": [ "FL", "FR" ]
          }
        ],
        "PropInfo": [
          {
            "id": "volume",
            "description": "Volume",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "mute",
            "description": "Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "channelVolumes",
            "description": "Channel Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "channelMap",
            "description": "Channel Map",
            "type": "",
            "container": "Array"
          },
          {
            "id": "monitorMute",
            "description": "Monitor Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "monitorVolumes",
            "description": "Monitor Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "softMute",
            "description": "Soft Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "softVolumes",
            "description": "Soft Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "name": "monitor.channel-volumes",
            "description": "Monitor channel volume",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.disable",
            "description": "Disable Channel mixing",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.min-volume",
            "description": "Minimum volume level",
            "type": { "default": 0.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.max-volume",
            "description": "Maximum volume level",
            "type": { "default": 10.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.normalize",
            "description": "Normalize Volumes",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.mix-lfe",
            "description": "Mix LFE into channels",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.upmix",
            "description": "Enable upmixing",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.lfe-cutoff",
            "description": "LFE cutoff frequency",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.fc-cutoff",
            "description": "FC cutoff frequency (Hz)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 48000.000000 },
            "params": true
          },
          {
            "name": "channelmix.rear-delay",
            "description": "Rear channels delay (ms)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.stereo-widen",
            "description": "Stereo widen",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1.000000 },
            "params": true
          },
          {
            "name": "channelmix.hilbert-taps",
            "description": "Taps for phase shift of rear",
            "type": { "default": 0, "min": 0, "max": 255 },
            "params": true
          },
          {
            "name": "channelmix.upmix-method",
            "description": "Upmix method to use",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "simple",
              "Simple upmixing",
              "psd",
              "Passive Surround Decoding"
            ]
          },
          {
            "id": "rate",
            "description": "Rate scaler",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "quality",
            "name": "resample.quality",
            "description": "Resample Quality",
            "type": { "default": 4, "min": 0, "max": 14 },
            "params": true
          },
          {
            "name": "resample.disable",
            "description": "Disable Resampling",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "dither.noise",
            "description": "Add noise bits",
            "type": { "default": 0, "min": 0, "max": 16 },
            "params": true
          },
          {
            "name": "dither.method",
            "description": "The dithering method",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "rectangular",
              "Rectangular dithering",
              "triangular",
              "Triangular dithering",
              "triangular-hf",
              "Sloped Triangular dithering",
              "wannamaker3",
              "Wannamaker 3 dithering",
              "shaped5",
              "Lipshitz 5 dithering"
            ]
          },
          {
            "name": "debug.wav-path",
            "description": "Path to WAV file",
            "type": "",
            "params": true
          },
          {
            "name": "channelmix.lock-volumes",
            "description": "Disable volume updates",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "id": "device",
            "name": "api.alsa.path",
            "description": "The ALSA device",
            "type": "front:0"
          },
          {
            "id": "deviceName",
            "description": "The ALSA device name",
            "type": ""
          },
          {
            "id": "cardName",
            "description": "The ALSA card name",
            "type": ""
          },
          {
            "id": "latencyOffsetNsec",
            "description": "Latency offset (ns)",
            "type": { "default": 0, "min": 0, "max": 2000000000 }
          },
          {
            "name": "audio.channels",
            "description": "Audio Channels",
            "type": 2,
            "params": true
          },
          {
            "name": "audio.rate",
            "description": "Audio Rate",
            "type": 0,
            "params": true
          },
          {
            "name": "audio.format",
            "description": "Audio Format",
            "type": "UNKNOWN",
            "params": true
          },
          {
            "name": "audio.position",
            "description": "Audio Position",
            "type": "[ FL, FR ]",
            "params": true
          },
          {
            "name": "audio.allowed-rates",
            "description": "Audio Allowed Rates",
            "type": "[  ]",
            "params": true
          },
          {
            "name": "api.alsa.period-size",
            "description": "Period Size",
            "type": { "default": 0, "min": 0, "max": 8192 },
            "params": true
          },
          {
            "name": "api.alsa.period-num",
            "description": "Number of Periods",
            "type": { "default": 0, "min": 0, "max": 1024 },
            "params": true
          },
          {
            "name": "api.alsa.headroom",
            "description": "Headroom",
            "type": { "default": 0, "min": 0, "max": 8192 },
            "params": true
          },
          {
            "name": "api.alsa.start-delay",
            "description": "Start Delay",
            "type": { "default": 0, "min": 0, "max": 8192 },
            "params": true
          },
          {
            "name": "api.alsa.disable-mmap",
            "description": "Disable MMAP",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "api.alsa.disable-batch",
            "description": "Disable Batch",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "api.alsa.disable-tsched",
            "description": "Disable timer based scheduling",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "api.alsa.use-chmap",
            "description": "Use the driver channelmap",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "api.alsa.multi-rate",
            "description": "Support multiple rates",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "api.alsa.htimestamp",
            "description": "Use hires timestamps",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "latency.internal.rate",
            "description": "Internal latency in samples",
            "type": { "default": 0, "min": 0, "max": 65536 },
            "params": true
          },
          {
            "name": "latency.internal.ns",
            "description": "Internal latency in nanoseconds",
            "type": { "default": 0, "min": 0, "max": 2000000000 },
            "params": true
          },
          {
            "name": "clock.name",
            "description": "The name of the clock",
            "type": "api.alsa.c-0",
            "params": true
          },
          {
            "name": "api.alsa.htimestamp.max-errors",
            "description": "Max errors before disabling htimestamp",
            "type": { "default": 64, "min": 0, "max": 2147483647 },
            "params": true
          }
        ],
        "Props": [
          {
            "volume": 1.000000,
            "mute": true,
            "channelVolumes": [ 0.103480, 0.103480 ],
            "channelMap": [ "FL", "FR" ],
            "softMute": true,
            "softVolumes": [ 1.034633, 1.034633 ],
            "monitorMute": false,
            "monitorVolumes": [ 1.000000, 1.000000 ],
            "params": [
              "monitor.channel-volumes",
              false,
              "channelmix.disable",
              false,
              "channelmix.min-volume",
              0.000000,
              "channelmix.max-volume",
              10.000000,
              "channelmix.normalize",
              false,
              "channelmix.mix-lfe",
              true,
              "channelmix.upmix",
              true,
              "channelmix.lfe-cutoff",
              0.000000,
              "channelmix.fc-cutoff",
              0.000000,
              "channelmix.rear-delay",
              0.000000,
              "channelmix.stereo-widen",
              0.000000,
              "channelmix.hilbert-taps",
              0,
              "channelmix.upmix-method",
              "none",
              "resample.quality",
              4,
              "resample.disable",
              false,
              "dither.noise",
              0,
              "dither.method",
              "none",
              "debug.wav-path",
              "",
              "channelmix.lock-volumes",
              false
            ]
          },
          {
            "device": "front:0",
            "deviceName": "",
            "cardName": "",
            "latencyOffsetNsec": 0,
            "params": [
              "audio.channels",
              2,
              "audio.rate",
              0,
              "audio.format",
              "UNKNOWN",
              "audio.position",
              "[ FL, FR ]",
              "audio.allowed-rates",
              "[  ]",
              "api.alsa.period-size",
              0,
              "api.alsa.period-num",
              0,
              "api.alsa.headroom",
              0,
              "api.alsa.start-delay",
              0,
              "api.alsa.disable-mmap",
              false,
              "api.alsa.disable-batch",
              false,
              "api.alsa.disable-tsched",
              false,
              "api.alsa.use-chmap",
              false,
              "api.alsa.multi-rate",
              true,
              "api.alsa.htimestamp",
              false,
              "api.alsa.htimestamp.max-errors",
              64,
              "latency.internal.rate",
              0,
              "latency.internal.ns",
              0,
              "clock.name",
              "api.alsa.c-0"
            ]
          }
        ],
        "Format": [ ],
        "EnumPortConfig": [
          {
            "direction": "Output",
            "mode": {
              "default": "none",
              "alt1": "none",
              "alt2": "dsp",
              "alt3": "convert"
            },
            "monitor": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "control": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          }
        ],
        "PortConfig": [
          {
            "direction": "Output",
            "mode": "dsp",
            "monitor": true,
            "control": false,
            "format": {
              "mediaType": "audio",
              "mediaSubtype": "raw",
              "format": "F32P",
              "channels": 2,
              "position": [ "FL", "FR" ]
            }
          }
        ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 32,
            "maxRate": 32,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "ProcessLatency": [
          {
            "quantum": 0.000000,
            "rate": 0,
            "ns": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 51,
    "type": "PipeWire:Interface:Device",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props", "params" ],
      "props": {
        "api.v4l2.cap.bus_info": "usb-0000:00:14.0-3",
        "api.v4l2.cap.capabilities": "84a00001",
        "api.v4l2.cap.card": "Integrated_Webcam_HD: Integrate",
        "api.v4l2.cap.device-caps": 4200001,
        "api.v4l2.cap.driver": "uvcvideo",
        "api.v4l2.cap.version": "6.7.7",
        "api.v4l2.path": "/dev/video0",
        "client.id": 40,
        "device.api": "v4l2",
        "device.bus": "usb",
        "device.bus-path": "pci-0000:00:14.0-usb-0:3:1.0",
        "device.capabilities": ":capture:",
        "device.description": "Integrated_Webcam_HD",
        "device.devids": 20736,
        "device.enum.api": "udev",
        "device.name": "v4l2_device.pci-0000_00_14.0-usb-0_3_1.0",
        "device.plugged.usec": 7566884,
        "device.product.id": "0x28c",
        "device.product.name": "Integrated_Webcam_HD",
        "device.serial": "CN07JXD18LG0017HBFXEA01_Integrated_Webcam_HD_01.00.00",
        "device.subsystem": "video4linux",
        "device.sysfs.path": "/devices/pci0000:00/0000:00:14.0/usb3/3-3/3-3:1.0/video4linux/video0",
        "device.vendor.id": "0x1bc",
        "device.vendor.name": "CN07JXD18LG0017HBFXEA01",
        "factory.id": 14,
        "media.class": "Video/Device",
        "object.id": 51,
        "object.path": "v4l2:/dev/video0",
        "object.serial": 52
      },
      "params": {
      }
    }
  },
  {
    "id": 52,
    "type": "PipeWire:Interface:Device",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props", "params" ],
      "props": {
        "api.v4l2.cap.bus_info": "usb-0000:00:14.0-3",
        "api.v4l2.cap.capabilities": "84a00001",
        "api.v4l2.cap.card": "Integrated_Webcam_HD: Integrate",
        "api.v4l2.cap.device-caps": "04a00000",
        "api.v4l2.cap.driver": "uvcvideo",
        "api.v4l2.cap.version": "6.7.7",
        "api.v4l2.path": "/dev/video1",
        "client.id": 40,
        "device.api": "v4l2",
        "device.bus": "usb",
        "device.bus-path": "pci-0000:00:14.0-usb-0:3:1.0",
        "device.capabilities": ":",
        "device.description": "Integrated_Webcam_HD",
        "device.devids": 20737,
        "device.enum.api": "udev",
        "device.name": "v4l2_device.pci-0000_00_14.0-usb-0_3_1.0",
        "device.plugged.usec": 7566915,
        "device.product.id": "0x28c",
        "device.product.name": "Integrated_Webcam_HD",
        "device.serial": "CN07JXD18LG0017HBFXEA01_Integrated_Webcam_HD_01.00.00",
        "device.subsystem": "video4linux",
        "device.sysfs.path": "/devices/pci0000:00/0000:00:14.0/usb3/3-3/3-3:1.0/video4linux/video1",
        "device.vendor.id": "0x1bc",
        "device.vendor.name": "CN07JXD18LG0017HBFXEA01",
        "factory.id": 14,
        "media.class": "Video/Device",
        "object.id": 52,
        "object.path": "v4l2:/dev/video1",
        "object.serial": 53
      },
      "params": {
      }
    }
  },
  {
    "id": 53,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 1,
      "max-output-ports": 1,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 1,
      "n-output-ports": 1,
      "state": "suspended",
      "error": null,
      "props": {
        "api.bluez5.role": "server",
        "api.glib.mainloop": true,
        "client.id": 40,
        "clock.quantum-limit": 8192,
        "device.api": "bluez5",
        "factory.id": 13,
        "factory.name": "api.bluez5.midi.node",
        "media.class": "Midi/Bridge",
        "node.description": "BLE MIDI 1",
        "node.name": "bluez_midi.server",
        "object.id": 53,
        "object.serial": 54
      },
      "params": {
        "PropInfo": [
          {
            "id": "latencyOffsetNsec",
            "description": "Latency offset (ns)",
            "type": { "default": 0, "min": -9223372036854775808, "max": 9223372036854775807 }
          },
          {
            "id": "deviceName",
            "description": "Device name",
            "type": "BLE MIDI 1"
          }
        ],
        "Props": [
          {
            "latencyOffsetNsec": 0,
            "deviceName": "BLE MIDI 1"
          }
        ],
        "IO": [
        ]
      }
    }
  },
  {
    "id": 54,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "input",
      "change-mask": [ "props", "params" ],
      "props": {
        "format.dsp": "8 bit raw midi",
        "node.id": 53,
        "object.id": 54,
        "object.path": "bluez_midi.server:input_0",
        "object.serial": 55,
        "port.alias": "in",
        "port.direction": "in",
        "port.id": 0,
        "port.name": "in",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "application",
            "mediaSubtype": "control"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ]
      }
    }
  },
  {
    "id": 55,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "format.dsp": "8 bit raw midi",
        "node.id": 53,
        "object.id": 55,
        "object.path": "bluez_midi.server:output_0",
        "object.serial": 56,
        "port.alias": "out",
        "port.direction": "out",
        "port.id": 0,
        "port.name": "out",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "application",
            "mediaSubtype": "control"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Output",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ]
      }
    }
  },
  {
    "id": 56,
    "type": "PipeWire:Interface:Device",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props", "params" ],
      "props": {
        "api.libcamera.location": "front",
        "api.libcamera.path": "\\_SB_.PC00.XHCI.RHUB.HS03-3:1.0-1bcf:28cc",
        "client.id": 40,
        "device.api": "libcamera",
        "device.description": "Integrated_Webcam_HD: Integrate",
        "device.devids": "20736 ",
        "device.enum.api": "libcamera.manager",
        "device.name": "libcamera_device.\\_SB_.PC00.XHCI.RHUB.HS03-3:1.0-1bcf:28cc",
        "device.product.name": "Integrated_Webcam_HD: Integrate",
        "factory.id": 14,
        "media.class": "Video/Device",
        "object.id": 56,
        "object.path": "libcamera:\\_SB_.PC00.XHCI.RHUB.HS03-3:1.0-1bcf:28cc",
        "object.serial": 57
      },
      "params": {
        "EnumProfile": [
        ],
        "Profile": [ ]
      }
    }
  },
  {
    "id": 48,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 0,
      "max-output-ports": 1,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 0,
      "n-output-ports": 1,
      "state": "suspended",
      "error": null,
      "props": {
        "api.v4l2.cap.bus_info": "usb-0000:00:14.0-3",
        "api.v4l2.cap.capabilities": "84a00001",
        "api.v4l2.cap.card": "Integrated_Webcam_HD: Integrate",
        "api.v4l2.cap.device-caps": 4200001,
        "api.v4l2.cap.driver": "uvcvideo",
        "api.v4l2.cap.version": "6.7.7",
        "api.v4l2.path": "/dev/video0",
        "client.id": 40,
        "clock.quantum-limit": 8192,
        "device.api": "v4l2",
        "device.devids": 20736,
        "device.id": 51,
        "device.product.id": "0x28c",
        "device.vendor.id": "0x1bc",
        "factory.id": 10,
        "factory.name": "api.v4l2.source",
        "media.class": "Video/Source",
        "media.role": "Camera",
        "node.description": "Integrated_Webcam_HD (V4L2)",
        "node.driver": true,
        "node.name": "v4l2_input.pci-0000_00_14.0-usb-0_3_1.0",
        "node.nick": "Integrated_Webcam_HD",
        "node.pause-on-idle": false,
        "object.id": 48,
        "object.path": "v4l2:/dev/video0",
        "object.serial": 58,
        "priority.session": 1000
      },
      "params": {
        "PropInfo": [
          {
            "id": "device",
            "description": "The V4L2 device",
            "type": "/dev/video0"
          },
          {
            "id": "deviceName",
            "description": "The V4L2 device name",
            "type": ""
          },
          {
            "id": "deviceFd",
            "description": "The V4L2 fd",
            "type": 0
          },
          {
            "id": "brightness",
            "type": { "default": 0, "min": -64, "max": 64, "step": 1 },
            "description": "Brightness"
          },
          {
            "id": "contrast",
            "type": { "default": 0, "min": 0, "max": 95, "step": 1 },
            "description": "Contrast"
          },
          {
            "id": "saturation",
            "type": { "default": 64, "min": 0, "max": 100, "step": 1 },
            "description": "Saturation"
          },
          {
            "id": "hue",
            "type": { "default": 0, "min": -2000, "max": 2000, "step": 1 },
            "description": "Hue"
          },
          {
            "id": "id-0198090c",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "description": "White Balance, Automatic"
          },
          {
            "id": "gamma",
            "type": { "default": 100, "min": 100, "max": 300, "step": 1 },
            "description": "Gamma"
          },
          {
            "id": "gain",
            "type": { "default": 1, "min": 1, "max": 8, "step": 1 },
            "description": "Gain"
          },
          {
            "id": "id-01980918",
            "type": {
              "default": 2
            },
            "description": "Power Line Frequency",
            "labels": [
              0,
              "Disabled",
              1,
              "50 Hz",
              2,
              "60 Hz"
            ]
          },
          {
            "id": "id-0198091a",
            "type": { "default": 4600, "min": 2800, "max": 6500, "step": 1 },
            "description": "White Balance Temperature"
          },
          {
            "id": "sharpness",
            "type": { "default": 2, "min": 1, "max": 7, "step": 1 },
            "description": "Sharpness"
          },
          {
            "id": "id-0198091c",
            "type": { "default": 3, "min": 0, "max": 3, "step": 1 },
            "description": "Backlight Compensation"
          },
          {
            "id": "id-019a0901",
            "type": {
              "default": 3
            },
            "description": "Auto Exposure",
            "labels": [
              1,
              "Manual Mode",
              3,
              "Aperture Priority Mode"
            ]
          },
          {
            "id": "id-019a0902",
            "type": { "default": 156, "min": 10, "max": 626, "step": 1 },
            "description": "Exposure Time, Absolute"
          },
          {
            "id": "id-019a0903",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "description": "Exposure, Dynamic Framerate"
          }
        ],
        "Props": [
          {
            "device": "/dev/video0",
            "deviceName": "",
            "deviceFd": 0,
            "brightness": 0,
            "contrast": 0,
            "saturation": 64,
            "hue": 0,
            "id-0198090c": true,
            "gamma": 100,
            "gain": 1,
            "id-01980918": 1,
            "id-0198091a": 4600,
            "sharpness": 2,
            "id-0198091c": 3,
            "id-019a0901": 3,
            "id-019a0902": 156,
            "id-019a0903": true
          }
        ],
        "EnumFormat": [
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 1280, "height": 720 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 960, "height": 540 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 848, "height": 480 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 640, "height": 480 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 640, "height": 360 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 640, "height": 480 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 640, "height": 360 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 424, "height": 240 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 320, "height": 240 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 320, "height": 180 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 160, "height": 120 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 1280, "height": 720 },
            "framerate": { "num": 10, "denom": 1 }
          }
        ],
        "Format": [ ]
      }
    }
  },
  {
    "id": 47,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "node.id": 48,
        "object.id": 47,
        "object.path": "v4l2:/dev/video0:capture_0",
        "object.serial": 59,
        "port.alias": "Integrated_Webcam_HD:capture_1",
        "port.direction": "out",
        "port.id": 0,
        "port.name": "capture_1",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "PropInfo": [
          {
            "id": "brightness",
            "type": { "default": 0, "min": -64, "max": 64, "step": 1 },
            "description": "Brightness"
          },
          {
            "id": "contrast",
            "type": { "default": 0, "min": 0, "max": 95, "step": 1 },
            "description": "Contrast"
          },
          {
            "id": "saturation",
            "type": { "default": 64, "min": 0, "max": 100, "step": 1 },
            "description": "Saturation"
          },
          {
            "id": "hue",
            "type": { "default": 0, "min": -2000, "max": 2000, "step": 1 },
            "description": "Hue"
          },
          {
            "id": "id-0198090c",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "description": "White Balance, Automatic"
          },
          {
            "id": "gamma",
            "type": { "default": 100, "min": 100, "max": 300, "step": 1 },
            "description": "Gamma"
          },
          {
            "id": "gain",
            "type": { "default": 1, "min": 1, "max": 8, "step": 1 },
            "description": "Gain"
          },
          {
            "id": "id-01980918",
            "type": {
              "default": 2
            },
            "description": "Power Line Frequency",
            "labels": [
              0,
              "Disabled",
              1,
              "50 Hz",
              2,
              "60 Hz"
            ]
          },
          {
            "id": "id-0198091a",
            "type": { "default": 4600, "min": 2800, "max": 6500, "step": 1 },
            "description": "White Balance Temperature"
          },
          {
            "id": "sharpness",
            "type": { "default": 2, "min": 1, "max": 7, "step": 1 },
            "description": "Sharpness"
          },
          {
            "id": "id-0198091c",
            "type": { "default": 3, "min": 0, "max": 3, "step": 1 },
            "description": "Backlight Compensation"
          },
          {
            "id": "id-019a0901",
            "type": {
              "default": 3
            },
            "description": "Auto Exposure",
            "labels": [
              1,
              "Manual Mode",
              3,
              "Aperture Priority Mode"
            ]
          },
          {
            "id": "id-019a0902",
            "type": { "default": 156, "min": 10, "max": 626, "step": 1 },
            "description": "Exposure Time, Absolute"
          },
          {
            "id": "id-019a0903",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "description": "Exposure, Dynamic Framerate"
          }
        ],
        "EnumFormat": [
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 1280, "height": 720 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 960, "height": 540 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 848, "height": 480 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 640, "height": 480 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "mjpg",
            "size": { "width": 640, "height": 360 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 640, "height": 480 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 640, "height": 360 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 424, "height": 240 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 320, "height": 240 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 320, "height": 180 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 160, "height": 120 },
            "framerate": { "num": 30, "denom": 1 }
          },
          {
            "mediaType": "video",
            "mediaSubtype": "raw",
            "format": "YUY2",
            "size": { "width": 1280, "height": 720 },
            "framerate": { "num": 10, "denom": 1 }
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          },
          {
            "id": "Clock",
            "size": 160
          },
          {
            "id": "Control",
            "size": 16
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ]
      }
    }
  },
  {
    "id": 46,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "input",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FL",
        "format.dsp": "32 bit float mono audio",
        "node.id": 49,
        "object.id": 46,
        "object.path": "alsa:pcm:0:front:0:playback:playback_0",
        "object.serial": 60,
        "port.alias": "ALC3204 Analog:playback_FL",
        "port.direction": "in",
        "port.id": 0,
        "port.name": "playback_FL",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 57,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FL",
        "format.dsp": "32 bit float mono audio",
        "node.id": 49,
        "object.id": 57,
        "object.path": "alsa:pcm:0:front:0:playback:monitor_0",
        "object.serial": 61,
        "port.alias": "ALC3204 Analog:monitor_FL",
        "port.direction": "out",
        "port.id": 0,
        "port.monitor": true,
        "port.name": "monitor_FL"
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 58,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "input",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FR",
        "format.dsp": "32 bit float mono audio",
        "node.id": 49,
        "object.id": 58,
        "object.path": "alsa:pcm:0:front:0:playback:playback_1",
        "object.serial": 62,
        "port.alias": "ALC3204 Analog:playback_FR",
        "port.direction": "in",
        "port.id": 1,
        "port.name": "playback_FR",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 59,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FR",
        "format.dsp": "32 bit float mono audio",
        "node.id": 49,
        "object.id": 59,
        "object.path": "alsa:pcm:0:front:0:playback:monitor_1",
        "object.serial": 63,
        "port.alias": "ALC3204 Analog:monitor_FR",
        "port.direction": "out",
        "port.id": 1,
        "port.monitor": true,
        "port.name": "monitor_FR"
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 60,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FL",
        "format.dsp": "32 bit float mono audio",
        "node.id": 50,
        "object.id": 60,
        "object.path": "alsa:pcm:0:front:0:capture:capture_0",
        "object.serial": 64,
        "port.alias": "ALC3204 Analog:capture_FL",
        "port.direction": "out",
        "port.id": 0,
        "port.name": "capture_FL",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 32,
            "maxRate": 32,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 61,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FR",
        "format.dsp": "32 bit float mono audio",
        "node.id": 50,
        "object.id": 61,
        "object.path": "alsa:pcm:0:front:0:capture:capture_1",
        "object.serial": 65,
        "port.alias": "ALC3204 Analog:capture_FR",
        "port.direction": "out",
        "port.id": 1,
        "port.name": "capture_FR",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 32,
            "maxRate": 32,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 62,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.language": "es_ES.UTF-8",
        "application.name": "xdg-desktop-portal",
        "application.process.binary": "xdg-desktop-portal",
        "application.process.host": "weatherwax",
        "application.process.id": 3912,
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "clock.power-of-two-quantum": true,
        "core.name": "pipewire-antonio-3912",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 0,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 62,
        "object.serial": 66,
        "pipewire.access": "unrestricted",
        "pipewire.access.portal.is_portal": true,
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 3912,
        "pipewire.sec.socket": "pipewire-0",
        "pipewire.sec.uid": 1000,
        "portal.monitor": "Camera",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0"
      }
    }
  },
  {
    "id": 63,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.language": "es_ES.UTF-8",
        "application.name": "pipewire",
        "application.process.binary": "pipewire",
        "application.process.host": "weatherwax",
        "application.process.id": 4303,
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "clock.power-of-two-quantum": true,
        "config.name": "pipewire-pulse.conf",
        "core.name": "pipewire-antonio-4303",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 63,
        "object.serial": 67,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 4303,
        "pipewire.sec.socket": "pipewire-0",
        "pipewire.sec.uid": 1000,
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0"
      }
    }
  },
  {
    "id": 65,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.icon-name": "audio-card",
        "application.id": "org.kde.plasma-pa",
        "application.language": "es_ES.UTF-8",
        "application.name": "Plasma PA",
        "application.process.binary": "plasmashell",
        "application.process.host": "weatherwax",
        "application.process.id": 3976,
        "application.process.machine-id": "66772bc9983f4d3cb35b59cfc92e099d",
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "client.api": "pipewire-pulse",
        "clock.power-of-two-quantum": true,
        "config.name": "pipewire-pulse.conf",
        "core.name": "pipewire-antonio-4303",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 65,
        "object.serial": 69,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 4303,
        "pipewire.sec.socket": "pipewire-0",
        "pipewire.sec.uid": 1000,
        "pulse.server.type": "unix",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0"
      }
    }
  },
  {
    "id": 66,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.language": "es_ES.UTF-8",
        "application.name": "Firefox",
        "application.process.binary": "firefox",
        "application.process.host": "weatherwax",
        "application.process.id": 4354,
        "application.process.machine-id": "66772bc9983f4d3cb35b59cfc92e099d",
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "client.api": "pipewire-pulse",
        "clock.power-of-two-quantum": true,
        "config.name": "pipewire-pulse.conf",
        "core.name": "pipewire-antonio-4303",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 66,
        "object.serial": 70,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 4303,
        "pipewire.sec.socket": "pipewire-0",
        "pipewire.sec.uid": 1000,
        "pulse.server.type": "unix",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0",
        "window.x11.screen": 0
      }
    }
  },
  {
    "id": 67,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.language": "es_ES.UTF-8",
        "application.name": "Thunderbird",
        "application.process.binary": "thunderbird-bin",
        "application.process.host": "weatherwax",
        "application.process.id": 4488,
        "application.process.machine-id": "66772bc9983f4d3cb35b59cfc92e099d",
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "client.api": "pipewire-pulse",
        "clock.power-of-two-quantum": true,
        "config.name": "pipewire-pulse.conf",
        "core.name": "pipewire-antonio-4303",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 67,
        "object.serial": 71,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 4303,
        "pipewire.sec.socket": "pipewire-0",
        "pipewire.sec.uid": 1000,
        "pulse.server.type": "unix",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0",
        "window.x11.screen": 0
      }
    }
  },
  {
    "id": 68,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.language": "es_ES.UTF-8",
        "application.name": "Firefox",
        "application.process.binary": "firefox",
        "application.process.host": "weatherwax",
        "application.process.id": 4354,
        "application.process.machine-id": "66772bc9983f4d3cb35b59cfc92e099d",
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "client.api": "pipewire-pulse",
        "clock.power-of-two-quantum": true,
        "config.name": "pipewire-pulse.conf",
        "core.name": "pipewire-antonio-4303",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 68,
        "object.serial": 72,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 4303,
        "pipewire.sec.socket": "pipewire-0",
        "pipewire.sec.uid": 1000,
        "pulse.server.type": "unix",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0"
      }
    }
  },
  {
    "id": 69,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.icon-name": "thunderbird",
        "application.language": "es_ES.UTF-8",
        "application.name": "Thunderbird",
        "application.process.binary": "thunderbird-bin",
        "application.process.host": "weatherwax",
        "application.process.id": 4488,
        "application.process.machine-id": "66772bc9983f4d3cb35b59cfc92e099d",
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "application.version": "115.8.0",
        "client.api": "pipewire-pulse",
        "clock.power-of-two-quantum": true,
        "config.name": "pipewire-pulse.conf",
        "core.name": "pipewire-antonio-4303",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 69,
        "object.serial": 73,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 4303,
        "pipewire.sec.socket": "pipewire-0",
        "pipewire.sec.uid": 1000,
        "pulse.server.type": "unix",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0"
      }
    }
  },
  {
    "id": 70,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.icon-name": "firefox",
        "application.language": "es_ES.UTF-8",
        "application.name": "Firefox",
        "application.process.binary": "firefox",
        "application.process.host": "weatherwax",
        "application.process.id": 4354,
        "application.process.machine-id": "66772bc9983f4d3cb35b59cfc92e099d",
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "application.version": 123.000000,
        "client.api": "pipewire-pulse",
        "clock.power-of-two-quantum": true,
        "config.name": "pipewire-pulse.conf",
        "core.name": "pipewire-antonio-4303",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 70,
        "object.serial": 85,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 4303,
        "pipewire.sec.socket": "pipewire-0",
        "pipewire.sec.uid": 1000,
        "pulse.server.type": "unix",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0"
      }
    }
  },
  {
    "id": 81,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.id": "org.freedesktop.libcanberra",
        "application.language": "es_ES.UTF-8",
        "application.name": "libcanberra",
        "application.process.binary": "plasmashell",
        "application.process.host": "weatherwax",
        "application.process.id": 3976,
        "application.process.machine-id": "66772bc9983f4d3cb35b59cfc92e099d",
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "application.version": 0.300000,
        "client.api": "pipewire-pulse",
        "clock.power-of-two-quantum": true,
        "config.name": "pipewire-pulse.conf",
        "core.name": "pipewire-antonio-4303",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 81,
        "object.serial": 297,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 4303,
        "pipewire.sec.socket": "pipewire-0",
        "pipewire.sec.uid": 1000,
        "pulse.server.type": "unix",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0"
      }
    }
  },
  {
    "id": 78,
    "type": "PipeWire:Interface:Device",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props", "params" ],
      "props": {
        "api.bluez5.address": "6C:D3:EE:5A:17:20",
        "api.bluez5.class": "0x240404",
        "api.bluez5.connection": "disconnected",
        "api.bluez5.device": "",
        "api.bluez5.icon": "audio-headset",
        "api.bluez5.id": 4,
        "api.bluez5.path": "/org/bluez/hci0/dev_6C_D3_EE_5A_17_20",
        "bluez5.profile": "off",
        "client.id": 40,
        "device.alias": "Redmi Buds 4 Pro",
        "device.api": "bluez5",
        "device.bus": "bluetooth",
        "device.description": "Redmi Buds 4 Pro",
        "device.form-factor": "headset",
        "device.icon-name": "audio-headset-bluetooth",
        "device.name": "bluez_card.6C_D3_EE_5A_17_20",
        "device.product.id": "0x0004",
        "device.string": "6C:D3:EE:5A:17:20",
        "device.vendor.id": "bluetooth:0094",
        "factory.id": 14,
        "media.class": "Audio/Device",
        "object.id": 78,
        "object.serial": 880
      },
      "params": {
        "EnumProfile": [
          {
            "index": 0,
            "name": "off",
            "description": "Off",
            "available": "yes",
            "priority": 0
          },
          {
            "index": 3,
            "name": "headset-head-unit",
            "description": "Headset Head Unit (HSP/HFP)",
            "available": "yes",
            "priority": 1,
            "classes": [
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 1 ]
              ]
            ]
          },
          {
            "index": 5,
            "name": "a2dp-sink-sbc",
            "description": "High Fidelity Playback (A2DP Sink, codec SBC)",
            "available": "yes",
            "priority": 18,
            "classes": [
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 1 ]
              ]
            ]
          },
          {
            "index": 6,
            "name": "a2dp-sink-sbc_xq",
            "description": "High Fidelity Playback (A2DP Sink, codec SBC-XQ)",
            "available": "yes",
            "priority": 17,
            "classes": [
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 1 ]
              ]
            ]
          },
          {
            "index": 8,
            "name": "a2dp-sink-aac",
            "description": "High Fidelity Playback (A2DP Sink, codec AAC)",
            "available": "yes",
            "priority": 19,
            "classes": [
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 1 ]
              ]
            ]
          },
          {
            "index": 11,
            "name": "a2dp-sink",
            "description": "High Fidelity Playback (A2DP Sink, codec LDAC)",
            "available": "yes",
            "priority": 20,
            "classes": [
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 1 ]
              ]
            ]
          },
          {
            "index": 260,
            "name": "headset-head-unit-cvsd",
            "description": "Headset Head Unit (HSP/HFP, codec CVSD)",
            "available": "yes",
            "priority": 2,
            "classes": [
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 1 ]
              ]
            ]
          },
          {
            "index": 261,
            "name": "headset-head-unit-msbc",
            "description": "Headset Head Unit (HSP/HFP, codec mSBC)",
            "available": "yes",
            "priority": 3,
            "classes": [
              [
                "Audio/Source",
                1,
                "card.profile.devices",
                [ 0 ]
              ],
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 1 ]
              ]
            ]
          }
        ],
        "Profile": [
          {
            "index": 5,
            "name": "a2dp-sink-sbc",
            "description": "High Fidelity Playback (A2DP Sink, codec SBC)",
            "available": "yes",
            "priority": 18,
            "classes": [
              [
                "Audio/Sink",
                1,
                "card.profile.devices",
                [ 1 ]
              ]
            ],
            "save": false
          }
        ],
        "EnumRoute": [
          {
            "index": 1,
            "direction": "Output",
            "name": "headset-output",
            "description": "Headset",
            "priority": 0,
            "available": "yes",
            "info": [
              1,
              "port.type",
              "headset"
            ],
            "profiles": [ 5, 6, 8, 11 ],
            "devices": [ 1 ]
          },
          {
            "index": 2,
            "direction": "Input",
            "name": "headset-hf-input",
            "description": "Handsfree",
            "priority": 0,
            "available": "yes",
            "info": [
              1,
              "port.type",
              "headset"
            ],
            "profiles": [ 3, 260, 261 ],
            "devices": [ 0 ]
          },
          {
            "index": 3,
            "direction": "Output",
            "name": "headset-hf-output",
            "description": "Handsfree",
            "priority": 0,
            "available": "yes",
            "info": [
              1,
              "port.type",
              "headset"
            ],
            "profiles": [ 3, 260, 261 ],
            "devices": [ 1 ]
          }
        ],
        "Route": [
          {
            "index": 1,
            "direction": "Output",
            "name": "headset-output",
            "description": "Headset",
            "priority": 0,
            "available": "yes",
            "info": [
              1,
              "port.type",
              "headset"
            ],
            "profiles": [ 5, 6, 8, 11 ],
            "device": 1,
            "props": {
              "mute": false,
              "channelVolumes": [ 0.418562, 0.418562 ],
              "volumeStep": 0.007812,
              "channelMap": [ "FL", "FR" ],
              "latencyOffsetNsec": 0
            },
            "save": true,
            "devices": [ 1 ],
            "profile": 2
          }
        ],
        "PropInfo": [
          {
            "id": "bluetoothAudioCodec",
            "description": "Air codec",
            "type": {
              "default": 7,
              "alt1": 7,
              "alt2": 4,
              "alt3": 1,
              "alt4": 2
            },
            "labels": [
              7,
              "LDAC",
              4,
              "AAC",
              1,
              "SBC",
              2,
              "SBC-XQ"
            ]
          },
          {
            "id": "bluetoothOffloadActive",
            "description": "Bluetooth audio offload active",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          }
        ],
        "Props": [
          {
            "bluetoothAudioCodec": "sbc",
            "bluetoothOffloadActive": false
          }
        ]
      }
    }
  },
  {
    "id": 99,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 65,
      "max-output-ports": 0,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 2,
      "n-output-ports": 2,
      "state": "suspended",
      "error": null,
      "props": {
        "api.bluez5.address": "6C:D3:EE:5A:17:20",
        "api.bluez5.codec": "sbc",
        "api.bluez5.profile": "a2dp-sink",
        "api.bluez5.transport": "",
        "audio.adapt.follower": "",
        "bluez5.loopback": false,
        "card.profile.device": 1,
        "client.id": 40,
        "clock.quantum-limit": 8192,
        "device.api": "bluez5",
        "device.id": 78,
        "device.routes": 1,
        "factory.id": 11,
        "factory.mode": "merge",
        "factory.name": "api.bluez5.a2dp.sink",
        "library.name": "audioconvert/libspa-audioconvert",
        "media.class": "Audio/Sink",
        "media.name": "Redmi Buds 4 Pro",
        "node.description": "Redmi Buds 4 Pro",
        "node.driver": true,
        "node.name": "bluez_output.6C_D3_EE_5A_17_20.1",
        "node.pause-on-idle": false,
        "object.id": 99,
        "object.serial": 881,
        "priority.driver": 1010,
        "priority.session": 1010
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "raw",
            "format": "S16LE",
            "rate": 48000,
            "channels": 2,
            "position": [ "FL", "FR" ]
          }
        ],
        "PropInfo": [
          {
            "id": "volume",
            "description": "Volume",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "mute",
            "description": "Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "channelVolumes",
            "description": "Channel Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "channelMap",
            "description": "Channel Map",
            "type": "",
            "container": "Array"
          },
          {
            "id": "monitorMute",
            "description": "Monitor Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "monitorVolumes",
            "description": "Monitor Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "softMute",
            "description": "Soft Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "softVolumes",
            "description": "Soft Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "name": "monitor.channel-volumes",
            "description": "Monitor channel volume",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.disable",
            "description": "Disable Channel mixing",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.min-volume",
            "description": "Minimum volume level",
            "type": { "default": 0.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.max-volume",
            "description": "Maximum volume level",
            "type": { "default": 10.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.normalize",
            "description": "Normalize Volumes",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.mix-lfe",
            "description": "Mix LFE into channels",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.upmix",
            "description": "Enable upmixing",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.lfe-cutoff",
            "description": "LFE cutoff frequency",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.fc-cutoff",
            "description": "FC cutoff frequency (Hz)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 48000.000000 },
            "params": true
          },
          {
            "name": "channelmix.rear-delay",
            "description": "Rear channels delay (ms)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.stereo-widen",
            "description": "Stereo widen",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1.000000 },
            "params": true
          },
          {
            "name": "channelmix.hilbert-taps",
            "description": "Taps for phase shift of rear",
            "type": { "default": 0, "min": 0, "max": 255 },
            "params": true
          },
          {
            "name": "channelmix.upmix-method",
            "description": "Upmix method to use",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "simple",
              "Simple upmixing",
              "psd",
              "Passive Surround Decoding"
            ]
          },
          {
            "id": "rate",
            "description": "Rate scaler",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "quality",
            "name": "resample.quality",
            "description": "Resample Quality",
            "type": { "default": 4, "min": 0, "max": 14 },
            "params": true
          },
          {
            "name": "resample.disable",
            "description": "Disable Resampling",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "dither.noise",
            "description": "Add noise bits",
            "type": { "default": 0, "min": 0, "max": 16 },
            "params": true
          },
          {
            "name": "dither.method",
            "description": "The dithering method",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "rectangular",
              "Rectangular dithering",
              "triangular",
              "Triangular dithering",
              "triangular-hf",
              "Sloped Triangular dithering",
              "wannamaker3",
              "Wannamaker 3 dithering",
              "shaped5",
              "Lipshitz 5 dithering"
            ]
          },
          {
            "name": "debug.wav-path",
            "description": "Path to WAV file",
            "type": "",
            "params": true
          },
          {
            "name": "channelmix.lock-volumes",
            "description": "Disable volume updates",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "id": "latencyOffsetNsec",
            "description": "Latency offset (ns)",
            "type": { "default": 0, "min": -9223372036854775808, "max": 9223372036854775807 }
          }
        ],
        "Props": [
          {
            "volume": 1.000000,
            "mute": false,
            "channelVolumes": [ 0.418562, 0.418562 ],
            "channelMap": [ "FL", "FR" ],
            "softMute": false,
            "softVolumes": [ 1.000000, 1.000000 ],
            "monitorMute": false,
            "monitorVolumes": [ 1.000000, 1.000000 ],
            "params": [
              "monitor.channel-volumes",
              false,
              "channelmix.disable",
              false,
              "channelmix.min-volume",
              0.000000,
              "channelmix.max-volume",
              10.000000,
              "channelmix.normalize",
              false,
              "channelmix.mix-lfe",
              true,
              "channelmix.upmix",
              true,
              "channelmix.lfe-cutoff",
              0.000000,
              "channelmix.fc-cutoff",
              0.000000,
              "channelmix.rear-delay",
              0.000000,
              "channelmix.stereo-widen",
              0.000000,
              "channelmix.hilbert-taps",
              0,
              "channelmix.upmix-method",
              "none",
              "resample.quality",
              4,
              "resample.disable",
              false,
              "dither.noise",
              0,
              "dither.method",
              "none",
              "debug.wav-path",
              "",
              "channelmix.lock-volumes",
              false
            ]
          },
          {
            "latencyOffsetNsec": 0
          }
        ],
        "Format": [ ],
        "EnumPortConfig": [
          {
            "direction": "Input",
            "mode": {
              "default": "none",
              "alt1": "none",
              "alt2": "dsp",
              "alt3": "convert"
            },
            "monitor": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "control": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          }
        ],
        "PortConfig": [
          {
            "direction": "Input",
            "mode": "dsp",
            "monitor": true,
            "control": false,
            "format": {
              "mediaType": "audio",
              "mediaSubtype": "raw",
              "format": "F32P",
              "channels": 2,
              "position": [ "FL", "FR" ]
            }
          }
        ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 1900000,
            "maxNs": 1900000
          }
        ],
        "ProcessLatency": [
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 72,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "input",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FL",
        "format.dsp": "32 bit float mono audio",
        "node.id": 99,
        "object.id": 72,
        "object.path": "bluez_output.6C_D3_EE_5A_17_20.1:playback_0",
        "object.serial": 115,
        "port.alias": "Redmi Buds 4 Pro:playback_FL",
        "port.direction": "in",
        "port.id": 0,
        "port.name": "playback_FL",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 1900000,
            "maxNs": 1900000
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 92,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FL",
        "format.dsp": "32 bit float mono audio",
        "node.id": 99,
        "object.id": 92,
        "object.path": "bluez_output.6C_D3_EE_5A_17_20.1:monitor_0",
        "object.serial": 116,
        "port.alias": "Redmi Buds 4 Pro:monitor_FL",
        "port.direction": "out",
        "port.id": 0,
        "port.monitor": true,
        "port.name": "monitor_FL"
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 88,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "input",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FR",
        "format.dsp": "32 bit float mono audio",
        "node.id": 99,
        "object.id": 88,
        "object.path": "bluez_output.6C_D3_EE_5A_17_20.1:playback_1",
        "object.serial": 117,
        "port.alias": "Redmi Buds 4 Pro:playback_FR",
        "port.direction": "in",
        "port.id": 1,
        "port.name": "playback_FR",
        "port.physical": true,
        "port.terminal": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 1.000000,
            "maxQuantum": 1.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 1900000,
            "maxNs": 1900000
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 96,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "FR",
        "format.dsp": "32 bit float mono audio",
        "node.id": 99,
        "object.id": 96,
        "object.path": "bluez_output.6C_D3_EE_5A_17_20.1:monitor_1",
        "object.serial": 118,
        "port.alias": "Redmi Buds 4 Pro:monitor_FR",
        "port.direction": "out",
        "port.id": 1,
        "port.monitor": true,
        "port.name": "monitor_FR"
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 95,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.language": "en_US.UTF-8",
        "application.name": "WirePlumber",
        "application.process.binary": "wireplumber",
        "application.process.host": "weatherwax",
        "application.process.id": 3785,
        "application.process.user": "antonio",
        "application.version": "0.5.0",
        "clock.power-of-two-quantum": true,
        "config.name": "null",
        "core.name": "pipewire-antonio-3785",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 2,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 95,
        "object.serial": 893,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 3785,
        "pipewire.sec.socket": "pipewire-0-manager",
        "pipewire.sec.uid": 1000,
        "remote.name": "[pipewire-0-manager,pipewire-0]",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "wireplumber.daemon": true,
        "wireplumber.profile": "main"
      }
    }
  },
  {
    "id": 102,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 0,
      "max-output-ports": 65,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 0,
      "n-output-ports": 1,
      "state": "suspended",
      "error": null,
      "props": {
        "adapt.follower.spa-node": "",
        "audio.adapt.follower": "",
        "audio.position": "[MONO]",
        "bluez5.loopback": true,
        "card.profile.device": 0,
        "client.id": 95,
        "clock.quantum-limit": 8192,
        "device.id": 78,
        "factory.id": 11,
        "factory.mode": "split",
        "filter.smart": true,
        "filter.smart.target": "{\"device.api\":\"bluez5\", \"factory.name\":\"api.bluez5.sco.source\", \"bluez5.loopback\":false, \"device.id\":78}",
        "library.name": "audioconvert/libspa-audioconvert",
        "media.class": "Audio/Source",
        "media.name": "Redmi Buds 4 Pro output",
        "node.autoconnect": true,
        "node.description": "Redmi Buds 4 Pro",
        "node.group": "loopback-3785-22",
        "node.link-group": "loopback-3785-22",
        "node.name": "bluez_input.6C:D3:EE:5A:17:20",
        "node.trigger": true,
        "node.virtual": true,
        "node.want-driver": true,
        "object.id": 102,
        "object.register": false,
        "object.serial": 894,
        "priority.driver": 2010,
        "priority.session": 2010,
        "resample.disable": true,
        "resample.prefill": true,
        "stream.is-live": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "raw",
            "format": "F32P",
            "channels": 1,
            "position": [ "MONO" ]
          }
        ],
        "PropInfo": [
          {
            "id": "volume",
            "description": "Volume",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "mute",
            "description": "Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "channelVolumes",
            "description": "Channel Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "channelMap",
            "description": "Channel Map",
            "type": "",
            "container": "Array"
          },
          {
            "id": "monitorMute",
            "description": "Monitor Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "monitorVolumes",
            "description": "Monitor Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "softMute",
            "description": "Soft Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "softVolumes",
            "description": "Soft Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "name": "monitor.channel-volumes",
            "description": "Monitor channel volume",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.disable",
            "description": "Disable Channel mixing",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.min-volume",
            "description": "Minimum volume level",
            "type": { "default": 0.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.max-volume",
            "description": "Maximum volume level",
            "type": { "default": 10.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.normalize",
            "description": "Normalize Volumes",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.mix-lfe",
            "description": "Mix LFE into channels",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.upmix",
            "description": "Enable upmixing",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.lfe-cutoff",
            "description": "LFE cutoff frequency",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.fc-cutoff",
            "description": "FC cutoff frequency (Hz)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 48000.000000 },
            "params": true
          },
          {
            "name": "channelmix.rear-delay",
            "description": "Rear channels delay (ms)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.stereo-widen",
            "description": "Stereo widen",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1.000000 },
            "params": true
          },
          {
            "name": "channelmix.hilbert-taps",
            "description": "Taps for phase shift of rear",
            "type": { "default": 0, "min": 0, "max": 255 },
            "params": true
          },
          {
            "name": "channelmix.upmix-method",
            "description": "Upmix method to use",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "simple",
              "Simple upmixing",
              "psd",
              "Passive Surround Decoding"
            ]
          },
          {
            "id": "rate",
            "description": "Rate scaler",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "quality",
            "name": "resample.quality",
            "description": "Resample Quality",
            "type": { "default": 4, "min": 0, "max": 14 },
            "params": true
          },
          {
            "name": "resample.disable",
            "description": "Disable Resampling",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "dither.noise",
            "description": "Add noise bits",
            "type": { "default": 0, "min": 0, "max": 16 },
            "params": true
          },
          {
            "name": "dither.method",
            "description": "The dithering method",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "rectangular",
              "Rectangular dithering",
              "triangular",
              "Triangular dithering",
              "triangular-hf",
              "Sloped Triangular dithering",
              "wannamaker3",
              "Wannamaker 3 dithering",
              "shaped5",
              "Lipshitz 5 dithering"
            ]
          },
          {
            "name": "debug.wav-path",
            "description": "Path to WAV file",
            "type": "",
            "params": true
          },
          {
            "name": "channelmix.lock-volumes",
            "description": "Disable volume updates",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          }
        ],
        "Props": [
          {
            "volume": 1.000000,
            "mute": false,
            "channelVolumes": [ 1.000000 ],
            "channelMap": [ "MONO" ],
            "softMute": false,
            "softVolumes": [ 1.000000 ],
            "monitorMute": false,
            "monitorVolumes": [ 1.000000 ],
            "params": [
              "monitor.channel-volumes",
              false,
              "channelmix.disable",
              false,
              "channelmix.min-volume",
              0.000000,
              "channelmix.max-volume",
              10.000000,
              "channelmix.normalize",
              false,
              "channelmix.mix-lfe",
              true,
              "channelmix.upmix",
              true,
              "channelmix.lfe-cutoff",
              0.000000,
              "channelmix.fc-cutoff",
              0.000000,
              "channelmix.rear-delay",
              0.000000,
              "channelmix.stereo-widen",
              0.000000,
              "channelmix.hilbert-taps",
              0,
              "channelmix.upmix-method",
              "none",
              "resample.quality",
              4,
              "resample.disable",
              true,
              "dither.noise",
              0,
              "dither.method",
              "none",
              "debug.wav-path",
              "",
              "channelmix.lock-volumes",
              false
            ]
          }
        ],
        "Format": [ ],
        "EnumPortConfig": [
          {
            "direction": "Output",
            "mode": {
              "default": "none",
              "alt1": "none",
              "alt2": "dsp",
              "alt3": "convert"
            },
            "monitor": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "control": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          }
        ],
        "PortConfig": [
          {
            "direction": "Output",
            "mode": "dsp",
            "monitor": true,
            "control": false,
            "format": {
              "mediaType": "audio",
              "mediaSubtype": "raw",
              "format": "F32P",
              "channels": 1,
              "position": [ "MONO" ]
            }
          }
        ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "ProcessLatency": [
        ],
        "Tag": [ ]
      }
    }
  },
  {
    "id": 93,
    "type": "PipeWire:Interface:Node",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "max-input-ports": 65,
      "max-output-ports": 0,
      "change-mask": [ "input-ports", "output-ports", "state", "props", "params" ],
      "n-input-ports": 1,
      "n-output-ports": 1,
      "state": "suspended",
      "error": null,
      "props": {
        "adapt.follower.spa-node": "",
        "audio.adapt.follower": "",
        "audio.channels": 1,
        "audio.position": "[MONO]",
        "bluez5.loopback": true,
        "client.id": 95,
        "clock.quantum-limit": 8192,
        "factory.id": 11,
        "factory.mode": "merge",
        "library.name": "audioconvert/libspa-audioconvert",
        "media.class": "Stream/Input/Audio/Internal",
        "media.name": "Bluetooth internal capture stream for Redmi Buds 4 Pro input",
        "node.autoconnect": true,
        "node.description": "Bluetooth internal capture stream for Redmi Buds 4 Pro",
        "node.dont-fallback": true,
        "node.group": "loopback-3785-22",
        "node.linger": true,
        "node.link-group": "loopback-3785-22",
        "node.name": "bluez_capture_internal.6C:D3:EE:5A:17:20",
        "node.passive": true,
        "node.virtual": true,
        "node.want-driver": true,
        "object.id": 93,
        "object.register": false,
        "object.serial": 895,
        "resample.disable": true,
        "resample.prefill": true,
        "stream.dont-remix": true,
        "stream.is-live": true
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "raw",
            "format": "F32P",
            "channels": 1,
            "position": [ "MONO" ]
          }
        ],
        "PropInfo": [
          {
            "id": "volume",
            "description": "Volume",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "mute",
            "description": "Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "channelVolumes",
            "description": "Channel Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "channelMap",
            "description": "Channel Map",
            "type": "",
            "container": "Array"
          },
          {
            "id": "monitorMute",
            "description": "Monitor Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "monitorVolumes",
            "description": "Monitor Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "id": "softMute",
            "description": "Soft Mute",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          },
          {
            "id": "softVolumes",
            "description": "Soft Volumes",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 },
            "container": "Array"
          },
          {
            "name": "monitor.channel-volumes",
            "description": "Monitor channel volume",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.disable",
            "description": "Disable Channel mixing",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.min-volume",
            "description": "Minimum volume level",
            "type": { "default": 0.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.max-volume",
            "description": "Maximum volume level",
            "type": { "default": 10.000000, "min": 0.000000, "max": 10.000000 },
            "params": true
          },
          {
            "name": "channelmix.normalize",
            "description": "Normalize Volumes",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          },
          {
            "name": "channelmix.mix-lfe",
            "description": "Mix LFE into channels",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.upmix",
            "description": "Enable upmixing",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "channelmix.lfe-cutoff",
            "description": "LFE cutoff frequency",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.fc-cutoff",
            "description": "FC cutoff frequency (Hz)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 48000.000000 },
            "params": true
          },
          {
            "name": "channelmix.rear-delay",
            "description": "Rear channels delay (ms)",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1000.000000 },
            "params": true
          },
          {
            "name": "channelmix.stereo-widen",
            "description": "Stereo widen",
            "type": { "default": 0.000000, "min": 0.000000, "max": 1.000000 },
            "params": true
          },
          {
            "name": "channelmix.hilbert-taps",
            "description": "Taps for phase shift of rear",
            "type": { "default": 0, "min": 0, "max": 255 },
            "params": true
          },
          {
            "name": "channelmix.upmix-method",
            "description": "Upmix method to use",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "simple",
              "Simple upmixing",
              "psd",
              "Passive Surround Decoding"
            ]
          },
          {
            "id": "rate",
            "description": "Rate scaler",
            "type": { "default": 1.000000, "min": 0.000000, "max": 10.000000 }
          },
          {
            "id": "quality",
            "name": "resample.quality",
            "description": "Resample Quality",
            "type": { "default": 4, "min": 0, "max": 14 },
            "params": true
          },
          {
            "name": "resample.disable",
            "description": "Disable Resampling",
            "type": {
              "default": true,
              "alt1": true,
              "alt2": false
            },
            "params": true
          },
          {
            "name": "dither.noise",
            "description": "Add noise bits",
            "type": { "default": 0, "min": 0, "max": 16 },
            "params": true
          },
          {
            "name": "dither.method",
            "description": "The dithering method",
            "type": "none",
            "params": true,
            "labels": [
              "none",
              "Disabled",
              "rectangular",
              "Rectangular dithering",
              "triangular",
              "Triangular dithering",
              "triangular-hf",
              "Sloped Triangular dithering",
              "wannamaker3",
              "Wannamaker 3 dithering",
              "shaped5",
              "Lipshitz 5 dithering"
            ]
          },
          {
            "name": "debug.wav-path",
            "description": "Path to WAV file",
            "type": "",
            "params": true
          },
          {
            "name": "channelmix.lock-volumes",
            "description": "Disable volume updates",
            "type": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "params": true
          }
        ],
        "Props": [
          {
            "volume": 1.000000,
            "mute": false,
            "channelVolumes": [ 1.000000 ],
            "channelMap": [ "MONO" ],
            "softMute": false,
            "softVolumes": [ 1.000000 ],
            "monitorMute": false,
            "monitorVolumes": [ 1.000000 ],
            "params": [
              "monitor.channel-volumes",
              false,
              "channelmix.disable",
              false,
              "channelmix.min-volume",
              0.000000,
              "channelmix.max-volume",
              10.000000,
              "channelmix.normalize",
              false,
              "channelmix.mix-lfe",
              true,
              "channelmix.upmix",
              true,
              "channelmix.lfe-cutoff",
              0.000000,
              "channelmix.fc-cutoff",
              0.000000,
              "channelmix.rear-delay",
              0.000000,
              "channelmix.stereo-widen",
              0.000000,
              "channelmix.hilbert-taps",
              0,
              "channelmix.upmix-method",
              "none",
              "resample.quality",
              4,
              "resample.disable",
              true,
              "dither.noise",
              0,
              "dither.method",
              "none",
              "debug.wav-path",
              "",
              "channelmix.lock-volumes",
              false
            ]
          }
        ],
        "Format": [ ],
        "EnumPortConfig": [
          {
            "direction": "Input",
            "mode": {
              "default": "none",
              "alt1": "none",
              "alt2": "dsp",
              "alt3": "convert"
            },
            "monitor": {
              "default": false,
              "alt1": false,
              "alt2": true
            },
            "control": {
              "default": false,
              "alt1": false,
              "alt2": true
            }
          }
        ],
        "PortConfig": [
          {
            "direction": "Input",
            "mode": "dsp",
            "monitor": true,
            "control": false,
            "format": {
              "mediaType": "audio",
              "mediaSubtype": "raw",
              "format": "F32P",
              "channels": 1,
              "position": [ "MONO" ]
            }
          }
        ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "ProcessLatency": [
        ],
        "Tag": [ ]
      }
    }
  },
  {
    "id": 71,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "input",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "MONO",
        "format.dsp": "32 bit float mono audio",
        "node.id": 93,
        "object.id": 71,
        "object.path": "bluez_capture_internal.6C:D3:EE:5A:17:20:input_0",
        "object.serial": 896,
        "port.alias": "Bluetooth internal capture stream for Redmi Buds 4 Pro:input_MONO",
        "port.direction": "in",
        "port.id": 0,
        "port.name": "input_MONO"
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 98,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "MONO",
        "format.dsp": "32 bit float mono audio",
        "node.id": 93,
        "object.id": 98,
        "object.path": "bluez_capture_internal.6C:D3:EE:5A:17:20:monitor_0",
        "object.serial": 897,
        "port.alias": "Bluetooth internal capture stream for Redmi Buds 4 Pro:monitor_MONO",
        "port.direction": "out",
        "port.id": 0,
        "port.monitor": true,
        "port.name": "monitor_MONO"
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 73,
    "type": "PipeWire:Interface:Port",
    "version": 3,
    "permissions": [ "r", "x", "m" ],
    "info": {
      "direction": "output",
      "change-mask": [ "props", "params" ],
      "props": {
        "audio.channel": "MONO",
        "format.dsp": "32 bit float mono audio",
        "node.id": 102,
        "object.id": 73,
        "object.path": "bluez_input.6C:D3:EE:5A:17:20:capture_0",
        "object.serial": 898,
        "port.alias": "Redmi Buds 4 Pro:capture_MONO",
        "port.direction": "out",
        "port.id": 0,
        "port.name": "capture_MONO"
      },
      "params": {
        "EnumFormat": [
          {
            "mediaType": "audio",
            "mediaSubtype": "dsp",
            "format": "F32P"
          }
        ],
        "Meta": [
          {
            "type": "Header",
            "size": 32
          }
        ],
        "IO": [
          {
            "id": "Buffers",
            "size": 8
          }
        ],
        "Format": [ ],
        "Buffers": [ ],
        "Latency": [
          {
            "direction": "Input",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          },
          {
            "direction": "Output",
            "minQuantum": 0.000000,
            "maxQuantum": 0.000000,
            "minRate": 0,
            "maxRate": 0,
            "minNs": 0,
            "maxNs": 0
          }
        ],
        "Tag": [
        ]
      }
    }
  },
  {
    "id": 83,
    "type": "PipeWire:Interface:Client",
    "version": 3,
    "permissions": [ "r", "w", "x", "m" ],
    "info": {
      "change-mask": [ "props" ],
      "props": {
        "application.language": "es_ES.UTF-8",
        "application.name": "pw-dump",
        "application.process.binary": "pw-dump",
        "application.process.host": "weatherwax",
        "application.process.id": 19735,
        "application.process.session-id": 3,
        "application.process.user": "antonio",
        "clock.power-of-two-quantum": true,
        "core.name": "pipewire-antonio-19735",
        "core.version": "1.0.4",
        "cpu.max-align": 64,
        "default.clock.max-quantum": 2048,
        "default.clock.min-quantum": 32,
        "default.clock.quantum": 1024,
        "default.clock.quantum-floor": 4,
        "default.clock.quantum-limit": 8192,
        "default.clock.rate": 48000,
        "default.video.height": 480,
        "default.video.rate.denom": 1,
        "default.video.rate.num": 25,
        "default.video.width": 640,
        "link.max-buffers": 64,
        "log.level": 0,
        "mem.allow-mlock": true,
        "mem.warn-mlock": false,
        "module.id": 2,
        "object.id": 83,
        "object.serial": 899,
        "pipewire.access": "unrestricted",
        "pipewire.protocol": "protocol-native",
        "pipewire.sec.gid": 100,
        "pipewire.sec.pid": 19735,
        "pipewire.sec.socket": "pipewire-0-manager",
        "pipewire.sec.uid": 1000,
        "remote.name": "[pipewire-0-manager,pipewire-0]",
        "settings.check-quantum": false,
        "settings.check-rate": false,
        "window.x11.display": ":0"
      }
    }
  }
]
   070701000000CE000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001A00000000wireplumber-0.5.0/modules 070701000000CF000081A400000000000000000000000165F86304000001DD000000000000000000000000000000000000003200000000wireplumber-0.5.0/modules/dbus-connection-state.h /* WirePlumber
 *
 * Copyright © 2023 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_DBUS_CONNECTION_STATE_H__
#define __WIREPLUMBER_DBUS_CONNECTION_STATE_H__

#include <glib-object.h>

G_BEGIN_DECLS

typedef enum {
  WP_DBUS_CONNECTION_STATE_CLOSED = 0,
  WP_DBUS_CONNECTION_STATE_CONNECTING,
  WP_DBUS_CONNECTION_STATE_CONNECTED,
} WpDBusConnectionState;

G_END_DECLS

#endif
   070701000000D0000081A400000000000000000000000165F8630400000E5A000000000000000000000000000000000000002600000000wireplumber-0.5.0/modules/meson.build shared_library(
  'wireplumber-module-settings',
  [
    'module-settings.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep],
)

dbus_connection_enums = gnome.mkenums_simple('dbus-connection-enums',
  sources: [ 'dbus-connection-state.h' ],
)
shared_library(
  'wireplumber-module-dbus-connection',
  [
    'module-dbus-connection.c',
    dbus_connection_enums,
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep],
)

shared_library(
  'wireplumber-module-default-nodes-api',
  [
    'module-default-nodes-api.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep],
)

subdir('module-reserve-device')
shared_library(
  'wireplumber-module-reserve-device',
  [
    'module-reserve-device/plugin.c',
    'module-reserve-device/reserve-device.c',
    'module-reserve-device/transitions.c',
    reserve_device_interface_src,
    reserve_device_enums,
  ],
  include_directories: reserve_device_includes,
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, giounix_dep],
)

shared_library(
  'wireplumber-module-portal-permissionstore',
  [
    'module-portal-permissionstore.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, giounix_dep],
)

shared_library(
  'wireplumber-module-si-audio-adapter',
  [
    'module-si-audio-adapter.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep],
)

shared_library(
  'wireplumber-module-si-audio-virtual',
  [
    'module-si-audio-virtual.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep],
)

shared_library(
  'wireplumber-module-si-node',
  [
    'module-si-node.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep],
)

shared_library(
  'wireplumber-module-si-standard-link',
  [
    'module-si-standard-link.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep],
)

subdir('module-lua-scripting')
shared_library(
  'wireplumber-module-lua-scripting',
  [
    'module-lua-scripting/module.c',
    'module-lua-scripting/script.c',
    'module-lua-scripting/api/pod.c',
    'module-lua-scripting/api/json.c',
    'module-lua-scripting/api/api.c',
     m_lua_scripting_resources,
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep, wplua_dep, libintl_dep],
)

shared_library(
  'wireplumber-module-mixer-api',
  [
    'module-mixer-api.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep, mathlib],
)

shared_library(
  'wireplumber-module-file-monitor-api',
  [
    'module-file-monitor-api.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep],
)

shared_library(
  'wireplumber-module-log-settings',
  [
    'module-log-settings.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep],
)

if libsystemd_dep.found() or libelogind_dep.found()
  shared_library(
    'wireplumber-module-logind',
    [
      'module-logind.c',
    ],
    install : true,
    install_dir : wireplumber_module_dir,
    dependencies : [wp_dep, pipewire_dep, libsystemd_dep, libelogind_dep],
  )
endif

shared_library(
  'wireplumber-module-standard-event-source',
  [
    'module-standard-event-source.c',
  ],
  install : true,
  install_dir : wireplumber_module_dir,
  dependencies : [wp_dep, pipewire_dep],
)
  070701000000D1000081A400000000000000000000000165F8630400001C1D000000000000000000000000000000000000003300000000wireplumber-0.5.0/modules/module-dbus-connection.c    /* WirePlumber
 *
 * Copyright © 2022-2023 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include "dbus-connection-state.h"
#include "dbus-connection-enums.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("m-dbus-connection")

enum
{
  PROP_0,
  PROP_BUS_TYPE,
  PROP_STATE,
  PROP_CONNECTION,
};

struct _WpDBusConnection
{
  WpPlugin parent;

  /* Props */
  GBusType bus_type;
  WpDBusConnectionState state;
  GDBusConnection *connection;

  GCancellable *cancellable;
};

static void on_connection_closed (GDBusConnection * connection,
    gboolean remote_peer_vanished, GError * error, gpointer data);

G_DECLARE_FINAL_TYPE (WpDBusConnection, wp_dbus_connection,
                      WP, DBUS_CONNECTION, WpPlugin)
G_DEFINE_TYPE (WpDBusConnection, wp_dbus_connection, WP_TYPE_PLUGIN)

static void
wp_dbus_connection_init (WpDBusConnection * self)
{
}

static void
wp_dbus_connection_set_state (WpDBusConnection * self, WpDBusConnectionState new_state)
{
  if (self->state != new_state) {
    self->state = new_state;
    g_object_notify (G_OBJECT (self), "state");
  }
}

static void
on_got_bus (GObject * obj, GAsyncResult * res, gpointer data)
{
  WpTransition *transition;
  WpDBusConnection *self;
  g_autoptr (GError) error = NULL;

  if (WP_IS_TRANSITION (data)) {
    // coming from wp_dbus_connection_enable
    transition = WP_TRANSITION (data);
    self = wp_transition_get_source_object (transition);
  } else {
    // coming from on_sync_reconnect
    transition = NULL;
    self = WP_DBUS_CONNECTION (data);
  }

  self->connection = g_dbus_connection_new_for_address_finish (res, &error);
  if (!self->connection) {
    if (transition) {
      g_prefix_error (&error, "Failed to connect to bus: ");
      wp_transition_return_error (transition, g_steal_pointer (&error));
    }
    return;
  }

  wp_debug_object (self, "Connected to bus");

  /* set up connection */
  g_signal_connect_object (self->connection, "closed",
      G_CALLBACK (on_connection_closed), self, 0);
  g_dbus_connection_set_exit_on_close (self->connection, FALSE);

  wp_dbus_connection_set_state (self, WP_DBUS_CONNECTION_STATE_CONNECTED);

  if (transition)
    wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static gboolean
do_connect (WpDBusConnection * self, GAsyncReadyCallback callback,
    gpointer data, GError ** error)
{
  g_autofree gchar *address = NULL;

  address = g_dbus_address_get_for_bus_sync (self->bus_type, NULL, error);
  if (!address) {
    g_prefix_error (error, "Error acquiring bus address: ");
    return FALSE;
  }

  wp_dbus_connection_set_state (self, WP_DBUS_CONNECTION_STATE_CONNECTING);

  wp_debug_object (self, "Connecting to bus: %s", address);
  g_dbus_connection_new_for_address (address,
      G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
      G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
      NULL, self->cancellable, callback, data);
  return TRUE;
}

static void
on_sync_reconnect (WpCore * core, GAsyncResult * res, WpDBusConnection * self)
{
  g_autoptr (GError) error = NULL;

  if (!wp_core_sync_finish (core, res, &error)) {
    wp_warning_object (self, "core sync error: %s", error->message);
    return;
  }

  if (!do_connect (self, on_got_bus, self, &error))
    wp_notice_object (self, "Cannot reconnect: %s", error->message);
}

static void
on_connection_closed (GDBusConnection * connection,
    gboolean remote_peer_vanished, GError * error, gpointer data)
{
  WpDBusConnection *self = WP_DBUS_CONNECTION (data);
  g_autoptr (WpCore) core = NULL;

  wp_notice_object (self, "DBus connection closed: %s", error->message);

  g_clear_object (&self->connection);
  wp_dbus_connection_set_state (self, WP_DBUS_CONNECTION_STATE_CLOSED);

  /* try to reconnect on idle if connection was closed */
  core = wp_object_get_core (WP_OBJECT (self));
  if (core && wp_core_is_connected (core)) {
    wp_notice_object (self, "Trying to reconnect after core sync");
    wp_core_sync_closure (core, NULL, g_cclosure_new_object (
        G_CALLBACK (on_sync_reconnect), G_OBJECT (self)));
  }
}

static void
wp_dbus_connection_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpDBusConnection *self = WP_DBUS_CONNECTION (plugin);
  g_autoptr (GError) error = NULL;
  if (!do_connect (self, on_got_bus, transition, &error)) {
    wp_transition_return_error (transition, g_steal_pointer (&error));
  }
}

static void
wp_dbus_connection_disable (WpPlugin * plugin)
{
  WpDBusConnection *self = WP_DBUS_CONNECTION (plugin);

  g_cancellable_cancel (self->cancellable);

  g_clear_object (&self->connection);
  wp_dbus_connection_set_state (self, WP_DBUS_CONNECTION_STATE_CLOSED);

  g_clear_object (&self->cancellable);
  self->cancellable = g_cancellable_new ();
}

static void
wp_dbus_connection_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpDBusConnection *self = WP_DBUS_CONNECTION (object);

  switch (property_id) {
  case PROP_BUS_TYPE:
    g_value_set_enum (value, self->bus_type);
    break;
  case PROP_STATE:
    g_value_set_enum (value, self->state);
    break;
  case PROP_CONNECTION:
    g_value_set_object (value, self->connection);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_dbus_connection_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpDBusConnection *self = WP_DBUS_CONNECTION (object);

  switch (property_id) {
  case PROP_BUS_TYPE:
    self->bus_type = g_value_get_enum (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_dbus_connection_class_init (WpDBusConnectionClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  object_class->get_property = wp_dbus_connection_get_property;
  object_class->set_property = wp_dbus_connection_set_property;

  plugin_class->enable = wp_dbus_connection_enable;
  plugin_class->disable = wp_dbus_connection_disable;

  g_object_class_install_property (object_class, PROP_BUS_TYPE,
      g_param_spec_enum ("bus-type", "bus-type", "The bus type",
          G_TYPE_BUS_TYPE, G_BUS_TYPE_NONE,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_STATE,
      g_param_spec_enum ("state", "state", "The dbus connection state",
          WP_TYPE_DBUS_CONNECTION_STATE, WP_DBUS_CONNECTION_STATE_CLOSED,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_CONNECTION,
      g_param_spec_object ("connection", "connection", "The dbus connection",
          G_TYPE_DBUS_CONNECTION, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (g_object_new (
      wp_dbus_connection_get_type(),
      "name", "dbus-connection",
      "core", core,
      "bus-type", G_BUS_TYPE_SESSION,
      NULL));
}
   070701000000D2000081A400000000000000000000000165F86304000025BD000000000000000000000000000000000000003500000000wireplumber-0.5.0/modules/module-default-nodes-api.c  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <spa/utils/defs.h>
#include <pipewire/keys.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-default-nodes-api")

/*
 * Module Provides the APIs to query the default device nodes. Module looks at
 * the default metadata to know the default devices.
 */

enum {
  AUDIO_SINK,
  AUDIO_SOURCE,
  VIDEO_SOURCE,
  N_DEFAULT_NODES
};

static const gchar * DEFAULT_KEY[N_DEFAULT_NODES] = {
  [AUDIO_SINK] = "default.audio.sink",
  [AUDIO_SOURCE] = "default.audio.source",
  [VIDEO_SOURCE] = "default.video.source",
};

static const gchar * NODE_TYPE_STR[N_DEFAULT_NODES] = {
  [AUDIO_SINK] = "Audio/Sink",
  [AUDIO_SOURCE] = "Audio/Source",
  [VIDEO_SOURCE] = "Video/Source",
};

static const gchar * DEFAULT_CONFIG_KEY[N_DEFAULT_NODES] = {
  [AUDIO_SINK] = "default.configured.audio.sink",
  [AUDIO_SOURCE] = "default.configured.audio.source",
  [VIDEO_SOURCE] = "default.configured.video.source",
};


typedef struct _WpDefaultNode WpDefaultNode;
struct _WpDefaultNode
{
  gchar *value;
  gchar *config_value;
};

struct _WpDefaultNodesApi
{
  WpPlugin parent;

  WpDefaultNode defaults[N_DEFAULT_NODES];
  WpObjectManager *om;
};

enum {
  ACTION_GET_DEFAULT_NODE,
  ACTION_GET_DEFAULT_CONFIGURED_NODE_NAME,
  ACTION_SET_DEFAULT_CONFIGURED_NODE_NAME,
  SIGNAL_CHANGED,
  N_SIGNALS
};

static guint signals[N_SIGNALS] = {0};

G_DECLARE_FINAL_TYPE (WpDefaultNodesApi, wp_default_nodes_api,
                      WP, DEFAULT_NODES_API, WpPlugin)
G_DEFINE_TYPE (WpDefaultNodesApi, wp_default_nodes_api, WP_TYPE_PLUGIN)

static void
wp_default_nodes_api_init (WpDefaultNodesApi * self)
{
}

static void
sync_changed_notification (WpCore * core, GAsyncResult * res,
    WpDefaultNodesApi * self)
{
  g_autoptr (GError) error = NULL;
  if (!wp_core_sync_finish (core, res, &error)) {
    wp_warning_object (self, "core sync error: %s", error->message);
    return;
  }

  g_signal_emit (self, signals[SIGNAL_CHANGED], 0);
  return;
}

static void
schedule_changed_notification (WpDefaultNodesApi *self)
{
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_return_if_fail (core);
  wp_core_sync_closure (core, NULL, g_cclosure_new_object (
      G_CALLBACK (sync_changed_notification), G_OBJECT (self)));
}

static void
on_metadata_changed (WpMetadata *m, guint32 subject,
    const gchar *key, const gchar *type, const gchar *value, gpointer d)
{
  WpDefaultNodesApi * self = WP_DEFAULT_NODES_API (d);
  gchar *new_value = NULL;

  if (subject != 0)
    return;

  for (gint i = 0; i < N_DEFAULT_NODES; i++) {
    if (!g_strcmp0 (key, DEFAULT_KEY[i])) {

      if (value && !g_strcmp0 (type, "Spa:String:JSON")) {
        g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_string (value);
        wp_spa_json_object_get (json, "name", "s", &new_value, NULL);
      }

      wp_debug_object (m, "'%s' changed from '%s' -> '%s'", key,
          self->defaults[i].value, new_value);

      g_clear_pointer (&self->defaults[i].value, g_free);
      self->defaults[i].value = new_value;

      schedule_changed_notification (self);
      break;
    } else if (!g_strcmp0 (key, DEFAULT_CONFIG_KEY[i])) {

      if (value && !g_strcmp0 (type, "Spa:String:JSON")) {
        g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_string (value);
        wp_spa_json_object_get (json, "name", "s", &new_value, NULL);
      }

      wp_debug_object (m, "'%s' changed from '%s' -> '%s'", key,
          self->defaults[i].config_value, new_value);

      g_clear_pointer (&self->defaults[i].config_value, g_free);
      self->defaults[i].config_value = new_value;

      break;
    }
  }
}

static void
on_metadata_added (WpObjectManager *om, WpObject *obj, WpDefaultNodesApi * self)
{
  if (WP_IS_METADATA (obj)) {
    g_autoptr (WpIterator) it = wp_metadata_new_iterator (WP_METADATA (obj), 0);
    g_auto (GValue) val = G_VALUE_INIT;

    for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpMetadataItem *mi = g_value_get_boxed (&val);
      guint32 subject = wp_metadata_item_get_subject (mi);
      const gchar *key = wp_metadata_item_get_key (mi);
      const gchar *type = wp_metadata_item_get_value_type (mi);
      const gchar *value = wp_metadata_item_get_value (mi);
      on_metadata_changed (WP_METADATA (obj), subject, key, type, value, self);
    }

    g_signal_connect_object (obj, "changed",
        G_CALLBACK (on_metadata_changed), self, 0);
  }
}

static void
on_om_installed (WpObjectManager * om, WpDefaultNodesApi * self)
{
  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
wp_default_nodes_api_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpDefaultNodesApi * self = WP_DEFAULT_NODES_API (plugin);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
  g_return_if_fail (core);

  /* Create the metadata object manager */
  self->om = wp_object_manager_new ();
  wp_object_manager_add_interest (self->om, WP_TYPE_METADATA,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
      NULL);
  wp_object_manager_add_interest (self->om, WP_TYPE_NODE, NULL);
  wp_object_manager_request_object_features (self->om,
      WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
  wp_object_manager_request_object_features (self->om,
      WP_TYPE_NODE, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  g_signal_connect_object (self->om, "object-added",
      G_CALLBACK (on_metadata_added), self, 0);
  g_signal_connect_object (self->om, "installed",
      G_CALLBACK (on_om_installed), self, 0);
  wp_core_install_object_manager (core, self->om);
}

static void
wp_default_nodes_api_disable (WpPlugin * plugin)
{
  WpDefaultNodesApi * self = WP_DEFAULT_NODES_API (plugin);

  for (guint i = 0; i < N_DEFAULT_NODES; i++) {
    g_clear_pointer (&self->defaults[i].value, g_free);
    g_clear_pointer (&self->defaults[i].config_value, g_free);
  }
  g_clear_object (&self->om);
}

static guint
wp_default_nodes_api_get_default_node (WpDefaultNodesApi * self,
    const gchar * media_class)
{
  gint node_t = -1;
  for (gint i = 0; i < N_DEFAULT_NODES; i++) {
    if (!g_strcmp0 (media_class, NODE_TYPE_STR[i])) {
      node_t = i;
      break;
    }
  }

  if (node_t != -1 && self->defaults[node_t].value) {
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) val = G_VALUE_INIT;
    it = wp_object_manager_new_filtered_iterator (self->om,
        WP_TYPE_NODE,
        WP_CONSTRAINT_TYPE_PW_PROPERTY,
        PW_KEY_NODE_NAME, "=s", self->defaults[node_t].value,
        NULL);
    for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpNode *node = g_value_get_object (&val);
      const gchar *mc = wp_pipewire_object_get_property (
          WP_PIPEWIRE_OBJECT (node), PW_KEY_MEDIA_CLASS);
      if (!g_str_has_prefix (mc, "Stream/"))
        return wp_proxy_get_bound_id (WP_PROXY (node));
    }
  }
  return SPA_ID_INVALID;
}

static gchar *
wp_default_nodes_api_get_default_configured_node_name (WpDefaultNodesApi * self,
    const gchar * media_class)
{
  for (gint i = 0; i < N_DEFAULT_NODES; i++)
    if (!g_strcmp0 (media_class, NODE_TYPE_STR[i]) &&
        self->defaults[i].config_value)
      return g_strdup (self->defaults[i].config_value);

  return NULL;
}

static gboolean
wp_default_nodes_api_set_default_configured_node_name (WpDefaultNodesApi * self,
    const gchar * media_class, const gchar * name)
{
  g_autoptr (WpMetadata) m = wp_object_manager_lookup (self->om,
      WP_TYPE_METADATA, NULL);
  if (!m)
    return FALSE;

  for (gint i = 0; i < N_DEFAULT_NODES; i++) {
    if (!g_strcmp0 (media_class, NODE_TYPE_STR[i])) {
      if (name) {
        g_autofree gchar *v = g_strdup_printf ("{ \"name\": \"%s\" }", name);
        wp_metadata_set (m, 0, DEFAULT_CONFIG_KEY[i], "Spa:String:JSON", v);
      } else {
        wp_metadata_set (m, 0, DEFAULT_CONFIG_KEY[i], NULL, NULL);
      }
      return TRUE;
    }
  }

  return FALSE;
}

static void
wp_default_nodes_api_class_init (WpDefaultNodesApiClass * klass)
{
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  plugin_class->enable = wp_default_nodes_api_enable;
  plugin_class->disable = wp_default_nodes_api_disable;

  signals[ACTION_GET_DEFAULT_NODE] = g_signal_new_class_handler (
      "get-default-node", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_default_nodes_api_get_default_node,
      NULL, NULL, NULL,
      G_TYPE_UINT, 1, G_TYPE_STRING);

  signals[ACTION_GET_DEFAULT_CONFIGURED_NODE_NAME] = g_signal_new_class_handler (
      "get-default-configured-node-name", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_default_nodes_api_get_default_configured_node_name,
      NULL, NULL, NULL,
      G_TYPE_STRING, 1, G_TYPE_STRING);

  signals[ACTION_SET_DEFAULT_CONFIGURED_NODE_NAME] = g_signal_new_class_handler (
      "set-default-configured-node-name", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_default_nodes_api_set_default_configured_node_name,
      NULL, NULL, NULL,
      G_TYPE_BOOLEAN, 2, G_TYPE_STRING, G_TYPE_STRING);

  signals[SIGNAL_CHANGED] = g_signal_new (
      "changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (g_object_new (wp_default_nodes_api_get_type (),
      "name", "default-nodes-api",
      "core", core,
      NULL));
}
   070701000000D3000081A400000000000000000000000165F86304000016C8000000000000000000000000000000000000003400000000wireplumber-0.5.0/modules/module-file-monitor-api.c   /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <stdio.h>

#include <wp/wp.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-file-monitor-api")

struct _WpFileMonitorApi
{
  WpPlugin parent;

  GHashTable *monitors;
};

enum {
  ACTION_ADD_WATCH,
  ACTION_REMOVE_WATCH,
  SIGNAL_CHANGED,
  N_SIGNALS
};

static guint signals[N_SIGNALS] = {0};

G_DECLARE_FINAL_TYPE (WpFileMonitorApi, wp_file_monitor_api, WP,
    FILE_MONITOR_API, WpPlugin)
G_DEFINE_TYPE (WpFileMonitorApi, wp_file_monitor_api, WP_TYPE_PLUGIN)

static void
wp_file_monitor_api_init (WpFileMonitorApi * self)
{
  self->monitors = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
      g_object_unref);
}

static void
wp_file_monitor_api_finalize (GObject * object)
{
  WpFileMonitorApi * self = WP_FILE_MONITOR_API (object);

  g_clear_pointer (&self->monitors, g_hash_table_unref);

  G_OBJECT_CLASS (wp_file_monitor_api_parent_class)->finalize (object);
}

static void
on_file_monitor_changed (GFileMonitor *monitor, GFile *file, GFile *other,
    GFileMonitorEvent evtype, gpointer data)
{
  WpFileMonitorApi * self = WP_FILE_MONITOR_API (data);

  g_autofree char *fpath = g_file_get_path (file);
  g_autofree char *opath = NULL;
  const gchar *evtype_str = NULL;

  if (other)
      opath = g_file_get_path (other);

  switch(evtype) {
    case G_FILE_MONITOR_EVENT_CHANGED:
      evtype_str = "changed";
      break;
    case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
      evtype_str = "changes-done-hint";
      break;
    case G_FILE_MONITOR_EVENT_DELETED:
      evtype_str = "deleted";
      break;
    case G_FILE_MONITOR_EVENT_CREATED:
      evtype_str = "created";
      break;
    case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
      evtype_str = "attribute-changed";
      break;
    case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
      evtype_str = "pre-unmount";
      break;
    case G_FILE_MONITOR_EVENT_UNMOUNTED:
      evtype_str = "unmounted";
      break;
    case G_FILE_MONITOR_EVENT_MOVED:
      evtype_str = "moved";
      break;
    case G_FILE_MONITOR_EVENT_RENAMED:
      evtype_str = "renamed";
      break;
    case G_FILE_MONITOR_EVENT_MOVED_IN:
      evtype_str = "moved-in";
      break;
    case G_FILE_MONITOR_EVENT_MOVED_OUT:
      evtype_str = "moved-out";
      break;
    default:
      wp_warning_object (self, "Unknown event type %d", evtype);
      break;
  }

  g_signal_emit (self, signals[SIGNAL_CHANGED], 0, fpath, opath, evtype_str);
}

static gboolean
wp_file_monitor_api_add_watch (WpFileMonitorApi * self, const gchar *path,
    const gchar *flags_str)
{
  g_autoptr (GError) e = NULL;
  g_autoptr (GFileMonitor) fm = NULL;
  g_autoptr (GFile) f = NULL;
  GFileMonitorFlags flags = G_FILE_MONITOR_NONE;

  /* don't do anything if the path is already being watched */
  if (g_hash_table_contains (self->monitors, path))
    return TRUE;

  /* get path */
  f = g_file_new_for_path (path);
  if (!f) {
    wp_warning_object (self, "Invalid path '%s'", path);
    return FALSE;
  }

  /* parse flags */
  for (guint i = 0; flags_str && i < strlen (flags_str); i++) {
    switch (flags_str[i]) {
      case 'o': flags |= G_FILE_MONITOR_WATCH_MOUNTS; break;
      case 's': flags |= G_FILE_MONITOR_SEND_MOVED; break;
      case 'h': flags |= G_FILE_MONITOR_WATCH_HARD_LINKS; break;
      case 'm': flags |= G_FILE_MONITOR_WATCH_MOVES; break;
      default:
        break;
    }
  }

  /* create the file monitor for that path */
  fm = g_file_monitor (f, flags, NULL, &e);
  if (e) {
    wp_warning_object (self, "Failed to add watch for path '%s': %s", path,
        e->message);
    return FALSE;
  }

  /* handle changed signal and add it to monitors table */
  g_signal_connect (fm, "changed", G_CALLBACK (on_file_monitor_changed), self);
  g_hash_table_insert (self->monitors, g_strdup (path), g_steal_pointer (&fm));
  return TRUE;
}

static void
wp_file_monitor_api_remove_watch (WpFileMonitorApi * self, const gchar *path)
{
  g_hash_table_remove (self->monitors, path);
}

static void
wp_file_monitor_api_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpFileMonitorApi * self = WP_FILE_MONITOR_API (plugin);

  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
wp_file_monitor_api_disable (WpPlugin * plugin)
{
  WpFileMonitorApi * self = WP_FILE_MONITOR_API (plugin);

  g_hash_table_remove_all (self->monitors);
}

static void
wp_file_monitor_api_class_init (WpFileMonitorApiClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  object_class->finalize = wp_file_monitor_api_finalize;

  plugin_class->enable = wp_file_monitor_api_enable;
  plugin_class->disable = wp_file_monitor_api_disable;

  signals[ACTION_ADD_WATCH] = g_signal_new_class_handler (
      "add-watch", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_file_monitor_api_add_watch,
      NULL, NULL, NULL,
      G_TYPE_BOOLEAN, 2, G_TYPE_STRING, G_TYPE_STRING);

  signals[ACTION_REMOVE_WATCH] = g_signal_new_class_handler (
      "remove-watch", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_file_monitor_api_remove_watch,
      NULL, NULL, NULL,
      G_TYPE_NONE, 1, G_TYPE_STRING);

  signals[SIGNAL_CHANGED] = g_signal_new (
      "changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
      G_TYPE_NONE, 3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (g_object_new (wp_file_monitor_api_get_type (),
      "name", "file-monitor-api",
      "core", core,
      NULL));
}
070701000000D4000081A400000000000000000000000165F8630400000BC7000000000000000000000000000000000000003000000000wireplumber-0.5.0/modules/module-log-settings.c   /* WirePlumber
 *
 * Copyright © 2023 Collabora Ltd.
 * Copyright © 2023 Pauli Virtanen <pav@iki.fi>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <errno.h>
#include <pipewire/pipewire.h>
#include <pipewire/keys.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-log-settings")

struct _WpLogSettingsPlugin
{
  WpPlugin parent;
  WpObjectManager *metadata_om;
};

G_DECLARE_FINAL_TYPE (WpLogSettingsPlugin, wp_log_settings_plugin,
                      WP, LOG_SETTINGS_PLUGIN, WpPlugin)
G_DEFINE_TYPE (WpLogSettingsPlugin, wp_log_settings_plugin, WP_TYPE_PLUGIN)

static void
wp_log_settings_plugin_init (WpLogSettingsPlugin * self)
{
}

static void
on_metadata_changed (WpMetadata *m, guint32 subject,
    const gchar *key, const gchar *type, const gchar *value, gpointer d)
{
  WpLogSettingsPlugin * self = WP_LOG_SETTINGS_PLUGIN (d);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_return_if_fail (core);

  if (subject != wp_core_get_own_bound_id (core))
    return;

  if (spa_streq(key, "log.level"))
    wp_log_set_level (value ? value : "2");
}

static void
on_metadata_added (WpObjectManager *om, WpMetadata *metadata, gpointer d)
{
  WpLogSettingsPlugin * self = WP_LOG_SETTINGS_PLUGIN (d);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_return_if_fail (core);

  /* Handle the changed signal */
  g_signal_connect_object (metadata, "changed",
      G_CALLBACK (on_metadata_changed), self, 0);
}

static void
wp_log_settings_plugin_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpLogSettingsPlugin * self = WP_LOG_SETTINGS_PLUGIN (plugin);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
  g_return_if_fail (core);

  /* Create the metadata object manager */
  self->metadata_om = wp_object_manager_new ();
  wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "settings",
      NULL);
  wp_object_manager_request_object_features (self->metadata_om,
      WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
  g_signal_connect_object (self->metadata_om, "object-added",
      G_CALLBACK (on_metadata_added), self, 0);
  wp_core_install_object_manager (core, self->metadata_om);

  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
wp_log_settings_plugin_disable (WpPlugin * plugin)
{
  WpLogSettingsPlugin * self = WP_LOG_SETTINGS_PLUGIN (plugin);

  g_clear_object (&self->metadata_om);
}

static void
wp_log_settings_plugin_class_init (WpLogSettingsPluginClass * klass)
{
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  plugin_class->enable = wp_log_settings_plugin_enable;
  plugin_class->disable = wp_log_settings_plugin_disable;
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
{
  return G_OBJECT (g_object_new (wp_log_settings_plugin_get_type (),
          "name", "log-settings",
          "core", core,
          NULL));
}
 070701000000D5000081A400000000000000000000000165F8630400000E89000000000000000000000000000000000000002A00000000wireplumber-0.5.0/modules/module-logind.c /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <glib-unix.h>

#include <systemd/sd-login.h>

#include <spa/utils/result.h>
#include <spa/utils/string.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-logind")

#define NAME "logind"

struct _WpLogind
{
  WpPlugin parent;
  sd_login_monitor *monitor;
  GSource *source;
  char *state;
};

enum {
  ACTION_GET_STATE,
  SIGNAL_STATE_CHANGED,
  N_SIGNALS
};

static guint signals[N_SIGNALS] = {0};

G_DECLARE_FINAL_TYPE (WpLogind, wp_logind, WP, LOGIND, WpPlugin)
G_DEFINE_TYPE (WpLogind, wp_logind, WP_TYPE_PLUGIN)

static void
wp_logind_init (WpLogind * self)
{
}

static gchar *
wp_logind_get_state (WpLogind *self)
{
  return g_strdup (self->state);
}

static gboolean
wp_logind_source_ready (gint fd, GIOCondition condition, gpointer user_data)
{
  WpLogind *self = WP_LOGIND (user_data);
  sd_login_monitor_flush (self->monitor);
  {
    char *state = NULL;
    sd_uid_get_state (getuid(), &state);
    if (g_strcmp0 (state, self->state) != 0) {
      char *tmp = state;
      state = self->state;
      self->state = tmp;
      g_signal_emit (self, signals[SIGNAL_STATE_CHANGED], 0, self->state);
    }
    free (state);
  }
  return G_SOURCE_CONTINUE;
}

static void
wp_logind_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpLogind *self = WP_LOGIND (plugin);
  int res = 0;

  if ((res = sd_login_monitor_new ("uid", &self->monitor)) < 0) {
    wp_transition_return_error (transition, g_error_new (G_IO_ERROR,
            g_io_error_from_errno (-res),
            "failed to start systemd logind monitor: %d (%s)",
            res, spa_strerror(res)));
    return;
  }

  if ((res = sd_uid_get_state (getuid(), &self->state)) < 0) {
    wp_transition_return_error (transition, g_error_new (G_IO_ERROR,
            g_io_error_from_errno (-res),
            "failed to get systemd login state: %d (%s)",
            res, spa_strerror(res)));
    g_clear_pointer (&self->monitor, sd_login_monitor_unref);
    return;
  }

  self->source = g_unix_fd_source_new (
      sd_login_monitor_get_fd (self->monitor),
      sd_login_monitor_get_events (self->monitor));
  g_source_set_callback (self->source, G_SOURCE_FUNC (wp_logind_source_ready),
      self, NULL);

  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
  GMainContext *context = wp_core_get_g_main_context (core);
  g_source_attach (self->source, context);

  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
wp_logind_disable (WpPlugin * plugin)
{
  WpLogind *self = WP_LOGIND (plugin);

  g_clear_pointer (&self->state, free);
  g_source_destroy (self->source);
  g_clear_pointer (&self->source, g_source_unref);
  g_clear_pointer (&self->monitor, sd_login_monitor_unref);
}

static void
wp_logind_class_init (WpLogindClass * klass)
{
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  plugin_class->enable = wp_logind_enable;
  plugin_class->disable = wp_logind_disable;

  signals[ACTION_GET_STATE] = g_signal_new_class_handler (
      "get-state", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_logind_get_state,
      NULL, NULL, NULL, G_TYPE_STRING, 0);

  signals[SIGNAL_STATE_CHANGED] = g_signal_new (
      "state-changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (g_object_new (wp_logind_get_type (),
      "name", NAME,
      "core", core,
      NULL));
}
   070701000000D6000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002F00000000wireplumber-0.5.0/modules/module-lua-scripting    070701000000D7000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000003300000000wireplumber-0.5.0/modules/module-lua-scripting/api    070701000000D8000081A400000000000000000000000165F8630400011B82000000000000000000000000000000000000003900000000wireplumber-0.5.0/modules/module-lua-scripting/api/api.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <glib/gstdio.h>
#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <wplua/wplua.h>
#include <libintl.h>

#define WP_LOCAL_LOG_TOPIC log_topic_lua_scripting
WP_LOG_TOPIC_EXTERN (log_topic_lua_scripting)

#define URI_API "resource:///org/freedesktop/pipewire/wireplumber/m-lua-scripting/api.lua"

void wp_lua_scripting_pod_init (lua_State *L);
void wp_lua_scripting_json_init (lua_State *L);
void push_luajson (lua_State *L, WpSpaJson *json, gint n_recursions);

/* helpers */

static WpCore *
get_wp_core (lua_State *L)
{
  WpCore *core = NULL;
  lua_pushliteral (L, "wireplumber_core");
  lua_gettable (L, LUA_REGISTRYINDEX);
  core = lua_touserdata (L, -1);
  lua_pop (L, 1);
  return core;
}

static WpCore *
get_wp_export_core (lua_State *L)
{
  WpCore *core = get_wp_core (L);
  g_autoptr (WpCore) export_core = wp_core_get_export_core (core);
  return export_core ? export_core : core;
}

/* GLib */

static int
glib_get_monotonic_time (lua_State *L)
{
  lua_pushinteger (L, g_get_monotonic_time ());
  return 1;
}

static int
glib_get_real_time (lua_State *L)
{
  lua_pushinteger (L, g_get_real_time ());
  return 1;
}

static gboolean
access_parse_mode (const gchar * mode_str, gint *mode)
{
  *mode = 0;

  if (!mode_str)
    return FALSE;
  else {
    for (guint i = 0; i < strlen (mode_str); i++) {
      switch (mode_str[i]) {
        case 'r': *mode |= R_OK; break;
        case 'w': *mode |= W_OK; break;
        case 'x': *mode |= X_OK; break;
        case 'f': *mode |= F_OK; break;
        case '-': break;
        default:
          return FALSE;
      }
    }
  }
  return TRUE;
}

static int
glib_access (lua_State *L)
{
  const gchar *filename = luaL_checkstring (L, 1);
  int mode = 0;
  if (!access_parse_mode (luaL_checkstring (L, 2), &mode))
      luaL_error (L, "invalid mode string: '%s'", lua_tostring (L, 2));
  lua_pushboolean (L, g_access (filename, mode) >= 0);
  return 1;
}

static const luaL_Reg glib_methods[] = {
  { "get_monotonic_time", glib_get_monotonic_time },
  { "get_real_time", glib_get_real_time },
  { "access", glib_access },
  { NULL, NULL }
};

/* GSource */

static int
source_destroy (lua_State *L)
{
  GSource *source = wplua_checkboxed (L, 1, G_TYPE_SOURCE);
  g_source_destroy (source);
  return 0;
}

static const luaL_Reg source_methods[] = {
  { "destroy", source_destroy },
  { NULL, NULL }
};

/* i18n */

static int
i18n_gettext (lua_State *L)
{
  const gchar * msgid = luaL_checkstring (L, 1);
  lua_pushstring (L, dgettext (GETTEXT_PACKAGE, msgid));
  return 1;
}

static int
i18n_ngettext (lua_State *L)
{
  const gchar * msgid = luaL_checkstring (L, 1);
  const gchar *msgid_plural = luaL_checkstring (L, 2);
  gulong n = luaL_checkinteger (L, 3);
  lua_pushstring (L, dngettext (GETTEXT_PACKAGE, msgid, msgid_plural, n));
  return 1;
}

static const luaL_Reg i18n_funcs[] = {
  { "gettext", i18n_gettext },
  { "ngettext", i18n_ngettext },
  { NULL, NULL }
};

/* WpCore */

static int
core_get_properties (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  g_autoptr (WpProperties) p = wp_core_get_properties (core);
  wplua_properties_to_table (L, p);
  return 1;
}

static int
core_get_info (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  g_autoptr (WpProperties) p = wp_core_get_remote_properties (core);

  lua_newtable (L);
  lua_pushinteger (L, wp_core_get_remote_cookie (core));
  lua_setfield (L, -2, "cookie");
  lua_pushstring (L, wp_core_get_remote_name (core));
  lua_setfield (L, -2, "name");
  lua_pushstring (L, wp_core_get_remote_user_name (core));
  lua_setfield (L, -2, "user_name");
  lua_pushstring (L, wp_core_get_remote_host_name (core));
  lua_setfield (L, -2, "host_name");
  lua_pushstring (L, wp_core_get_remote_version (core));
  lua_setfield (L, -2, "version");
  wplua_properties_to_table (L, p);
  lua_setfield (L, -2, "properties");
  return 1;
}

static int
core_get_vm_type (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  g_autofree gchar *vm = wp_core_get_vm_type (core);
  lua_pushstring (L, vm);
  return 1;
}

static int
core_get_own_bound_id (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  guint32 id = wp_core_get_own_bound_id (core);
  lua_pushinteger (L, id);
  return 1;
}

static int
core_idle_add (lua_State *L)
{
  GSource *source = NULL;
  luaL_checktype (L, 1, LUA_TFUNCTION);
  wp_core_idle_add_closure (get_wp_core (L), &source,
      wplua_function_to_closure (L, 1));
  wplua_pushboxed (L, G_TYPE_SOURCE, source);
  return 1;
}

static int
core_timeout_add (lua_State *L)
{
  GSource *source = NULL;
  lua_Integer timeout_ms = luaL_checkinteger (L, 1);
  luaL_checktype (L, 2, LUA_TFUNCTION);
  wp_core_timeout_add_closure (get_wp_core (L), &source, timeout_ms,
      wplua_function_to_closure (L, 2));
  wplua_pushboxed (L, G_TYPE_SOURCE, source);
  return 1;
}

static void
on_core_done (WpCore * core, GAsyncResult * res, GClosure * closure)
{
  g_autoptr (GError) error = NULL;
  GValue val = G_VALUE_INIT;
  int n_vals = 0;

  if (!wp_core_sync_finish (core, res, &error)) {
    g_value_init (&val, G_TYPE_STRING);
    g_value_set_string (&val, error->message);
    n_vals = 1;
  }
  g_closure_invoke (closure, NULL, n_vals, &val, NULL);
  g_value_unset (&val);
  g_closure_invalidate (closure);
  g_closure_unref (closure);
}

static int
core_sync (lua_State *L)
{
  luaL_checktype (L, 1, LUA_TFUNCTION);
  GClosure * closure = wplua_function_to_closure (L, 1);
  g_closure_sink (g_closure_ref (closure));
  wp_core_sync (get_wp_core (L), NULL, (GAsyncReadyCallback) on_core_done,
      closure);
  return 0;
}

static gboolean
core_disconnect (WpCore * core)
{
  wp_core_disconnect (core);
  return G_SOURCE_REMOVE;
}

static int
core_quit (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  g_autoptr (WpProperties) p = wp_core_get_properties (core);
  const gchar *daemon = wp_properties_get (p, "wireplumber.daemon");
  if (!g_strcmp0 (daemon, "true")) {
    wp_warning ("script attempted to quit, but the engine is "
        "running in the wireplumber daemon; ignoring");
    return 0;
  }

  /* wp_core_disconnect() will immediately destroy the lua plugin
     and the lua engine, so we cannot call it directly */
  wp_core_idle_add (core, NULL, G_SOURCE_FUNC (core_disconnect), core, NULL);
  return 0;
}

#include "require.c"

static int
core_require_api (lua_State *L)
{
  WpCore * core = get_wp_core (L);
  g_autoptr (WpProperties) p = wp_core_get_properties (core);
  const gchar *daemon = wp_properties_get (p, "wireplumber.daemon");
  if (!g_strcmp0 (daemon, "true")) {
    wp_warning ("script attempted to load an API module, but the engine is "
        "running in the wireplumber daemon; ignoring");
    return 0;
  }
  return wp_require_api_transition_new_from_lua (L, core);
}

static int
core_test_feature (lua_State *L)
{
  WpCore *core = get_wp_core(L);
  const char *f = luaL_checkstring (L, 1);
  lua_pushboolean (L, wp_core_test_feature (core, f));
  return 1;
}

static const luaL_Reg core_funcs[] = {
  { "get_properties", core_get_properties },
  { "get_info", core_get_info },
  { "get_vm_type", core_get_vm_type },
  { "get_own_bound_id", core_get_own_bound_id },
  { "idle_add", core_idle_add },
  { "timeout_add", core_timeout_add },
  { "sync", core_sync },
  { "quit", core_quit },
  { "require_api", core_require_api },
  { "test_feature", core_test_feature },
  { NULL, NULL }
};

/* WpLog */

typedef WpLogTopic WpLuaLogTopic;

static WpLuaLogTopic *
wp_lua_log_topic_new (const char *name)
{
  WpLuaLogTopic *topic = g_new0 (WpLuaLogTopic, 1);
  topic->topic_name = g_ref_string_new (name);
  wp_log_topic_register (topic);
  return topic;
}

static WpLuaLogTopic *
wp_lua_log_topic_copy (WpLuaLogTopic *topic)
{
  WpLuaLogTopic *copy = g_new0 (WpLuaLogTopic, 1);
  copy->topic_name = g_ref_string_acquire ((char *) copy->topic_name);
  wp_log_topic_register (copy);
  return copy;
}

static void
wp_lua_log_topic_free (WpLuaLogTopic *topic)
{
  wp_log_topic_unregister (topic);
  g_ref_string_release ((char *) topic->topic_name);
  g_free (topic);
}

G_DEFINE_BOXED_TYPE (WpLuaLogTopic, wp_lua_log_topic, wp_lua_log_topic_copy,
    wp_lua_log_topic_free)

static int
log_log (lua_State *L, GLogLevelFlags lvl)
{
  lua_Debug ar = {0};
  const gchar *message;
  gchar line_str[11];
  gconstpointer instance = NULL;
  GType type = G_TYPE_INVALID;
  int index = 1;
  WpLogTopic *topic = log_topic_lua_scripting;

  /* if called with log topic object */
  if (lua_istable (L, index)) {
    if (lua_getmetatable (L, index)) {
      lua_getfield (L, -1, "__topic");
      if (wplua_isboxed (L, -1, wp_lua_log_topic_get_type ())) {
        topic = wplua_toboxed (L, -1);
      }
      lua_pop (L, 2);
    }
    index++;
  }

  if (!wp_log_topic_is_enabled (topic, lvl))
    return 0;

  g_warn_if_fail (lua_getstack (L, 1, &ar) == 1);
  g_warn_if_fail (lua_getinfo (L, "nSl", &ar) == 1);

  if (wplua_isobject (L, index, G_TYPE_OBJECT)) {
    instance = wplua_toobject (L, index);
    type = G_TYPE_FROM_INSTANCE (instance);
    index++;
  }
  else if (wplua_isboxed (L, index, G_TYPE_BOXED)) {
    instance = wplua_toboxed (L, index);
    type = wplua_gvalue_userdata_type (L, index);
    index++;
  }

  message = luaL_checkstring (L, index);
  snprintf (line_str, 11, "%d", ar.currentline);
  ar.name = ar.name ? ar.name : "chunk";

  wp_log_checked (topic->topic_name, lvl,
      ar.source, line_str, ar.name, type, instance, "%s", message);
  return 0;
}

static int
log_warning (lua_State *L) { return log_log (L, G_LOG_LEVEL_WARNING); }

static int
log_notice (lua_State *L) { return log_log (L, G_LOG_LEVEL_MESSAGE); }

static int
log_info (lua_State *L) { return log_log (L, G_LOG_LEVEL_INFO); }

static int
log_debug (lua_State *L) { return log_log (L, G_LOG_LEVEL_DEBUG); }

static int
log_trace (lua_State *L) { return log_log (L, WP_LOG_LEVEL_TRACE); }

static const luaL_Reg log_obj_funcs[] = {
  { "warning", log_warning },
  { "notice", log_notice },
  { "info", log_info },
  { "debug", log_debug },
  { "trace", log_trace },
  { NULL, NULL }
};

static int
log_open_topic (lua_State *L)
{
  const char *name = luaL_checkstring (L, 1);
  WpLuaLogTopic *topic = wp_lua_log_topic_new (name);

  lua_newtable (L); // empty table
  lua_newtable (L); // metatable
  luaL_newlib (L, log_obj_funcs);
  lua_setfield (L, -2, "__index");
  wplua_pushboxed (L, wp_lua_log_topic_get_type (), topic);
  lua_setfield (L, -2, "__topic");
  lua_setmetatable (L, -2);
  return 1;
}

static const luaL_Reg log_funcs[] = {
  { "open_topic", log_open_topic },
  { "warning", log_warning },
  { "notice", log_notice },
  { "info", log_info },
  { "debug", log_debug },
  { "trace", log_trace },
  { NULL, NULL }
};

/* WpPlugin */

static int
plugin_find (lua_State *L)
{
  const char *name = luaL_checkstring (L, 1);
  WpPlugin *plugin = wp_plugin_find (get_wp_core (L), name);
  if (plugin)
    wplua_pushobject (L, plugin);
  else
    lua_pushnil (L);
  return 1;
}

static const luaL_Reg plugin_funcs[] = {
  { "find", plugin_find },
  { NULL, NULL }
};

/* WpObject */

static void
object_activate_done (WpObject *o, GAsyncResult * res, GClosure * closure)
{
  g_autoptr (GError) error = NULL;
  GValue val[2] = { G_VALUE_INIT, G_VALUE_INIT };
  int n_vals = 1;

  if (!wp_object_activate_finish (o, res, &error)) {
    wp_notice_object (o, "%s", error->message);
    if (closure) {
      g_value_init (&val[1], G_TYPE_STRING);
      g_value_set_string (&val[1], error->message);
      n_vals = 2;
    }
  }
  if (closure) {
    g_value_init_from_instance (&val[0], o);
    g_closure_invoke (closure, NULL, n_vals, val, NULL);
    g_value_unset (&val[0]);
    g_value_unset (&val[1]);
    g_closure_invalidate (closure);
    g_closure_unref (closure);
  }
}

static int
object_activate (lua_State *L)
{
  WpObject *o = wplua_checkobject (L, 1, WP_TYPE_OBJECT);
  WpObjectFeatures features = luaL_checkinteger (L, 2);
  GClosure * closure = luaL_opt (L, wplua_checkclosure, 3, NULL);
  if (closure)
    g_closure_sink (g_closure_ref (closure));
  wp_object_activate (o, features, NULL,
      (GAsyncReadyCallback) object_activate_done, closure);
  return 0;
}

static int
object_deactivate (lua_State *L)
{
  WpObject *o = wplua_checkobject (L, 1, WP_TYPE_OBJECT);
  WpObjectFeatures features = luaL_checkinteger (L, 2);
  wp_object_deactivate (o, features);
  return 0;
}

static int
object_get_active_features (lua_State *L)
{
  WpObject *o = wplua_checkobject (L, 1, WP_TYPE_OBJECT);
  WpObjectFeatures features = wp_object_get_active_features (o);
  lua_pushinteger (L, features);
  return 1;
}

static int
object_get_supported_features (lua_State *L)
{
  WpObject *o = wplua_checkobject (L, 1, WP_TYPE_OBJECT);
  WpObjectFeatures features = wp_object_get_supported_features (o);
  lua_pushinteger (L, features);
  return 1;
}

static const luaL_Reg object_methods[] = {
  { "activate", object_activate },
  { "deactivate", object_deactivate },
  { "get_active_features", object_get_active_features },
  { "get_supported_features", object_get_supported_features },
  { NULL, NULL }
};

/* WpProxy */

static int
proxy_get_interface_type (lua_State *L)
{
  WpProxy * p = wplua_checkobject (L, 1, WP_TYPE_PROXY);
  guint32 version = 0;
  const gchar *type = wp_proxy_get_interface_type (p, &version);
  lua_pushstring (L, type);
  lua_pushinteger (L, version);
  return 2;
}

static const luaL_Reg proxy_methods[] = {
  { "get_interface_type", proxy_get_interface_type },
  { NULL, NULL }
};

/* WpGlobalProxy */

static int
global_proxy_request_destroy (lua_State *L)
{
  WpGlobalProxy * p = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
  wp_global_proxy_request_destroy (p);
  return 0;
}

static const luaL_Reg global_proxy_methods[] = {
  { "request_destroy", global_proxy_request_destroy },
  { NULL, NULL }
};

/* WpIterator */

static int
iterator_next (lua_State *L)
{
  WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
  g_auto (GValue) v = G_VALUE_INIT;
  if (it && wp_iterator_next (it, &v)) {
    return wplua_gvalue_to_lua (L, &v);
  } else {
    lua_pushnil (L);
    return 1;
  }
}

static int
push_wpiterator (lua_State *L, WpIterator *it)
{
  lua_pushcfunction (L, iterator_next);
  wplua_pushboxed (L, WP_TYPE_ITERATOR, it);
  return 2;
}

/* Settings WpIterator */

static int
settings_iterator_next (lua_State *L)
{
  WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
  g_auto (GValue) item = G_VALUE_INIT;
  if (wp_iterator_next (it, &item)) {
    WpSettingsItem *si = g_value_get_boxed (&item);
    const gchar *k = wp_settings_item_get_key (si);
    WpSpaJson *v = wp_settings_item_get_value (si);
    lua_pushstring (L, k);
    wplua_pushboxed (L, WP_TYPE_SPA_JSON, v);
    return 2;
  } else {
    lua_pushnil (L);
    return 1;
  }
}

static int
push_settings_wpiterator (lua_State *L, WpIterator *it)
{
  lua_pushcfunction (L, settings_iterator_next);
  wplua_pushboxed (L, WP_TYPE_ITERATOR, it);
  return 2;
}

/* Metadata WpIterator */

static int
metadata_iterator_next (lua_State *L)
{
  WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
  g_auto (GValue) item = G_VALUE_INIT;
  if (wp_iterator_next (it, &item)) {
    WpMetadataItem *mi = g_value_get_boxed (&item);
    guint32 s = wp_metadata_item_get_subject (mi);
    const gchar *k = wp_metadata_item_get_key (mi);
    const gchar *t = wp_metadata_item_get_value_type (mi);
    const gchar *v = wp_metadata_item_get_value (mi);
    lua_pushinteger (L, s);
    lua_pushstring (L, k);
    lua_pushstring (L, t);
    lua_pushstring (L, v);
    return 4;
  } else {
    lua_pushnil (L);
    return 1;
  }
}

static int
push_metadata_wpiterator (lua_State *L, WpIterator *it)
{
  lua_pushcfunction (L, metadata_iterator_next);
  wplua_pushboxed (L, WP_TYPE_ITERATOR, it);
  return 2;
}

/* WpObjectInterest */

static GVariant *
constraint_value_to_variant (lua_State *L, int idx)
{
  switch (lua_type (L, idx)) {
  case LUA_TBOOLEAN:
    return g_variant_new_boolean (lua_toboolean (L, idx));
  case LUA_TSTRING:
    return g_variant_new_string (lua_tostring (L, idx));
  case LUA_TNUMBER:
    if (lua_isinteger (L, idx))
      return g_variant_new_int64 (lua_tointeger (L, idx));
    else
      return g_variant_new_double (lua_tonumber (L, idx));
  default:
    return NULL;
  }
}

static void
object_interest_new_add_constraint (lua_State *L, GType type,
    WpObjectInterest *interest)
{
  int constraint_idx;
  WpConstraintType ctype;
  const gchar *subject;
  WpConstraintVerb verb;
  GVariant *value = NULL;

  constraint_idx = lua_absindex (L, -1);

  /* verify this is a Constraint{} */
  if (lua_type (L, constraint_idx) != LUA_TTABLE) {
    luaL_error (L, "Interest: expected Constraint at index %d",
        lua_tointeger (L, -2));
  }

  if (luaL_getmetafield (L, constraint_idx, "__name") == LUA_TNIL ||
      g_strcmp0 (lua_tostring (L, -1), "Constraint") != 0) {
    luaL_error (L, "Interest: expected Constraint at index %d",
        lua_tointeger (L, -2));
  }
  lua_pop (L, 1);

  /* get the constraint type */
  lua_pushliteral (L, "type");
  if (lua_gettable (L, constraint_idx) == LUA_TNUMBER)
    ctype = lua_tointeger (L, -1);
  else
    ctype = WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY;
  lua_pop (L, 1);

  /* get t[1] (the subject) and t[2] (the verb) */
  lua_geti (L, constraint_idx, 1);
  subject = lua_tostring (L, -1);

  lua_geti (L, constraint_idx, 2);
  verb = lua_tostring (L, -1)[0];

  switch (verb) {
  case WP_CONSTRAINT_VERB_EQUALS:
  case WP_CONSTRAINT_VERB_NOT_EQUALS:
  case WP_CONSTRAINT_VERB_MATCHES: {
    lua_geti (L, constraint_idx, 3);
    value = constraint_value_to_variant (L, -1);
    if (G_UNLIKELY (!value))
      luaL_error (L, "Constraint: bad value type");
    break;
  }
  case WP_CONSTRAINT_VERB_IN_RANGE: {
    GVariant *values[2];
    lua_geti (L, constraint_idx, 3);
    lua_geti (L, constraint_idx, 4);
    values[0] = constraint_value_to_variant (L, -2);
    values[1] = constraint_value_to_variant (L, -1);
    if (G_UNLIKELY (!values[0] || !values[1])) {
      g_clear_pointer (&values[0], g_variant_unref);
      g_clear_pointer (&values[1], g_variant_unref);
      luaL_error (L, "Constraint: bad value type");
    }
    value = g_variant_new_tuple (values, 2);
    break;
  }
  case WP_CONSTRAINT_VERB_IN_LIST: {
    GPtrArray *values =
        g_ptr_array_new_with_free_func ((GDestroyNotify) g_variant_unref);
    int i = 3;
    while (lua_geti (L, constraint_idx, i++) != LUA_TNIL) {
      GVariant *tmp = constraint_value_to_variant (L, -1);
      if (G_UNLIKELY (!tmp)) {
        g_ptr_array_unref (values);
        luaL_error (L, "Constraint: bad value type");
      }
      g_ptr_array_add (values, g_variant_ref_sink (tmp));
      lua_pop (L, 1);
    }
    value = g_variant_new_tuple ((GVariant **) values->pdata, values->len);
    g_ptr_array_unref (values);
    break;
  }
  default:
    break;
  }

  wp_object_interest_add_constraint (interest, ctype, subject, verb, value);
  lua_settop (L, constraint_idx);
}

static GType
parse_gtype (const gchar *str)
{
  g_autofree gchar *typestr = NULL;
  GType res = G_TYPE_INVALID;

  g_return_val_if_fail (str, res);

  /* "device" -> "WpDevice" */
  typestr = g_strdup_printf ("Wp%s", str);
  if (typestr[2] != 0) {
    typestr[2] = g_ascii_toupper (typestr[2]);
    res = g_type_from_name (typestr);
  }

  return res;
}

static int
object_interest_new_index (lua_State *L, int idx, GType def_type)
{
  WpObjectInterest *interest = NULL;
  GType type = def_type;

  luaL_checktype (L, idx, LUA_TTABLE);

  /* type = "string" */
  lua_pushliteral (L, "type");
  if (lua_gettable (L, idx) == LUA_TSTRING) {
    type = parse_gtype (lua_tostring (L, -1));
    if (type == G_TYPE_INVALID)
      luaL_error (L, "Interest: unknown type '%s'", lua_tostring (L, -1));
  }
  else if (def_type == G_TYPE_INVALID)
    luaL_error (L, "Interest: expected 'type' as string");
  lua_pop (L, 1);

  interest = wp_object_interest_new_type (type);
  wplua_pushboxed (L, WP_TYPE_OBJECT_INTEREST, interest);

  /* add constraints */
  lua_pushnil (L);
  while (lua_next (L, idx)) {
    /* if the key isn't "type" */
    if (!(lua_type (L, -2) == LUA_TSTRING &&
          !g_strcmp0 ("type", lua_tostring (L, -2))))
      object_interest_new_add_constraint (L, type, interest);
    lua_pop (L, 1);
  }

  return 1;
}

static int
object_interest_new (lua_State *L)
{
  return object_interest_new_index (L, 1, G_TYPE_INVALID);
}

static int
object_interest_matches (lua_State *L)
{
  WpObjectInterest *interest = wplua_checkboxed (L, 1, WP_TYPE_OBJECT_INTEREST);
  gboolean matches = FALSE;

  if (wplua_isobject (L, 2, G_TYPE_OBJECT)) {
    matches = wp_object_interest_matches (interest, wplua_toobject (L, 2));
  }
  else if (lua_istable (L, 2)) {
    g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
    matches = wp_object_interest_matches (interest, props);
  } else
    luaL_argerror (L, 2, "expected GObject or table");

  lua_pushboolean (L, matches);
  return 1;
}

static const luaL_Reg object_interest_methods[] = {
  { "matches", object_interest_matches },
  { NULL, NULL }
};

static WpObjectInterest *
get_optional_object_interest (lua_State *L, int idx, GType def_type)
{
  if (lua_isnoneornil (L, idx))
    return NULL;
  else if (lua_isuserdata (L, idx))
    return wplua_checkboxed (L, idx, WP_TYPE_OBJECT_INTEREST);
  else if (lua_istable (L, idx)) {
    object_interest_new_index (L, idx, def_type);
    return wplua_toboxed (L, -1);
  } else {
    luaL_error (L, "expected Interest or none/nil");
    return NULL;
  }
}

/* WpObjectManager */

static int
object_manager_new (lua_State *L)
{
  WpObjectManager *om;

  /* validate arguments */
  luaL_checktype (L, 1, LUA_TTABLE);

  /* push to Lua asap to have a way to unref in case of error */
  om = wp_object_manager_new ();
  wplua_pushobject (L, om);

  lua_pushnil (L);
  while (lua_next (L, 1)) {
    WpObjectInterest *interest =
        wplua_checkboxed (L, -1, WP_TYPE_OBJECT_INTEREST);
    wp_object_manager_add_interest_full (om, wp_object_interest_ref (interest));
    lua_pop (L, 1);
  }

  /* request all the features for Lua scripts to make their job easier */
  wp_object_manager_request_object_features (om,
      WP_TYPE_OBJECT, WP_OBJECT_FEATURES_ALL);

  return 1;
}

static int
object_manager_activate (lua_State *L)
{
  WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
  wp_core_install_object_manager (get_wp_core (L), om);
  return 0;
}

static int
object_manager_get_n_objects (lua_State *L)
{
  WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
  lua_pushinteger (L, wp_object_manager_get_n_objects (om));
  return 1;
}

static int
object_manager_iterate (lua_State *L)
{
  WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
  WpObjectInterest *oi = get_optional_object_interest (L, 2, G_TYPE_OBJECT);
  WpIterator *it = oi ?
      wp_object_manager_new_filtered_iterator_full (om,
          wp_object_interest_ref (oi)) :
      wp_object_manager_new_iterator (om);
  return push_wpiterator (L, it);
}

static int
object_manager_lookup (lua_State *L)
{
  WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
  WpObjectInterest *oi = get_optional_object_interest (L, 2, G_TYPE_OBJECT);
  WpObject *o = oi ?
      wp_object_manager_lookup_full (om, wp_object_interest_ref (oi)) :
      wp_object_manager_lookup (om, G_TYPE_OBJECT, NULL);
  if (o) {
    wplua_pushobject (L, o);
    return 1;
  }
  return 0;
}

static const luaL_Reg object_manager_methods[] = {
  { "activate", object_manager_activate },
  { "get_n_objects", object_manager_get_n_objects },
  { "iterate", object_manager_iterate },
  { "lookup", object_manager_lookup },
  { NULL, NULL }
};

/* WpMetadata */

static int
metadata_iterate (lua_State *L)
{
  WpMetadata *metadata = wplua_checkobject (L, 1, WP_TYPE_METADATA);
  lua_Integer subject = luaL_checkinteger (L, 2);
  WpIterator *it = wp_metadata_new_iterator (metadata, subject);
  return push_metadata_wpiterator (L, it);
}

static int
metadata_find (lua_State *L)
{
  WpMetadata *metadata = wplua_checkobject (L, 1, WP_TYPE_METADATA);
  lua_Integer subject = luaL_checkinteger (L, 2);
  const char *key = luaL_checkstring (L, 3), *v = NULL, *t = NULL;
  v = wp_metadata_find (metadata, subject, key, &t);
  lua_pushstring (L, v);
  lua_pushstring (L, t);
  return 2;
}

static int
metadata_set (lua_State *L)
{
  WpMetadata *metadata = wplua_checkobject (L, 1, WP_TYPE_METADATA);
  lua_Integer subject = luaL_checkinteger (L, 2);
  const char *key = luaL_opt (L, luaL_checkstring, 3, NULL);
  const char *type = luaL_opt (L, luaL_checkstring, 4, NULL);
  const char *value = luaL_opt (L, luaL_checkstring, 5, NULL);
  wp_metadata_set (metadata, subject, key, type, value);
  return 0;
}

static const luaL_Reg metadata_methods[] = {
  { "iterate", metadata_iterate },
  { "find", metadata_find },
  { "set", metadata_set },
  { NULL, NULL }
};

/* WpImplMetadata */

static int
impl_metadata_new (lua_State *L)
{
  const char *name = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpImplMetadata *m = wp_impl_metadata_new_full (get_wp_core (L),
      name, properties);
  if (m)
    wplua_pushobject (L, m);
  return m ? 1 : 0;
}

/* Device */

static int
device_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpDevice *d = wp_device_new_from_factory (get_wp_export_core (L),
      factory, properties);
  if (d)
    wplua_pushobject (L, d);
  return d ? 1 : 0;
}

/* WpSpaDevice */

static int
spa_device_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpSpaDevice *d = wp_spa_device_new_from_spa_factory (get_wp_export_core (L),
      factory, properties);
  if (d)
    wplua_pushobject (L, d);
  return d ? 1 : 0;
}

static int
spa_device_iterate_managed_objects (lua_State *L)
{
  WpSpaDevice *device = wplua_checkobject (L, 1, WP_TYPE_SPA_DEVICE);
  WpIterator *it = wp_spa_device_new_managed_object_iterator (device);
  return push_wpiterator (L, it);
}

static int
spa_device_get_managed_object (lua_State *L)
{
  WpSpaDevice *device = wplua_checkobject (L, 1, WP_TYPE_SPA_DEVICE);
  guint id = luaL_checkinteger (L, 2);
  GObject *obj = wp_spa_device_get_managed_object (device, id);
  if (obj)
    wplua_pushobject (L, obj);
  return obj ? 1 : 0;
}

static int
spa_device_store_managed_object (lua_State *L)
{
  WpSpaDevice *device = wplua_checkobject (L, 1, WP_TYPE_SPA_DEVICE);
  guint id = luaL_checkinteger (L, 2);
  GObject *obj = (lua_type (L, 3) != LUA_TNIL) ?
      g_object_ref (wplua_checkobject (L, 3, G_TYPE_OBJECT)) : NULL;

  wp_spa_device_store_managed_object (device, id, obj);
  return 0;
}

static const luaL_Reg spa_device_methods[] = {
  { "iterate_managed_objects", spa_device_iterate_managed_objects },
  { "get_managed_object", spa_device_get_managed_object },
  { "store_managed_object", spa_device_store_managed_object },
  { NULL, NULL }
};

/* Node */

static int
node_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpNode *d = wp_node_new_from_factory (get_wp_export_core (L),
      factory, properties);
  if (d)
    wplua_pushobject (L, d);
  return d ? 1 : 0;
}

static int
node_get_state (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  const gchar *error = NULL;
  WpNodeState state = wp_node_get_state (node, &error);
  wplua_enum_to_lua (L, state, WP_TYPE_NODE_STATE);
  lua_pushstring (L, error ? error : "");
  return 2;
}

static int
node_get_n_input_ports (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  guint max = 0;
  guint ports = wp_node_get_n_input_ports (node, &max);
  lua_pushinteger (L, ports);
  lua_pushinteger (L, max);
  return 2;
}

static int
node_get_n_output_ports (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  guint max = 0;
  guint ports = wp_node_get_n_output_ports (node, &max);
  lua_pushinteger (L, ports);
  lua_pushinteger (L, max);
  return 2;
}

static int
node_get_n_ports (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  guint ports = wp_node_get_n_ports (node);
  lua_pushinteger (L, ports);
  return 1;
}

static int
node_iterate_ports (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  WpObjectInterest *oi = get_optional_object_interest (L, 2, WP_TYPE_PORT);
  WpIterator *it = oi ?
      wp_node_new_ports_filtered_iterator_full (node,
          wp_object_interest_ref (oi)) :
      wp_node_new_ports_iterator (node);
  return push_wpiterator (L, it);
}

static int
node_lookup_port (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  WpObjectInterest *oi = get_optional_object_interest (L, 2, WP_TYPE_PORT);
  WpPort *port = oi ?
      wp_node_lookup_port_full (node, wp_object_interest_ref (oi)) :
      wp_node_lookup_port (node, G_TYPE_OBJECT, NULL);
  if (port) {
    wplua_pushobject (L, port);
    return 1;
  }
  return 0;
}

static int
node_send_command (lua_State *L)
{
  WpNode *node = wplua_checkobject (L, 1, WP_TYPE_NODE);
  const char *command = luaL_checkstring (L, 2);
  wp_node_send_command (node, command);
  return 0;
}

static const luaL_Reg node_methods[] = {
  { "get_state", node_get_state },
  { "get_n_input_ports", node_get_n_input_ports },
  { "get_n_output_ports", node_get_n_output_ports },
  { "get_n_ports", node_get_n_ports },
  { "iterate_ports", node_iterate_ports },
  { "lookup_port", node_lookup_port },
  { "send_command", node_send_command },
  { NULL, NULL }
};

/* ImplNode */

static int
impl_node_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpImplNode *d = wp_impl_node_new_from_pw_factory (get_wp_export_core (L),
     factory, properties);
  if (d)
    wplua_pushobject (L, d);
  return d ? 1 : 0;
}

/* Port */

static int
port_get_direction (lua_State *L)
{
  WpPort *port = wplua_checkobject (L, 1, WP_TYPE_PORT);
  WpDirection direction = wp_port_get_direction (port);
  wplua_enum_to_lua (L, direction, WP_TYPE_DIRECTION);
  return 1;
}

static const luaL_Reg port_methods[] = {
  { "get_direction", port_get_direction },
  { NULL, NULL }
};

/* Link */

static int
link_new (lua_State *L)
{
  const char *factory = luaL_checkstring (L, 1);
  WpProperties *properties = NULL;

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL) {
    luaL_checktype (L, 2, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 2);
  }

  WpLink *l = wp_link_new_from_factory (get_wp_core (L), factory, properties);
  if (l)
    wplua_pushobject (L, l);
  return l ? 1 : 0;
}

/* Client */

static gboolean
client_parse_permissions (const gchar * perms_str, guint32 *perms)
{
  *perms = 0;

  if (!perms_str)
    return FALSE;
  else if (g_strcmp0 (perms_str, "all") == 0)
    *perms = PW_PERM_ALL;
  else {
    for (guint i = 0; i < strlen (perms_str); i++) {
      switch (perms_str[i]) {
        case 'r': *perms |= PW_PERM_R; break;
        case 'w': *perms |= PW_PERM_W; break;
        case 'x': *perms |= PW_PERM_X; break;
        case 'm': *perms |= PW_PERM_M; break;
        case 'l': *perms |= PW_PERM_L; break;
        case '-': break;
        default:
          return FALSE;
      }
    }
  }
  return TRUE;
}

static int
client_update_permissions (lua_State *L)
{
  WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
  g_autoptr (GArray) arr = NULL;

  luaL_checktype (L, 2, LUA_TTABLE);

  lua_pushnil(L);
  while (lua_next (L, -2)) {
    struct pw_permission perm = {0};

    if (lua_type (L, -2) == LUA_TSTRING &&
        (!g_ascii_strcasecmp (lua_tostring(L, -2), "any") ||
         !g_ascii_strcasecmp (lua_tostring(L, -2), "all")))
      perm.id = PW_ID_ANY;
    else if (lua_isinteger (L, -2))
      perm.id = lua_tointeger (L, -2);
    else
      luaL_error (L, "invalid key for permissions array");

    if (!client_parse_permissions (lua_tostring (L, -1), &perm.permissions))
      luaL_error (L, "invalid permission string: '%s'", lua_tostring (L, -1));

    if (!arr)
      arr = g_array_new (FALSE, FALSE, sizeof (struct pw_permission));

    g_array_append_val (arr, perm);
    lua_pop (L, 1);
  }

  wp_client_update_permissions_array (client, arr->len,
      (const struct pw_permission *) arr->data);
  return 0;
}

static int
client_update_properties (lua_State *L)
{
  WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);

  luaL_checktype (L, 2, LUA_TTABLE);
  WpProperties *properties = wplua_table_to_properties (L, 2);

  wp_client_update_properties (client, properties);
  return 0;
}

static int
client_send_error (lua_State *L)
{
  WpClient *client = wplua_checkobject (L, 1, WP_TYPE_CLIENT);
  guint id = luaL_checkinteger (L, 2);
  int res = luaL_checkinteger (L, 3);
  const char *message = luaL_checkstring (L, 4);
  wp_client_send_error (client, id, res, message);
  return 0;
}

static const luaL_Reg client_methods[] = {
  { "update_permissions", client_update_permissions },
  { "update_properties", client_update_properties },
  { "send_error", client_send_error },
  { NULL, NULL }
};

/* WpSessionItem */

static int
session_item_new (lua_State *L)
{
  const char *type = luaL_checkstring (L, 1);
  WpSessionItem *si = wp_session_item_make (get_wp_core (L), type);
  if (si)
    wplua_pushobject (L, si);
  return si ? 1 : 0;
}

static int
session_item_get_associated_proxy (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  const char *typestr = luaL_checkstring (L, 2);
  WpProxy *proxy = wp_session_item_get_associated_proxy (si,
      parse_gtype (typestr));
  if (proxy)
    wplua_pushobject (L, proxy);
  return proxy ? 1 : 0;
}

static int
session_item_reset (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  wp_session_item_reset (si);
  return 0;
}

static int
session_item_configure (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  WpProperties *props = wp_properties_new_empty ();

  /* validate arguments */
  luaL_checktype (L, 2, LUA_TTABLE);

  /* build the configuration properties */
  lua_pushnil (L);
  while (lua_next (L, 2)) {
    const gchar *key = NULL;
    g_autofree gchar *var = NULL;

    switch (lua_type (L, -1)) {
      case LUA_TBOOLEAN:
        var = g_strdup_printf ("%u", lua_toboolean (L, -1));
        break;
      case LUA_TNUMBER:
        if (lua_isinteger (L, -1))
          var = g_strdup_printf ("%lld", lua_tointeger (L, -1));
        else
          var = g_strdup_printf ("%f", lua_tonumber (L, -1));
        break;
      case LUA_TSTRING:
        var = g_strdup (lua_tostring (L, -1));
        break;
      case LUA_TUSERDATA: {
        GValue *v = lua_touserdata (L, -1);
        gpointer p = g_value_peek_pointer (v);
        var = g_strdup_printf ("%p", p);
        break;
      }
      default:
        luaL_error (L, "configure does not support lua type ",
            lua_typename(L, lua_type(L, -1)));
        break;
    }

    key = luaL_tolstring (L, -2, NULL);
    wp_properties_set (props, key, var);
    lua_pop (L, 2);
  }

  lua_pushboolean (L, wp_session_item_configure (si, props));
  return 1;
}

static int
session_item_register (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  wp_session_item_register (g_object_ref (si));
  return 0;
}

static int
session_item_remove (lua_State *L)
{
  WpSessionItem *si = wplua_checkobject (L, 1, WP_TYPE_SESSION_ITEM);
  wp_session_item_remove (si);
  return 0;
}

static const luaL_Reg session_item_methods[] = {
  { "get_associated_proxy", session_item_get_associated_proxy },
  { "reset", session_item_reset },
  { "configure", session_item_configure },
  { "register", session_item_register },
  { "remove", session_item_remove },
  { NULL, NULL }
};

/* WpSiAdapter */

static int
si_adapter_get_ports_format (lua_State *L)
{
  WpSiAdapter *adapter = wplua_checkobject (L, 1, WP_TYPE_SI_ADAPTER);
  const gchar *mode = NULL;
  WpSpaPod *format = wp_si_adapter_get_ports_format (adapter, &mode);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, format);
  lua_pushstring (L, mode);
  return 2;
}

static void
si_adapter_set_ports_format_done (WpObject *o, GAsyncResult * res,
    GClosure * closure)
{
  g_autoptr (GError) error = NULL;
  GValue val[2] = { G_VALUE_INIT, G_VALUE_INIT };
  int n_vals = 1;

  if (!wp_si_adapter_set_ports_format_finish (WP_SI_ADAPTER (o), res, &error)) {
    wp_notice_object (o, "%s", error->message);
    if (closure) {
      g_value_init (&val[1], G_TYPE_STRING);
      g_value_set_string (&val[1], error->message);
      n_vals = 2;
    }
  }
  if (closure) {
    g_value_init_from_instance (&val[0], o);
    g_closure_invoke (closure, NULL, n_vals, val, NULL);
    g_value_unset (&val[0]);
    g_value_unset (&val[1]);
    g_closure_invalidate (closure);
    g_closure_unref (closure);
  }
}

static int
si_adapter_set_ports_format (lua_State *L)
{
  WpSiAdapter *adapter = wplua_checkobject (L, 1, WP_TYPE_SI_ADAPTER);
  WpSpaPod *format = wplua_checkboxed (L, 2, WP_TYPE_SPA_POD);
  const gchar *mode = luaL_checkstring (L, 3);
  GClosure * closure = luaL_opt (L, wplua_checkclosure, 4, NULL);
  if (closure)
    g_closure_sink (g_closure_ref (closure));
  wp_si_adapter_set_ports_format (adapter, wp_spa_pod_ref (format), mode,
      (GAsyncReadyCallback) si_adapter_set_ports_format_done, closure);
  return 0;
}

static const luaL_Reg si_adapter_methods[] = {
  { "get_ports_format", si_adapter_get_ports_format },
  { "set_ports_format", si_adapter_set_ports_format },
  { NULL, NULL }
};

/* WpPipewireObject */

static int
pipewire_object_iterate_params (lua_State *L)
{
  WpPipewireObject *pwobj = wplua_checkobject (L, 1, WP_TYPE_PIPEWIRE_OBJECT);
  const gchar *id = luaL_checkstring (L, 2);
  WpIterator *it = wp_pipewire_object_enum_params_sync (pwobj, id, NULL);
  return push_wpiterator (L, it);
}

static int
pipewire_object_set_param (lua_State *L)
{
  WpPipewireObject *pwobj = wplua_checkobject (L, 1, WP_TYPE_PIPEWIRE_OBJECT);
  const gchar *id = luaL_checkstring (L, 2);
  WpSpaPod *pod = wplua_checkboxed (L, 3, WP_TYPE_SPA_POD);
  wp_pipewire_object_set_param (pwobj, id, 0, wp_spa_pod_ref (pod));
  return 0;
}

static const luaL_Reg pipewire_object_methods[] = {
  { "iterate_params", pipewire_object_iterate_params },
  { "set_param" , pipewire_object_set_param },
  { "set_params" , pipewire_object_set_param }, /* deprecated, compat only */
  { NULL, NULL }
};

/* WpState */

static int
state_new (lua_State *L)
{
  const gchar *name = luaL_checkstring (L, 1);
  WpState *state = wp_state_new (name);
  wplua_pushobject (L, state);
  return 1;
}

static int
state_clear (lua_State *L)
{
  WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
  wp_state_clear (state);
  return 0;
}

static int
state_save (lua_State *L)
{
  WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
  luaL_checktype (L, 2, LUA_TTABLE);
  g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
  g_autoptr (GError) error = NULL;
  gboolean saved = wp_state_save (state, props, &error);
  lua_pushboolean (L, saved);
  lua_pushstring (L, error ? error->message : "");
  return 2;
}

static int
state_save_after_timeout (lua_State *L)
{
  WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
  luaL_checktype (L, 2, LUA_TTABLE);
  g_autoptr (WpProperties) props = wplua_table_to_properties (L, 2);
  wp_state_save_after_timeout (state, get_wp_core (L), props);
  return 0;
}

static int
state_load (lua_State *L)
{
  WpState *state = wplua_checkobject (L, 1, WP_TYPE_STATE);
  g_autoptr (WpProperties) props = wp_state_load (state);
  wplua_properties_to_table (L, props);
  return 1;
}

static const luaL_Reg state_methods[] = {
  { "clear", state_clear },
  { "save" , state_save },
  { "save_after_timeout", state_save_after_timeout },
  { "load" , state_load },
  { NULL, NULL }
};

/* ImplModule */

static int
impl_module_new (lua_State *L)
{
  const char *name, *args = NULL;
  WpProperties *properties = NULL;

  name = luaL_checkstring (L, 1);

  if (lua_type (L, 2) != LUA_TNONE && lua_type (L, 2) != LUA_TNIL)
    args = luaL_checkstring (L, 2);

  if (lua_type (L, 3) != LUA_TNONE && lua_type (L, 3) != LUA_TNIL) {
    luaL_checktype (L, 3, LUA_TTABLE);
    properties = wplua_table_to_properties (L, 3);
  }

  WpImplModule *m = wp_impl_module_load (get_wp_export_core (L),
     name, args, properties);

  if (m) {
    wplua_pushobject (L, m);
    return 1;
  } else {
    return 0;
  }
}

/* WpConf */

static int
conf_get_section_as_properties (lua_State *L)
{
  const char *section = luaL_checkstring (L, 1);
  g_autoptr (WpConf) conf = wp_core_get_conf (get_wp_core (L));
  g_autoptr (WpSpaJson) s = NULL;
  g_autoptr (WpProperties) props = NULL;

  if (lua_istable (L, 2))
    props = wplua_table_to_properties (L, 2);
  else
    props = wp_properties_new_empty ();

  if (conf) {
    s = wp_conf_get_section (conf, section);
    if (s && wp_spa_json_is_object (s))
      wp_properties_update_from_json (props, s);
  }
  wplua_properties_to_table (L, props);
  return 1;
}

static int
conf_get_section_as_object (lua_State *L)
{
  const char *section = luaL_checkstring (L, 1);
  g_autoptr (WpConf) conf = wp_core_get_conf (get_wp_core (L));
  g_autoptr (WpSpaJson) s = NULL;

  if (conf) {
    s = wp_conf_get_section (conf, section);
    if (s && wp_spa_json_is_object (s)) {
      push_luajson (L, s, INT_MAX);
      return 1;
    }
  }

  if (lua_istable (L, 2))
    lua_pushvalue (L, 2);
  else
    lua_newtable (L);
  return 1;
}

static int
conf_get_section_as_array (lua_State *L)
{
  const char *section = luaL_checkstring (L, 1);
  g_autoptr (WpConf) conf = wp_core_get_conf (get_wp_core (L));
  g_autoptr (WpSpaJson) s = NULL;

  if (conf) {
    s = wp_conf_get_section (conf, section);
    if (s && wp_spa_json_is_array (s)) {
      push_luajson (L, s, INT_MAX);
      return 1;
    }
  }

  if (lua_istable (L, 2))
    lua_pushvalue (L, 2);
  else
    lua_newtable (L);
  return 1;
}

static int
conf_get_section_as_json (lua_State *L)
{
  const char *section = luaL_checkstring (L, 1);
  g_autoptr (WpConf) conf = NULL;
  g_autoptr (WpSpaJson) s = NULL;
  WpSpaJson *fb = NULL;

  if (lua_isuserdata (L, 2))
    fb = wplua_checkboxed (L, 2, WP_TYPE_SPA_JSON);

  conf = wp_core_get_conf (get_wp_core (L));
  if (conf) {
    s = wp_conf_get_section (conf, section);
    if (!s && fb)
      s = wp_spa_json_ref (fb);
    if (s) {
      wplua_pushboxed (L, WP_TYPE_SPA_JSON,
          wp_spa_json_ensure_unique_owner (g_steal_pointer (&s)));
      return 1;
    }
  }

  lua_pushnil (L);
  return 1;
}

static const luaL_Reg conf_methods[] = {
  { "get_section_as_properties", conf_get_section_as_properties },
  { "get_section_as_object", conf_get_section_as_object },
  { "get_section_as_array", conf_get_section_as_array },
  { "get_section_as_json", conf_get_section_as_json },
  { NULL, NULL }
};

/* JsonUtils */

static gboolean
json_utils_match_rules_cb (gpointer data, const gchar * action,
    WpSpaJson * value, GError ** error)
{
  lua_State *L = data;
  g_autoptr (GError) pcall_err = NULL;
  gboolean ret = TRUE;
  int top = lua_gettop (L);

  /* this callback is called within the context of json_utils_match_rules()
   * and the Lua callback function is at the top of the stack at this point */

  /* re-push the function because lua_call pops it; then, push arguments */
  lua_pushvalue (L, -1);
  lua_pushstring (L, action);
  wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_ref (value));

  lua_call (L, 2, 2);

  ret = lua_toboolean (L, -2);
  if (!ret) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "%s", lua_tostring (L, -1));
  }

  lua_settop (L, top);
  return ret;
}

static int
json_utils_match_rules (lua_State *L)
{
  g_autoptr (WpProperties) properties = NULL;
  g_autoptr (GError) error = NULL;
  WpSpaJson *json;
  gboolean res;

  json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  luaL_checktype (L, 2, LUA_TTABLE);
  luaL_checktype (L, 3, LUA_TFUNCTION);

  properties = wplua_table_to_properties (L, 2);

  res = wp_json_utils_match_rules (json, properties, json_utils_match_rules_cb,
      L, &error);

  lua_pushboolean (L, res);
  if (error)
    lua_pushstring (L, error->message);
  else
    lua_pushnil (L);
  return 2;
}

static int
json_utils_match_rules_update_properties (lua_State *L)
{
  g_autoptr (WpProperties) properties = NULL;
  WpSpaJson *json;
  int count;

  json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  luaL_checktype (L, 2, LUA_TTABLE);
  properties = wplua_table_to_properties (L, 2);

  count = wp_json_utils_match_rules_update_properties (json, properties);

  wplua_properties_to_table (L, properties);
  lua_pushinteger (L, count);
  return 2;
}

static const luaL_Reg json_utils_funcs[] = {
  { "match_rules", json_utils_match_rules },
  { "match_rules_update_properties", json_utils_match_rules_update_properties },
  { NULL, NULL }
};

/* WpSettings */

static int
settings_get (lua_State *L)
{
  const char *setting = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  if (s) {
    WpSpaJson *j = wp_settings_get (s, setting);
    if (j)
      wplua_pushboxed (L, WP_TYPE_SPA_JSON, j);
    else
      lua_pushnil (L);
  } else
    lua_pushnil (L);
  return 1;
}

static int
settings_get_boolean (lua_State *L)
{
  const char *setting = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  gboolean val = FALSE;

  if (s) {
    g_autoptr (WpSpaJson) j = wp_settings_get (s, setting);
    if (j)
      wp_spa_json_parse_boolean (j, &val);
  }

  lua_pushboolean (L, val);
  return 1;
}

static int
settings_get_int (lua_State *L)
{
  const char *setting = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  gint val = 0;

  if (s) {
    g_autoptr (WpSpaJson) j = wp_settings_get (s, setting);
    if (j)
      wp_spa_json_parse_int (j, &val);
  }

  lua_pushinteger (L, val);
  return 1;
}

static int
settings_get_float (lua_State *L)
{
  const char *setting = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  float val = 0.0;

  if (s) {
    g_autoptr (WpSpaJson) j = wp_settings_get (s, setting);
    if (j)
      wp_spa_json_parse_float (j, &val);
  }

  lua_pushnumber (L, val);
  return 1;
}

static int
settings_get_string (lua_State *L)
{
  const char *setting = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);

  if (s) {
    g_autoptr (WpSpaJson) j = wp_settings_get (s, setting);
    if (j) {
      g_autofree gchar *val = wp_spa_json_parse_string (j);
      if (val) {
        lua_pushstring (L, val);
        return 1;
      }
    }
  }

  lua_pushstring (L, "");
  return 1;
}

static int
settings_get_array (lua_State *L)
{
  const char *setting = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  g_autoptr (WpSpaJson) val = NULL;

  if (s) {
    g_autoptr (WpSpaJson) j = wp_settings_get (s, setting);
    if (j && wp_spa_json_is_array (j)) {
      push_luajson (L, j, INT_MAX);
      return 1;
    }
  }

  val = wp_spa_json_new_array (NULL, NULL);
  push_luajson (L, val, INT_MAX);
  return 1;
}

static int
settings_get_object (lua_State *L)
{
  const char *setting = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  g_autoptr (WpSpaJson) val = NULL;

  if (s) {
    g_autoptr (WpSpaJson) j = wp_settings_get (s, setting);
    if (j && wp_spa_json_is_object (j)) {
      push_luajson (L, j, INT_MAX);
      return 1;
    }
  }

  val = wp_spa_json_new_object (NULL, NULL, NULL);
  push_luajson (L, val, INT_MAX);
  return 1;
}

static int
settings_get_saved (lua_State *L)
{
  const char *setting = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  if (s) {
    WpSpaJson *j = wp_settings_get_saved (s, setting);
    if (j)
      wplua_pushboxed (L, WP_TYPE_SPA_JSON, j);
    else
      lua_pushnil (L);
  } else
    lua_pushnil (L);
  return 1;
}

static int
settings_set (lua_State *L)
{
  const char *key = luaL_checkstring (L, 1);
  WpSpaJson *val = wplua_checkboxed (L, 2, WP_TYPE_SPA_JSON);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  if (s) {
    lua_pushboolean (L, wp_settings_set (s, key, val));
  } else {
    lua_pushboolean (L, FALSE);
  }
  return 1;
}

static int
settings_reset (lua_State *L)
{
  const char *key = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  if (s) {
    lua_pushboolean (L, wp_settings_reset (s, key));
  } else {
    lua_pushboolean (L, FALSE);
  }
  return 1;
}

static int
settings_save (lua_State *L)
{
  const char *key = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  if (s) {
    lua_pushboolean (L, wp_settings_save (s, key));
  } else {
    lua_pushboolean (L, FALSE);
  }
  return 1;
}

static int
settings_delete (lua_State *L)
{
  const char *key = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  if (s) {
    lua_pushboolean (L, wp_settings_delete (s, key));
  } else {
    lua_pushboolean (L, FALSE);
  }
  return 1;
}

static int
settings_reset_all (lua_State *L)
{
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  if (s)
    wp_settings_reset_all (s);
  return 0;
}

static int
settings_save_all (lua_State *L)
{
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  if (s)
    wp_settings_save_all (s);
  return 0;
}

static int
settings_delete_all (lua_State *L)
{
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  if (s)
    wp_settings_delete_all (s);
  return 0;
}

static int
settings_iterate (lua_State *L)
{
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);
  WpIterator *it = wp_settings_new_iterator (s);
  return push_settings_wpiterator (L, it);
}

static int
settings_subscribe (lua_State *L)
{
  const gchar *pattern = luaL_checkstring (L, 1);
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);

  guintptr sub_id = 0;

  GClosure * closure = wplua_function_to_closure (L, -1);

  if (s)
    sub_id = wp_settings_subscribe_closure (s, pattern, closure);

  lua_pushinteger (L, sub_id);
  return 1;
}

static int
settings_unsubscribe (lua_State *L)
{
  guintptr sub_id = luaL_checkinteger (L, 1);
  gboolean ret = FALSE;
  g_autoptr (WpSettings) s = wp_settings_find (get_wp_core (L), NULL);

  if (s)
    ret = wp_settings_unsubscribe (s, sub_id);

  lua_pushboolean (L, ret);
  return 1;
}

static const luaL_Reg settings_methods[] = {
  { "get", settings_get },
  { "get_boolean", settings_get_boolean },
  { "get_int", settings_get_int },
  { "get_float", settings_get_float },
  { "get_string", settings_get_string },
  { "get_array", settings_get_array },
  { "get_object", settings_get_object },
  { "get_saved", settings_get_saved },
  { "set", settings_set },
  { "reset", settings_reset },
  { "save", settings_save },
  { "delete", settings_delete },
  { "reset_all", settings_reset_all },
  { "save_all", settings_save_all },
  { "delete_all", settings_delete_all },
  { "iterate", settings_iterate },
  { "subscribe", settings_subscribe },
  { "unsubscribe", settings_unsubscribe },
  { NULL, NULL }
};

/* WpEvent */

static int
event_get_properties (lua_State *L)
{
  WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
  g_autoptr (WpProperties) props = wp_event_get_properties (event);
  wplua_properties_to_table (L, props);
  return 1;
}

static int
event_get_source (lua_State *L)
{
  WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
  wplua_pushobject (L, wp_event_get_source (event));
  return 1;
}

static int
event_get_subject (lua_State *L)
{
  WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
  wplua_pushobject (L, wp_event_get_subject (event));
  return 1;
}

static int
event_stop_processing (lua_State *L)
{
  WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
  wp_event_stop_processing (event);
  return 0;
}

static int
event_set_data (lua_State *L)
{
  WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
  const gchar *key = luaL_checkstring (L, 2);
  GType type = G_TYPE_INVALID;
  g_auto (GValue) value = G_VALUE_INIT;
  const GValue *data = NULL;

  switch (lua_type (L, 3)) {
  case LUA_TNONE:
  case LUA_TNIL:
    break;
  case LUA_TUSERDATA:
    type = wplua_gvalue_userdata_type (L, 3);
    if (G_UNLIKELY (type == G_TYPE_INVALID))
      wp_warning ("cannot set userdata on event data (not GValue userdata)");
    break;
  case LUA_TBOOLEAN:
    type = G_TYPE_BOOLEAN;
    break;
  case LUA_TNUMBER:
    type = lua_isinteger (L, 3) ? G_TYPE_INT64 : G_TYPE_DOUBLE;
    break;
  case LUA_TSTRING:
    type = G_TYPE_STRING;
    break;
  case LUA_TTABLE:
    type = WP_TYPE_PROPERTIES;
    break;
  default:
    wp_warning ("cannot set value on event data (value type not supported)");
    break;
  }

  if (type != G_TYPE_INVALID) {
    g_value_init (&value, type);
    wplua_lua_to_gvalue (L, 3, &value);
    data = &value;
  }

  wp_event_set_data (event, key, data);
  return 0;
}

static int
event_get_data (lua_State *L)
{
  WpEvent *event = wplua_checkboxed (L, 1, WP_TYPE_EVENT);
  const gchar *key = luaL_checkstring (L, 2);
  const GValue *data = wp_event_get_data (event, key);
  if (data)
    wplua_gvalue_to_lua (L, data);
  else
    lua_pushnil (L);
  return 1;
}

static const luaL_Reg event_methods[] = {
  { "get_properties", event_get_properties },
  { "get_source", event_get_source },
  { "get_subject", event_get_subject },
  { "stop_processing", event_stop_processing },
  { "set_data", event_set_data },
  { "get_data", event_get_data },
  { NULL, NULL }
};

/* WpEventDispatcher */

static int
event_dispatcher_push_event (lua_State *L)
{
  const gchar *type = NULL;
  gint priority = 0;
  WpProperties *properties = NULL;
  GObject *source = NULL;
  GObject *subject = NULL;
  WpEvent *event = NULL;

  if (lua_type (L, 1) == LUA_TTABLE) {
    lua_pushliteral (L, "type");
    if (lua_gettable (L, 1) != LUA_TSTRING)
      luaL_error (L, "EventDispatcher.push_event: expected 'type' as string");
    type = lua_tostring (L, -1);
    // not popping to keep the string allocated

    lua_pushliteral (L, "priority");
    if (lua_gettable (L, 1) != LUA_TNUMBER)
      luaL_error (L, "EventDispatcher.push_event: expected 'priority' as number");
    priority = lua_tointeger (L, -1);
    lua_pop (L, 1);

    lua_pushliteral (L, "properties");
    if (lua_gettable (L, 1) != LUA_TNIL) {
      luaL_checktype (L, -1, LUA_TTABLE);
      properties = wplua_table_to_properties (L, -1);
    }
    lua_pop (L, 1);

    lua_pushliteral (L, "source");
    if (lua_gettable (L, 1) != LUA_TNIL)
      source = wplua_checkobject (L, -1, G_TYPE_OBJECT);
    lua_pop (L, 1);

    lua_pushliteral (L, "subject");
    if (lua_gettable (L, 1) != LUA_TNIL)
      subject = wplua_checkobject (L, -1, G_TYPE_OBJECT);
    lua_pop (L, 1);

    event = wp_event_new (type, priority, properties, source, subject);
  } else {
    event = wp_event_ref (wplua_checkboxed (L, 1, WP_TYPE_EVENT));
  }

  g_autoptr (WpEventDispatcher) dispatcher =
      wp_event_dispatcher_get_instance (get_wp_core (L));
  wp_event_dispatcher_push_event (dispatcher, wp_event_ref (event));
  wplua_pushboxed (L, WP_TYPE_EVENT, event);
  return 1;
}

static const luaL_Reg event_dispatcher_funcs[] = {
  { "push_event", event_dispatcher_push_event },
  { NULL, NULL }
};

/* WpEventHook */

static int
event_hook_register (lua_State *L)
{
  WpEventHook *hook = wplua_checkobject (L, 1, WP_TYPE_EVENT_HOOK);
  g_autoptr (WpEventDispatcher) dispatcher =
      wp_event_dispatcher_get_instance (get_wp_core (L));
  wp_event_dispatcher_register_hook (dispatcher, hook);
  return 0;
}

static int
event_hook_remove (lua_State *L)
{
  WpEventHook *hook = wplua_checkobject (L, 1, WP_TYPE_EVENT_HOOK);
  g_autoptr (WpEventDispatcher) dispatcher =
      wp_event_dispatcher_get_instance (get_wp_core (L));
  wp_event_dispatcher_unregister_hook (dispatcher, hook);
  return 0;
}

static const luaL_Reg event_hook_methods[] = {
  { "register", event_hook_register },
  { "remove", event_hook_remove },
  { NULL, NULL }
};

/* WpSimpleEventHook */

static int
simple_event_hook_new (lua_State *L)
{
  WpEventHook *hook = NULL;
  int before_size = 0, after_size = 0, i = 0;
  const gchar **before, **after;
  const gchar *name;
  GClosure *closure = NULL;

  /* discard any possible arguments after the first one to avoid
     any surprises when working with absolute stack indices below */
  lua_settop (L, 1);

  /* validate arguments */
  luaL_checktype (L, 1, LUA_TTABLE);

  if (lua_getfield (L, 1, "name") != LUA_TSTRING)
    luaL_error(L, "SimpleEventHook: expected 'name' as string");

  if (lua_getfield (L, 1, "execute") != LUA_TFUNCTION)
    luaL_error (L, "SimpleEventHook: expected 'execute' as function");

  switch (lua_getfield (L, 1, "before")) {
    case LUA_TTABLE:
      lua_len (L, -1);
      before_size = lua_tointeger (L, -1);
      lua_pop (L, 1);
      break;
    case LUA_TSTRING:
      before_size = 1;
      break;
    case LUA_TNIL:
      before_size = 0;
      break;
    default:
      luaL_error(L, "SimpleEventHook: unexpected value type for 'before'; "
          "should be table or string");
  }

  switch (lua_getfield (L, 1, "after")) {
    case LUA_TTABLE:
      lua_len (L, -1);
      after_size = lua_tointeger (L, -1);
      lua_pop (L, 1);
      break;
    case LUA_TSTRING:
      after_size = 1;
      break;
    case LUA_TNIL:
      after_size = 0;
      break;
    default:
      luaL_error(L, "SimpleEventHook: unexpected value type for 'after'; "
          "should be table or string");
  }

  /* allocate C stack space for before & after arrays */
  before = before_size > 0 ?
      (const gchar **) g_newa (gpointer, before_size + 1) : NULL;
  after = after_size > 0 ?
      (const gchar **) g_newa (gpointer, after_size + 1) : NULL;

  /* parse before */
  if (lua_type (L, 4) == LUA_TTABLE && before_size > 0) {
    i = 0;
    lua_pushnil (L);
    while (lua_next (L, 4) && i < before_size) {
      before[i++] = luaL_checkstring (L, -1);
      /* bring the key on top without popping the string value */
      lua_rotate (L, lua_gettop (L) - 1, 1);
    }
    before[i] = NULL;
  } else if (lua_type (L, 4) == LUA_TSTRING) {
    before[0] = lua_tostring (L, 4);
    before[1] = NULL;
  }

  /* parse after */
  if (lua_type (L, 5) == LUA_TTABLE && after_size > 0) {
    i = 0;
    lua_pushnil (L);
    while (lua_next (L, 5) && i < after_size) {
      after[i++] = luaL_checkstring (L, -1);
      /* bring the key on top without popping the string value */
      lua_rotate (L, lua_gettop (L) - 1, 1);
    }
    after[i] = NULL;
  } else if (lua_type (L, 5) == LUA_TSTRING) {
    after[0] = lua_tostring (L, 5);
    after[1] = NULL;
  }

  name = lua_tostring (L, 2);
  closure = wplua_function_to_closure (L, 3);

  hook = wp_simple_event_hook_new (name, before, after, closure);

  /* clear the lua stack now to make some space */
  lua_settop (L, 1);

  wplua_pushobject (L, hook);

  if (lua_getfield (L, 1, "interests") == LUA_TTABLE) {
    lua_pushnil (L);
    while (lua_next (L, -2)) {
      WpObjectInterest *interest =
          wplua_checkboxed (L, -1, WP_TYPE_OBJECT_INTEREST);
      wp_interest_event_hook_add_interest_full (WP_INTEREST_EVENT_HOOK (hook),
          wp_object_interest_ref (interest));
      lua_pop (L, 1);
    }
  }
  lua_pop (L, 1);

  return 1;
}

/* WpAsyncEventHook */

static int
async_event_hook_get_next_step (lua_State *L)
{
  WpTransition *transition = wplua_checkobject (L, 1, WP_TYPE_TRANSITION);
  guint step = luaL_checkinteger (L, 2);

  wp_trace_object (transition, "prev step: %u", step);

  if (step == WP_TRANSITION_STEP_NONE) {
    lua_pushinteger (L, WP_TRANSITION_STEP_CUSTOM_START);
    return 1;
  }

  /* step number is the value on the stack at this point */
  if (G_UNLIKELY (lua_gettable (L, lua_upvalueindex (1)) != LUA_TSTRING)) {
    wp_critical_object (transition, "unknown step number");
    lua_pushinteger (L, WP_TRANSITION_STEP_ERROR);
    return 1;
  }
  /* step string is now on the stack */
  if (G_UNLIKELY (lua_gettable (L, lua_upvalueindex (1)) != LUA_TTABLE)) {
    wp_critical_object (transition, "unknown step string");
    lua_pushinteger (L, WP_TRANSITION_STEP_ERROR);
    return 1;
  }
  lua_pushliteral (L, "next_idx");
  if (G_UNLIKELY (lua_gettable (L, -2) != LUA_TNUMBER)) {
    wp_critical_object (transition, "next_idx not found");
    lua_pushinteger (L, WP_TRANSITION_STEP_ERROR);
    return 1;
  }
  return 1;
}

static int
async_event_hook_execute_step (lua_State *L)
{
  WpTransition *transition = wplua_checkobject (L, 1, WP_TYPE_TRANSITION);
  WpEvent *event = wp_transition_get_data (transition);
  guint step = luaL_checkinteger (L, 2);
  const char *step_str = NULL;

  wp_trace_object (transition, "execute step: %u", step);

  if (G_LIKELY (step != WP_TRANSITION_STEP_ERROR)) {
    /* step_str = steps_table[step_number] */
    /* step number is the value on the stack at this point */
    if (G_UNLIKELY (lua_gettable (L, lua_upvalueindex (1)) != LUA_TSTRING)) {
      wp_critical_object (transition, "unknown step number %u", step);
      wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
          WP_LIBRARY_ERROR_INVARIANT, "unknown step number %u", step));
      return 0;
    }
  } else {
    /* try to execute a step called "error", if it exists */
    lua_pushliteral (L, "error");
  }
  step_str = lua_tostring (L, -1);

  /* step string is now on the stack */
  if (G_UNLIKELY (lua_gettable (L, lua_upvalueindex (1)) != LUA_TTABLE)) {
    /* it's ok if the "error" step is missing */
    if (step != WP_TRANSITION_STEP_ERROR) {
      wp_critical_object (transition, "unknown step string '%s'", step_str);
      wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
          WP_LIBRARY_ERROR_INVARIANT, "unknown step string '%s", step_str));
    }
    return 0;
  }

  lua_pushliteral (L, "execute");
  if (G_UNLIKELY (lua_gettable (L, -2) != LUA_TFUNCTION)) {
    wp_critical_object (transition, "no execute function defined for '%s'",
        step_str);
    wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_INVARIANT, "no execute function defined for '%s'",
        step_str));
    return 0;
  }

  wplua_pushboxed (L, WP_TYPE_EVENT, wp_event_ref (event));
  wplua_pushobject (L, g_object_ref (transition));
  lua_call (L, 2, 0);
  return 0;
}

static void
async_event_hook_prepare_steps_table (lua_State *L, int steps_tbl)
{
  const char *step_str = NULL;
  int step_str_index = 0;
  int step = WP_TRANSITION_STEP_CUSTOM_START;

  steps_tbl = lua_absindex (L, steps_tbl);

  lua_pushliteral (L, "start");
  step_str_index = lua_absindex (L, -1);
  step_str = lua_tostring (L, -1);

  while (step != WP_TRANSITION_STEP_NONE) {
    /* steps[step number] = step string */
    lua_pushvalue (L, -1);
    lua_seti (L, steps_tbl, step);

    lua_pushvalue (L, -1);
    if (lua_gettable (L, steps_tbl) != LUA_TTABLE)
      luaL_error (L, "AsyncEventHook: expected '%s' in 'steps'", step_str);

    lua_pushinteger (L, step++);
    lua_setfield (L, -2, "idx");

    lua_pushliteral (L, "next");
    if (lua_gettable (L, -2) != LUA_TSTRING)
      luaL_error (L, "AsyncEventHook: expected 'next' in step '%s'", step_str);
    lua_replace (L, step_str_index);
    step_str = lua_tostring (L, step_str_index);

    if (!g_strcmp0 (step_str, "none"))
      step = WP_TRANSITION_STEP_NONE;

    lua_pushinteger (L, step);
    lua_setfield (L, -2, "next_idx");

    lua_settop (L, step_str_index);
  }

  lua_pop (L, 1);
}

static int
async_event_hook_new (lua_State *L)
{
  WpEventHook *hook = NULL;
  int before_size = 0, after_size = 0, i = 0;
  const gchar **before, **after;
  const gchar *name;
  GClosure *get_next_step = NULL;
  GClosure *execute_step = NULL;

  /* discard any possible arguments after the first one to avoid
     any surprises when working with absolute stack indices below */
  lua_settop (L, 1);

  /* validate arguments */
  luaL_checktype (L, 1, LUA_TTABLE);

  if (lua_getfield (L, 1, "name") != LUA_TSTRING)
    luaL_error(L, "AsyncEventHook: expected 'name' as string");

  if (lua_getfield (L, 1, "steps") != LUA_TTABLE)
    luaL_error (L, "AsyncEventHook: expected 'steps' as table");

  switch (lua_getfield (L, 1, "before")) {
    case LUA_TTABLE:
      lua_len (L, -1);
      before_size = lua_tointeger (L, -1);
      lua_pop (L, 1);
      break;
    case LUA_TSTRING:
      before_size = 1;
      break;
    case LUA_TNIL:
      before_size = 0;
      break;
    default:
      luaL_error(L, "AsyncEventHook: unexpected value type for 'before'; "
          "should be table or string");
  }

  switch (lua_getfield (L, 1, "after")) {
    case LUA_TTABLE:
      lua_len (L, -1);
      after_size = lua_tointeger (L, -1);
      lua_pop (L, 1);
      break;
    case LUA_TSTRING:
      after_size = 1;
      break;
    case LUA_TNIL:
      after_size = 0;
      break;
    default:
      luaL_error(L, "AsyncEventHook: unexpected value type for 'after'; "
          "should be table or string");
  }

  /* allocate C stack space for before & after arrays */
  before = before_size > 0 ?
      (const gchar **) g_newa (gpointer, before_size + 1) : NULL;
  after = after_size > 0 ?
      (const gchar **) g_newa (gpointer, after_size + 1) : NULL;

  /* parse before */
  if (lua_type (L, 4) == LUA_TTABLE && before_size > 0) {
    i = 0;
    lua_pushnil (L);
    while (lua_next (L, 4) && i < before_size) {
      before[i++] = luaL_checkstring (L, -1);
      /* bring the key on top without popping the string value */
      lua_rotate (L, lua_gettop (L) - 1, 1);
    }
    before[i] = NULL;
  } else if (lua_type (L, 4) == LUA_TSTRING) {
    before[0] = lua_tostring (L, 4);
    before[1] = NULL;
  }

  /* parse after */
  if (lua_type (L, 5) == LUA_TTABLE && after_size > 0) {
    i = 0;
    lua_pushnil (L);
    while (lua_next (L, 5) && i < after_size) {
      after[i++] = luaL_checkstring (L, -1);
      /* bring the key on top without popping the string value */
      lua_rotate (L, lua_gettop (L) - 1, 1);
    }
    after[i] = NULL;
  } else if (lua_type (L, 5) == LUA_TSTRING) {
    after[0] = lua_tostring (L, 5);
    after[1] = NULL;
  }

  name = lua_tostring (L, 2);
  async_event_hook_prepare_steps_table (L, 3);

  lua_pushvalue (L, 3); /* pass 'steps' table as upvalue */
  lua_pushcclosure (L, async_event_hook_get_next_step, 1);
  get_next_step = wplua_function_to_closure (L, -1);
  lua_pop (L, 1);

  lua_pushvalue (L, 3); /* pass 'steps' table as upvalue */
  lua_pushcclosure (L, async_event_hook_execute_step, 1);
  execute_step = wplua_function_to_closure (L, -1);
  lua_pop (L, 1);

  hook = wp_async_event_hook_new (name, before, after, get_next_step,
      execute_step);

  /* clear the lua stack now to make some space */
  lua_settop (L, 1);

  wplua_pushobject (L, hook);

  if (lua_getfield (L, 1, "interests") == LUA_TTABLE) {
    lua_pushnil (L);
    while (lua_next (L, -2)) {
      WpObjectInterest *interest =
          wplua_checkboxed (L, -1, WP_TYPE_OBJECT_INTEREST);
      wp_interest_event_hook_add_interest_full (WP_INTEREST_EVENT_HOOK (hook),
          wp_object_interest_ref (interest));
      lua_pop (L, 1);
    }
  }
  lua_pop (L, 1);

  return 1;
}

static int
transition_advance (lua_State *L)
{
  WpTransition *t = wplua_checkobject (L, 1, WP_TYPE_TRANSITION);
  wp_transition_advance (t);
  return 0;
}

static int
transition_return_error (lua_State *L)
{
  WpTransition *t = wplua_checkobject (L, 1, WP_TYPE_TRANSITION);
  const char *err = luaL_checkstring (L, 2);
  wp_transition_return_error (t, g_error_new (WP_DOMAIN_LIBRARY,
          WP_LIBRARY_ERROR_OPERATION_FAILED, "%s", err));
  return 0;
}

static const luaL_Reg transition_methods[] = {
  { "advance", transition_advance },
  { "return_error", transition_return_error },
  { NULL, NULL }
};

void
wp_lua_scripting_api_init (lua_State *L)
{
  g_autoptr (GError) error = NULL;

  luaL_newlib (L, glib_methods);
  lua_setglobal (L, "GLib");

  luaL_newlib (L, i18n_funcs);
  lua_setglobal (L, "I18n");

  luaL_newlib (L, log_funcs);
  lua_setglobal (L, "WpLog");

  luaL_newlib (L, core_funcs);
  lua_setglobal (L, "WpCore");

  luaL_newlib (L, plugin_funcs);
  lua_setglobal (L, "WpPlugin");

  luaL_newlib (L, conf_methods);
  lua_setglobal (L, "WpConf");

  luaL_newlib (L, json_utils_funcs);
  lua_setglobal (L, "JsonUtils");

  luaL_newlib (L, settings_methods);
  lua_setglobal (L, "WpSettings");

  luaL_newlib (L, event_dispatcher_funcs);
  lua_setglobal (L, "WpEventDispatcher");

  wp_lua_scripting_pod_init (L);
  wp_lua_scripting_json_init (L);

  wplua_register_type_methods (L, G_TYPE_SOURCE,
      NULL, source_methods);
  wplua_register_type_methods (L, WP_TYPE_OBJECT,
      NULL, object_methods);
  wplua_register_type_methods (L, WP_TYPE_PROXY,
      NULL, proxy_methods);
  wplua_register_type_methods (L, WP_TYPE_GLOBAL_PROXY,
      NULL, global_proxy_methods);
  wplua_register_type_methods (L, WP_TYPE_OBJECT_INTEREST,
      object_interest_new, object_interest_methods);
  wplua_register_type_methods (L, WP_TYPE_OBJECT_MANAGER,
      object_manager_new, object_manager_methods);
  wplua_register_type_methods (L, WP_TYPE_METADATA,
      NULL, metadata_methods);
  wplua_register_type_methods (L, WP_TYPE_IMPL_METADATA,
      impl_metadata_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_DEVICE,
      device_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_SPA_DEVICE,
      spa_device_new, spa_device_methods);
  wplua_register_type_methods (L, WP_TYPE_NODE,
      node_new, node_methods);
  wplua_register_type_methods (L, WP_TYPE_IMPL_NODE,
      impl_node_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_PORT,
      NULL, port_methods);
  wplua_register_type_methods (L, WP_TYPE_LINK,
      link_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_CLIENT,
      NULL, client_methods);
  wplua_register_type_methods (L, WP_TYPE_SESSION_ITEM,
      session_item_new, session_item_methods);
  wplua_register_type_methods (L, WP_TYPE_SI_ADAPTER,
      NULL, si_adapter_methods);
  wplua_register_type_methods (L, WP_TYPE_PIPEWIRE_OBJECT,
      NULL, pipewire_object_methods);
  wplua_register_type_methods (L, WP_TYPE_STATE,
      state_new, state_methods);
  wplua_register_type_methods (L, WP_TYPE_IMPL_MODULE,
      impl_module_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_EVENT,
      NULL, event_methods);
  wplua_register_type_methods (L, WP_TYPE_EVENT_HOOK,
      NULL, event_hook_methods);
  wplua_register_type_methods (L, WP_TYPE_SIMPLE_EVENT_HOOK,
      simple_event_hook_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_ASYNC_EVENT_HOOK,
      async_event_hook_new, NULL);
  wplua_register_type_methods (L, WP_TYPE_TRANSITION,
      NULL, transition_methods);

  if (!wplua_load_uri (L, URI_API, &error) ||
      !wplua_pcall (L, 0, 0, &error)) {
    wp_critical ("Failed to load api: %s", error->message);
  }
}
  070701000000D9000081A400000000000000000000000165F863040000171F000000000000000000000000000000000000003B00000000wireplumber-0.5.0/modules/module-lua-scripting/api/api.lua    -- WirePlumber
--
-- This file contains the API that is made available to the Lua scripts
--
-- Copyright © 2020 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT

local function Constraint (spec)
  assert (type(spec[1]) == "string", "Constraint: expected subject as string");
  assert (type(spec[2]) == "string", "Constraint: expected verb as string");

  local subject = spec[1]
  local verb = spec[2]
  local verbs = {
    ["="] = "equals",
    ["!"] = "not-equals",
    ["c"] = "in-list",
    ["~"] = "in-range",
    ["#"] = "matches",
    ["+"] = "is-present",
    ["-"] = "is-absent"
  }

  -- check and convert verb to its short version
  local verb_is_valid = false
  for k, v in pairs(verbs) do
    if verb == k or verb == v then
      verb = k
      spec[2] = k
      verb_is_valid = true
      break
    end
  end
  assert (verb_is_valid, "Constraint: invalid verb '" .. verb .. "'")

  -- check and convert type to its integer value
  local type = spec["type"]
  if type then
    local valid_types = { "pw-global", "pw", "gobject" }
    local type_is_valid = false

    for i, v in ipairs(valid_types) do
      if type == v then
        spec["type"] = i
        type_is_valid = true
        break
      end
    end

    assert(type_is_valid, "Constraint: invalid subject type '" .. type .. "'")
  end

  -- check if we got the right amount of values
  if verb == "=" or verb == "!" or verb == "#" then
    assert (spec[3] ~= nil,
      "Constraint: " .. verbs[verb] .. ": expected constraint value")
  elseif verb == "c" then
    assert (spec[3] ~= nil,
      "Constraint: " .. verbs[verb] .. ": expected at least one constraint value")
  elseif verb == "~" then
    assert (spec[3] ~= nil and spec[4] ~= nil,
      "Constraint: " .. verbs[verb] .. ": expected two values")
  else
    assert (spec[3] == nil,
      "Constraint: " .. verbs[verb] .. ": expected no value, but there is one")
  end

  return debug.setmetatable(spec, { __name = "Constraint" })
end

local function EventInterest(spec)
  spec.type = "event"
  return WpObjectInterest_new(spec)
end

local function dump_table(t, indent)
  local indent_str = ""
  indent = indent or 1
  for i = 1, indent, 1 do
    indent_str = indent_str .. "\t"
  end

  local kvpairs = {}
  for k, v in pairs(t) do
    table.insert(kvpairs, { k, v })
  end

  table.sort(kvpairs, function (lhs, rhs)
    local left_key, right_key = lhs[1], rhs[1]

    -- If the types are different, we sort by the type
    -- in alphabetical order. This means that numbers
    -- come before before strings, etc
    if type(left_key) ~= type(right_key) then
      return type(left_key) < type(right_key)
    end

    local key_type = type(left_key)

    -- Only numbers and strings have a well-defined order
    -- that's guaranteed to fulfill the requirements of
    -- table.sort (strict weak order)
    if key_type == "number" or key_type == "string" then
      return left_key < right_key
    end

    -- At this point, we have no good way to order the objects.
    -- We can't just do `left_key < right_key`, because this may fail
    -- if there's no `__lt` metamethod, and even if there is one,
    -- it might not be a strict weak order. (The Lua reference does
    -- not say what happens if the order is not strict weak, so it's
    -- undefined behaviour)

    -- That said, it's always mathematically "permitted" to return `false`,
    -- in which case, since both x < y and y < x are false, the elements
    -- are considered "equivalent" and may appear in any order in relation
    -- to each other. The elements are still sorted in relation to the
    -- *other* keys.
    return false

    -- To be a strict weak order, if x and y are equivalent, and y and z
    -- are equivalent, then x and z must be equivalent too. Otherwise the
    -- ordering is only a strict *partial* order.

    -- Note that the Lua 5.3 reference states that the order merely has to
    -- be a strict *partial* order, but since all weak orders are partial
    -- orders, this is not a problem.
  end)

  for _, pair in ipairs(kvpairs) do
    local k, v = table.unpack(pair)

    if (type(v) == "table") then
      print (indent_str .. tostring(k) .. ": ")
      dump_table(v, indent + 1)
    else
      print (indent_str .. tostring(k) .. ": " .. tostring(v))
    end
  end
end

local Debug = {
  dump_table = dump_table,
}

local Id = {
  INVALID = 0xffffffff,
  ANY = 0xffffffff,
}

local Features = {
  PipewireObject = {
    MINIMAL = 0x11,
  },
  ALL = 0xffffffff,
}

local Feature = {
  Proxy = {
    BOUND             = 1,
  },
  PipewireObject = {
    INFO              = (1 << 4),
    PARAM_PROPS       = (1 << 5),
    PARAM_FORMAT      = (1 << 6),
    PARAM_PROFILE     = (1 << 7),
    PARAM_PORT_CONFIG = (1 << 8),
    PARAM_ROUTE       = (1 << 9),
  },
  SpaDevice = {
    ENABLED           = (1 << 16),
  },
  Node = {
    PORTS             = (1 << 16),
  },
  Metadata = {
    DATA              = (1 << 16),
  },
  SessionItem = {
    ACTIVE            = (1 << 0),
    EXPORTED          = (1 << 1),
  },
}

SANDBOX_EXPORT = {
  Debug = Debug,
  Id = Id,
  Features = Features,
  Feature = Feature,
  GLib = GLib,
  I18n = I18n,
  Log = WpLog,
  Core = WpCore,
  Plugin = WpPlugin,
  EventDispatcher = WpEventDispatcher,
  ObjectManager = WpObjectManager_new,
  Interest = WpObjectInterest_new,
  SessionItem = WpSessionItem_new,
  Constraint = Constraint,
  EventInterest = EventInterest,
  Device = WpDevice_new,
  SpaDevice = WpSpaDevice_new,
  Node = WpNode_new,
  LocalNode = WpImplNode_new,
  Link = WpLink_new,
  Pod = WpSpaPod,
  Json = WpSpaJson,
  State = WpState_new,
  LocalModule = WpImplModule_new,
  ImplMetadata = WpImplMetadata_new,
  Settings = WpSettings,
  Conf = WpConf,
  JsonUtils = JsonUtils,
  SimpleEventHook = WpSimpleEventHook_new,
  AsyncEventHook = WpAsyncEventHook_new,
}
 070701000000DA000081A400000000000000000000000165F86304000000CA000000000000000000000000000000000000004100000000wireplumber-0.5.0/modules/module-lua-scripting/api/gresource.xml  <?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/org/freedesktop/pipewire/wireplumber/m-lua-scripting/">
    <file compressed="true">api.lua</file>
  </gresource>
</gresources>
  070701000000DB000081A400000000000000000000000165F8630400002456000000000000000000000000000000000000003A00000000wireplumber-0.5.0/modules/module-lua-scripting/api/json.c /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <wplua/wplua.h>

#define WP_LOCAL_LOG_TOPIC log_topic_lua_scripting
WP_LOG_TOPIC_EXTERN (log_topic_lua_scripting)

/* API */

static int
spa_json_get_data (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  lua_pushstring (L, wp_spa_json_get_data (json));
  return 1;
}

static int
spa_json_get_size (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  lua_pushinteger (L, wp_spa_json_get_size (json));
  return 1;
}

static int
spa_json_to_string (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  /* Instead of using wp_spa_json_to_string() and lua_pushstring, we can avoid
   * an extra allocation if we use lua_pushlstring with wp_spa_json_get_data()
   * and wp_spa_json_get_size () */
  lua_pushlstring (L, wp_spa_json_get_data (json), wp_spa_json_get_size (json));
  return 1;
}

static int
spa_json_is_null (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  lua_pushboolean (L, wp_spa_json_is_null (json));
  return 1;
}

static int
spa_json_is_boolean (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  lua_pushboolean (L, wp_spa_json_is_boolean (json));
  return 1;
}

static int
spa_json_is_int (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  lua_pushboolean (L, wp_spa_json_is_int (json));
  return 1;
}

static int
spa_json_is_float (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  lua_pushboolean (L, wp_spa_json_is_float (json));
  return 1;
}

static int
spa_json_is_string (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  lua_pushboolean (L, wp_spa_json_is_string (json));
  return 1;
}

static int
spa_json_is_array (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  lua_pushboolean (L, wp_spa_json_is_array (json));
  return 1;
}

static int
spa_json_is_object (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  lua_pushboolean (L, wp_spa_json_is_object (json));
  return 1;
}

void
push_luajson (lua_State *L, WpSpaJson *json, gint n_recursions)
{
  /* Null */
  if (wp_spa_json_is_null (json)) {
    lua_pushnil (L);
  }

  /* Boolean */
  else if (wp_spa_json_is_boolean (json)) {
    gboolean value = FALSE;
    g_warn_if_fail (wp_spa_json_parse_boolean (json, &value));
    lua_pushboolean (L, value);
  }

  /* Int */
  else if (wp_spa_json_is_int (json)) {
    gint value = 0;
    g_warn_if_fail (wp_spa_json_parse_int (json, &value));
    lua_pushinteger (L, value);
  }

  /* Float */
  else if (wp_spa_json_is_float (json)) {
    float value = 0;
    g_warn_if_fail (wp_spa_json_parse_float (json, &value));
    lua_pushnumber (L, value);
  }

  /* Array */
  else if (wp_spa_json_is_array (json) && n_recursions > 0) {
    g_auto (GValue) item = G_VALUE_INIT;
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
    guint i = 1;
    lua_newtable (L);
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaJson *j = g_value_get_boxed (&item);
      push_luajson (L, j, n_recursions - 1);
      lua_rawseti (L, -2, i++);
    }
  }

  /* Object */
  else if (wp_spa_json_is_object (json) && n_recursions > 0) {
    g_auto (GValue) item = G_VALUE_INIT;
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
    lua_newtable (L);
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaJson *key = g_value_get_boxed (&item);
      g_autofree gchar *key_str = NULL;
      WpSpaJson *value = NULL;
      key_str = wp_spa_json_parse_string (key);
      g_warn_if_fail (key_str);
      g_value_unset (&item);
      if (!wp_iterator_next (it, &item))
        break;
      value = g_value_get_boxed (&item);
      push_luajson (L, value, n_recursions - 1);
      lua_setfield (L, -2, key_str);
    }
  }

  /* Otherwise alwyas parse as String to allow parsing strings without quotes */
  else {
    g_autofree gchar *value = wp_spa_json_parse_string (json);
    g_warn_if_fail (value);
    lua_pushstring (L, value);
  }
}

static int
spa_json_parse (lua_State *L)
{
  WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON);
  gint n_recursions = luaL_opt (L, luaL_checkinteger, 2, INT_MAX);
  push_luajson (L, json, n_recursions);
  return 1;
}

/* Raw */

static int
spa_json_raw_new (lua_State *L)
{
  const gchar *value = lua_tostring (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_from_string (value));
  return 1;
}

/* None */

static int
spa_json_null_new (lua_State *L)
{
  wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_null ());
  return 1;
}

/* Boolean */

static int
spa_json_boolean_new (lua_State *L)
{
  gboolean value = lua_toboolean (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_boolean (value));
  return 1;
}

/* Int */

static int
spa_json_int_new (lua_State *L)
{
  gint64 value = lua_tointeger (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_int (value));
  return 1;
}

/* Float */

static int
spa_json_float_new (lua_State *L)
{
  float value = lua_tonumber (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_float (value));
  return 1;
}

/* String */

static int
spa_json_string_new (lua_State *L)
{
  const gchar *value = lua_tostring (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_string (value));
  return 1;
}

/* Array */

static int
spa_json_array_new (lua_State *L)
{
  g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_array ();

  luaL_checktype (L, 1, LUA_TTABLE);

  lua_pushnil (L);
  while (lua_next (L, -2)) {
    /* We only add table values with integer keys */
    if (lua_isinteger (L, -2)) {
      switch (lua_type (L, -1)) {
        case LUA_TBOOLEAN:
          wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1));
          break;
        case LUA_TNUMBER:
          if (lua_isinteger (L, -1))
            wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1));
          else
            wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1));
          break;
        case LUA_TSTRING:
          wp_spa_json_builder_add_string (builder, lua_tostring (L, -1));
          break;
        case LUA_TUSERDATA: {
          WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON);
          wp_spa_json_builder_add_json (builder, json);
          break;
        }
        default:
          luaL_error (L, "Json does not support lua type ",
              lua_typename(L, lua_type(L, -1)));
          break;
      }
    }
    lua_pop (L, 1);
  }

  wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder));
  return 1;
}

/* Object */

static int
spa_json_object_new (lua_State *L)
{
  g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_object ();

  luaL_checktype (L, 1, LUA_TTABLE);

  lua_pushnil (L);
  while (lua_next (L, -2)) {
    /* We only add table values with string keys */
    if (lua_type (L, -2) == LUA_TSTRING) {
      wp_spa_json_builder_add_property (builder, lua_tostring (L, -2));

      switch (lua_type (L, -1)) {
        case LUA_TBOOLEAN:
          wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1));
          break;
        case LUA_TNUMBER:
          if (lua_isinteger (L, -1))
            wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1));
          else
            wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1));
          break;
        case LUA_TSTRING:
          wp_spa_json_builder_add_string (builder, lua_tostring (L, -1));
          break;
        case LUA_TUSERDATA: {
          WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON);
          wp_spa_json_builder_add_json (builder, json);
          break;
        }
        default:
          luaL_error (L, "Json does not support lua type ",
              lua_typename(L, lua_type(L, -1)));
          break;
      }
    }

    lua_pop (L, 1);
  }

  wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder));
  return 1;
}

/* Init */

static const luaL_Reg spa_json_methods[] = {
  { "get_data", spa_json_get_data },
  { "get_size", spa_json_get_size },
  { "to_string", spa_json_to_string },
  { "is_null", spa_json_is_null },
  { "is_boolean", spa_json_is_boolean },
  { "is_int", spa_json_is_int },
  { "is_float", spa_json_is_float },
  { "is_string", spa_json_is_string },
  { "is_array", spa_json_is_array },
  { "is_object", spa_json_is_object },
  { "parse", spa_json_parse },
  { NULL, NULL }
};

static const luaL_Reg spa_json_constructors[] = {
  { "Raw", spa_json_raw_new },
  { "Null", spa_json_null_new },
  { "Boolean", spa_json_boolean_new },
  { "Int", spa_json_int_new },
  { "Float", spa_json_float_new },
  { "String", spa_json_string_new },
  { "Array", spa_json_array_new },
  { "Object", spa_json_object_new },
  { NULL, NULL }
};

void
wp_lua_scripting_json_init (lua_State *L)
{
  luaL_newlib (L, spa_json_constructors);
  lua_setglobal (L, "WpSpaJson");

  wplua_register_type_methods (L, WP_TYPE_SPA_JSON, NULL, spa_json_methods);
}
  070701000000DC000081A400000000000000000000000165F86304000000C1000000000000000000000000000000000000003F00000000wireplumber-0.5.0/modules/module-lua-scripting/api/meson.build    m_lua_scripting_resources = gnome.compile_resources(
    'm-lua-scripting-resources',
    'gresource.xml',
    source_dir: meson.current_source_dir(),
    c_name: '_m_lua_scripting_resources')
   070701000000DD000081A400000000000000000000000165F86304000081B3000000000000000000000000000000000000003900000000wireplumber-0.5.0/modules/module-lua-scripting/api/pod.c  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <wplua/wplua.h>

#include <spa/utils/type.h>

#define WP_LOCAL_LOG_TOPIC log_topic_lua_scripting
WP_LOG_TOPIC_EXTERN (log_topic_lua_scripting)

#define MAX_LUA_TYPES 9

/* Builder */

typedef gboolean (*primitive_lua_add_func) (WpSpaPodBuilder *, WpSpaIdValue,
    lua_State *, int);

struct primitive_lua_type {
  WpSpaType primitive_type;
  primitive_lua_add_func primitive_lua_add_funcs[MAX_LUA_TYPES];
};

static inline gboolean
builder_add_boolean_lua_boolean (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  wp_spa_pod_builder_add_boolean (b, lua_toboolean (L, idx));
  return TRUE;
}

static inline gboolean
builder_add_boolean_lua_number (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  if (lua_isinteger (L, idx)) {
    wp_spa_pod_builder_add_boolean (b, lua_tointeger (L, idx) > 0);
    return TRUE;
  }
  return FALSE;
}

static inline gboolean
builder_add_boolean_lua_string (WpSpaPodBuilder *b, WpSpaIdValue key_id,
   lua_State *L, int idx)
{
  const gchar *value = lua_tostring (L, idx);
  wp_spa_pod_builder_add_boolean (b,
     g_strcmp0 (value, "true") == 0 || g_strcmp0 (value, "1") == 0);
  return TRUE;
}

static inline gboolean
builder_add_id_lua_number (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  if (lua_isinteger (L, idx)) {
    wp_spa_pod_builder_add_id (b, lua_tointeger (L, idx));
    return TRUE;
  }
  return FALSE;
}

static inline gboolean
builder_add_id_lua_string (WpSpaPodBuilder *b, WpSpaIdValue key_id,
   lua_State *L, int idx)
{
  const gchar *value = lua_tostring (L, idx);
  WpSpaIdTable id_table = NULL;
  WpSpaIdValue id_val = NULL;
  if (key_id) {
    wp_spa_id_value_get_value_type (key_id, &id_table);
    if (id_table) {
      id_val = wp_spa_id_table_find_value_from_short_name (id_table, value);
      wp_spa_pod_builder_add_id (b, wp_spa_id_value_number (id_val));
      return TRUE;
    }
  }
  return FALSE;
}

static inline gboolean
builder_add_int_lua_boolean (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  gboolean value = lua_toboolean (L, idx);
  wp_spa_pod_builder_add_int (b, value ? 1 : 0);
  return TRUE;
}

static inline gboolean
builder_add_int_lua_number (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  if (lua_isinteger (L, idx)) {
    wp_spa_pod_builder_add_int (b, lua_tointeger (L, idx));
    return TRUE;
  }
  return FALSE;
}

static inline gboolean
builder_add_int_lua_string (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  const gchar *value = lua_tostring (L, idx);
  wp_spa_pod_builder_add_int (b, strtol (value, NULL, 10));
  return TRUE;
}

static inline gboolean
builder_add_long_lua_boolean (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  gboolean value = lua_toboolean (L, idx);
  wp_spa_pod_builder_add_long (b, value ? 1 : 0);
  return TRUE;
}

static inline gboolean
builder_add_long_lua_number (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  if (lua_isinteger (L, idx)) {
    wp_spa_pod_builder_add_long (b, lua_tointeger (L, idx));
    return TRUE;
  }
  return FALSE;
}

static inline gboolean
builder_add_long_lua_string (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  const gchar *value = lua_tostring (L, idx);
  wp_spa_pod_builder_add_long (b, strtol (value, NULL, 10));
  return TRUE;
}

static inline gboolean
builder_add_float_lua_boolean (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  gboolean value = lua_toboolean (L, idx);
  wp_spa_pod_builder_add_float (b, value ? 1.0f : 0.0f);
  return TRUE;
}

static inline gboolean
builder_add_float_lua_number (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  if (lua_isnumber (L, idx) && !lua_isinteger (L, idx)) {
    wp_spa_pod_builder_add_float (b, lua_tonumber (L, idx));
    return TRUE;
  }
  return FALSE;
}

static inline gboolean
builder_add_double_lua_boolean (WpSpaPodBuilder *b, WpSpaIdValue key_id,
   lua_State *L, int idx)
{
  gboolean value = lua_toboolean (L, idx);
  wp_spa_pod_builder_add_double (b, value ? 1.0f : 0.0f);
  return TRUE;
}

static inline gboolean
builder_add_double_lua_number (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  if (lua_isnumber (L, idx) && !lua_isinteger (L, idx)) {
    wp_spa_pod_builder_add_double (b, lua_tonumber (L, idx));
    return TRUE;
  }
  return FALSE;
}

static inline gboolean
builder_add_string_lua_boolean (WpSpaPodBuilder *b, WpSpaIdValue key_id,
   lua_State *L, int idx)
{
  gboolean value = lua_toboolean (L, idx);
  wp_spa_pod_builder_add_string (b, value ? "true" : "false");
  return TRUE;
}

static inline gboolean
builder_add_string_lua_number (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  g_autofree gchar *value = NULL;
  value = lua_isinteger (L, idx) ?
      g_strdup_printf ("%lld", lua_tointeger (L, idx)) :
      g_strdup_printf ("%f", lua_tonumber (L, idx));
  wp_spa_pod_builder_add_string (b, value);
  return TRUE;
}

static inline gboolean
builder_add_string_lua_string (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  const gchar *value = lua_tostring (L, idx);
  wp_spa_pod_builder_add_string (b, value);
  return TRUE;
}

static inline gboolean
builder_add_bytes_lua_number (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  if (lua_isinteger (L, idx)) {
    gint64 value = lua_tointeger (L, idx);
    wp_spa_pod_builder_add_bytes (b, (gconstpointer)&value, sizeof (value));
  } else {
    double value = lua_tonumber (L, idx);
    wp_spa_pod_builder_add_bytes (b, (gconstpointer)&value, sizeof (value));
  }
  return TRUE;
}

static inline gboolean
builder_add_bytes_lua_string (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  const gchar *value = lua_tostring (L, idx);
  wp_spa_pod_builder_add_bytes (b, (gconstpointer)value, strlen (value));
  return TRUE;
}

static inline gboolean
builder_add_fd_lua_number (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  if (lua_isinteger (L, idx)) {
    wp_spa_pod_builder_add_fd (b, lua_tointeger (L, idx));
    return TRUE;
  }
  return FALSE;
}

static inline gboolean
builder_add_fd_lua_string (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  const gchar *value = lua_tostring (L, idx);
  wp_spa_pod_builder_add_fd (b, strtol (value, NULL, 10));
  return TRUE;
}

static inline gboolean
is_pod_type_compatible (WpSpaType type, WpSpaPod *pod)
{
  /* Check if the pod type matches */
  if (type == wp_spa_pod_get_spa_type (pod))
    return TRUE;

  /* Otherwise, check if the child type of Choice pod matches */
  if (wp_spa_pod_is_choice (pod)) {
    g_autoptr (WpSpaPod) child = wp_spa_pod_get_choice_child (pod);
    WpSpaType child_type = wp_spa_pod_get_spa_type (child);
    if (type == child_type)
      return TRUE;
  }

  return FALSE;
}

static inline gboolean
builder_add_lua_userdata (WpSpaPodBuilder *b, WpSpaIdValue key_id,
    lua_State *L, int idx)
{
  WpSpaPod *pod = wplua_checkboxed (L, idx, WP_TYPE_SPA_POD);
  if (pod) {
    if (key_id) {
      WpSpaType prop_type = wp_spa_id_value_get_value_type (key_id, NULL);
      if (is_pod_type_compatible (prop_type, pod)) {
        wp_spa_pod_builder_add_pod (b, pod);
        return TRUE;
      }
    } else {
      wp_spa_pod_builder_add_pod (b, pod);
      return TRUE;
    }
  }
  return FALSE;
}

static const struct primitive_lua_type primitive_lua_types[] = {
  {SPA_TYPE_Bool, {
    [LUA_TBOOLEAN] = builder_add_boolean_lua_boolean,
    [LUA_TNUMBER] = builder_add_boolean_lua_number,
    [LUA_TSTRING] = builder_add_boolean_lua_string,
  }},
  {SPA_TYPE_Id, {
    [LUA_TNUMBER] = builder_add_id_lua_number,
    [LUA_TSTRING] = builder_add_id_lua_string,
  }},
  {SPA_TYPE_Int, {
    [LUA_TBOOLEAN] = builder_add_int_lua_boolean,
    [LUA_TNUMBER] = builder_add_int_lua_number,
    [LUA_TSTRING] = builder_add_int_lua_string,
  }},
  {SPA_TYPE_Long, {
    [LUA_TBOOLEAN] = builder_add_long_lua_boolean,
    [LUA_TNUMBER] = builder_add_long_lua_number,
    [LUA_TSTRING] = builder_add_long_lua_string,
  }},
  {SPA_TYPE_Float, {
    [LUA_TBOOLEAN] = builder_add_float_lua_boolean,
    [LUA_TNUMBER] = builder_add_float_lua_number,
  }},
  {SPA_TYPE_Double, {
    [LUA_TBOOLEAN] = builder_add_double_lua_boolean,
    [LUA_TNUMBER] = builder_add_double_lua_number,
  }},
  {SPA_TYPE_String, {
    [LUA_TBOOLEAN] = builder_add_string_lua_boolean,
    [LUA_TNUMBER] = builder_add_string_lua_number,
    [LUA_TSTRING] = builder_add_string_lua_string,
  }},
  {SPA_TYPE_Bytes, {
    [LUA_TNUMBER] = builder_add_bytes_lua_number,
    [LUA_TSTRING] = builder_add_bytes_lua_string,
  }},
  {SPA_TYPE_Fd, {
    [LUA_TNUMBER] = builder_add_fd_lua_number,
    [LUA_TSTRING] = builder_add_fd_lua_string,
  }},
  {0, {}},
};

static gboolean
builder_add_key (WpSpaPodBuilder *b, WpSpaIdTable table, lua_State *L, int idx)
{
  /* Number */
  if (lua_type (L, -1) == LUA_TNUMBER) {
    wp_spa_pod_builder_add_id (b, lua_tonumber (L, idx));
    return TRUE;
  }

  /* String */
  else if (lua_type (L, -1) == LUA_TSTRING) {
    const gchar *key = lua_tostring (L, idx);
    WpSpaIdValue val = wp_spa_id_table_find_value_from_short_name (table, key);
    if (val) {
      wp_spa_pod_builder_add_id (b, wp_spa_id_value_number (val));
      return TRUE;
    }
  }

  return FALSE;
}

static gboolean
builder_add_value (WpSpaPodBuilder *b, WpSpaType array_type, lua_State *L,
    int idx)
{
  int ltype = lua_type (L, idx);
  guint i;

  if (ltype < 0 || ltype >= MAX_LUA_TYPES)
    return FALSE;

  for (i = 0; primitive_lua_types[i].primitive_type; i++) {
    const struct primitive_lua_type *t = primitive_lua_types + i;
    if (t->primitive_type == array_type) {
      primitive_lua_add_func f = t->primitive_lua_add_funcs[ltype];
      if (f) {
        return f (b, NULL, L, idx);
      }
    }
  }
  return FALSE;
}

static void
builder_add_table (lua_State *L, WpSpaPodBuilder *builder)
{
  const gchar *type_name = NULL;
  WpSpaType type = WP_SPA_TYPE_INVALID;
  WpSpaIdTable table = NULL;

  luaL_checktype (L, 1, LUA_TTABLE);

  lua_pushnil (L);
  while (lua_next (L, 1)) {
    /* First filed is always the item type or table */
    if (type == WP_SPA_TYPE_INVALID && !table) {
      if (lua_type (L, -1) == LUA_TSTRING) {
        type_name = lua_tostring (L, -1);
        type = wp_spa_type_from_name (type_name);
        if (type == WP_SPA_TYPE_INVALID) {
          table = wp_spa_id_table_from_name (type_name);
          if (!table)
            luaL_error (L, "Unknown type '%s'", type_name);
        }
      } else {
        luaL_error (L,
            "must have the item type or table on its first field");
      }
    }

    /* Add remaining table key elements */
    else if (table) {
      if (!builder_add_key (builder, table, L, -1))
        luaL_error (L, "key could not be added");
    }

    /* Add remaining value elements */
    else if (type != WP_SPA_TYPE_INVALID) {
      if (!builder_add_value (builder, type, L, -1))
        luaL_error (L, "value could not be added");
    }

    lua_pop (L, 1);
  }
}

/* None */

static int
spa_pod_none_new (lua_State *L)
{
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_none ());
  return 1;
}

/* Boolean */

static int
spa_pod_boolean_new (lua_State *L)
{
  gboolean value = lua_toboolean (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_boolean (value));
  return 1;
}

/* Id */

static int
spa_pod_id_new (lua_State *L)
{
  /* Id number */
  if (lua_type (L, 1) == LUA_TNUMBER) {
    gint64 value = lua_tointeger (L, 1);
    wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_id (value));
    return 1;
  }

  /* Table name and key name */
  else if (lua_type (L, 1) == LUA_TSTRING) {
    const gchar *table_name = lua_tostring (L, 1);
    const gchar *key_name = lua_tostring (L, 2);
    WpSpaIdTable table = NULL;
    WpSpaIdValue value = NULL;
    table = wp_spa_id_table_from_name (table_name);
    if (!table)
      luaL_error (L, "table '%s' does not exist", table_name);
    value = wp_spa_id_table_find_value_from_short_name (table, key_name);
    if (!value)
      luaL_error (L, "key '%s' does not exist in '%s'", key_name, table_name);
    wplua_pushboxed (L, WP_TYPE_SPA_POD,
        wp_spa_pod_new_id (wp_spa_id_value_number (value)));
    return 1;
  }

  /* Error */
  luaL_error (L, "Invalid parameters");
  return 0;
}

/* Int */

static int
spa_pod_int_new (lua_State *L)
{
  gint64 value = lua_tointeger (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_int (value));
  return 1;
}

/* Long */

static int
spa_pod_long_new (lua_State *L)
{
  gint64 value = lua_tointeger (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_long (value));
  return 1;
}

/* Float */

static int
spa_pod_float_new (lua_State *L)
{
  float value = lua_tonumber (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_float (value));
  return 1;
}

/* Double */

static int
spa_pod_double_new (lua_State *L)
{
  double value = lua_tonumber (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_double (value));
  return 1;
}

/* String */

static int
spa_pod_string_new (lua_State *L)
{
  const gchar *value = lua_tostring (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_string (value));
  return 1;
}

/* Bytes */

static int
spa_pod_bytes_new (lua_State *L)
{
  switch (lua_type (L, 1)) {
    case LUA_TNUMBER: {
      if (lua_isinteger (L, 1)) {
        guint64 value = lua_tointeger (L, 1);
        wplua_pushboxed (L, WP_TYPE_SPA_POD,
            wp_spa_pod_new_bytes (&value, sizeof (guint64)));
      } else {
        double value = lua_tonumber (L, 1);
        wplua_pushboxed (L, WP_TYPE_SPA_POD,
            wp_spa_pod_new_bytes (&value, sizeof (double)));
      }
      return 1;
    }
    case LUA_TSTRING: {
      const gchar *str = lua_tostring (L, 1);
      wplua_pushboxed (L, WP_TYPE_SPA_POD,
          wp_spa_pod_new_bytes (str, strlen (str)));
      return 1;
    }
    default:
      luaL_error (L, "Only number and strings are valid for bytes pod");
      break;
  }
  return 0;
}

/* Pointer */

static int
spa_pod_pointer_new (lua_State *L)
{
  const gchar *type = lua_tostring (L, 1);
  gconstpointer value = lua_touserdata (L, 2);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_pointer (type, value));
  return 1;
}

/* Fd */

static int
spa_pod_fd_new (lua_State *L)
{
  gint64 value = lua_tointeger (L, 1);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_fd (value));
  return 1;
}

/* Rectangle */

static int
spa_pod_rectangle_new (lua_State *L)
{
  gint64 width = lua_tointeger (L, 1);
  gint64 height = lua_tointeger (L, 2);
  wplua_pushboxed (L, WP_TYPE_SPA_POD,
      wp_spa_pod_new_rectangle (width, height));
  return 1;
}

/* Fraction */

static int
spa_pod_fraction_new (lua_State *L)
{
  gint64 num = lua_tointeger (L, 1);
  gint64 denom = lua_tointeger (L, 2);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_new_fraction (num, denom));
  return 1;
}


/* Object */

static gboolean
object_add_property (WpSpaPodBuilder *b, WpSpaIdTable table,
    const gchar *key, lua_State *L, int idx)
{
  guint i;
  WpSpaIdValue prop_id = NULL;
  int ltype = lua_type (L, idx);

  if (ltype < 0 || ltype >= MAX_LUA_TYPES)
    return FALSE;

  /* Check if we can add primitive property directly from LUA type */
  prop_id = wp_spa_id_table_find_value_from_short_name (table, key);
  if (prop_id) {
    WpSpaType prop_type = wp_spa_id_value_get_value_type (prop_id, NULL);
    if (prop_type != WP_SPA_TYPE_INVALID) {
      for (i = 0; primitive_lua_types[i].primitive_type; i++) {
        const struct primitive_lua_type *t = primitive_lua_types + i;
        if (t->primitive_type == prop_type) {
          primitive_lua_add_func f = t->primitive_lua_add_funcs[ltype];
          if (f) {
            wp_spa_pod_builder_add_property (b, key);
            return f (b, prop_id, L, idx);
          }
        }
      }
    }
  }

  /* Otherwise just add pod property */
  if (lua_type (L, idx) == LUA_TUSERDATA) {
    wp_spa_pod_builder_add_property (b, key);
    return builder_add_lua_userdata (b, prop_id, L, idx);
  }

  return FALSE;
}

static int
spa_pod_object_new (lua_State *L)
{
  g_autoptr (WpSpaPodBuilder) builder = NULL;
  const gchar *fields[2] = { NULL, NULL };  // type_name, name_id
  WpSpaType object_type = WP_SPA_TYPE_INVALID;
  WpSpaIdTable table = NULL;

  luaL_checktype (L, 1, LUA_TTABLE);

  lua_geti (L, 1, 1);
  fields[0] = lua_tostring (L, -1);
  lua_geti (L, 1, 2);
  fields[1] = lua_tostring (L, -1);

  object_type = wp_spa_type_from_name (fields[0]);
  if (object_type == WP_SPA_TYPE_INVALID)
    luaL_error (L, "Invalid object type '%s'", fields[0]);

  table = wp_spa_type_get_values_table (object_type);
  if (!table)
    luaL_error (L, "Object type '%s' has incomplete type information",
        fields[0]);

  builder = wp_spa_pod_builder_new_object (fields[0], fields[1]);
  if (!builder)
    luaL_error (L, "Could not create pod object");

  lua_pop (L, 2);

  lua_pushnil(L);
  while (lua_next (L, -2)) {
    /* Remaining fields with string keys are the object properties */
    if (lua_type (L, -2) == LUA_TSTRING) {
      const gchar *key = lua_tostring (L, -2);
      if (!object_add_property (builder, table, key, L, -1))
        luaL_error (L, "Property '%s' could not be added", key);
    }

    lua_pop (L, 1);
  }

  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_builder_end (builder));
  return 1;
}

/* Struct */

static int
spa_pod_struct_new (lua_State *L)
{
  g_autoptr (WpSpaPodBuilder) builder = NULL;

  luaL_checktype (L, 1, LUA_TTABLE);

  builder = wp_spa_pod_builder_new_struct ();

  lua_pushnil (L);
  while (lua_next (L, 1)) {
    switch (lua_type (L, -1)) {
      case LUA_TBOOLEAN:
        wp_spa_pod_builder_add_boolean (builder, lua_toboolean (L, -1));
        break;
      case LUA_TNUMBER:
        if (lua_isinteger (L, -1))
          wp_spa_pod_builder_add_long (builder, lua_tointeger (L, -1));
        else
          wp_spa_pod_builder_add_double (builder, lua_tonumber (L, -1));
        break;
      case LUA_TSTRING:
        wp_spa_pod_builder_add_string (builder, lua_tostring (L, -1));
        break;
      case LUA_TUSERDATA: {
        WpSpaPod *pod = wplua_checkboxed (L, -1, WP_TYPE_SPA_POD);
        wp_spa_pod_builder_add_pod (builder, pod);
        break;
      }
      default:
        luaL_error (L, "Struct does not support lua type ",
            lua_typename(L, lua_type(L, -1)));
        break;
    }
    lua_pop (L, 1);
  }

  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_builder_end (builder));
  return 1;
}

/* Sequence */

static int
spa_pod_sequence_new (lua_State *L)
{
  g_autoptr (WpSpaPodBuilder) builder = NULL;

  luaL_checktype (L, 1, LUA_TTABLE);

  builder = wp_spa_pod_builder_new_sequence (0);

  lua_pushnil (L);
  while (lua_next (L, -2)) {
    guint32 offset = 0;
    const gchar *type_name = NULL;
    WpSpaPod *value = NULL;

    /* Read Control */
    if (lua_istable(L, -1)) {
      lua_pushnil (L);
      while (lua_next (L, -2)) {
        const gchar *key = lua_tostring (L, -2);
        if (g_strcmp0 (key, "offset") == 0) {
          offset = lua_tointeger (L, -1);
        } else if (!type_name && g_strcmp0 (key, "typename") == 0) {
          type_name = lua_tostring (L, -1);
        } else if (!value && g_strcmp0 (key, "value") == 0) {
          switch (lua_type (L, -1)) {
	    case LUA_TBOOLEAN:
	      value = wp_spa_pod_new_boolean (lua_toboolean (L, -1));
              break;
	    case LUA_TNUMBER:
              if (lua_isinteger (L, -1))
	        value = wp_spa_pod_new_long (lua_tointeger (L, -1));
	      else
                value = wp_spa_pod_new_double (lua_tonumber (L, -1));
	      break;
	    case LUA_TSTRING:
	      value = wp_spa_pod_new_string (lua_tostring (L, -1));
	      break;
	    case LUA_TUSERDATA: {
              value = wplua_checkboxed (L, -1, WP_TYPE_SPA_POD);
	      break;
	    }
	    default: {
              luaL_error (L, "Control value does not support lua type ",
	          lua_typename(L, lua_type(L, -1)));
	      value = NULL;
	      break;
	    }
	  }
        }
        lua_pop(L, 1);
      }
    }

    /* Add control */
    if (type_name && value) {
      wp_spa_pod_builder_add_control (builder, offset, type_name);
      wp_spa_pod_builder_add_pod (builder, value);
      wp_spa_pod_unref (value);
    }

    lua_pop(L, 1);
  }

  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_builder_end (builder));
  return 1;
}

/* Array */

static int
spa_pod_array_new (lua_State *L)
{
  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_array ();
  builder_add_table (L, builder);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_builder_end (builder));
  return 1;
}

/* Choice */

static int
spa_pod_choice_none_new (lua_State *L)
{
  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_choice ("None");
  builder_add_table (L, builder);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_builder_end (builder));
  return 1;
}

static int
spa_pod_choice_range_new (lua_State *L)
{
  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_choice ("Range");
  builder_add_table (L, builder);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_builder_end (builder));
  return 1;
}

static int
spa_pod_choice_step_new (lua_State *L)
{
  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_choice ("Step");
  builder_add_table (L, builder);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_builder_end (builder));
  return 1;
}

static int
spa_pod_choice_enum_new (lua_State *L)
{
  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_choice ("Enum");
  builder_add_table (L, builder);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_builder_end (builder));
  return 1;
}

static int
spa_pod_choice_flags_new (lua_State *L)
{
  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_choice ("Flags");
  builder_add_table (L, builder);
  wplua_pushboxed (L, WP_TYPE_SPA_POD, wp_spa_pod_builder_end (builder));
  return 1;
}

/* API */

static int
spa_pod_get_type_name (lua_State *L)
{
  WpSpaPod *pod = wplua_checkboxed (L, 1, WP_TYPE_SPA_POD);
  lua_pushstring (L, wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
  return 1;
}

static void
push_primitive_values (lua_State *L, WpSpaPod *pod, WpSpaType type,
    guint start_index, WpSpaIdTable idtable)
{
  g_auto (GValue) item = G_VALUE_INIT;
  g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (pod);
  for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
    gpointer p = g_value_get_pointer (&item);
    if (!p)
      continue;
    switch (type) {
    case SPA_TYPE_Bool:
      lua_pushboolean (L, *(gboolean *)p);
      break;
    case SPA_TYPE_Id: {
      WpSpaIdValue idval = NULL;
      if (idtable)
        idval = wp_spa_id_table_find_value (idtable, *(guint32 *)p);
      if (idval)
        lua_pushstring (L, wp_spa_id_value_short_name (idval));
      else
        lua_pushinteger (L, *(guint32 *)p);
      break;
    }
    case SPA_TYPE_Int:
      lua_pushinteger (L, *(gint *)p);
      break;
    case SPA_TYPE_Long:
      lua_pushinteger (L, *(long *)p);
      break;
    case SPA_TYPE_Float:
      lua_pushnumber (L, *(float *)p);
      break;
    case SPA_TYPE_Double:
      lua_pushnumber (L, *(double *)p);
      break;
    case SPA_TYPE_Fd:
      lua_pushnumber (L, *(gint64 *)p);
      break;
    default:
      continue;
    }
    lua_rawseti (L, -2, start_index++);
  }
}

static void
push_luapod (lua_State *L, WpSpaPod *pod, WpSpaIdValue field_idval)
{
  /* None */
  if (wp_spa_pod_is_none (pod)) {
    lua_pushnil (L);
  }

  /* Boolean */
  else if (wp_spa_pod_is_boolean (pod)) {
    gboolean value = FALSE;
    g_warn_if_fail (wp_spa_pod_get_boolean (pod, &value));
    lua_pushboolean (L, value);
  }

  /* Id */
  else if (wp_spa_pod_is_id (pod)) {
    guint32 value = 0;
    WpSpaIdTable idtable = NULL;
    WpSpaIdValue idval = NULL;
    g_warn_if_fail (wp_spa_pod_get_id (pod, &value));
    if (field_idval && SPA_TYPE_Id ==
            wp_spa_id_value_get_value_type (field_idval, &idtable)) {
      idval = wp_spa_id_table_find_value (idtable, value);
    }
    if (idval)
      lua_pushstring (L, wp_spa_id_value_short_name (idval));
    else
      lua_pushinteger (L, value);
  }

  /* Int */
  else if (wp_spa_pod_is_int (pod)) {
    gint value = 0;
    g_warn_if_fail (wp_spa_pod_get_int (pod, &value));
    lua_pushinteger (L, value);
  }

  /* Long */
  else if (wp_spa_pod_is_long (pod)) {
    gint64 value = 0;
    wp_spa_pod_get_long (pod, &value);
    lua_pushinteger (L, value);
  }

  /* Float */
  else if (wp_spa_pod_is_float (pod)) {
    float value = 0;
    g_warn_if_fail (wp_spa_pod_get_float (pod, &value));
    lua_pushnumber (L, value);
  }

  /* Double */
  else if (wp_spa_pod_is_double (pod)) {
    double value = 0;
    g_warn_if_fail (wp_spa_pod_get_double (pod, &value));
    lua_pushnumber (L, value);
  }

  /* String */
  else if (wp_spa_pod_is_string (pod)) {
    const gchar *value = NULL;
    g_warn_if_fail (wp_spa_pod_get_string (pod, &value));
    lua_pushstring (L, value);
  }

  /* Bytes */
  else if (wp_spa_pod_is_bytes (pod)) {
    gconstpointer value = NULL;
    guint32 size = 0;
    g_warn_if_fail (wp_spa_pod_get_bytes (pod, &value, &size));
    char str[size + 1];
    for (guint i = 0; i < size; i++)
      str[i] = ((const gchar *)value)[i];
    str[size] = '\0';
    lua_pushstring (L, str);
  }

  /* Pointer */
  else if (wp_spa_pod_is_pointer (pod)) {
    gconstpointer value = NULL;
    g_warn_if_fail (wp_spa_pod_get_pointer (pod, &value));
    if (!value)
      lua_pushnil (L);
    else
      lua_pushlightuserdata (L, (gpointer)value);
  }

  /* Fd */
  else if (wp_spa_pod_is_fd (pod)) {
    gint64 value = 0;
    g_warn_if_fail (wp_spa_pod_get_fd (pod, &value));
    lua_pushinteger (L, value);
  }

  /* Rectangle */
  else if (wp_spa_pod_is_rectangle (pod)) {
    guint32 width = 0, height = 0;
    g_warn_if_fail (wp_spa_pod_get_rectangle (pod, &width, &height));
    lua_newtable (L);
    lua_pushstring (L, "Rectangle");
    lua_setfield (L, -2, "pod_type");
    lua_pushinteger (L, width);
    lua_setfield (L, -2, "width");
    lua_pushinteger (L, height);
    lua_setfield (L, -2, "height");
  }

  /* Fraction */
  else if (wp_spa_pod_is_fraction (pod)) {
    guint32 num = 0, denom = 0;
    g_warn_if_fail (wp_spa_pod_get_fraction (pod, &num, &denom));
    lua_newtable (L);
    lua_pushstring (L, "Fraction");
    lua_setfield (L, -2, "pod_type");
    lua_pushinteger (L, num);
    lua_setfield (L, -2, "num");
    lua_pushinteger (L, denom);
    lua_setfield (L, -2, "denom");
  }

  /* Object */
  else if (wp_spa_pod_is_object (pod)) {
    WpSpaType type = wp_spa_pod_get_spa_type (pod);
    WpSpaIdTable values_table = wp_spa_type_get_values_table (type);
    const gchar *id_name = NULL;
    g_auto (GValue) item = G_VALUE_INIT;
    g_autoptr (WpIterator) it = NULL;
    g_warn_if_fail (wp_spa_pod_get_object (pod, &id_name, NULL));
    lua_newtable (L);
    lua_pushstring (L, "Object");
    lua_setfield (L, -2, "pod_type");
    lua_pushstring (L, id_name);
    lua_setfield (L, -2, "object_id");
    it = wp_spa_pod_new_iterator (pod);
    lua_newtable (L);
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaPod *prop = g_value_get_boxed (&item);
      const gchar *key = NULL;
      g_autoptr (WpSpaPod) val = NULL;
      //FIXME: this is suboptimal because _get_property() converts
      // the key to a short name and we convert it back
      g_warn_if_fail (wp_spa_pod_get_property (prop, &key, &val));
      if (key) {
        push_luapod (L, val,
            wp_spa_id_table_find_value_from_short_name (values_table, key));
        lua_setfield (L, -2, key);
      }
    }
    lua_setfield (L, -2, "properties");
  }

  /* Struct */
  else if (wp_spa_pod_is_struct (pod)) {
    g_auto (GValue) item = G_VALUE_INIT;
    g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (pod);
    guint i = 1;
    lua_newtable (L);
    lua_pushstring (L, "Struct");
    lua_setfield (L, -2, "pod_type");
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaPod *val = g_value_get_boxed (&item);
      push_luapod (L, val, NULL);
      lua_rawseti (L, -2, i++);
    }
  }

  /* Sequence */
  else if (wp_spa_pod_is_sequence (pod)) {
    g_auto (GValue) item = G_VALUE_INIT;
    g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (pod);
    guint i = 1;
    lua_newtable (L);
    lua_pushstring (L, "Sequence");
    lua_setfield (L, -2, "pod_type");
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaPod *control = g_value_get_boxed (&item);
      guint32 offset = 0;
      const char *type_name = NULL;
      g_autoptr (WpSpaPod) val = NULL;
      g_warn_if_fail (wp_spa_pod_get_control (control, &offset, &type_name,
          &val));
      lua_newtable (L);
      lua_pushinteger (L, offset);
      lua_setfield (L, -2, "offset");
      lua_pushstring (L, type_name);
      lua_setfield (L, -2, "typename");
      push_luapod (L, val, NULL);
      lua_setfield (L, -2, "value");
      lua_rawseti (L, -2, i++);
    }
  }

  /* Array */
  else if (wp_spa_pod_is_array (pod)) {
    g_autoptr (WpSpaPod) child = wp_spa_pod_get_array_child (pod);
    WpSpaType type = wp_spa_pod_get_spa_type (child);
    WpSpaIdTable idtable = NULL;
    if (field_idval && type == SPA_TYPE_Id && SPA_TYPE_Array ==
            wp_spa_id_value_get_value_type (field_idval, &idtable))
        wp_spa_id_value_array_get_item_type (field_idval, &idtable);
    lua_newtable (L);
    lua_pushstring (L, "Array");
    lua_setfield (L, -2, "pod_type");
    lua_pushstring (L, wp_spa_type_name (type));
    lua_setfield (L, -2, "value_type");
    push_primitive_values (L, pod, type, 1, idtable);
  }

  /* Choice */
  else if (wp_spa_pod_is_choice (pod)) {
    g_autoptr (WpSpaPod) child = wp_spa_pod_get_choice_child (pod);
    WpSpaType type = wp_spa_pod_get_spa_type (child);
    g_autofree const gchar *choice_type = g_strdup_printf ("Choice.%s",
        wp_spa_id_value_short_name (wp_spa_pod_get_choice_type (pod)));
    WpSpaIdTable idtable = NULL;
    if (field_idval && type == SPA_TYPE_Id)
      wp_spa_id_value_get_value_type (field_idval, &idtable);
    lua_newtable (L);
    lua_pushstring (L, choice_type);
    lua_setfield (L, -2, "pod_type");
    lua_pushstring (L, wp_spa_type_name (type));
    lua_setfield (L, -2, "value_type");
    push_primitive_values (L, pod, type, 1, idtable);
  }

  /* Error */
  else {
    luaL_error (L, "Unsupported pod type ",
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
  }
}

static int
spa_pod_parse (lua_State *L)
{
  WpSpaPod *pod = wplua_checkboxed (L, 1, WP_TYPE_SPA_POD);
  push_luapod (L, pod, NULL);
  return 1;
}

static int
spa_pod_fixate (lua_State *L)
{
  WpSpaPod *pod = wplua_checkboxed (L, 1, WP_TYPE_SPA_POD);
  gboolean res = wp_spa_pod_fixate (pod);
  lua_pushboolean (L, res);
  return 1;
}

static inline WpSpaPod *
_checkpod (lua_State *L, int n)
{
  return wplua_checkboxed (L, n, WP_TYPE_SPA_POD);
}

static int
spa_pod_filter (lua_State *L)
{
  WpSpaPod *pod = wplua_checkboxed (L, 1, WP_TYPE_SPA_POD);
  WpSpaPod *filter = luaL_opt (L, _checkpod, 2, NULL);
  WpSpaPod *result = wp_spa_pod_filter (pod, filter);
  if (result) {
    wplua_pushboxed (L, WP_TYPE_SPA_POD, result);
    return 1;
  }
  return 0;
}

static const luaL_Reg spa_pod_methods[] = {
  { "get_type_name", spa_pod_get_type_name },
  { "parse", spa_pod_parse },
  { "fixate", spa_pod_fixate },
  { "filter", spa_pod_filter },
  { NULL, NULL }
};

static const luaL_Reg spa_pod_constructors[] = {
  { "None", spa_pod_none_new },
  { "Boolean", spa_pod_boolean_new },
  { "Id", spa_pod_id_new },
  { "Int", spa_pod_int_new },
  { "Long", spa_pod_long_new },
  { "Float", spa_pod_float_new },
  { "Double", spa_pod_double_new },
  { "String", spa_pod_string_new },
  { "Bytes", spa_pod_bytes_new },
  { "Pointer", spa_pod_pointer_new },
  { "Fd", spa_pod_fd_new },
  { "Rectangle", spa_pod_rectangle_new },
  { "Fraction", spa_pod_fraction_new },
  { "Object", spa_pod_object_new },
  { "Struct", spa_pod_struct_new },
  { "Sequence", spa_pod_sequence_new },
  { "Array", spa_pod_array_new },
  { NULL, NULL }
};

static const luaL_Reg spa_pod_choice_constructors[] = {
  { "None", spa_pod_choice_none_new },
  { "Range", spa_pod_choice_range_new },
  { "Step", spa_pod_choice_step_new },
  { "Enum", spa_pod_choice_enum_new },
  { "Flags", spa_pod_choice_flags_new },
  { NULL, NULL }
};

/* Init */

void
wp_lua_scripting_pod_init (lua_State *L)
{
  luaL_newlib (L, spa_pod_constructors);
  luaL_newlib (L, spa_pod_choice_constructors);
  lua_setfield (L, -2, "Choice");
  lua_setglobal (L, "WpSpaPod");

  wplua_register_type_methods (L, WP_TYPE_SPA_POD, NULL, spa_pod_methods);
}
 070701000000DE000081A400000000000000000000000165F8630400001418000000000000000000000000000000000000003D00000000wireplumber-0.5.0/modules/module-lua-scripting/api/require.c  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <wplua/wplua.h>

#define WP_LOCAL_LOG_TOPIC log_topic_lua_scripting
WP_LOG_TOPIC_EXTERN (log_topic_lua_scripting)

struct _WpRequireApiTransition
{
  WpTransition parent;
  GPtrArray *apis;
  guint pending_plugins;
};

enum {
  STEP_LOAD_PLUGINS = WP_TRANSITION_STEP_CUSTOM_START,
};

G_DECLARE_FINAL_TYPE (WpRequireApiTransition, wp_require_api_transition,
                      WP, REQUIRE_API_TRANSITION, WpTransition)
G_DEFINE_TYPE (WpRequireApiTransition, wp_require_api_transition, WP_TYPE_TRANSITION)

static void
wp_require_api_transition_init (WpRequireApiTransition * self)
{
  self->apis = g_ptr_array_new_with_free_func (g_free);
}

static void
wp_require_api_transition_finalize (GObject * object)
{
  WpRequireApiTransition *self = WP_REQUIRE_API_TRANSITION (object);

  g_clear_pointer (&self->apis, g_ptr_array_unref);

  G_OBJECT_CLASS (wp_require_api_transition_parent_class)->finalize (object);
}

static guint
wp_require_api_transition_get_next_step (WpTransition * transition, guint step)
{
  WpRequireApiTransition *self = WP_REQUIRE_API_TRANSITION (transition);

  switch (step) {
  case WP_TRANSITION_STEP_NONE: return STEP_LOAD_PLUGINS;
  case STEP_LOAD_PLUGINS:
    return (self->pending_plugins > 0) ?
        STEP_LOAD_PLUGINS : WP_TRANSITION_STEP_NONE;
  default:
    g_return_val_if_reached (WP_TRANSITION_STEP_ERROR);
  }
}

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res,
    WpRequireApiTransition *self)
{
  GError *error = NULL;

  if (!wp_core_load_component_finish (core, res, &error)) {
    wp_transition_return_error (WP_TRANSITION (self), error);
    return;
  }

  --self->pending_plugins;
  wp_transition_advance (WP_TRANSITION (self));
}

static void
wp_require_api_transition_execute_step (WpTransition * transition, guint step)
{
  WpRequireApiTransition *self = WP_REQUIRE_API_TRANSITION (transition);
  WpCore *core = wp_transition_get_source_object (transition);

  switch (step) {
  case STEP_LOAD_PLUGINS:
    wp_debug_object (self, "Loading plugins...");

    for (guint i = 0; i < self->apis->len; i++) {
      const gchar *api_name = g_ptr_array_index (self->apis, i);
      g_autoptr (WpPlugin) plugin = wp_plugin_find (core, api_name);
      if (!plugin) {
        gchar module_name[50];
        g_snprintf (module_name, sizeof (module_name),
            "libwireplumber-module-%s", api_name);

        self->pending_plugins++;
        wp_core_load_component (core, module_name, "module", NULL, NULL, NULL,
            (GAsyncReadyCallback) on_plugin_loaded, self);
      }
    }
    wp_transition_advance (transition);
    break;

  case WP_TRANSITION_STEP_ERROR:
    break;

  default:
    g_assert_not_reached ();
  }
}

static void
wp_require_api_transition_class_init (WpRequireApiTransitionClass * klass)
{
  GObjectClass * object_class = (GObjectClass *) klass;
  WpTransitionClass * transition_class = (WpTransitionClass *) klass;

  object_class->finalize = wp_require_api_transition_finalize;

  transition_class->get_next_step = wp_require_api_transition_get_next_step;
  transition_class->execute_step = wp_require_api_transition_execute_step;
}

static void
on_require_api_transition_done (WpCore * core, GAsyncResult * res, gpointer data)
{
  g_autoptr (GClosure) closure = data;
  g_autoptr (GError) error = NULL;

  if (!wp_transition_finish (res, &error)) {
    wp_warning ("Core.require_api failed: %s", error->message);
    wp_core_idle_add (core, NULL, G_SOURCE_FUNC (core_disconnect), core, NULL);
    return;
  }

  WpRequireApiTransition *t = WP_REQUIRE_API_TRANSITION (res);
  g_autoptr (GArray) params = g_array_new (FALSE, TRUE, sizeof (GValue));

  g_array_set_clear_func (params, (GDestroyNotify) g_value_unset);
  g_array_set_size (params, t->apis->len);

  for (guint i = 0; i < t->apis->len; i++) {
    const gchar *api_name = g_ptr_array_index (t->apis, i);
    g_autoptr (WpPlugin) plugin = wp_plugin_find (core, api_name);
    g_value_init_from_instance (&g_array_index (params, GValue, i), plugin);
  }

  g_closure_invoke (closure, NULL,
      params->len, (const GValue *) params->data, NULL);
  g_closure_invalidate (closure);
}

static int
wp_require_api_transition_new_from_lua (lua_State *L, WpCore * core)
{
  int n_args = lua_gettop (L);

  wp_info("n_args = %d", n_args);

  for (int i = 1; i < n_args; i++)
    luaL_checktype (L, i, LUA_TSTRING);
  luaL_checktype (L, n_args, LUA_TFUNCTION);

  GClosure *closure = wplua_function_to_closure (L, n_args);
  g_closure_ref (closure);
  g_closure_sink (closure);

  WpRequireApiTransition *t = (WpRequireApiTransition *)
      wp_transition_new (wp_require_api_transition_get_type (), core, NULL,
          (GAsyncReadyCallback) on_require_api_transition_done, closure);

  for (int i = 1; i < n_args; i++) {
    const char * api_name = lua_tostring (L, i);
    g_ptr_array_add (t->apis, g_strdup_printf ("%s-api", api_name));
  }

  wp_transition_advance (WP_TRANSITION (t));
  return 0;
}
070701000000DF000081A400000000000000000000000165F863040000004C000000000000000000000000000000000000003B00000000wireplumber-0.5.0/modules/module-lua-scripting/meson.build    wplua_include_dir = include_directories('.')

subdir('wplua')
subdir('api')
070701000000E0000081A400000000000000000000000165F8630400001958000000000000000000000000000000000000003800000000wireplumber-0.5.0/modules/module-lua-scripting/module.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <wplua/wplua.h>
#include <pipewire/keys.h>

#include "script.h"

#define WP_LOCAL_LOG_TOPIC log_topic_lua_scripting
WP_LOG_TOPIC (log_topic_lua_scripting, "m-lua-scripting")

void wp_lua_scripting_api_init (lua_State *L);

struct _WpLuaScriptingPlugin
{
  WpPlugin parent;
  lua_State *L;
};

static int
wp_lua_scripting_package_loader (lua_State *L)
{
  luaL_checktype (L, 2, LUA_TFUNCTION);
  wplua_push_sandbox (L);
  lua_pushvalue (L, 2);
  lua_call (L, 1, 1);
  return 1;
}

static int
wp_lua_scripting_package_searcher (lua_State *L)
{
  const gchar *name = luaL_checkstring (L, 1);
  g_autoptr (GError) error = NULL;
  g_autofree gchar *filename = g_strdup_printf ("%s.lua", name);
  g_autofree gchar *script =
      wp_base_dirs_find_file (WP_BASE_DIRS_DATA, "scripts/lib", filename);

  if (!script)  {
    lua_pushliteral (L, "script not found");
    return 1;
  }

  /* 1. loader (function) */
  lua_pushcfunction (L, wp_lua_scripting_package_loader);

  /* 2. loader data (param to 1) */
  wp_debug ("Executing script %s", script);
  if (!wplua_load_path (L, script, &error)) {
    lua_pop (L, 1);
    lua_pushstring (L, error->message);
    return 1;
  }

  /* 3. script path */
  lua_pushstring (L, script);
  return 3;
}

static void
wp_lua_scripting_enable_package_searcher (lua_State *L)
{
  /* table.insert(package.searchers, 2, wp_lua_scripting_package_searcher) */
  lua_getglobal (L, "table");
  lua_getfield (L, -1, "insert");
  lua_remove (L, -2);
  lua_getglobal (L, "package");
  lua_getfield (L, -1, "searchers");
  lua_remove (L, -2);
  lua_pushinteger (L, 2);
  lua_pushcfunction (L, wp_lua_scripting_package_searcher);
  lua_call (L, 3, 0);
}

static void wp_lua_scripting_component_loader_init (WpComponentLoaderInterface * iface);

G_DECLARE_FINAL_TYPE (WpLuaScriptingPlugin, wp_lua_scripting_plugin,
                      WP, LUA_SCRIPTING_PLUGIN, WpPlugin)
G_DEFINE_TYPE_WITH_CODE (WpLuaScriptingPlugin, wp_lua_scripting_plugin,
                         WP_TYPE_PLUGIN, G_IMPLEMENT_INTERFACE (
                            WP_TYPE_COMPONENT_LOADER,
                            wp_lua_scripting_component_loader_init))

static void
wp_lua_scripting_plugin_init (WpLuaScriptingPlugin * self)
{
}

static void
wp_lua_scripting_plugin_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (plugin);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));

  /* init lua engine */
  self->L = wplua_new ();

  lua_pushliteral (self->L, "wireplumber_core");
  lua_pushlightuserdata (self->L, core);
  lua_settable (self->L, LUA_REGISTRYINDEX);

  wp_lua_scripting_api_init (self->L);
  wp_lua_scripting_enable_package_searcher (self->L);
  wplua_enable_sandbox (self->L, WP_LUA_SANDBOX_ISOLATE_ENV);

  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
wp_lua_scripting_plugin_disable (WpPlugin * plugin)
{
  WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (plugin);

  g_clear_pointer (&self->L, wplua_unref);
}

static gboolean
wp_lua_scripting_plugin_supports_type (WpComponentLoader * cl,
    const gchar * type)
{
  return g_str_equal (type, "script/lua");
}

static gchar *
find_script (const gchar * script, WpCore *core)
{
  g_autoptr (WpProperties) p = wp_core_get_properties (core);
  const gchar *str = wp_properties_get (p, "wireplumber.daemon");
  gboolean daemon = !g_strcmp0 (str, "true");

  if ((!daemon || g_path_is_absolute (script)) &&
      g_file_test (script, G_FILE_TEST_IS_REGULAR))
    return g_strdup (script);

  return wp_base_dirs_find_file (WP_BASE_DIRS_DATA, "scripts", script);
}

static void
wp_lua_scripting_plugin_load (WpComponentLoader * cl, WpCore * core,
    const gchar * component, const gchar * type, WpSpaJson * args,
    GCancellable * cancellable, GAsyncReadyCallback callback, gpointer data)
{
  WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (cl);
  g_autoptr (GTask) task = task = g_task_new (self, cancellable, callback, data);
  g_autofree gchar *filepath = NULL;
  g_autofree gchar *pluginname = NULL;
  g_autoptr (WpPlugin) script = NULL;

  g_task_set_source_tag (task, wp_lua_scripting_plugin_load);

  /* make sure the component loader is activated */
  if (!self->L) {
    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
        "Lua script component loader cannot load Lua scripts if not enabled");
    return;
  }

  /* make sure the type is supported */
  if (!g_str_equal (type, "script/lua")) {
    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
        "Could not load script '%s' as its type is not 'script/lua'",
        component);
    return;
  }

  /* find the script */
  filepath = find_script (component, core);
  if (!filepath) {
    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
        "Could not locate script '%s'", component);
    return;
  }

  pluginname = g_strdup_printf ("script:%s", component);

  script = g_object_new (WP_TYPE_LUA_SCRIPT,
      "core", core,
      "name", pluginname,
      "lua-engine", self->L,
      "filename", filepath,
      "arguments", args,
      NULL);

  g_task_return_pointer (task, g_steal_pointer (&script), g_object_unref);
}

static GObject *
wp_lua_scripting_plugin_load_finish (WpComponentLoader * self,
    GAsyncResult * res, GError ** error)
{
  g_return_val_if_fail (
    g_async_result_is_tagged (res, wp_lua_scripting_plugin_load), NULL);

  return g_task_propagate_pointer (G_TASK (res), error);
}

static void
wp_lua_scripting_plugin_class_init (WpLuaScriptingPluginClass * klass)
{
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  plugin_class->enable = wp_lua_scripting_plugin_enable;
  plugin_class->disable = wp_lua_scripting_plugin_disable;
}

static void
wp_lua_scripting_component_loader_init (WpComponentLoaderInterface * iface)
{
  iface->supports_type = wp_lua_scripting_plugin_supports_type;
  iface->load = wp_lua_scripting_plugin_load;
  iface->load_finish = wp_lua_scripting_plugin_load_finish;
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (g_object_new (wp_lua_scripting_plugin_get_type (),
      "name", "lua-scripting",
      "core", core,
      NULL));
}
070701000000E1000081A400000000000000000000000165F8630400001F54000000000000000000000000000000000000003800000000wireplumber-0.5.0/modules/module-lua-scripting/script.c   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "script.h"
#include <pipewire/keys.h>

#define WP_LOCAL_LOG_TOPIC log_topic_lua_scripting
WP_LOG_TOPIC_EXTERN (log_topic_lua_scripting)

/*
 * This is a WpPlugin subclass that wraps a single lua script and acts like
 * a handle for that script. When enabled, through the WpObject activation
 * mechanism, the script is executed. It then provides an API for the script
 * to declare when it has finished its activation procedure, which can be
 * asynchronous (this is Script.finish_activation in Lua).
 * When disabled, this class destroys the global environment that was used
 * in the Lua engine for excecuting that script, effectively destroying all
 * objects that were held in Lua as global variables.
 */

struct _WpLuaScript
{
  WpPlugin parent;

  lua_State *L;
  gchar *filename;
  WpSpaJson *args;
};

enum {
  PROP_0,
  PROP_LUA_ENGINE,
  PROP_FILENAME,
  PROP_ARGUMENTS,
};

G_DEFINE_TYPE (WpLuaScript, wp_lua_script, WP_TYPE_PLUGIN)

static void
wp_lua_script_init (WpLuaScript * self)
{
}

static void
wp_lua_script_cleanup (WpLuaScript * self)
{
  /* LUA_REGISTRYINDEX[self] = nil */
  if (self->L) {
    lua_pushnil (self->L);
    lua_rawsetp (self->L, LUA_REGISTRYINDEX, self);
  }
}

static void
wp_lua_script_finalize (GObject * object)
{
  WpLuaScript *self = WP_LUA_SCRIPT (object);

  wp_lua_script_cleanup (self);
  g_clear_pointer (&self->L, wplua_unref);
  g_clear_pointer (&self->filename, g_free);
  g_clear_pointer (&self->args, wp_spa_json_unref);

  G_OBJECT_CLASS (wp_lua_script_parent_class)->finalize (object);
}

static void
wp_lua_script_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpLuaScript *self = WP_LUA_SCRIPT (object);

  switch (property_id) {
  case PROP_LUA_ENGINE:
    g_return_if_fail (self->L == NULL);
    self->L = g_value_get_pointer (value);
    if (self->L)
      self->L = wplua_ref (self->L);
    break;
  case PROP_FILENAME:
    self->filename = g_value_dup_string (value);
    break;
  case PROP_ARGUMENTS:
    self->args = g_value_dup_boxed (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static gboolean
wp_lua_script_check_async_activation (WpLuaScript * self)
{
  gboolean ret;
  lua_rawgetp (self->L, LUA_REGISTRYINDEX, self);
  lua_pushliteral (self->L, "Script");
  lua_gettable (self->L, -2);
  lua_pushliteral (self->L, "async_activation");
  lua_gettable (self->L, -2);
  ret = lua_toboolean (self->L, -1);
  lua_pop (self->L, 3);
  return ret;
}

static void
wp_lua_script_detach_transition (WpLuaScript * self)
{
  lua_rawgetp (self->L, LUA_REGISTRYINDEX, self);
  lua_pushliteral (self->L, "Script");
  lua_gettable (self->L, -2);
  lua_pushliteral (self->L, "__transition");
  lua_pushnil (self->L);
  lua_settable (self->L, -3);
  lua_pop (self->L, 2);
}

static int
script_finish_activation (lua_State * L)
{
  WpLuaScript *self;

  luaL_checktype (L, 1, LUA_TTABLE);

  lua_pushliteral (L, "__self");
  lua_gettable (L, 1);
  luaL_checktype (L, -1, LUA_TLIGHTUSERDATA);
  self = WP_LUA_SCRIPT ((gpointer) lua_topointer (L, -1));
  lua_pop (L, 2);

  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
  return 0;
}

static int
script_finish_activation_with_error (lua_State * L)
{
  WpTransition *transition = NULL;
  const char *msg = NULL;

  luaL_checktype (L, 1, LUA_TTABLE);
  msg = luaL_checkstring (L, 2);

  lua_pushliteral (L, "__transition");
  lua_gettable (L, 1);
  if (lua_type (L, -1) == LUA_TLIGHTUSERDATA)
    transition = WP_TRANSITION ((gpointer) lua_topointer (L, -1));
  lua_pop (L, 2);

  if (transition)
    wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_OPERATION_FAILED, "%s", msg));
  return 0;
}

static const luaL_Reg script_api_methods[] = {
  { "finish_activation", script_finish_activation },
  { "finish_activation_with_error", script_finish_activation_with_error },
  { NULL, NULL }
};

static int
wp_lua_script_sandbox (lua_State *L)
{
  luaL_checktype (L, 1, LUA_TLIGHTUSERDATA); // self
  luaL_checktype (L, 2, LUA_TLIGHTUSERDATA); // transition
  luaL_checktype (L, 3, LUA_TFUNCTION); // the script chunk

  /* create unique environment for this script */
  lua_getglobal (L, "create_sandbox_env");
  lua_call (L, 0, 1);

  /* create "Script" API */
  lua_pushliteral (L, "Script");
  luaL_newlib (L, script_api_methods);
  lua_pushliteral (L, "__self");
  lua_pushvalue (L, 1);
  lua_settable (L, -3);
  lua_pushliteral (L, "__transition");
  lua_pushvalue (L, 2);
  lua_settable (L, -3);
  lua_settable (L, -3);

  /* store the environment */
  /* LUA_REGISTRYINDEX[self] = env */
  lua_pushvalue (L, 1); // self
  lua_pushvalue (L, -2); // the table returned by create_sandbox_env
  lua_rawset (L, LUA_REGISTRYINDEX);

  /* set it as the 1st upvalue (_ENV) on the loaded script chunk (at index 3) */
  lua_setupvalue (L, 3, 1);

  /* anything remaining on the stack are function arguments */
  int nargs = lua_gettop (L) - 3;

  /* execute script */
  lua_call (L, nargs, 0);
  return 0;
}

static void
wp_lua_script_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpLuaScript *self = WP_LUA_SCRIPT (plugin);
  g_autoptr (GError) error = NULL;
  int top, nargs = 3;

  if (!self->L) {
    error = g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
        "No lua state open; lua-scripting plugin is not enabled");
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  top = lua_gettop (self->L);
  lua_pushcfunction (self->L, wp_lua_script_sandbox);
  lua_pushlightuserdata (self->L, self);
  lua_pushlightuserdata (self->L, transition);

  /* load script */
  if (!wplua_load_path (self->L, self->filename, &error)) {
    lua_settop (self->L, top);
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  /* push script arguments */
  if (self->args) {
    wplua_pushboxed (self->L, WP_TYPE_SPA_JSON, wp_spa_json_ref (self->args));
    nargs++;
  }

  /* execute script */
  if (!wplua_pcall (self->L, nargs, 0, &error)) {
    lua_settop (self->L, top);
    wp_transition_return_error (transition, g_steal_pointer (&error));
    wp_lua_script_cleanup (self);
    return;
  }

  if (!wp_lua_script_check_async_activation (self)) {
    wp_lua_script_detach_transition (self);
    wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
  } else {
    g_signal_connect_object (transition, "notify::completed",
        (GCallback) wp_lua_script_detach_transition, self, G_CONNECT_SWAPPED);
  }

  lua_settop (self->L, top);
}

static void
wp_lua_script_disable (WpPlugin * plugin)
{
  WpLuaScript *self = WP_LUA_SCRIPT (plugin);
  wp_lua_script_cleanup (self);
}

static void
wp_lua_script_class_init (WpLuaScriptClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  object_class->finalize = wp_lua_script_finalize;
  object_class->set_property = wp_lua_script_set_property;

  plugin_class->enable = wp_lua_script_enable;
  plugin_class->disable = wp_lua_script_disable;

  g_object_class_install_property (object_class, PROP_LUA_ENGINE,
      g_param_spec_pointer ("lua-engine", "lua-engine", "lua-engine",
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_FILENAME,
      g_param_spec_string ("filename", "filename", "filename", NULL,
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_ARGUMENTS,
      g_param_spec_boxed ("arguments", "arguments", "arguments",
          WP_TYPE_SPA_JSON,
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}
070701000000E2000081A400000000000000000000000165F86304000001CB000000000000000000000000000000000000003800000000wireplumber-0.5.0/modules/module-lua-scripting/script.h   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __MODULE_LUA_SCRIPTING_SCRIPT_H__
#define __MODULE_LUA_SCRIPTING_SCRIPT_H__

#include <wp/wp.h>
#include <wplua/wplua.h>

G_BEGIN_DECLS

#define WP_TYPE_LUA_SCRIPT (wp_lua_script_get_type ())
G_DECLARE_FINAL_TYPE (WpLuaScript, wp_lua_script, WP, LUA_SCRIPT, WpPlugin)

G_END_DECLS

#endif
 070701000000E3000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000003500000000wireplumber-0.5.0/modules/module-lua-scripting/wplua  070701000000E4000081A400000000000000000000000165F8630400000AB1000000000000000000000000000000000000003D00000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/boxed.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "wplua.h"
#include "private.h"
#include <wp/wp.h>

static lua_CFunction
find_method_in_luaL_Reg (luaL_Reg *reg, const gchar *method)
{
  if (reg) {
    while (reg->name) {
      if (!g_strcmp0 (method, reg->name))
        return reg->func;
      reg++;
    }
  }
  return NULL;
}

static int
_wplua_gboxed___index (lua_State *L)
{
  luaL_argcheck (L, wplua_isboxed (L, 1, G_TYPE_BOXED), 1,
      "expected userdata storing GValue<GBoxed>");
  GValue *obj_v = lua_touserdata (L, 1);
  const gchar *key = luaL_checkstring (L, 2);
  GType type = G_VALUE_TYPE (obj_v);
  lua_CFunction func = NULL;
  GHashTable *vtables;

  lua_pushliteral (L, "wplua_vtables");
  lua_gettable (L, LUA_REGISTRYINDEX);
  vtables = wplua_toboxed (L, -1);
  lua_pop (L, 1);

  /* search in registered vtables */
  while (!func && type) {
    luaL_Reg *reg = g_hash_table_lookup (vtables, GUINT_TO_POINTER (type));
    func = find_method_in_luaL_Reg (reg, key);
    type = g_type_parent (type);
  }

  wp_trace_boxed (type, g_value_get_boxed (obj_v),
      "indexing GBoxed, looking for '%s', found: %p", key, func);

  if (func) {
    lua_pushcfunction (L, func);
    return 1;
  }
  return 0;
}

void
_wplua_init_gboxed (lua_State *L)
{
  static const luaL_Reg gboxed_meta[] = {
    { "__gc", _wplua_gvalue_userdata___gc },
    { "__eq", _wplua_gvalue_userdata___eq },
    { "__index", _wplua_gboxed___index },
    { NULL, NULL }
  };

  luaL_newmetatable (L, "GBoxed");
  luaL_setfuncs (L, gboxed_meta, 0);
  lua_pop (L, 1);
}

void
wplua_pushboxed (lua_State * L, GType type, gpointer object)
{
  g_return_if_fail (G_TYPE_FUNDAMENTAL (type) == G_TYPE_BOXED);

  GValue *v = _wplua_pushgvalue_userdata (L, type);
  wp_trace_boxed (type, object, "pushing to Lua, v=%p", v);
  g_value_take_boxed (v, object);

  luaL_getmetatable (L, "GBoxed");
  lua_setmetatable (L, -2);
}

gpointer
wplua_toboxed (lua_State *L, int idx)
{
  g_return_val_if_fail (_wplua_isgvalue_userdata (L, idx, G_TYPE_BOXED), NULL);
  return g_value_get_boxed ((GValue *) lua_touserdata (L, idx));
}

gpointer
wplua_checkboxed (lua_State *L, int idx, GType type)
{
  if (G_UNLIKELY (!_wplua_isgvalue_userdata (L, idx, type))) {
    wp_critical ("expected userdata storing GValue<%s>", g_type_name (type));
    luaL_argerror (L, idx, "expected userdata storing GValue<GBoxed>");
  }
  return g_value_get_boxed ((GValue *) lua_touserdata (L, idx));
}

gboolean
wplua_isboxed (lua_State *L, int idx, GType type)
{
  if (!g_type_is_a (type, G_TYPE_BOXED)) return FALSE;
  return _wplua_isgvalue_userdata (L, idx, type);
}
   070701000000E5000081A400000000000000000000000165F8630400001288000000000000000000000000000000000000003F00000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/closure.c    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "wplua.h"
#include "private.h"
#include <wp/wp.h>

/* This structure is added to a lua global and it's only referenced from there;
   When the lua_State closes, it is unrefed and its finalize function below
   invalidates all the closures so that nothing attempts to call into a lua
   function after the state is closed */
typedef struct _WpLuaClosureStore WpLuaClosureStore;
struct _WpLuaClosureStore
{
  GPtrArray *closures;
};

static WpLuaClosureStore *
_wplua_closure_store_new (void)
{
  WpLuaClosureStore *self = g_rc_box_new (WpLuaClosureStore);
  self->closures = g_ptr_array_new ();
  return self;
}

static void
_wplua_closure_store_finalize (WpLuaClosureStore * self)
{
  for (guint i = self->closures->len; i > 0; i--) {
    GClosure *c = g_ptr_array_index (self->closures, i-1);
    g_closure_ref (c);
    g_closure_invalidate (c);
    g_ptr_array_remove_index_fast (self->closures, i-1);
    g_closure_unref (c);
  }
  g_ptr_array_unref (self->closures);
}

static WpLuaClosureStore *
_wplua_closure_store_ref (WpLuaClosureStore * self)
{
  return g_rc_box_acquire (self);
}

static void
_wplua_closure_store_unref (WpLuaClosureStore * self)
{
  g_rc_box_release_full (self, (GDestroyNotify) _wplua_closure_store_finalize);
}

G_DEFINE_BOXED_TYPE(WpLuaClosureStore, _wplua_closure_store,
    _wplua_closure_store_ref, _wplua_closure_store_unref)


typedef struct _WpLuaClosure WpLuaClosure;
struct _WpLuaClosure
{
  GClosure closure;
  int func_ref;
  GPtrArray *closures;
};

static void
_wplua_closure_marshal (GClosure *closure, GValue *return_value,
    guint n_param_values, const GValue *param_values,
    gpointer invocation_hint, gpointer marshal_data)
{
  static int reentrant = 0;
  lua_State *L = closure->data;
  int func_ref = ((WpLuaClosure *) closure)->func_ref;

  /* invalid closure, skip it */
  if (func_ref == LUA_NOREF || func_ref == LUA_REFNIL)
    return;

  /* stop the garbage collector */
  if (reentrant == 0)
    lua_gc (L, LUA_GCSTOP, 0);

  /* push the function */
  lua_rawgeti (L, LUA_REGISTRYINDEX, func_ref);

  /* push arguments */
  for (guint i = 0; i < n_param_values; i++)
    wplua_gvalue_to_lua (L, &param_values[i]);

  /* call in protected mode */
  reentrant++;
  int res = _wplua_pcall (L, n_param_values, return_value ? 1 : 0);
  reentrant--;

  /* handle the result */
  if (res == LUA_OK && return_value) {
    wplua_lua_to_gvalue (L, -1, return_value);
    lua_pop (L, 1);
  }

  /* clean up */
  lua_gc (L, LUA_GCCOLLECT, 0);
  if (reentrant == 0)
    lua_gc (L, LUA_GCRESTART, 0);
}

static void
_wplua_closure_invalidate (lua_State *L, WpLuaClosure *c)
{
  wp_trace_boxed (G_TYPE_CLOSURE, c, "invalidated");
  luaL_unref (L, LUA_REGISTRYINDEX, c->func_ref);
  c->func_ref = LUA_NOREF;
}

static void
_wplua_closure_finalize (lua_State *L, WpLuaClosure *c)
{
  g_ptr_array_remove_fast (c->closures, c);
  g_ptr_array_unref (c->closures);
}

GClosure *
wplua_checkclosure (lua_State *L, int idx)
{
  luaL_checktype (L, idx, LUA_TFUNCTION);
  return wplua_function_to_closure (L, idx);
}

/**
 * wplua_function_to_closure:
 *
 * Make a GClosure out of a Lua function at index @em idx
 *
 * Returns: (transfer floating): the new closure
 */
GClosure *
wplua_function_to_closure (lua_State *L, int idx)
{
  g_return_val_if_fail (lua_isfunction(L, idx), NULL);

  GClosure *c = g_closure_new_simple (sizeof (WpLuaClosure), L);
  WpLuaClosure *wlc = (WpLuaClosure *) c;
  WpLuaClosureStore *store;

  lua_pushvalue (L, idx);
  wlc->func_ref = luaL_ref (L, LUA_REGISTRYINDEX);

  wp_trace_boxed (G_TYPE_CLOSURE, c, "created, func_ref = %d", wlc->func_ref);

  g_closure_set_marshal (c, _wplua_closure_marshal);
  g_closure_add_invalidate_notifier (c, L,
      (GClosureNotify) _wplua_closure_invalidate);
  g_closure_add_finalize_notifier (c, L,
      (GClosureNotify) _wplua_closure_finalize);

  /* keep a weak ref of the closure in the store's array,
     so that we can invalidate the closure when lua_State closes;
     keep a strong ref of the array in the closure so that
     _wplua_closure_finalize() works even after the state is closed */
  lua_pushliteral (L, "wplua_closures");
  lua_gettable (L, LUA_REGISTRYINDEX);
  store = wplua_toboxed (L, -1);
  lua_pop (L, 1);

  g_ptr_array_add (store->closures, c);
  wlc->closures = g_ptr_array_ref (store->closures);

  return c;
}

void
_wplua_init_closure (lua_State *L)
{
  lua_pushliteral (L, "wplua_closures");
  wplua_pushboxed (L,
      _wplua_closure_store_get_type (),
      _wplua_closure_store_new ());
  lua_settable (L, LUA_REGISTRYINDEX);
}
070701000000E6000081A400000000000000000000000165F86304000000C4000000000000000000000000000000000000004300000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/gresource.xml    <?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/org/freedesktop/pipewire/wireplumber/wplua/">
    <file compressed="true">sandbox.lua</file>
  </gresource>
</gresources>
070701000000E7000081A400000000000000000000000165F863040000027F000000000000000000000000000000000000004100000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/meson.build  wplua_lib_sources = [
  'boxed.c',
  'closure.c',
  'object.c',
  'userdata.c',
  'value.c',
  'wplua.c',
]

wplua_resources = gnome.compile_resources(
    'wplua-resources',
    'gresource.xml',
    c_name: '_wplua',
    extra_args: '--manual-register',
    source_dir: meson.current_source_dir())

wplua_lib = static_library('wplua-' + wireplumber_api_version,
  [ wplua_lib_sources, wplua_resources ],
  install: false,
  include_directories: wplua_include_dir,
  dependencies : [wp_dep, lua_dep],
)

wplua_dep = declare_dependency(
  link_with: wplua_lib,
  include_directories: wplua_include_dir,
  dependencies: [wp_dep, lua_dep],
)
 070701000000E8000081A400000000000000000000000165F8630400001A4F000000000000000000000000000000000000003E00000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/object.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "wplua.h"
#include "private.h"
#include <wp/wp.h>

static int
_wplua_gobject_call (lua_State *L)
{
  GObject *obj = wplua_checkobject (L, 1, G_TYPE_OBJECT);
  const char *sig_name = lua_tostring (L, 2);
  guint n_params = lua_gettop (L) - 2;
  GSignalQuery query;
  guint sig_id = 0;
  GQuark detail = 0;

  if (G_UNLIKELY (!g_signal_parse_name (sig_name, G_TYPE_FROM_INSTANCE (obj),
                                        &sig_id, &detail, FALSE)))
    luaL_error (L, "unknown signal '%s::%s'", G_OBJECT_TYPE_NAME (obj),
        sig_name);

  g_signal_query (sig_id, &query);

  if (G_UNLIKELY (!(query.signal_flags & G_SIGNAL_ACTION)))
    luaL_error (L, "lua code is not allowed to emit non-action signal '%s::%s'",
        G_OBJECT_TYPE_NAME (obj), sig_name);

  if (G_UNLIKELY (query.n_params > n_params))
    luaL_error (L, "not enough arguments for '%s::%s': expected %d, got %d",
        G_OBJECT_TYPE_NAME (obj), sig_name, query.n_params, n_params);

  GValue ret = G_VALUE_INIT;
  GValue *vals = g_newa (GValue, n_params + 1);
  memset (vals, 0, sizeof (GValue) * (n_params + 1));

  if (query.return_type != G_TYPE_NONE)
    g_value_init (&ret, query.return_type);

  g_value_init_from_instance (&vals[0], obj);
  for (guint i = 0; i < n_params; i++) {
    g_value_init (&vals[i+1], query.param_types[i]);
    wplua_lua_to_gvalue (L, i+3, &vals[i+1]);
  }

  g_signal_emitv (vals, sig_id, detail, &ret);

  for (guint i = 0; i < n_params + 1; i++) {
    g_value_unset (&vals[i]);
  }

  int n_ret = 0;
  if (query.return_type != G_TYPE_NONE)
    n_ret = wplua_gvalue_to_lua (L, &ret);

  g_value_unset (&ret);
  return n_ret;
}

static int
_wplua_gobject_connect (lua_State *L)
{
  GObject *obj = wplua_checkobject (L, 1, G_TYPE_OBJECT);
  const char *sig_name = luaL_checkstring (L, 2);
  luaL_checktype (L, 3, LUA_TFUNCTION);

  guint sig_id = 0;
  GQuark detail = 0;

  if (G_UNLIKELY (!g_signal_parse_name (sig_name, G_TYPE_FROM_INSTANCE (obj),
                                        &sig_id, &detail, FALSE)))
    luaL_error (L, "unknown signal '%s::%s'", G_OBJECT_TYPE_NAME (obj),
        sig_name);

  GClosure *closure = wplua_function_to_closure (L, 3);
  gulong handler =
      g_signal_connect_closure_by_id (obj, sig_id, detail, closure, FALSE);

  lua_pushinteger (L, handler);
  return 1;
}

static lua_CFunction
find_method_in_luaL_Reg (luaL_Reg *reg, const gchar *method)
{
  if (reg) {
    while (reg->name) {
      if (!g_strcmp0 (method, reg->name))
        return reg->func;
      reg++;
    }
  }
  return NULL;
}

static int
_wplua_gobject___index (lua_State *L)
{
  GObject *obj = wplua_checkobject (L, 1, G_TYPE_OBJECT);
  const gchar *key = luaL_checkstring (L, 2);
  lua_CFunction func = NULL;
  GHashTable *vtables;

  lua_pushliteral (L, "wplua_vtables");
  lua_gettable (L, LUA_REGISTRYINDEX);
  vtables = wplua_toboxed (L, -1);
  lua_pop (L, 1);

  if (!g_strcmp0 (key, "call"))
    func = _wplua_gobject_call;
  else if (!g_strcmp0 (key, "connect"))
    func = _wplua_gobject_connect;

  /* search in registered vtables */
  if (!func) {
    GType type = G_TYPE_FROM_INSTANCE (obj);
    while (!func && type) {
      luaL_Reg *reg = g_hash_table_lookup (vtables, GUINT_TO_POINTER (type));
      func = find_method_in_luaL_Reg (reg, key);
      type = g_type_parent (type);
    }
  }

  /* search in registered vtables of interfaces */
  if (!func) {
    g_autofree GType *interfaces =
        g_type_interfaces (G_TYPE_FROM_INSTANCE (obj), NULL);
    GType *type = interfaces;
    while (!func && *type) {
      luaL_Reg *reg = g_hash_table_lookup (vtables, GUINT_TO_POINTER (*type));
      func = find_method_in_luaL_Reg (reg, key);
      type++;
    }
  }

  if (func) {
    lua_pushcfunction (L, func);
    return 1;
  }
  else {
    /* search in properties */
    GObjectClass *klass = G_OBJECT_GET_CLASS (obj);
    GParamSpec *pspec = g_object_class_find_property (klass, key);
    if (pspec && (pspec->flags & G_PARAM_READABLE)) {
      g_auto (GValue) v = G_VALUE_INIT;
      g_value_init (&v, pspec->value_type);
      g_object_get_property (obj, key, &v);
      return wplua_gvalue_to_lua (L, &v);
    }
  }

  return 0;
}

static int
_wplua_gobject___newindex (lua_State *L)
{
  GObject *obj = wplua_checkobject (L, 1, G_TYPE_OBJECT);
  const gchar *key = luaL_checkstring (L, 2);

  /* search in properties */
  GObjectClass *klass = G_OBJECT_GET_CLASS (obj);
  GParamSpec *pspec = g_object_class_find_property (klass, key);
  if (pspec && (pspec->flags & G_PARAM_WRITABLE)) {
    g_auto (GValue) v = G_VALUE_INIT;
    g_value_init (&v, pspec->value_type);
    wplua_lua_to_gvalue (L, 3, &v);
    g_object_set_property (obj, key, &v);
  } else {
    luaL_error (L, "attempted to assign unknown or non-writable property '%s'",
        key);
  }
  return 0;
}

static int
_wplua_gobject__tostring (lua_State *L)
{
  GObject *obj;
  gchar *str;

  obj = wplua_checkobject (L, 1, G_TYPE_OBJECT);
  str = g_strdup_printf (WP_OBJECT_FORMAT, WP_OBJECT_ARGS (obj));
  lua_pushstring (L, str);
  g_free (str);
  return 1;
}

void
_wplua_init_gobject (lua_State *L)
{
  static const luaL_Reg gobject_meta[] = {
    { "__gc", _wplua_gvalue_userdata___gc },
    { "__eq", _wplua_gvalue_userdata___eq },
    { "__index", _wplua_gobject___index },
    { "__newindex", _wplua_gobject___newindex },
    { "__tostring", _wplua_gobject__tostring },
    { NULL, NULL }
  };

  luaL_newmetatable (L, "GObject");
  luaL_setfuncs (L, gobject_meta, 0);
  lua_pop (L, 1);
}

void
wplua_pushobject (lua_State * L, gpointer object)
{
  g_return_if_fail (G_IS_OBJECT (object));

  GValue *v = _wplua_pushgvalue_userdata (L, G_TYPE_FROM_INSTANCE (object));
  wp_trace_object (object, "pushing to Lua, v=%p", v);
  g_value_take_object (v, object);

  luaL_getmetatable (L, "GObject");
  lua_setmetatable (L, -2);
}

gpointer
wplua_toobject (lua_State *L, int idx)
{
  g_return_val_if_fail (_wplua_isgvalue_userdata (L, idx, G_TYPE_OBJECT), NULL);
  return g_value_get_object ((GValue *) lua_touserdata (L, idx));
}

gpointer
wplua_checkobject (lua_State *L, int idx, GType type)
{
  if (G_UNLIKELY (!_wplua_isgvalue_userdata (L, idx, type))) {
    wp_critical ("expected userdata storing GValue<%s>", g_type_name (type));
    luaL_argerror (L, idx, "expected userdata storing GValue<GObject>");
  }
  return g_value_get_object ((GValue *) lua_touserdata (L, idx));
}

gboolean
wplua_isobject (lua_State *L, int idx, GType type)
{
  if (!g_type_is_a (type, G_TYPE_OBJECT)) return FALSE;
  return _wplua_isgvalue_userdata (L, idx, type);
}
 070701000000E9000081A400000000000000000000000165F863040000034E000000000000000000000000000000000000003F00000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/private.h    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WPLUA_PRIVATE_H__
#define __WPLUA_PRIVATE_H__

#include "wplua.h"

G_BEGIN_DECLS

#define WP_LOCAL_LOG_TOPIC log_topic_wplua
WP_LOG_TOPIC_EXTERN (log_topic_wplua)

/* boxed.c */
void _wplua_init_gboxed (lua_State *L);

/* closure.c */
void _wplua_init_closure (lua_State *L);

/* object.c */
void _wplua_init_gobject (lua_State *L);

/* userdata.c */
GValue * _wplua_pushgvalue_userdata (lua_State * L, GType type);
gboolean _wplua_isgvalue_userdata (lua_State *L, int idx, GType type);

int _wplua_gvalue_userdata___gc (lua_State *L);
int _wplua_gvalue_userdata___eq (lua_State *L);

/* wplua.c */
int _wplua_pcall (lua_State *L, int nargs, int nret);

G_END_DECLS

#endif
  070701000000EA000081A400000000000000000000000165F8630400000ADF000000000000000000000000000000000000004100000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/sandbox.lua  -- WirePlumber
--
-- Copyright © 2020 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on https://github.com/kikito/sandbox.lua
-- Copyright © 2013 Enrique García Cota
--
-- SPDX-License-Identifier: MIT

local SANDBOX_CONFIG = ...
local SANDBOX_ENV = {}

function create_sandbox_env()
  local function populate_env(id)
    local module, method = id:match('([^%.]+)%.([^%.]+)')
    if module then
      SANDBOX_ENV[module]         = SANDBOX_ENV[module] or {}
      SANDBOX_ENV[module][method] = _G[module][method]
    else
      SANDBOX_ENV[id] = _G[id]
    end
  end

  if not SANDBOX_ENV._VERSION then
    -- List of exported functions and packages
    ([[ _VERSION assert error ipairs   next pairs  tonumber
        pcall    select print tostring type xpcall require
        table    string math  package  utf8 debug  coroutine
        os.clock  os.difftime os.time  os.date     os.getenv
        setmetatable getmetatable
    ]]):gsub('%S+', populate_env)

    -- Additionally export everything in SANDBOX_EXPORT
    if type(SANDBOX_EXPORT) == "table" then
      for k, v in pairs(SANDBOX_EXPORT) do
        SANDBOX_ENV[k] = v
      end
    end

    -- Additionally protect packages from malicious scripts trying to override methods
    for k, v in pairs(SANDBOX_ENV) do
      if type(v) == "table" then
        SANDBOX_ENV[k] = setmetatable({}, {
          __index = v,
          __newindex = function(_, attr_name, _)
            error('Can not modify ' .. k .. '.' .. attr_name .. '. Protected by the sandbox.')
          end
        })
      end
    end
  end

  -- chunk's environment will be an empty table with __index
  -- to access our SANDBOX_ENV (without being able to write it)
  return setmetatable({}, {
    __index = SANDBOX_ENV,
  })
end

if SANDBOX_CONFIG["isolate_env"] then
  -- in isolate_env mode, use a separate enviornment for each loaded chunk and
  -- store all of them in a global table so that they are not garbage collected
  SANDBOX_ENV_LIST = {}

  function sandbox(chunk, ...)
    local env = create_sandbox_env()
    -- store the chunk's environment so that it is not garbage collected
    table.insert(SANDBOX_ENV_LIST, env)
    -- set it as the chunk's 1st upvalue (__ENV)
    debug.setupvalue(chunk, 1, env)
    -- execute the chunk
    return chunk(...)
  end
else
  -- in common_env mode, use the same environment for all loaded chunks
  -- chunk's environment will be an empty table with __index
  -- to access our SANDBOX_ENV (without being able to write it)
  SANDBOX_COMMON_ENV = create_sandbox_env()

  function sandbox(chunk, ...)
    -- set it as the chunk's 1st upvalue (__ENV)
    debug.setupvalue(chunk, 1, SANDBOX_COMMON_ENV)
    -- execute the chunk
    return chunk(...)
  end
end
 070701000000EB000081A400000000000000000000000165F86304000006B3000000000000000000000000000000000000004000000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/userdata.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "wplua.h"
#include "private.h"
#include <wp/wp.h>

GValue *
_wplua_pushgvalue_userdata (lua_State * L, GType type)
{
  GValue *v = lua_newuserdata (L, sizeof (GValue));
  memset (v, 0, sizeof (GValue));
  g_value_init (v, type);
  return v;
}

gboolean
_wplua_isgvalue_userdata (lua_State *L, int idx, GType type)
{
  GValue *v;

  if (!lua_isuserdata (L, idx))
    return FALSE;
  if (lua_rawlen (L, idx) != sizeof (GValue))
    return FALSE;
  if (!(v = lua_touserdata (L, idx)))
    return FALSE;
  if (type != G_TYPE_NONE && !g_type_is_a (G_VALUE_TYPE (v), type))
    return FALSE;

  return TRUE;
}

GType
wplua_gvalue_userdata_type (lua_State *L, int idx)
{
  GValue *v;

  if (!lua_isuserdata (L, idx))
    return G_TYPE_INVALID;
  if (lua_rawlen (L, idx) != sizeof (GValue))
    return G_TYPE_INVALID;
  if (!(v = lua_touserdata (L, idx)))
    return G_TYPE_INVALID;

  return G_VALUE_TYPE (v);
}

int
_wplua_gvalue_userdata___gc (lua_State *L)
{
  GValue *v = lua_touserdata (L, 1);
  wp_trace_boxed (G_VALUE_TYPE (v), g_value_peek_pointer (v),
      "collected, v=%p", v);
  g_value_unset (v);
  return 0;
}

int
_wplua_gvalue_userdata___eq (lua_State *L)
{
  if (_wplua_isgvalue_userdata (L, 1, G_TYPE_NONE) &&
      _wplua_isgvalue_userdata (L, 2, G_TYPE_NONE)) {
    GValue *v1 = lua_touserdata (L, 1);
    GValue *v2 = lua_touserdata (L, 2);
    gpointer p1 = g_value_peek_pointer (v1);
    gpointer p2 = g_value_peek_pointer (v2);
    lua_pushboolean (L, (p1 == p2));
  } else {
    lua_pushboolean (L, FALSE);
  }
  return 1;
}
 070701000000EC000081A400000000000000000000000165F8630400002832000000000000000000000000000000000000003D00000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/value.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "wplua.h"
#include "private.h"
#include <wp/wp.h>

WpProperties *
wplua_table_to_properties (lua_State *L, int idx)
{
  WpProperties *p = wp_properties_new_empty ();
  const gchar *key, *value;
  int table = lua_absindex (L, idx);

  if (lua_type (L, table) != LUA_TTABLE) {
    wp_critical ("skipping non-table value");
    return p;
  }

  lua_pushnil(L);
  while (lua_next (L, table) != 0) {
    /* copy key & value to convert them to string */
    key = luaL_tolstring (L, -2, NULL);
    value = luaL_tolstring (L, -2, NULL);
    wp_properties_set (p, key, value);
    lua_pop (L, 3);
  }

  /* sort, because the lua table has a random order and it's too messy to read */
  wp_properties_sort (p);

  return p;
}

void
wplua_properties_to_table (lua_State *L, WpProperties *p)
{
  lua_newtable (L);
  if (p) {
    g_autoptr (WpIterator) it = wp_properties_new_iterator (p);
    GValue v = G_VALUE_INIT;
    const gchar *key, *value;

    while (wp_iterator_next (it, &v)) {
      WpPropertiesItem *pi = g_value_get_boxed (&v);
      key = wp_properties_item_get_key (pi);
      value = wp_properties_item_get_value (pi);
      lua_pushstring (L, key);
      lua_pushstring (L, value);
      lua_settable (L, -3);
      g_value_unset (&v);
    }
  }
}

GVariant *
wplua_lua_to_gvariant (lua_State *L, int idx)
{
  switch (lua_type (L, idx)) {
  case LUA_TNIL:
    return g_variant_new ("()");
  case LUA_TBOOLEAN:
    return g_variant_new_boolean (lua_toboolean (L, idx));
  case LUA_TNUMBER:
    if (lua_isinteger (L, idx))
      return g_variant_new_int64 (lua_tointeger (L, idx));
    else
      return g_variant_new_double (lua_tonumber (L, idx));
  case LUA_TSTRING:
    return g_variant_new_string (lua_tostring (L, idx));
  case LUA_TTABLE: {
    GVariantBuilder b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
    const gchar *key;
    int table = lua_absindex (L, idx);

    lua_pushnil (L);
    while (lua_next (L, table) != 0) {
      /* copy key to convert it to string */
      lua_pushvalue (L, -2);
      key = lua_tostring (L, -1);
      g_variant_builder_add (&b, "{sv}", key, wplua_lua_to_gvariant (L, -2));
      lua_pop (L, 2);
    }
    return g_variant_builder_end (&b);
  }
  default:
    wp_warning ("skipping bad value (its type cannot be represented in GVariant)");
    return NULL;
  }
}

void
wplua_gvariant_to_lua (lua_State *L, GVariant  *variant)
{
  if (variant == NULL || g_variant_is_of_type (variant, G_VARIANT_TYPE_UNIT)) {
    lua_pushnil (L);
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_INT16)) {
    lua_pushinteger (L, g_variant_get_int16 (variant));
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_INT32)) {
    lua_pushinteger (L, g_variant_get_int32 (variant));
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_INT64)) {
    lua_pushinteger (L, g_variant_get_int64 (variant));
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_UINT16)) {
    lua_pushinteger (L, g_variant_get_uint16 (variant));
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_UINT32)) {
    lua_pushinteger (L, g_variant_get_uint32 (variant));
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_UINT64)) {
    lua_pushinteger (L, g_variant_get_uint64 (variant));
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_DOUBLE)) {
    lua_pushnumber (L, g_variant_get_double (variant));
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_BOOLEAN)) {
    lua_pushboolean (L, g_variant_get_boolean (variant));
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING)) {
    lua_pushstring (L, g_variant_get_string (variant, NULL));
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARIANT)) {
    g_autoptr (GVariant) v = g_variant_get_variant (variant);
    wplua_gvariant_to_lua (L, v);
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_DICTIONARY)) {
    gsize n_children, i;
    n_children = g_variant_n_children (variant);
    lua_createtable (L, 0, n_children);
    for (i = 0; i < n_children; i++) {
      g_autoptr (GVariant) key, value;
      g_variant_get_child (variant, i, "{@?@*}", &key, &value);
      wplua_gvariant_to_lua (L, key);
      /* if the key is a string convertible to integer, convert it */
      if (lua_type (L, -1) == LUA_TSTRING) {
        int isnum = 0;
        lua_Integer num = lua_tointegerx (L, -1, &isnum);
        if (isnum) {
          lua_pop (L, 1);
          lua_pushinteger (L, num);
        }
      }
      wplua_gvariant_to_lua (L, value);
      lua_settable (L, -3);
    }
  }
  else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_ARRAY)) {
    gsize n_children, i;
    n_children = g_variant_n_children (variant);
    lua_createtable (L, n_children, 0);
    for (i = 0; i < n_children; i++) {
      g_autoptr (GVariant) value;
      value = g_variant_get_child_value (variant, i);
      wplua_gvariant_to_lua (L, value);
      lua_seti (L, -2, i + 1);
    }
  }
  else {
    g_autofree gchar *type_name =
        g_variant_type_dup_string (g_variant_get_type (variant));
    wp_warning ("Unhandled GVariant type %s", type_name);
    lua_pushnil (L);
  }
}

gint
wplua_lua_to_enum (lua_State *L, int idx, GType enum_type)
{
  if (lua_type (L, idx) == LUA_TSTRING) {
    g_autoptr (GEnumClass) klass = g_type_class_ref (enum_type);
    GEnumValue *value = g_enum_get_value_by_nick (klass, lua_tostring (L, idx));
    if (value)
      return value->value;
    else
      luaL_error (L, "Invalid enum value '%s'", lua_tostring (L, idx));
  }
  return lua_tointeger (L, idx);
}

void
wplua_enum_to_lua (lua_State *L, gint enum_val, GType enum_type)
{
  g_autoptr (GEnumClass) klass = g_type_class_ref (enum_type);
  GEnumValue *value = g_enum_get_value (klass, enum_val);
  if (value)
    lua_pushstring (L, value->value_nick);
  else
    lua_pushinteger (L, enum_val);
}

void
wplua_lua_to_gvalue (lua_State *L, int idx, GValue *v)
{
  switch (g_type_fundamental (G_VALUE_TYPE (v))) {
  case G_TYPE_CHAR:
    if (lua_type (L, idx) == LUA_TSTRING)
      g_value_set_schar (v, *lua_tostring (L, idx));
    else
      g_value_set_schar (v, lua_tointeger (L, idx));
    break;
  case G_TYPE_UCHAR:
    g_value_set_uchar (v, lua_tointeger (L, idx));
    break;
  case G_TYPE_INT:
    g_value_set_int (v, lua_tointeger (L, idx));
    break;
  case G_TYPE_UINT:
    g_value_set_uint (v, lua_tointeger (L, idx));
    break;
  case G_TYPE_LONG:
    g_value_set_long (v, lua_tointeger (L, idx));
    break;
  case G_TYPE_ULONG:
    g_value_set_ulong (v, lua_tointeger (L, idx));
    break;
  case G_TYPE_INT64:
    g_value_set_int64 (v, lua_tointeger (L, idx));
    break;
  case G_TYPE_UINT64:
    g_value_set_uint64 (v, lua_tonumber (L, idx));
    break;
  case G_TYPE_FLOAT:
    g_value_set_float (v, lua_tonumber (L, idx));
    break;
  case G_TYPE_DOUBLE:
    g_value_set_double (v, lua_tonumber (L, idx));
    break;
  case G_TYPE_BOOLEAN:
    g_value_set_boolean (v, lua_toboolean (L, idx));
    break;
  case G_TYPE_STRING:
    g_value_set_string (v, lua_tostring (L, idx));
    break;
  case G_TYPE_POINTER:
    if (lua_type (L, idx) == LUA_TLIGHTUSERDATA)
      g_value_set_pointer (v, lua_touserdata (L, idx));
    break;
  case G_TYPE_BOXED:
    if (_wplua_isgvalue_userdata (L, idx, G_VALUE_TYPE (v)))
      g_value_set_boxed (v, wplua_toboxed (L, idx));
    /* table -> WpProperties */
    else if (lua_istable (L, idx) && G_VALUE_TYPE (v) == WP_TYPE_PROPERTIES)
      g_value_take_boxed (v, wplua_table_to_properties (L, idx));
    break;
  case G_TYPE_OBJECT:
  case G_TYPE_INTERFACE:
    if (_wplua_isgvalue_userdata (L, idx, G_VALUE_TYPE (v)))
      g_value_set_object (v, wplua_toobject (L, idx));
    break;
  case G_TYPE_ENUM:
    g_value_set_enum (v, wplua_lua_to_enum (L, idx, G_VALUE_TYPE (v)));
    break;
  case G_TYPE_FLAGS:
    g_value_set_flags (v, lua_tointeger (L, idx));
    break;
  case G_TYPE_VARIANT:
    g_value_set_variant (v, wplua_lua_to_gvariant (L, idx));
    break;
  default:
    break;
  }
}

int
wplua_gvalue_to_lua (lua_State *L, const GValue *v)
{
  switch (g_type_fundamental (G_VALUE_TYPE (v))) {
  case G_TYPE_CHAR:
    lua_pushinteger (L, g_value_get_schar (v));
    break;
  case G_TYPE_UCHAR:
    lua_pushinteger (L, g_value_get_uchar (v));
    break;
  case G_TYPE_INT:
    lua_pushinteger (L, g_value_get_int (v));
    break;
  case G_TYPE_UINT:
    lua_pushinteger (L, g_value_get_uint (v));
    break;
  case G_TYPE_LONG:
    lua_pushinteger (L, g_value_get_long (v));
    break;
  case G_TYPE_ULONG:
    lua_pushinteger (L, g_value_get_ulong (v));
    break;
  case G_TYPE_INT64:
    lua_pushinteger (L, g_value_get_int64 (v));
    break;
  case G_TYPE_UINT64:
    lua_pushnumber (L, g_value_get_uint64 (v));
    break;
  case G_TYPE_FLOAT:
    lua_pushnumber (L, g_value_get_float (v));
    break;
  case G_TYPE_DOUBLE:
    lua_pushnumber (L, g_value_get_double (v));
    break;
  case G_TYPE_BOOLEAN:
    lua_pushboolean (L, g_value_get_boolean (v));
    break;
  case G_TYPE_STRING:
    lua_pushstring (L, g_value_get_string (v));
    break;
  case G_TYPE_POINTER:
    lua_pushlightuserdata (L, g_value_get_pointer (v));
    break;
  case G_TYPE_BOXED:
    if (G_VALUE_TYPE (v) == WP_TYPE_PROPERTIES)
      wplua_properties_to_table (L, g_value_get_boxed (v));
    else
      wplua_pushboxed (L, G_VALUE_TYPE (v), g_value_dup_boxed (v));
    break;
  case G_TYPE_OBJECT:
  case G_TYPE_INTERFACE: {
    GObject *object = g_value_dup_object (v);
    if (object)
      wplua_pushobject (L, object);
    else
      lua_pushnil (L);
    break;
  }
  case G_TYPE_ENUM:
    wplua_enum_to_lua (L, g_value_get_enum (v), G_VALUE_TYPE (v));
    break;
  case G_TYPE_FLAGS:
    /* FIXME: push as userdata with methods */
    lua_pushinteger (L, g_value_get_flags (v));
    break;
  case G_TYPE_PARAM: {
    GParamSpec *pspec = g_value_get_param (v);
    lua_pushstring (L, pspec->name);
    break;
  }
  case G_TYPE_VARIANT: {
    GVariant *variant = g_value_get_variant (v);
    wplua_gvariant_to_lua (L, variant);
    break;
  }
  default:
    lua_pushnil (L);
    break;
  }
  return 1;
}
  070701000000ED000081A400000000000000000000000165F8630400001CF3000000000000000000000000000000000000003D00000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/wplua.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "wplua.h"
#include "private.h"
#include <wp/wp.h>

WP_LOG_TOPIC (log_topic_wplua, "wplua")

#define URI_SANDBOX "resource:///org/freedesktop/pipewire/wireplumber/wplua/sandbox.lua"

extern void _wplua_register_resource (void);

G_DEFINE_QUARK (wplua, wp_domain_lua);

static void
_wplua_openlibs (lua_State *L)
{
  /* http://www.lua.org/manual/5.3/manual.html#luaL_requiref
   * http://www.lua.org/source/5.3/linit.c.html */
  static const luaL_Reg loadedlibs[] = {
    {"_G", luaopen_base},
    {LUA_LOADLIBNAME, luaopen_package},
    {LUA_COLIBNAME, luaopen_coroutine},
    {LUA_TABLIBNAME, luaopen_table},
    /* {LUA_IOLIBNAME, luaopen_io}, */
    {LUA_OSLIBNAME, luaopen_os},
    {LUA_STRLIBNAME, luaopen_string},
    {LUA_MATHLIBNAME, luaopen_math},
    {LUA_UTF8LIBNAME, luaopen_utf8},
    {LUA_DBLIBNAME, luaopen_debug},
    {NULL, NULL}
  };
  const luaL_Reg *lib;

  for (lib = loadedlibs; lib->func; lib++) {
    luaL_requiref (L, lib->name, lib->func, 1);
    lua_pop (L, 1);
  }
}

static int
_wplua_errhandler (lua_State *L)
{
  luaL_traceback (L, L, NULL, 1);
  wp_warning ("%s\n%s", lua_tostring (L, -2), lua_tostring (L, -1));
  lua_pop (L, 2);
  return 0;
}

int
_wplua_pcall (lua_State *L, int nargs, int nret)
{
  int hpos = lua_gettop (L) - nargs;
  int ret = LUA_OK;

  lua_pushcfunction (L, _wplua_errhandler);
  lua_insert (L, hpos);

  ret = lua_pcall (L, nargs, nret, hpos);
  switch (ret) {
  case LUA_ERRMEM:
    wp_critical ("not enough memory");
    break;
  case LUA_ERRERR:
    wp_critical ("error running the message handler");
    break;
  default:
    break;
  }

  lua_remove (L, hpos);
  return ret;
}

lua_State *
wplua_new (void)
{
  static gboolean resource_registered = FALSE;
  lua_State *L = luaL_newstate ();

  wp_debug ("initializing lua_State %p", L);

  if (!resource_registered) {
    _wplua_register_resource ();
    resource_registered = TRUE;
  }

  _wplua_openlibs (L);
  _wplua_init_gboxed (L);
  _wplua_init_gobject (L);
  _wplua_init_closure (L);

  {
    GHashTable *t = g_hash_table_new (g_direct_hash, g_direct_equal);
    lua_pushliteral (L, "wplua_vtables");
    wplua_pushboxed (L, G_TYPE_HASH_TABLE, t);
    lua_settable (L, LUA_REGISTRYINDEX);
  }

  /* refcount */
  lua_pushinteger (L, 1);
  lua_rawsetp (L, LUA_REGISTRYINDEX, L);

  return L;
}

lua_State *
wplua_ref (lua_State *L)
{
  lua_Integer refcount;
  lua_rawgetp (L, LUA_REGISTRYINDEX, L);
  refcount = lua_tointeger (L, -1);
  lua_pushinteger (L, refcount + 1);
  lua_rawsetp (L, LUA_REGISTRYINDEX, L);
  lua_pop (L, 1);
  return L;
}

void
wplua_unref (lua_State * L)
{
  lua_Integer refcount;
  lua_rawgetp (L, LUA_REGISTRYINDEX, L);
  refcount = lua_tointeger (L, -1);
  if (refcount > 1) {
    lua_pushinteger (L, refcount - 1);
    lua_rawsetp (L, LUA_REGISTRYINDEX, L);
    lua_pop (L, 1);
  } else {
    wp_debug ("closing lua_State %p", L);
    lua_close (L);
  }
}

void
wplua_enable_sandbox (lua_State * L, WpLuaSandboxFlags flags)
{
  g_autoptr (GError) error = NULL;
  wp_debug ("enabling Lua sandbox");

  if (!wplua_load_uri (L, URI_SANDBOX, &error)) {
    wp_critical ("Failed to load sandbox: %s", error->message);
    return;
  }

  lua_newtable (L);
  lua_pushliteral (L, "isolate_env");
  lua_pushboolean (L, (flags & WP_LUA_SANDBOX_ISOLATE_ENV));
  lua_settable (L, -3);

  if (!wplua_pcall (L, 1, 0, &error)) {
    wp_critical ("Failed to load sandbox: %s", error->message);
  }
}

int
wplua_push_sandbox (lua_State * L)
{
  return (lua_getglobal (L, "sandbox") == LUA_TFUNCTION) ? 1 : 0;
}

void
wplua_register_type_methods (lua_State * L, GType type,
    lua_CFunction constructor, const luaL_Reg * methods)
{
  g_return_if_fail (L != NULL);
  g_return_if_fail (G_TYPE_FUNDAMENTAL (type) == G_TYPE_OBJECT ||
                    G_TYPE_FUNDAMENTAL (type) == G_TYPE_BOXED ||
                    G_TYPE_FUNDAMENTAL (type) == G_TYPE_INTERFACE);

  /* register methods */
  if (methods) {
    GHashTable *vtables;

    lua_pushliteral (L, "wplua_vtables");
    lua_gettable (L, LUA_REGISTRYINDEX);
    vtables = wplua_toboxed (L, -1);
    lua_pop (L, 1);

    wp_debug ("Registering methods for '%s'", g_type_name (type));

    if (G_UNLIKELY (g_hash_table_contains (vtables, GUINT_TO_POINTER (type)))) {
      wp_critical ("type '%s' was already registered", g_type_name (type));
      return;
    }

    g_hash_table_insert (vtables, GUINT_TO_POINTER (type), (gpointer) methods);
  }

  /* register constructor */
  if (constructor) {
    luaL_Buffer b;

    wp_debug ("Registering class for '%s'", g_type_name (type));

    luaL_buffinit (L, &b);
    luaL_addstring (&b, g_type_name (type));
    luaL_addchar (&b, '_');
    luaL_addstring (&b, "new");
    luaL_pushresult (&b);
    lua_pushcfunction (L, constructor);
    lua_setglobal (L, lua_tostring (L, -2));
    lua_pop (L, 1);
  }
}

static gboolean
_wplua_load_buffer (lua_State * L, const gchar *buf, gsize size,
    const gchar * name, GError **error)
{
  int ret;

  /* skip shebang, if present */
  if (g_str_has_prefix (buf, "#!/")) {
    const char *tmp = strchr (buf, '\n');
    size -= (tmp - buf);
    buf = tmp;
  }

  ret = luaL_loadbuffer (L, buf, size, name);
  if (ret != LUA_OK) {
    g_set_error (error, WP_DOMAIN_LUA, WP_LUA_ERROR_COMPILATION,
        "Failed to compile: %s", lua_tostring (L, -1));
    lua_pop (L, 1);
    return FALSE;
  }
  return TRUE;
}

gboolean
wplua_load_buffer (lua_State * L, const gchar *buf, gsize size, GError **error)
{
  g_return_val_if_fail (L != NULL, FALSE);
  g_return_val_if_fail (buf != NULL, FALSE);
  g_return_val_if_fail (size != 0, FALSE);

  g_autofree gchar *name =
      g_strdup_printf ("buffer@%p;size=%" G_GSIZE_FORMAT, buf, size);
  return _wplua_load_buffer (L, buf, size, name, error);
}

gboolean
wplua_load_uri (lua_State * L, const gchar *uri, GError **error)
{
  g_autoptr (GFile) file = NULL;
  g_autoptr (GBytes) bytes = NULL;
  g_autoptr (GError) err = NULL;
  g_autofree gchar *name = NULL;
  gconstpointer data;
  gsize size;

  g_return_val_if_fail (L != NULL, FALSE);
  g_return_val_if_fail (uri != NULL, FALSE);

  file = g_file_new_for_uri (uri);
  if (!(bytes = g_file_load_bytes (file, NULL, NULL, &err))) {
    g_propagate_prefixed_error (error, err, "Failed to load '%s':", uri);
    err = NULL;
    return FALSE;
  }

  name = g_path_get_basename (uri);
  data = g_bytes_get_data (bytes, &size);
  return _wplua_load_buffer (L, data, size, name, error);
}

gboolean
wplua_load_path (lua_State * L, const gchar *path, GError **error)
{
  g_autofree gchar *abs_path = NULL;
  g_autofree gchar *uri = NULL;

  g_return_val_if_fail (L != NULL, FALSE);
  g_return_val_if_fail (path != NULL, FALSE);

  if (!g_path_is_absolute (path)) {
    g_autofree gchar *cwd = g_get_current_dir ();
    abs_path = g_build_filename (cwd, path, NULL);
  }

  if (!(uri = g_filename_to_uri (abs_path ? abs_path : path, NULL, error)))
    return FALSE;

  return wplua_load_uri (L, uri, error);
}

gboolean
wplua_pcall (lua_State * L, int nargs, int nres, GError **error)
{
  int ret = _wplua_pcall (L, nargs, nres);
  if (ret != LUA_OK) {
    g_set_error (error, WP_DOMAIN_LUA, WP_LUA_ERROR_RUNTIME,
        "Lua runtime error");
    return FALSE;
  }
  return TRUE;
}
 070701000000EE000081A400000000000000000000000165F8630400000C35000000000000000000000000000000000000003D00000000wireplumber-0.5.0/modules/module-lua-scripting/wplua/wplua.h  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WPLUA_H__
#define __WPLUA_H__

#include <wp/wp.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

G_BEGIN_DECLS

/**
 * WP_DOMAIN_LUA:
 *
 * @brief A <a href="https://developer.gnome.org/glib/stable/glib-Error-Reporting.html#GError">
 * GError</a> domain for errors that occurred within the context of the
 * WirePlumber lua library.
 */
#define WP_DOMAIN_LUA (wp_domain_lua_quark ())
GQuark wp_domain_lua_quark (void);

/**
 * WpLuaError:
 *
 * @brief
 * @em WP_LUA_ERROR_COMPILATION: a compilation error, i.e. invalid Lua code
 * @em WP_LUA_ERROR_RUNTIME: a runtime error, i.e. misbehaving Lua code
 *
 * Error codes that can appear in a
 * <a href="https://developer.gnome.org/glib/stable/glib-Error-Reporting.html#GError">
 * GError</a> when the error domain is %WP_DOMAIN_LUA
 */
typedef enum {
  WP_LUA_ERROR_COMPILATION,
  WP_LUA_ERROR_RUNTIME,
} WpLuaError;

typedef enum {
  WP_LUA_SANDBOX_ISOLATE_ENV = 1,
} WpLuaSandboxFlags;

lua_State * wplua_new (void);
lua_State * wplua_ref (lua_State *L);
void wplua_unref (lua_State * L);

void wplua_enable_sandbox (lua_State * L, WpLuaSandboxFlags flags);
int wplua_push_sandbox (lua_State * L);

void wplua_register_type_methods (lua_State * L, GType type,
    lua_CFunction constructor, const luaL_Reg * methods);

GType wplua_gvalue_userdata_type (lua_State *L, int idx);

/* push -> transfer full; get -> transfer none */
void wplua_pushobject (lua_State * L, gpointer object);
gpointer wplua_toobject (lua_State *L, int idx);
gpointer wplua_checkobject (lua_State *L, int idx, GType type);
gboolean wplua_isobject (lua_State *L, int idx, GType type);

/* push -> transfer full; get -> transfer none */
void wplua_pushboxed (lua_State * L, GType type, gpointer object);
gpointer wplua_toboxed (lua_State *L, int idx);
gpointer wplua_checkboxed (lua_State *L, int idx, GType type);
gboolean wplua_isboxed (lua_State *L, int idx, GType type);

/* transfer floating */
GClosure * wplua_checkclosure (lua_State *L, int idx);
GClosure * wplua_function_to_closure (lua_State *L, int idx);

void wplua_enum_to_lua (lua_State *L, gint enum_val, GType enum_type);
gint wplua_lua_to_enum (lua_State *L, int idx, GType enum_type);

void wplua_lua_to_gvalue (lua_State *L, int idx, GValue *v);
int wplua_gvalue_to_lua (lua_State *L, const GValue *v);

GVariant * wplua_lua_to_gvariant (lua_State *L, int idx);
void wplua_gvariant_to_lua (lua_State *L, GVariant *p);

WpProperties * wplua_table_to_properties (lua_State *L, int idx);
void wplua_properties_to_table (lua_State *L, WpProperties *p);

gboolean wplua_load_buffer (lua_State * L, const gchar *buf, gsize size,
    GError **error);
gboolean wplua_load_uri (lua_State * L, const gchar *uri, GError **error);
gboolean wplua_load_path (lua_State * L, const gchar *path, GError **error);

gboolean wplua_pcall (lua_State * L, int nargs, int nres, GError **error);

G_DEFINE_AUTOPTR_CLEANUP_FUNC(lua_State, wplua_unref)

G_END_DECLS

#endif
   070701000000EF000081A400000000000000000000000165F8630400004AE3000000000000000000000000000000000000002D00000000wireplumber-0.5.0/modules/module-mixer-api.c  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <math.h>
#include <pipewire/pipewire.h>
#include <spa/pod/iter.h>
#include <spa/param/audio/raw.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-mixer-api")

struct volume {
  uint8_t channels;
  float values[SPA_AUDIO_MAX_CHANNELS];
};

struct channel_map {
  uint8_t channels;
  uint32_t map[SPA_AUDIO_MAX_CHANNELS];
};

struct node_info {
  guint32 seq;

  guint32 device_id;
  gint32 route_index;
  gint32 route_device;

  struct volume volume;
  struct volume monitorVolume;
  struct channel_map map;
  gboolean mute;
  gboolean monitorMute;
  float svolume;
  float base;
  float step;
};

struct _WpMixerApi
{
  WpPlugin parent;
  WpObjectManager *om;
  GHashTable *node_infos;
  guint32 seq;

  /* properties */
  gint scale;
};

enum {
  ACTION_SET_VOLUME,
  ACTION_GET_VOLUME,
  SIGNAL_CHANGED,
  N_SIGNALS
};

enum {
  PROP_0,
  PROP_SCALE,
};

static guint signals[N_SIGNALS] = {0};

G_DECLARE_FINAL_TYPE (WpMixerApi, wp_mixer_api, WP, MIXER_API, WpPlugin)
G_DEFINE_TYPE (WpMixerApi, wp_mixer_api, WP_TYPE_PLUGIN)

enum {
  SCALE_LINEAR,
  SCALE_CUBIC,
};

static GType
wp_mixer_api_volume_scale_enum_get_type (void)
{
  static gsize gtype_id = 0;
  static const GEnumValue values[] = {
    { (gint) SCALE_LINEAR, "SCALE_LINEAR", "linear" },
    { (gint) SCALE_CUBIC, "SCALE_CUBIC", "cubic" },
    { 0, NULL, NULL }
  };
  if (g_once_init_enter (&gtype_id)) {
    GType new_type = g_enum_register_static (
        g_intern_static_string ("WpMixerApiVolumeScale"), values);
    g_once_init_leave (&gtype_id, new_type);
  }
  return (GType) gtype_id;
}

static void
wp_mixer_api_init (WpMixerApi * self)
{
}

static void
wp_mixer_api_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpMixerApi *self = WP_MIXER_API (object);

  switch (property_id) {
  case PROP_SCALE:
    g_value_set_enum (value, self->scale);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_mixer_api_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpMixerApi *self = WP_MIXER_API (object);

  switch (property_id) {
  case PROP_SCALE:
    self->scale = g_value_get_enum (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static gboolean
node_info_fill (struct node_info * info, WpSpaPod * props)
{
  g_autoptr (WpSpaPod) channelVolumes = NULL;
  g_autoptr (WpSpaPod) channelMap = NULL;
  g_autoptr (WpSpaPod) monitorVolumes = NULL;

  if (!wp_spa_pod_get_object (props, NULL,
          "mute", "b", &info->mute,
          "channelVolumes", "P", &channelVolumes,
          NULL))
    return FALSE;

  /* default values */
  info->svolume = 1.0;
  info->base = 1.0;
  info->step = 1.0 / 65536.0;

  wp_spa_pod_get_object (props, NULL,
      "channelMap", "?P", &channelMap,
      "volumeBase", "?f", &info->base,
      "volumeStep", "?f", &info->step,
      "volume",     "?f", &info->svolume,
      "monitorVolumes", "?P", &monitorVolumes,
      "monitorMute", "?b", &info->monitorMute,
      NULL);

  info->volume.channels = spa_pod_copy_array (
      wp_spa_pod_get_spa_pod (channelVolumes), SPA_TYPE_Float,
      info->volume.values, SPA_AUDIO_MAX_CHANNELS);

  if (channelMap)
    info->map.channels = spa_pod_copy_array (
        wp_spa_pod_get_spa_pod (channelMap), SPA_TYPE_Id,
        info->map.map, SPA_AUDIO_MAX_CHANNELS);

  if (monitorVolumes)
    info->monitorVolume.channels = spa_pod_copy_array (
        wp_spa_pod_get_spa_pod (monitorVolumes), SPA_TYPE_Float,
        info->monitorVolume.values, SPA_AUDIO_MAX_CHANNELS);

  return TRUE;
}

static void
collect_node_info (WpMixerApi * self, struct node_info *info,
    WpPipewireObject * node)
{
  g_autoptr (WpPipewireObject) dev = NULL;
  const gchar *str = NULL;
  gboolean have_volume = FALSE;

  info->device_id = SPA_ID_INVALID;
  info->route_index = -1;
  info->route_device = -1;

  if ((str = wp_pipewire_object_get_property (node, PW_KEY_DEVICE_ID))) {
    dev = wp_object_manager_lookup (self->om, WP_TYPE_DEVICE,
        WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=s", str, NULL);
  }

  if (dev && (str = wp_pipewire_object_get_property (node, "card.profile.device"))) {
    gint32 p_device = atoi (str);
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) val = G_VALUE_INIT;

    it = wp_pipewire_object_enum_params_sync (dev, "Route", NULL);
    for (; it && wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpSpaPod *param = g_value_get_boxed (&val);
      gint32 r_index = -1, r_device = -1;
      g_autoptr (WpSpaPod) props = NULL;

      if (!wp_spa_pod_get_object (param, NULL,
              "index", "i", &r_index,
              "device", "i", &r_device,
              "props", "P", &props,
              NULL))
        continue;
      if (r_device != p_device)
        continue;

      if (props && node_info_fill (info, props)) {
        info->device_id = wp_proxy_get_bound_id (WP_PROXY (dev));
        info->route_index = r_index;
        info->route_device = r_device;
        have_volume = TRUE;
        g_value_unset (&val);
        break;
      }
    }
  }

  if (!have_volume) {
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) val = G_VALUE_INIT;

    it = wp_pipewire_object_enum_params_sync (node, "Props", NULL);
    for (; it && wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpSpaPod *param = g_value_get_boxed (&val);
      if (node_info_fill (info, param)) {
        g_value_unset (&val);
        break;
      }
    }
  }
}

static void on_objects_changed (WpObjectManager * om, WpMixerApi * self);

static void
on_sync_done (WpCore * core, GAsyncResult * res, WpMixerApi * self)
{
  g_autoptr (GError) error = NULL;
  if (!wp_core_sync_finish (core, res, &error))
    wp_warning_object (core, "sync error: %s", error->message);
  if (self->om) {
    on_objects_changed (self->om, self);
  }
}

static void
on_params_changed (WpPipewireObject * obj, const gchar * param_name,
    WpMixerApi * self)
{
  if ((WP_IS_NODE (obj) && !g_strcmp0 (param_name, "Props")) ||
      (WP_IS_DEVICE (obj) && !g_strcmp0 (param_name, "Route"))) {
    g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
    wp_core_sync (core, NULL, (GAsyncReadyCallback) on_sync_done, self);
  }
}

static void
on_objects_changed (WpObjectManager * om, WpMixerApi * self)
{
  g_autoptr (WpIterator) it =
      wp_object_manager_new_filtered_iterator (om, WP_TYPE_NODE, NULL);
  g_auto (GValue) val = G_VALUE_INIT;
  GHashTableIter infos_it;
  struct node_info *info;
  struct node_info old;

  self->seq++;

  for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
    WpPipewireObject *node = g_value_get_object (&val);
    guint id = wp_proxy_get_bound_id (WP_PROXY (node));

    info = g_hash_table_lookup (self->node_infos, GUINT_TO_POINTER (id));
    if (!info) {
      info = g_slice_new0 (struct node_info);
      g_hash_table_insert (self->node_infos, GUINT_TO_POINTER (id), info);
    }
    info->seq = self->seq;

    old = *info;
    collect_node_info (self, info, node);
    if (memcmp (&old, info, sizeof (struct node_info)) != 0) {
      wp_debug_object (self, "node %u changed volume props", id);
      g_signal_emit (self, signals[SIGNAL_CHANGED], 0, id);
    }
  }

  /* remove node_info of nodes that were removed from the object manager */
  g_hash_table_iter_init (&infos_it, self->node_infos);
  while (g_hash_table_iter_next (&infos_it, NULL, (gpointer *) &info)) {
    if (info->seq != self->seq)
      g_hash_table_iter_remove (&infos_it);
  }
}

static void
on_object_added (WpObjectManager * om, WpProxy * obj, WpMixerApi * self)
{
  g_signal_connect (obj, "params-changed", G_CALLBACK (on_params_changed), self);
}

static void
on_object_removed (WpObjectManager * om, WpProxy * obj, WpMixerApi * self)
{
  g_signal_handlers_disconnect_by_func (obj, G_CALLBACK (on_params_changed), self);
}

static void
node_info_free (gpointer info)
{
  g_slice_free (struct node_info, info);
}

static void
on_om_installed (WpObjectManager * om, WpMixerApi * self)
{
  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
wp_mixer_api_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpMixerApi * self = WP_MIXER_API (plugin);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
  g_return_if_fail (core);

  self->node_infos = g_hash_table_new_full (g_direct_hash, g_direct_equal,
      NULL, node_info_free);

  self->om = wp_object_manager_new ();
  wp_object_manager_add_interest (self->om, WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "#s", "*Audio*",
      NULL);
  wp_object_manager_add_interest (self->om, WP_TYPE_DEVICE,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "=s", "Audio/Device",
      NULL);
  wp_object_manager_request_object_features (self->om,
      WP_TYPE_GLOBAL_PROXY, WP_OBJECT_FEATURES_ALL);
  g_signal_connect_object (self->om, "objects-changed",
      G_CALLBACK (on_objects_changed), self, 0);
  g_signal_connect_object (self->om, "object-added",
      G_CALLBACK (on_object_added), self, 0);
  g_signal_connect_object (self->om, "object-removed",
      G_CALLBACK (on_object_removed), self, 0);
  g_signal_connect_object (self->om, "installed",
      G_CALLBACK (on_om_installed), self, 0);
  wp_core_install_object_manager (core, self->om);
}

static void
wp_mixer_api_disable (WpPlugin * plugin)
{
  WpMixerApi * self = WP_MIXER_API (plugin);

  {
    g_autoptr (WpIterator) it = wp_object_manager_new_iterator (self->om);
    g_auto (GValue) val = G_VALUE_INIT;

    for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpProxy *obj = g_value_get_object (&val);
      on_object_removed (self->om, obj, self);
    }
  }

  g_clear_object (&self->om);
  g_clear_pointer (&self->node_infos, g_hash_table_unref);
}

static inline gdouble
volume_from_linear (float vol, gint scale)
{
  if (vol <= 0.0f)
    return 0.0;
  else if (scale == SCALE_CUBIC)
    return cbrt(vol);
  else
    return vol;
}

static inline float
volume_to_linear (gdouble vol, gint scale)
{
  if (vol <= 0.0f)
    return 0.0;
  else if (scale == SCALE_CUBIC)
    return vol * vol * vol;
  else
    return vol;
}

static gboolean
wp_mixer_api_set_volume (WpMixerApi * self, guint32 id, GVariant * vvolume)
{
  struct node_info *info = self->node_infos ?
      g_hash_table_lookup (self->node_infos, GUINT_TO_POINTER (id)) : NULL;
  struct volume new_volume = {0};
  struct volume new_monVolume = {0};
  gboolean has_mute = FALSE;
  gboolean has_monitorMute = FALSE;
  gboolean mute = FALSE;
  gboolean monitorMute = FALSE;
  WpSpaIdTable t_audioChannel =
      wp_spa_id_table_from_name ("Spa:Enum:AudioChannel");

  if (!info || !vvolume)
    return FALSE;

  if (g_variant_is_of_type (vvolume, G_VARIANT_TYPE_DOUBLE)) {
    gdouble val = g_variant_get_double (vvolume);
    new_volume = info->volume;
    for (uint i = 0; i < new_volume.channels; i++)
      new_volume.values[i] = volume_to_linear (val, self->scale);
  }
  else if (g_variant_is_of_type (vvolume, G_VARIANT_TYPE_VARDICT)) {
    GVariantIter *iter;
    const gchar *idx_str;
    GVariant *v;
    gdouble val;

    has_mute = g_variant_lookup (vvolume, "mute", "b", &mute);
    has_monitorMute = g_variant_lookup (vvolume, "monitorMute", "b", &monitorMute);

    if (g_variant_lookup (vvolume, "volume", "d", &val)) {
      new_volume = info->volume;
      for (uint i = 0; i < new_volume.channels; i++)
        new_volume.values[i] = volume_to_linear (val, self->scale);
    }

    if (g_variant_lookup (vvolume, "monitorVolume", "d", &val)) {
      new_monVolume = info->monitorVolume;
      for (uint i = 0; i < new_monVolume.channels; i++)
        new_monVolume.values[i] = volume_to_linear (val, self->scale);
    }

    if (g_variant_lookup (vvolume, "channelVolumes", "a{sv}", &iter)) {
      /* keep the existing volume values for unspecified channels */
      new_volume = info->volume;
      new_monVolume = info->monitorVolume;

      while (g_variant_iter_loop (iter, "{&sv}", &idx_str, &v)) {
        guint index = atoi (idx_str);
        const gchar *channel_str = NULL;
        WpSpaIdValue channel = NULL;

        if (g_variant_lookup (v, "channel", "&s", &channel_str)) {
          channel = wp_spa_id_table_find_value_from_short_name (
              t_audioChannel, channel_str);
          if (!channel)
            wp_notice_object (self, "invalid channel: %s", channel_str);
        }

        if (channel) {
          for (uint i = 0; i < info->map.channels; i++)
            if (info->map.map[i] == wp_spa_id_value_number (channel)) {
              index = i;
              break;
            }
        }

        if (index >= MIN(new_volume.channels, SPA_AUDIO_MAX_CHANNELS)) {
          wp_notice_object (self, "invalid channel index: %u", index);
          continue;
        }

        if (g_variant_lookup (v, "volume", "d", &val)) {
          new_volume.values[index] = volume_to_linear (val, self->scale);
        }
        if (g_variant_lookup (v, "monitorVolume", "d", &val)) {
          new_monVolume.values[index] = volume_to_linear (val, self->scale);
        }
      }
      g_variant_iter_free (iter);
    }
  } else {
    return FALSE;
  }

  /* set param */
  g_autoptr (WpSpaPod) props = NULL;
  g_autoptr (WpSpaPodBuilder) b =
      wp_spa_pod_builder_new_object ("Spa:Pod:Object:Param:Props", "Props");

  if (new_volume.channels > 0)
    wp_spa_pod_builder_add (b, "channelVolumes", "a",
        sizeof(float), SPA_TYPE_Float,
        new_volume.channels, new_volume.values, NULL);
  if (new_monVolume.channels > 0)
    wp_spa_pod_builder_add (b, "monitorVolumes", "a",
        sizeof(float), SPA_TYPE_Float,
        new_monVolume.channels, new_monVolume.values, NULL);
  if (has_mute)
    wp_spa_pod_builder_add (b, "mute", "b", mute, NULL);
  if (has_monitorMute)
    wp_spa_pod_builder_add (b, "monitorMute", "b", monitorMute, NULL);

  props = wp_spa_pod_builder_end (b);

  if (info->device_id != SPA_ID_INVALID) {
    WpPipewireObject *device = wp_object_manager_lookup (self->om,
        WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY,
        "bound-id", "=u", info->device_id, NULL);
    g_return_val_if_fail (device != NULL, FALSE);

    wp_pipewire_object_set_param (device, "Route", 0, wp_spa_pod_new_object (
        "Spa:Pod:Object:Param:Route", "Route",
        "index", "i", info->route_index,
        "device", "i", info->route_device,
        "props", "P", props,
        "save", "b", true,
        NULL));
  } else {
    WpPipewireObject *node = wp_object_manager_lookup (self->om,
        WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY,
        "bound-id", "=u", id, NULL);
    g_return_val_if_fail (node != NULL, FALSE);

    wp_pipewire_object_set_param (node, "Props", 0, g_steal_pointer (&props));
  }

  return TRUE;
}

static GVariant *
wp_mixer_api_get_volume (WpMixerApi * self, guint32 id)
{
  struct node_info *info = self->node_infos ?
      g_hash_table_lookup (self->node_infos, GUINT_TO_POINTER (id)) : NULL;
  g_auto (GVariantBuilder) b =
      G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  g_auto (GVariantBuilder) b_vol =
      G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  WpSpaIdTable t_audioChannel =
      wp_spa_id_table_from_name ("Spa:Enum:AudioChannel");

  if (!info)
    return NULL;

  g_variant_builder_add (&b, "{sv}", "id", g_variant_new_uint32 (id));
  g_variant_builder_add (&b, "{sv}", "mute", g_variant_new_boolean (info->mute));
  g_variant_builder_add (&b, "{sv}", "base", g_variant_new_double (info->base));
  g_variant_builder_add (&b, "{sv}", "step", g_variant_new_double (info->step));
  g_variant_builder_add (&b, "{sv}", "volume", g_variant_new_double (
          volume_from_linear ((info->volume.channels > 0) ?
              info->volume.values[0] : info->svolume, self->scale)));
  if (info->monitorVolume.channels > 0) {
    g_variant_builder_add (&b, "{sv}", "monitorVolume", g_variant_new_double (
          volume_from_linear (info->monitorVolume.values[0], self->scale)));
  }
  g_variant_builder_add (&b, "{sv}", "monitorMute", g_variant_new_boolean (info->monitorMute));

  for (guint i = 0; i < info->volume.channels; i++) {
    gchar index_str[10];
    g_auto (GVariantBuilder) b_vol_nested =
        G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);

    g_variant_builder_add (&b_vol_nested, "{sv}",
        "volume", g_variant_new_double (
            volume_from_linear (info->volume.values[i], self->scale)));

    if (i < info->map.channels) {
      WpSpaIdValue v =
          wp_spa_id_table_find_value (t_audioChannel, info->map.map[i]);
      if (v) {
        const gchar *channel_str = wp_spa_id_value_short_name (v);
        g_variant_builder_add (&b_vol_nested, "{sv}",
          "channel", g_variant_new_string (channel_str));
      }
    }

    if (i < info->monitorVolume.channels) {
      g_variant_builder_add (&b_vol_nested, "{sv}",
          "monitorVolume", g_variant_new_double (
              volume_from_linear (info->monitorVolume.values[i], self->scale)));
    }

    g_snprintf (index_str, 10, "%u", i);
    g_variant_builder_add (&b_vol, "{sv}", index_str,
        g_variant_builder_end (&b_vol_nested));
  }

  g_variant_builder_add (&b, "{sv}",
      "channelVolumes", g_variant_builder_end (&b_vol));
  return g_variant_builder_end (&b);
}

static void
wp_mixer_api_class_init (WpMixerApiClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  object_class->set_property = wp_mixer_api_set_property;
  object_class->get_property = wp_mixer_api_get_property;

  plugin_class->enable = wp_mixer_api_enable;
  plugin_class->disable = wp_mixer_api_disable;

  g_object_class_install_property (object_class, PROP_SCALE,
      g_param_spec_enum ("scale", "scale", "scale",
          wp_mixer_api_volume_scale_enum_get_type (),
          SCALE_LINEAR, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  signals[ACTION_SET_VOLUME] = g_signal_new_class_handler (
      "set-volume", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_mixer_api_set_volume,
      NULL, NULL, NULL,
      G_TYPE_BOOLEAN, 2, G_TYPE_UINT, G_TYPE_VARIANT);

  signals[ACTION_GET_VOLUME] = g_signal_new_class_handler (
      "get-volume", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_mixer_api_get_volume,
      NULL, NULL, NULL,
      G_TYPE_VARIANT, 1, G_TYPE_UINT);

  signals[SIGNAL_CHANGED] = g_signal_new (
      "changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_UINT);
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (g_object_new (wp_mixer_api_get_type (),
      "name", "mixer-api",
      "core", core,
      NULL));
}
 070701000000F0000081A400000000000000000000000165F8630400001FF1000000000000000000000000000000000000003A00000000wireplumber-0.5.0/modules/module-portal-permissionstore.c /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include "dbus-connection-state.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("m-portal-permissionstore")

#define DBUS_INTERFACE_NAME "org.freedesktop.impl.portal.PermissionStore"
#define DBUS_OBJECT_PATH "/org/freedesktop/impl/portal/PermissionStore"

enum
{
  ACTION_GET_DBUS,
  ACTION_LOOKUP,
  ACTION_SET,
  SIGNAL_CHANGED,
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

struct _WpPortalPermissionStorePlugin
{
  WpPlugin parent;

  WpPlugin *dbus;
  guint signal_id;
};

G_DECLARE_FINAL_TYPE (WpPortalPermissionStorePlugin,
    wp_portal_permissionstore_plugin, WP, PORTAL_PERMISSIONSTORE_PLUGIN,
    WpPlugin)
G_DEFINE_TYPE (WpPortalPermissionStorePlugin, wp_portal_permissionstore_plugin,
    WP_TYPE_PLUGIN)

static gpointer
wp_portal_permissionstore_plugin_get_dbus (WpPortalPermissionStorePlugin *self)
{
  return self->dbus ? g_object_ref (self->dbus) : NULL;
}

static GVariant *
wp_portal_permissionstore_plugin_lookup (WpPortalPermissionStorePlugin *self,
    const gchar *table, const gchar *id)
{
  g_autoptr (GDBusConnection) conn = NULL;
  g_autoptr (GError) error = NULL;
  g_autoptr (GVariant) res = NULL;
  GVariant *permissions = NULL, *data = NULL;

  g_object_get (self->dbus, "connection", &conn, NULL);
  g_return_val_if_fail (conn, NULL);

  /* Lookup */
  res = g_dbus_connection_call_sync (conn, DBUS_INTERFACE_NAME,
      DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, "Lookup",
      g_variant_new ("(ss)", table, id), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL,
      &error);
  if (error) {
    wp_warning_object (self, "Failed to call Lookup: %s", error->message);
    return NULL;
  }

  /* Get the permissions */
  g_variant_get (res, "(@a{sas}@v)", &permissions, &data);

  return permissions ? g_variant_ref (permissions) : NULL;
}

static void
wp_portal_permissionstore_plugin_set (WpPortalPermissionStorePlugin *self,
    const gchar *table, gboolean create, const gchar *id, GVariant *permissions)
{
  g_autoptr (GDBusConnection) conn = NULL;
  g_autoptr (GError) error = NULL;
  g_autoptr (GVariant) res = NULL;
  GVariant *data = NULL;

  g_object_get (self->dbus, "connection", &conn, NULL);
  g_return_if_fail (conn);

  /* Set */
  res = g_dbus_connection_call_sync (conn, DBUS_INTERFACE_NAME,
      DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, "Set",
      g_variant_new ("(sbs@a{sas}@v)", table, id, permissions, data), NULL,
      G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
  if (error)
    wp_warning_object (self, "Failed to call Set: %s", error->message);
}

static void
wp_portal_permissionstore_plugin_changed (GDBusConnection *connection,
    const gchar *sender_name, const gchar *object_path,
    const gchar *interface_name, const gchar *signal_name,
    GVariant *parameters, gpointer user_data)
{
  WpPortalPermissionStorePlugin *self =
      WP_PORTAL_PERMISSIONSTORE_PLUGIN (user_data);
  const char *table = NULL, *id = NULL;
  gboolean deleted = FALSE;
  GVariant *permissions = NULL, *data = NULL;

  g_return_if_fail (parameters);
  g_variant_get (parameters, "(ssb@v@a{sas})", &table, &id, &deleted, &data,
      &permissions);

  g_signal_emit (self, signals[SIGNAL_CHANGED], 0, table, id, deleted,
      permissions);
}

static void
clear_signal (WpPortalPermissionStorePlugin *self)
{
  g_autoptr (GDBusConnection) conn = NULL;

  g_object_get (self->dbus, "connection", &conn, NULL);
  if (conn && self->signal_id > 0) {
    g_dbus_connection_signal_unsubscribe (conn, self->signal_id);
    self->signal_id = 0;
  }
}

static void
on_dbus_state_changed (GObject * dbus, GParamSpec * spec,
    WpPortalPermissionStorePlugin *self)
{
  WpDBusConnectionState state = -1;
  g_object_get (dbus, "state", &state, NULL);

  switch (state) {
    case WP_DBUS_CONNECTION_STATE_CONNECTED: {
      g_autoptr (GDBusConnection) conn = NULL;

      g_object_get (dbus, "connection", &conn, NULL);
      g_return_if_fail (conn);

      self->signal_id = g_dbus_connection_signal_subscribe (conn,
          DBUS_INTERFACE_NAME, DBUS_INTERFACE_NAME, "Changed", NULL, NULL,
          G_DBUS_SIGNAL_FLAGS_NONE, wp_portal_permissionstore_plugin_changed,
          self, NULL);
      break;
    }

    case WP_DBUS_CONNECTION_STATE_CONNECTING:
    case WP_DBUS_CONNECTION_STATE_CLOSED:
      clear_signal (self);
      break;

    default:
      break;
  }
}

static void
wp_portal_permissionstore_plugin_init (WpPortalPermissionStorePlugin * self)
{
}

static void
wp_portal_permissionstore_plugin_enable (WpPlugin * plugin,
    WpTransition * transition)
{
  WpPortalPermissionStorePlugin *self =
      WP_PORTAL_PERMISSIONSTORE_PLUGIN (plugin);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));

  self->dbus = wp_plugin_find (core, "dbus-connection");
  if (!self->dbus) {
    wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_INVARIANT,
        "dbus-connection module must be loaded before portal-permissionstore"));
    return;
  }

  g_signal_connect_object (self->dbus, "notify::state",
       G_CALLBACK (on_dbus_state_changed), self, 0);
  on_dbus_state_changed (G_OBJECT (self->dbus), NULL, self);

  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
wp_portal_permissionstore_plugin_disable (WpPlugin * plugin)
{
  WpPortalPermissionStorePlugin *self =
      WP_PORTAL_PERMISSIONSTORE_PLUGIN (plugin);

  clear_signal (self);
  g_clear_object (&self->dbus);

  wp_object_update_features (WP_OBJECT (self), 0, WP_PLUGIN_FEATURE_ENABLED);
}

static void
wp_portal_permissionstore_plugin_class_init (
    WpPortalPermissionStorePluginClass * klass)
{
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  plugin_class->enable = wp_portal_permissionstore_plugin_enable;
  plugin_class->disable = wp_portal_permissionstore_plugin_disable;

  /**
   * WpPortalPermissionStorePlugin::get-dbus:
   *
   * Returns: (transfer full): the dbus object
   */
  signals[ACTION_GET_DBUS] = g_signal_new_class_handler (
      "get-dbus", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_portal_permissionstore_plugin_get_dbus,
      NULL, NULL, NULL,
      G_TYPE_OBJECT, 0);

  /**
   * WpPortalPermissionStorePlugin::lookup:
   *
   * @brief
   * @em table: the table name
   * @em id: the Id name
   *
   * Returns: (transfer full): the GVariant with permissions
   */
  signals[ACTION_LOOKUP] = g_signal_new_class_handler (
      "lookup", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_portal_permissionstore_plugin_lookup,
      NULL, NULL, NULL, G_TYPE_VARIANT,
      2, G_TYPE_STRING, G_TYPE_STRING);

  /**
   * WpPortalPermissionStorePlugin::set:
   *
   * @brief
   * @em table: the table name
   * @em create: whether to create the table if it does not exist
   * @em id: the Id name
   * @em permissions: the permissions
   *
   * Sets the permissions in the permission store
   */
  signals[ACTION_SET] = g_signal_new_class_handler (
      "set", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_portal_permissionstore_plugin_set,
      NULL, NULL, NULL, G_TYPE_NONE,
      4, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_VARIANT);

  /**
   * WpPortalPermissionStorePlugin::changed:
   *
   * @brief
   * @em table: the table name
   * @em id: the Id name
   * @em deleted: whether the permission was deleted or not
   * @em permissions: the GVariant with permissions
   *
   * Signaled when the permissions changed
   */
  signals[SIGNAL_CHANGED] = g_signal_new (
      "changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0,
      NULL, NULL, NULL, G_TYPE_NONE, 4,
      G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_VARIANT);
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (g_object_new (
      wp_portal_permissionstore_plugin_get_type(),
      "name", "portal-permissionstore",
      "core", core,
      NULL));
}
   070701000000F1000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000003000000000wireplumber-0.5.0/modules/module-reserve-device   070701000000F2000081A400000000000000000000000165F8630400000177000000000000000000000000000000000000003C00000000wireplumber-0.5.0/modules/module-reserve-device/meson.build   reserve_device_interface_src = gnome.gdbus_codegen('reserve-device-interface',
  sources: 'org.freedesktop.ReserveDevice1.xml',
  interface_prefix : 'org.freedesktop.ReserveDevice1.',
  namespace : 'Wp'
)

reserve_device_enums = gnome.mkenums_simple('reserve-device-enums',
  sources: [ 'plugin.h', 'reserve-device.h' ],
)

reserve_device_includes = include_directories('.')
 070701000000F3000081A400000000000000000000000165F8630400000193000000000000000000000000000000000000005300000000wireplumber-0.5.0/modules/module-reserve-device/org.freedesktop.ReserveDevice1.xml    <node>
 <interface name="org.freedesktop.ReserveDevice1">
  <method name="RequestRelease">
   <arg name="priority" type="i" direction="in"/>
   <arg name="result" type="b" direction="out"/>
  </method>
  <property name="Priority" type="i" access="read"/>
  <property name="ApplicationName" type="s" access="read"/>
  <property name="ApplicationDeviceName" type="s" access="read"/>
 </interface>
</node>
 070701000000F4000081A400000000000000000000000165F8630400001C9B000000000000000000000000000000000000003900000000wireplumber-0.5.0/modules/module-reserve-device/plugin.c  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "plugin.h"
#include "reserve-device.h"
#include "reserve-device-enums.h"
#include "../dbus-connection-state.h"

WP_LOG_TOPIC (log_topic_rd, "m-reserve-device")

G_DEFINE_TYPE (WpReserveDevicePlugin, wp_reserve_device_plugin, WP_TYPE_PLUGIN)

enum
{
  ACTION_CREATE_RESERVATION,
  ACTION_DESTROY_RESERVATION,
  ACTION_GET_RESERVATION,
  ACTION_GET_DBUS,
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

static void
rd_unref (gpointer data)
{
  WpReserveDevice *rd = data;
  g_signal_emit_by_name (rd, "release");
  g_object_unref (rd);
}

static void
clear_reservation (WpReserveDevicePlugin *self)
{
  g_hash_table_remove_all (self->reserve_devices);
  g_clear_object (&self->manager);
}

static void
on_dbus_state_changed (GObject * dbus, GParamSpec * spec,
    WpReserveDevicePlugin *self)
{
  WpDBusConnectionState state = -1;
  g_object_get (dbus, "state", &state, NULL);

  switch (state) {
    case WP_DBUS_CONNECTION_STATE_CONNECTED: {
      g_autoptr (GDBusConnection) conn = NULL;

      g_object_get (dbus, "connection", &conn, NULL);
      g_return_if_fail (conn);

      self->manager = g_dbus_object_manager_server_new (
          FDO_RESERVE_DEVICE1_PATH);
      g_dbus_object_manager_server_set_connection (self->manager, conn);
      break;
    }

    case WP_DBUS_CONNECTION_STATE_CONNECTING:
    case WP_DBUS_CONNECTION_STATE_CLOSED:
      clear_reservation (self);
      break;

    default:
      g_assert_not_reached ();
  }
}

static void
wp_reserve_device_plugin_init (WpReserveDevicePlugin * self)
{
  self->reserve_devices = g_hash_table_new_full (g_str_hash, g_str_equal,
      NULL, rd_unref);
}

static void
wp_reserve_device_plugin_finalize (GObject * object)
{
  WpReserveDevicePlugin *self = WP_RESERVE_DEVICE_PLUGIN (object);

  g_clear_pointer (&self->reserve_devices, g_hash_table_unref);

  G_OBJECT_CLASS (wp_reserve_device_plugin_parent_class)->finalize (object);
}

static void
wp_reserve_device_plugin_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpReserveDevicePlugin *self = WP_RESERVE_DEVICE_PLUGIN (plugin);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));

  self->dbus = wp_plugin_find (core, "dbus-connection");
  if (!self->dbus) {
    wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_INVARIANT,
        "dbus-connection module must be loaded before reserve-device"));
    return;
  }

  g_signal_connect_object (self->dbus, "notify::state",
       G_CALLBACK (on_dbus_state_changed), self, 0);
  on_dbus_state_changed (G_OBJECT (self->dbus), NULL, self);

  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
wp_reserve_device_plugin_disable (WpPlugin * plugin)
{
  WpReserveDevicePlugin *self = WP_RESERVE_DEVICE_PLUGIN (plugin);

  clear_reservation (self);
  g_clear_object (&self->dbus);

  wp_object_update_features (WP_OBJECT (self), 0, WP_PLUGIN_FEATURE_ENABLED);
}

static gpointer
wp_reserve_device_plugin_create_reservation (WpReserveDevicePlugin *self,
    const gchar *name, const gchar *app_name, const gchar *app_dev_name,
    gint priority)
{
  WpDBusConnectionState state = WP_DBUS_CONNECTION_STATE_CLOSED;
  g_object_get (self->dbus, "state", &state, NULL);

  if (state != WP_DBUS_CONNECTION_STATE_CONNECTED) {
    wp_notice_object (self, "not connected to D-Bus");
    return NULL;
  }

  WpReserveDevice *rd = g_object_new (wp_reserve_device_get_type (),
      "plugin", self,
      "name", name,
      "application-name", app_name,
      "application-device-name", app_dev_name,
      "priority", priority,
      NULL);

  /* use rd->name to avoid copying @em name again */
  g_hash_table_replace (self->reserve_devices, rd->name, rd);

  return g_object_ref (rd);
}

static void
wp_reserve_device_plugin_destroy_reservation (WpReserveDevicePlugin *self,
    const gchar *name)
{
  WpDBusConnectionState state = WP_DBUS_CONNECTION_STATE_CLOSED;
  g_object_get (self->dbus, "state", &state, NULL);

  if (state != WP_DBUS_CONNECTION_STATE_CONNECTED) {
    wp_notice_object (self, "not connected to D-Bus");
    return;
  }
  g_hash_table_remove (self->reserve_devices, name);
}

static gpointer
wp_reserve_device_plugin_get_reservation (WpReserveDevicePlugin *self,
    const gchar *name)
{
  WpDBusConnectionState state = WP_DBUS_CONNECTION_STATE_CLOSED;
  g_object_get (self->dbus, "state", &state, NULL);

  if (state != WP_DBUS_CONNECTION_STATE_CONNECTED) {
    wp_notice_object (self, "not connected to D-Bus");
    return NULL;
  }

  WpReserveDevice *rd = g_hash_table_lookup (self->reserve_devices, name);
  return rd ? g_object_ref (rd) : NULL;
}

static gpointer
wp_reserve_device_plugin_get_dbus (WpReserveDevicePlugin *self)
{
  return self->dbus ? g_object_ref (self->dbus) : NULL;
}

static void
wp_reserve_device_plugin_class_init (WpReserveDevicePluginClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  object_class->finalize = wp_reserve_device_plugin_finalize;

  plugin_class->enable = wp_reserve_device_plugin_enable;
  plugin_class->disable = wp_reserve_device_plugin_disable;

  /**
   * WpReserveDevicePlugin::create-reservation:
   *
   * @brief
   * @em name:
   * @em app_name:
   * @em app_dev_name:
   * @em priority:
   *
   * Returns: (transfer full): the reservation object
   */
  signals[ACTION_CREATE_RESERVATION] = g_signal_new_class_handler (
      "create-reservation", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_plugin_create_reservation,
      NULL, NULL, NULL,
      G_TYPE_OBJECT, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT);

  /**
   * WpReserveDevicePlugin::destroy-reservation:
   *
   * @brief
   * @em name:
   *
   */
  signals[ACTION_DESTROY_RESERVATION] = g_signal_new_class_handler (
      "destroy-reservation", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_plugin_destroy_reservation,
      NULL, NULL, NULL,
      G_TYPE_NONE, 1, G_TYPE_STRING);

  /**
   * WpReserveDevicePlugin::get-reservation:
   *
   * @brief
   * @em name:
   *
   * Returns: (transfer full): the reservation object
   */
  signals[ACTION_GET_RESERVATION] = g_signal_new_class_handler (
      "get-reservation", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_plugin_get_reservation,
      NULL, NULL, NULL,
      G_TYPE_OBJECT, 1, G_TYPE_STRING);

  /**
   * WpReserveDevicePlugin::get-dbus:
   *
   * Returns: (transfer full): the dbus object
   */
  signals[ACTION_GET_DBUS] = g_signal_new_class_handler (
      "get-dbus", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_plugin_get_dbus,
      NULL, NULL, NULL,
      G_TYPE_OBJECT, 0);

}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (g_object_new (wp_reserve_device_plugin_get_type (),
      "name", "reserve-device",
      "core", core,
      NULL));
}
 070701000000F5000081A400000000000000000000000165F863040000030E000000000000000000000000000000000000003900000000wireplumber-0.5.0/modules/module-reserve-device/plugin.h  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_RESERVE_DEVICE_PLUGIN_H__
#define __WIREPLUMBER_RESERVE_DEVICE_PLUGIN_H__

#include <wp/wp.h>

G_BEGIN_DECLS

#define WP_LOCAL_LOG_TOPIC log_topic_rd
WP_LOG_TOPIC_EXTERN (log_topic_rd)

#define FDO_RESERVE_DEVICE1_SERVICE "org.freedesktop.ReserveDevice1"
#define FDO_RESERVE_DEVICE1_PATH "/org/freedesktop/ReserveDevice1"

G_DECLARE_FINAL_TYPE (WpReserveDevicePlugin, wp_reserve_device_plugin,
    WP, RESERVE_DEVICE_PLUGIN, WpPlugin)

struct _WpReserveDevicePlugin
{
  WpPlugin parent;

  WpPlugin *dbus;
  GHashTable *reserve_devices;
  GDBusObjectManagerServer *manager;
};

G_END_DECLS

#endif
  070701000000F6000081A400000000000000000000000165F86304000041A4000000000000000000000000000000000000004100000000wireplumber-0.5.0/modules/module-reserve-device/reserve-device.c  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "reserve-device.h"
#include "plugin.h"
#include "transitions.h"
#include "reserve-device-interface.h"
#include "reserve-device-enums.h"

/*
 * WpReserveDevice:
 */
G_DEFINE_TYPE (WpReserveDevice, wp_reserve_device, G_TYPE_OBJECT)

enum
{
  ACTION_ACQUIRE,
  ACTION_RELEASE,
  ACTION_DENY_RELEASE,
  SIGNAL_RELEASE_REQUESTED,
  LAST_SIGNAL
};

enum
{
  PROP_0,
  PROP_PLUGIN,
  PROP_NAME,
  PROP_APP_NAME,
  PROP_APP_DEV_NAME,
  PROP_PRIORITY,
  PROP_STATE,
  PROP_OWNER_APP_NAME,
};

static guint signals[LAST_SIGNAL] = { 0 };

static void
wp_reserve_device_init (WpReserveDevice * self)
{
  g_weak_ref_init (&self->plugin, NULL);
  g_weak_ref_init (&self->transition, NULL);
}

static void
on_got_proxy (GObject * src, GAsyncResult * res, WpReserveDevice *self)
{
  g_autoptr (GError) error = NULL;
  g_autoptr (WpOrgFreedesktopReserveDevice1) proxy =
      wp_org_freedesktop_reserve_device1_proxy_new_finish (res, &error);
  if (!proxy) {
    wp_info_object (self, "%s: Could not get proxy of remote reservation: %s",
        self->name, error->message);
    return;
  }

  wp_debug_object (self, "%s owned by: %s", self->name,
      wp_org_freedesktop_reserve_device1_get_application_name (proxy));

  /* ensure that we are still busy and there is no owner_app_name */
  if (self->state == WP_RESERVE_DEVICE_STATE_BUSY && !self->owner_app_name) {
    self->owner_app_name =
        wp_org_freedesktop_reserve_device1_dup_application_name (proxy);
    g_object_notify (G_OBJECT (self), "owner-application-name");
  }
}

static void
update_owner_app_name (WpReserveDevice *self)
{
  if (self->state == WP_RESERVE_DEVICE_STATE_BUSY && !self->owner_app_name) {
    /* create proxy */
    g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
    g_autoptr (GDBusConnection) conn = NULL;
    g_object_get (plugin->dbus, "connection", &conn, NULL);
    wp_org_freedesktop_reserve_device1_proxy_new (conn,
        G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
        G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
        self->service_name, self->object_path, NULL,
        (GAsyncReadyCallback) on_got_proxy, self);
  }
  else if (self->state != WP_RESERVE_DEVICE_STATE_BUSY && self->owner_app_name) {
    g_clear_pointer (&self->owner_app_name, g_free);
    g_object_notify (G_OBJECT (self), "owner-application-name");
  }
}

static void
on_name_appeared (GDBusConnection *connection, const gchar *name,
    const gchar *owner, gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);

  if (!t || wp_transition_get_completed (t)) {
    self->state = WP_RESERVE_DEVICE_STATE_BUSY;
    wp_info_object (self, "%s busy (by %s)", name, owner);
    g_object_notify (G_OBJECT (self), "state");
    update_owner_app_name (self);
  }
}

static void
on_name_vanished (GDBusConnection *connection, const gchar *name,
    gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);

  if (!t || wp_transition_get_completed (t)) {
    self->state = WP_RESERVE_DEVICE_STATE_AVAILABLE;
    wp_info_object (self, "%s released", name);
    g_object_notify (G_OBJECT (self), "state");
    update_owner_app_name (self);
  }
}

static void
wp_reserve_device_constructed (GObject * object)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (object);
  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  g_autoptr (GDBusConnection) conn = NULL;

  g_object_get (plugin->dbus, "connection", &conn, NULL);

  self->service_name =
      g_strdup_printf (FDO_RESERVE_DEVICE1_SERVICE ".%s", self->name);
  self->object_path =
      g_strdup_printf (FDO_RESERVE_DEVICE1_PATH "/%s", self->name);

  /* Watch for the name */
  self->watcher_id = g_bus_watch_name_on_connection (conn,
      self->service_name, G_BUS_NAME_WATCHER_FLAGS_NONE,
      on_name_appeared, on_name_vanished, self, NULL);

  G_OBJECT_CLASS (wp_reserve_device_parent_class)->constructed (object);
}

static void
wp_reserve_device_finalize (GObject * object)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (object);

  if (self->watcher_id > 0)
    g_bus_unwatch_name (self->watcher_id);
  if (self->owner_id > 0)
    g_bus_unown_name (self->owner_id);

  g_weak_ref_clear (&self->plugin);
  g_weak_ref_clear (&self->transition);
  g_clear_pointer (&self->name, g_free);
  g_clear_pointer (&self->app_name, g_free);
  g_clear_pointer (&self->app_dev_name, g_free);
  g_clear_pointer (&self->service_name, g_free);
  g_clear_pointer (&self->object_path, g_free);

  G_OBJECT_CLASS (wp_reserve_device_parent_class)->finalize (object);
}

static void
on_acquire_transition_done (GObject *rd, GAsyncResult *res, gpointer data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (data);
  g_autoptr (GError) error = NULL;

  gboolean acquired = wp_reserve_device_acquire_transition_finish (res, &error);
  if (error) {
    wp_notice_object (self, "%s: Acquire error: %s", self->name,
        error->message);
  }

  self->state = acquired ?
      WP_RESERVE_DEVICE_STATE_ACQUIRED : WP_RESERVE_DEVICE_STATE_BUSY;
  g_object_notify (G_OBJECT (self), "state");
  update_owner_app_name (self);
}

static void
wp_reserve_device_acquire (WpReserveDevice * self)
{
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);
  if (self->state == WP_RESERVE_DEVICE_STATE_ACQUIRED ||
    (t && !wp_transition_get_completed (t))) {
    wp_debug_object (self, "%s: already acquired or operation in progress",
        self->name);
    return;
  }

  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  WpTransition *transition = wp_reserve_device_acquire_transition_new (self,
      NULL, on_acquire_transition_done, self);
  g_weak_ref_set (&self->transition, transition);
  wp_transition_advance (transition);
}

static void
wp_reserve_device_release (WpReserveDevice * self)
{
  if (self->state != WP_RESERVE_DEVICE_STATE_ACQUIRED) {
    wp_debug_object (self, "%s: not acquired", self->name);
    return;
  }

  /* set state to AVAILABLE to ensure that on_name_lost()
     does not emit SIGNAL_REQUEST_RELEASE */
  /* on_name_vanished() will emit the state change */
  self->state = WP_RESERVE_DEVICE_STATE_AVAILABLE;
  wp_reserve_device_unown_name (self);

  if (self->req_rel_invocation) {
    wp_org_freedesktop_reserve_device1_complete_request_release (NULL,
        self->req_rel_invocation, TRUE);
    self->req_rel_invocation = NULL;
  }
}

static void
wp_reserve_device_deny_release (WpReserveDevice * self, gboolean success)
{
  if (self->req_rel_invocation) {
    wp_org_freedesktop_reserve_device1_complete_request_release (NULL,
        self->req_rel_invocation, FALSE);
    self->req_rel_invocation = NULL;
  }
}

static gboolean
wp_reserve_device_handle_request_release (WpOrgFreedesktopReserveDevice1 *iface,
    GDBusMethodInvocation *invocation, gint priority, gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);

  /* deny release if the priority is lower than ours */
  if (priority < self->priority) {
    wp_org_freedesktop_reserve_device1_complete_request_release (iface,
        g_object_ref (invocation), FALSE);
    return TRUE;
  }

  /* else, request the release of the device from the implementation;
     if signal handlers are connected, assume the functionality is implemented,
     otherwise return FALSE to let the iface return UnknownMethod */
  if (g_signal_has_handler_pending (self,
          signals[SIGNAL_RELEASE_REQUESTED], 0, FALSE)) {
    self->req_rel_invocation = g_object_ref (invocation);
    g_signal_emit (self, signals[SIGNAL_RELEASE_REQUESTED], 0, FALSE);
    return TRUE;
  }
  return FALSE;
}

static void
wp_reserve_device_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (object);

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, self->name);
    break;
  case PROP_APP_NAME:
    g_value_set_string (value, self->app_name);
    break;
  case PROP_APP_DEV_NAME:
    g_value_set_string (value, self->app_dev_name);
    break;
  case PROP_PRIORITY:
    g_value_set_int (value, self->priority);
    break;
  case PROP_STATE:
    g_value_set_enum (value, self->state);
    break;
  case PROP_OWNER_APP_NAME:
    switch (self->state) {
    case WP_RESERVE_DEVICE_STATE_ACQUIRED:
      g_value_set_string (value, self->app_name);
      break;
    default:
      g_value_set_string (value, self->owner_app_name);
      break;
    }
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_reserve_device_set_property (GObject * object,
    guint property_id, const GValue * value, GParamSpec * pspec)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (object);

  switch (property_id) {
  case PROP_PLUGIN:
    g_weak_ref_set (&self->plugin, g_value_get_object (value));
    break;
  case PROP_NAME:
    g_clear_pointer (&self->name, g_free);
    self->name = g_value_dup_string (value);
    break;
  case PROP_APP_NAME:
    g_clear_pointer (&self->app_name, g_free);
    self->app_name = g_value_dup_string (value);
    break;
  case PROP_APP_DEV_NAME:
    g_clear_pointer (&self->app_dev_name, g_free);
    self->app_dev_name = g_value_dup_string (value);
    break;
  case PROP_PRIORITY:
    self->priority = g_value_get_int(value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

/**
* wp_reserve_device_class_init
*
* @param klass: the reserve device class
*/
static void
wp_reserve_device_class_init (WpReserveDeviceClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->constructed = wp_reserve_device_constructed;
  object_class->finalize = wp_reserve_device_finalize;
  object_class->get_property = wp_reserve_device_get_property;
  object_class->set_property = wp_reserve_device_set_property;

  g_object_class_install_property (object_class, PROP_PLUGIN,
      g_param_spec_object ("plugin", "plugin",
          "The parent plugin instance", wp_reserve_device_plugin_get_type (),
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name",
          "The reservation name", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_APP_NAME,
      g_param_spec_string ("application-name", "application-name",
          "The application name", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_APP_DEV_NAME,
      g_param_spec_string ("application-device-name", "application-device-name",
          "The application device name", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PRIORITY,
      g_param_spec_int ("priority", "priority",
          "The priority", G_MININT, G_MAXINT, 0,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_STATE,
      g_param_spec_enum ("state", "state", "The state",
          WP_TYPE_RESERVE_DEVICE_STATE, WP_RESERVE_DEVICE_STATE_UNKNOWN,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_OWNER_APP_NAME,
      g_param_spec_string ("owner-application-name", "owner-application-name",
          "The owner application name", NULL,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /**
   * WpReserveDevice acquire:
   *
   * @section signal_acquire_section acquire
   */
  signals[ACTION_ACQUIRE] = g_signal_new_class_handler (
      "acquire", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_acquire,
      NULL, NULL, NULL, G_TYPE_NONE, 0);

  /**
   * WpReserveDevice release:
   *
   * @section signal_release_section release
   */
  signals[ACTION_RELEASE] = g_signal_new_class_handler (
      "release", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_release,
      NULL, NULL, NULL, G_TYPE_NONE, 0);

  /**
   * WpReserveDevice deny-release:
   *
   * @section signal_deny_release_section deny-release
   */
  signals[ACTION_DENY_RELEASE] = g_signal_new_class_handler (
      "deny-release", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_reserve_device_deny_release,
      NULL, NULL, NULL, G_TYPE_NONE, 0);

  /**
   * WpReserveDevice release-requested:
   *
   * @section signal_release_requested_section release-requested
   *
   * @em forced: %TRUE if the name was forcibly taken from us,
   *    %FALSE if the `RequestRelease()` d-bus method was called
   *
   * @brief Signaled when the device needs to be released. If @em forced is %FALSE,
   * call [release](@ref signal_release_section) to release or
   * [deny-release](@ref signal_deny_release_section)
   * to refuse and return %FALSE from the `RequestRelease()` d-bus method.
   */
  signals[SIGNAL_RELEASE_REQUESTED] = g_signal_new (
      "release-requested", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
      G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
}

void
wp_reserve_device_export_object (WpReserveDevice *self)
{
  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  if (plugin) {
    g_autoptr (GDBusObjectSkeleton) skeleton =
        g_dbus_object_skeleton_new (self->object_path);
    g_autoptr (WpOrgFreedesktopReserveDevice1) iface =
        wp_org_freedesktop_reserve_device1_skeleton_new ();
    g_object_set (iface,
        "priority", self->priority,
        "application-name", self->app_name,
        "application-device-name", self->app_dev_name,
        NULL);
    g_signal_connect_object (iface, "handle-request-release",
        (GCallback) wp_reserve_device_handle_request_release, self, 0);
    g_dbus_object_skeleton_add_interface (skeleton,
        G_DBUS_INTERFACE_SKELETON (iface));
    g_dbus_object_manager_server_export (plugin->manager, skeleton);
  }
}

void
wp_reserve_device_unexport_object (WpReserveDevice *self)
{
  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  if (plugin) {
    wp_debug_object (self, "unexport %s", self->object_path);
    g_dbus_object_manager_server_unexport (plugin->manager, self->object_path);
  }
}

static void
on_name_acquired (GDBusConnection *connection, const gchar *name,
    gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);

  wp_debug_object (self, "%s acquired", name);

  if (t)
    wp_reserve_device_acquire_transition_name_acquired (t);
}

static void
on_name_lost (GDBusConnection *connection, const gchar *name,
    gpointer user_data)
{
  WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
  g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);

  wp_debug_object (self, "%s lost", name);

  if (t) {
    wp_reserve_device_acquire_transition_name_lost (t);
    return;
  }

  if (self->state == WP_RESERVE_DEVICE_STATE_ACQUIRED) {
    /* Emit release signal with forced set to TRUE */
    g_signal_emit (self, signals[SIGNAL_RELEASE_REQUESTED], 0, TRUE);
    wp_reserve_device_unown_name (self);
  }

  wp_reserve_device_unexport_object (self);
}

void
wp_reserve_device_own_name (WpReserveDevice * self, gboolean force)
{
  g_return_if_fail (self->owner_id == 0);

  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
  if (plugin) {
    g_autoptr (GDBusConnection) conn = NULL;
    g_object_get (plugin->dbus, "connection", &conn, NULL);

    GBusNameOwnerFlags flags = G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE;
    if (self->priority != G_MAXINT32)
      flags |= G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT;
    if (force)
      flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE;

    wp_debug_object (self, "request ownership of %s", self->service_name);

    self->owner_id = g_bus_own_name_on_connection (conn,
        self->service_name, flags, on_name_acquired, on_name_lost, self, NULL);
  }
}

void
wp_reserve_device_unown_name (WpReserveDevice * self)
{
  if (self->owner_id) {
    wp_debug_object (self, "drop ownership of %s", self->service_name);
    g_bus_unown_name (self->owner_id);
    self->owner_id = 0;
  }
}
070701000000F7000081A400000000000000000000000165F86304000004A4000000000000000000000000000000000000004100000000wireplumber-0.5.0/modules/module-reserve-device/reserve-device.h  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_RESERVE_DEVICE_H__
#define __WIREPLUMBER_RESERVE_DEVICE_H__

#include <wp/wp.h>

G_BEGIN_DECLS

typedef enum {
  WP_RESERVE_DEVICE_STATE_UNKNOWN = 0,
  WP_RESERVE_DEVICE_STATE_BUSY,
  WP_RESERVE_DEVICE_STATE_AVAILABLE,
  WP_RESERVE_DEVICE_STATE_ACQUIRED,
} WpReserveDeviceState;

G_DECLARE_FINAL_TYPE (WpReserveDevice, wp_reserve_device,
    WP, RESERVE_DEVICE, GObject)

struct _WpReserveDevice
{
  GObject parent;

  GWeakRef plugin;
  gchar *name;
  gchar *app_name;
  gchar *app_dev_name;
  gint priority;
  gchar *owner_app_name;

  gchar *service_name;
  gchar *object_path;

  GWeakRef transition;
  GDBusMethodInvocation *req_rel_invocation;
  WpReserveDeviceState state;
  guint watcher_id;
  guint owner_id;
};

void wp_reserve_device_export_object (WpReserveDevice *self);
void wp_reserve_device_unexport_object (WpReserveDevice *self);

void wp_reserve_device_own_name (WpReserveDevice * self, gboolean force);
void wp_reserve_device_unown_name (WpReserveDevice * self);

G_END_DECLS

#endif
070701000000F8000081A400000000000000000000000165F8630400001C70000000000000000000000000000000000000003E00000000wireplumber-0.5.0/modules/module-reserve-device/transitions.c /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "transitions.h"
#include "plugin.h"
#include "reserve-device.h"
#include "reserve-device-interface.h"

struct _WpReserveDeviceAcquireTransition
{
  WpTransition parent;
  gint owner_state;
  WpOrgFreedesktopReserveDevice1 *proxy;
};

enum {
  OWNER_STATE_NONE = 0,
  OWNER_STATE_ACQUIRED,
  OWNER_STATE_LOST,
};

enum {
  STEP_EXPORT_OBJECT = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_ACQUIRE_NO_FORCE,
  STEP_GET_PROXY,
  STEP_REQUEST_RELEASE,
  STEP_ACQUIRE_WITH_FORCE,
  STEP_UNEXPORT_OBJECT,
};

G_DEFINE_TYPE (WpReserveDeviceAcquireTransition,
    wp_reserve_device_acquire_transition, WP_TYPE_TRANSITION)

static void
wp_reserve_device_acquire_transition_init (
    WpReserveDeviceAcquireTransition * self)
{
}

static void
wp_reserve_device_acquire_transition_finalize (GObject * object)
{
  WpReserveDeviceAcquireTransition *self =
      WP_RESERVE_DEVICE_ACQUIRE_TRANSITION (object);

  g_clear_object (&self->proxy);

  G_OBJECT_CLASS (wp_reserve_device_acquire_transition_parent_class)->
      finalize (object);
}

static guint
wp_reserve_device_acquire_transition_get_next_step (
    WpTransition * transition, guint step)
{
  WpReserveDeviceAcquireTransition *self =
        WP_RESERVE_DEVICE_ACQUIRE_TRANSITION (transition);

  switch (step) {
  case WP_TRANSITION_STEP_NONE:
    return STEP_EXPORT_OBJECT;

  case STEP_EXPORT_OBJECT:
    return STEP_ACQUIRE_NO_FORCE;

  case STEP_ACQUIRE_NO_FORCE:
    switch (self->owner_state) {
      case OWNER_STATE_ACQUIRED:
        return WP_TRANSITION_STEP_NONE;

      case OWNER_STATE_LOST:
        return STEP_GET_PROXY;

      default:
        return WP_TRANSITION_STEP_ERROR;
    }

  case STEP_GET_PROXY:
    if (self->proxy)
      return STEP_REQUEST_RELEASE;
    else
      return STEP_ACQUIRE_WITH_FORCE;

  case STEP_REQUEST_RELEASE:
    switch (self->owner_state) {
      case OWNER_STATE_ACQUIRED:
        return STEP_ACQUIRE_WITH_FORCE;

      case OWNER_STATE_LOST:
        return STEP_UNEXPORT_OBJECT;

      default:
        return WP_TRANSITION_STEP_ERROR;
    }

  case STEP_ACQUIRE_WITH_FORCE:
    return WP_TRANSITION_STEP_NONE;

  case STEP_UNEXPORT_OBJECT:
    return WP_TRANSITION_STEP_NONE;

  default:
    return WP_TRANSITION_STEP_ERROR;
  }
}

static void
on_got_proxy (GObject * src, GAsyncResult * res, WpTransition * transition)
{
  WpReserveDeviceAcquireTransition *self =
      WP_RESERVE_DEVICE_ACQUIRE_TRANSITION (transition);
  g_autoptr (GError) error = NULL;

  self->proxy =
      wp_org_freedesktop_reserve_device1_proxy_new_finish (res, &error);
  if (!self->proxy) {
    WpReserveDevice *rd = wp_transition_get_source_object (transition);
    wp_info_object (rd, "%s: Could not get proxy of remote reservation: %s",
        rd->name, error->message);
  }

  wp_transition_advance (transition);
}

static void
on_request_release_done (GObject * src, GAsyncResult * res,
    WpTransition * transition)
{
  WpReserveDeviceAcquireTransition *self =
      WP_RESERVE_DEVICE_ACQUIRE_TRANSITION (transition);
  g_autoptr (GError) error = NULL;
  gboolean released = FALSE;

  if (!wp_org_freedesktop_reserve_device1_call_request_release_finish (
          self->proxy, &released, res, &error)) {
    WpReserveDevice *rd = wp_transition_get_source_object (transition);
    wp_info_object (rd, "%s: Could not call RequestRelease: %s",
        rd->name, error->message);
  }

  self->owner_state = released ? OWNER_STATE_ACQUIRED : OWNER_STATE_LOST;
  wp_transition_advance (transition);
}

static void
wp_reserve_device_acquire_transition_execute_step (
    WpTransition * transition, guint step)
{
  WpReserveDeviceAcquireTransition *self =
      WP_RESERVE_DEVICE_ACQUIRE_TRANSITION (transition);
  WpReserveDevice *rd = wp_transition_get_source_object (transition);
  g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&rd->plugin);

  if (G_UNLIKELY (!plugin && step != WP_TRANSITION_STEP_ERROR)) {
    wp_transition_return_error (transition, g_error_new (
            WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "plugin destroyed while Acquire was in progress"));
    return;
  }

  switch (step) {
  case STEP_EXPORT_OBJECT:
    wp_reserve_device_export_object (rd);
    wp_transition_advance (transition);
    break;

  case STEP_ACQUIRE_NO_FORCE:
    wp_reserve_device_own_name (rd, FALSE);
    break;

  case STEP_GET_PROXY: {
    g_autoptr (GDBusConnection) conn = NULL;
    g_object_get (plugin->dbus, "connection", &conn, NULL);
    wp_org_freedesktop_reserve_device1_proxy_new (conn,
        G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
        G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
        G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
        rd->service_name, rd->object_path, NULL,
        (GAsyncReadyCallback) on_got_proxy, self);
    break;
  }

  case STEP_REQUEST_RELEASE:
    self->owner_state = OWNER_STATE_NONE;
    wp_org_freedesktop_reserve_device1_call_request_release (
        self->proxy, rd->priority, NULL,
        (GAsyncReadyCallback) on_request_release_done, self);
    break;

  case STEP_ACQUIRE_WITH_FORCE:
    wp_reserve_device_unown_name (rd);
    self->owner_state = OWNER_STATE_NONE;
    wp_reserve_device_own_name (rd, TRUE);
    break;

  case STEP_UNEXPORT_OBJECT:
    wp_reserve_device_unown_name (rd);
    wp_reserve_device_unexport_object (rd);
    wp_transition_advance (transition);
    break;

  case WP_TRANSITION_STEP_ERROR:
    wp_reserve_device_unown_name (rd);
    break;

  default:
    g_return_if_reached ();
  }
}

static void
wp_reserve_device_acquire_transition_class_init (
    WpReserveDeviceAcquireTransitionClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpTransitionClass *transition_class = (WpTransitionClass *) klass;

  object_class->finalize =
      wp_reserve_device_acquire_transition_finalize;

  transition_class->get_next_step =
      wp_reserve_device_acquire_transition_get_next_step;
  transition_class->execute_step =
      wp_reserve_device_acquire_transition_execute_step;
}

WpTransition *
wp_reserve_device_acquire_transition_new (WpReserveDevice *rd,
    GCancellable * cancellable, GAsyncReadyCallback callback,
    gpointer callback_data)
{
  return wp_transition_new (wp_reserve_device_acquire_transition_get_type (),
      rd, cancellable, callback, callback_data);
}

void
wp_reserve_device_acquire_transition_name_acquired (WpTransition * tr)
{
  WpReserveDeviceAcquireTransition *self =
      WP_RESERVE_DEVICE_ACQUIRE_TRANSITION (tr);
  self->owner_state = OWNER_STATE_ACQUIRED;
  wp_transition_advance (tr);
}

void
wp_reserve_device_acquire_transition_name_lost (WpTransition * tr)
{
  WpReserveDeviceAcquireTransition *self =
      WP_RESERVE_DEVICE_ACQUIRE_TRANSITION (tr);
  self->owner_state = OWNER_STATE_LOST;
  wp_transition_advance (tr);
}

gboolean
wp_reserve_device_acquire_transition_finish (GAsyncResult * res,
    GError ** error)
{
  if (!wp_transition_finish (res, error))
    return FALSE;

  WpReserveDeviceAcquireTransition *self =
      WP_RESERVE_DEVICE_ACQUIRE_TRANSITION (res);
  return (self->owner_state == OWNER_STATE_ACQUIRED) ? TRUE : FALSE;
}
070701000000F9000081A400000000000000000000000165F863040000038B000000000000000000000000000000000000003E00000000wireplumber-0.5.0/modules/module-reserve-device/transitions.h /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef __WIREPLUMBER_RESERVE_DEVICE_TRANSITIONS_H__
#define __WIREPLUMBER_RESERVE_DEVICE_TRANSITIONS_H__

#include "reserve-device.h"

G_BEGIN_DECLS

G_DECLARE_FINAL_TYPE (WpReserveDeviceAcquireTransition,
    wp_reserve_device_acquire_transition,
    WP, RESERVE_DEVICE_ACQUIRE_TRANSITION, WpTransition)

WpTransition * wp_reserve_device_acquire_transition_new (WpReserveDevice *rd,
    GCancellable * cancellable, GAsyncReadyCallback callback,
    gpointer callback_data);

void wp_reserve_device_acquire_transition_name_acquired (WpTransition * tr);
void wp_reserve_device_acquire_transition_name_lost (WpTransition * tr);

gboolean wp_reserve_device_acquire_transition_finish (GAsyncResult * res,
    GError ** error);

G_END_DECLS

#endif
 070701000000FA000081A400000000000000000000000165F86304000035BF000000000000000000000000000000000000002C00000000wireplumber-0.5.0/modules/module-settings.c   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <spa/utils/json.h>
#include <spa/utils/defs.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-settings")

/*
 * This module parses the "wireplumber.settings" section from the .conf file.
 *
 * Creates "sm-settings"(default) metadata and pushes the settings to it.
 * Looks out for changes done in the metadata via the pw-metadata interface.
 *
 * If persistent settings is enabled stores the settings in a state file
 * and retains the settings from there on subsequent reboots ignoring the
 * contents of .conf file.
 */

struct _WpSettingsPlugin
{
  WpPlugin parent;

  /* Props */
  gchar *metadata_name;
  gchar *metadata_schema_name;
  gchar *metadata_persistent_name;

  WpImplMetadata *impl_metadata;
  WpImplMetadata *schema_impl_metadata;
  WpImplMetadata *persistent_impl_metadata;
  WpState *state;
  WpProperties *persistent_settings;
};

enum {
  PROP_0,
  PROP_METADATA_NAME,
  PROP_PROPERTIES,
};

G_DECLARE_FINAL_TYPE (WpSettingsPlugin, wp_settings_plugin,
                      WP, SETTINGS_PLUGIN, WpPlugin)
G_DEFINE_TYPE (WpSettingsPlugin, wp_settings_plugin, WP_TYPE_PLUGIN)

#define NAME "sm-settings"

static void
wp_settings_plugin_init (WpSettingsPlugin * self)
{
}

static void
on_persistent_metadata_changed (WpMetadata *m, guint32 subject,
   const gchar *key, const gchar *type, const gchar *value, gpointer d)
{
  WpSettingsPlugin *self = WP_SETTINGS_PLUGIN (d);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));

  /* Update persistent settings with new value and timeout save it */
  wp_properties_set (self->persistent_settings, key, value);
  if (value)
    wp_info_object (self, "persistent setting updated: %s = %s", key, value);
  else
    wp_info_object (self, "persistent setting removed: %s", key);
  wp_state_save_after_timeout (self->state, core, self->persistent_settings);

  /* Also update current settings with new value */
  if (value)
    wp_metadata_set (WP_METADATA (self->impl_metadata), 0, key, type, value);
}

WpProperties *
load_configuration_settings (WpSettingsPlugin *self)
{
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_autoptr (WpConf) conf = NULL;
  g_autoptr (WpSpaJson) json = NULL;
  g_autoptr (WpProperties) res = wp_properties_new_empty ();

  g_return_val_if_fail (core, NULL);
  conf = wp_core_get_conf (core);
  g_return_val_if_fail (conf, NULL);

  json = wp_conf_get_section (conf, "wireplumber.settings");
  if (!json)
    return g_steal_pointer (&res);

  if (!wp_spa_json_is_object (json)) {
    wp_warning_object (self,
        "ignoring wireplumber.settings from conf as it isn't a JSON object");
    return g_steal_pointer (&res);
  }

  {
    g_autoptr (WpIterator) iter = wp_spa_json_new_iterator (json);
    g_auto (GValue) item = G_VALUE_INIT;
    while (wp_iterator_next (iter, &item)) {
      WpSpaJson *j = g_value_get_boxed (&item);
      g_autofree gchar *name = wp_spa_json_parse_string (j);
      g_autofree gchar *value = NULL;

      g_value_unset (&item);
      if (!wp_iterator_next (iter, &item)) {
        wp_warning_object (self, "malformed wireplumber.settings from conf");
        return res;
      }
      j = g_value_get_boxed (&item);

      value = wp_spa_json_to_string (j);
      g_value_unset (&item);

      if (name && value)
        wp_properties_set (res, name, value);
    }
  }

  return g_steal_pointer (&res);
}

static void
on_metadata_activated (WpMetadata * m, GAsyncResult * res,
    gpointer user_data)
{
  WpTransition *transition = WP_TRANSITION (user_data);
  WpSettingsPlugin *self = wp_transition_get_source_object (transition);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_autoptr (WpProperties) config_settings = NULL;
  g_autoptr (GError) error = NULL;
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;

  if (!wp_object_activate_finish (WP_OBJECT (m), res, &error)) {
    g_prefix_error (&error, "Failed to activate \"%s\": "
        "Metadata object ", self->metadata_name);
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  /* Load settings from configuration */
  config_settings = load_configuration_settings (self);
  if (!config_settings) {
    wp_transition_return_error (transition, g_error_new (
        WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "failed to parse settings"));
    return;
  }

  /* Update the configuration properties with persistent settings */
  wp_properties_update (config_settings, self->persistent_settings);

  /* Populate settings metadata from schema using values from configuration if
   * they are present, otherwise use default values */
  it = wp_metadata_new_iterator (WP_METADATA (self->schema_impl_metadata), 0);
  for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
    WpMetadataItem *mi = g_value_get_boxed (&item);
    const gchar *key = wp_metadata_item_get_key (mi);
    const gchar *spec_str = wp_metadata_item_get_value (mi);
    const gchar *value;
    g_autoptr (WpSpaJson) spec_json = NULL;
    g_autoptr (WpSpaJson) def_value = NULL;

    /* Use configuration value if found, otherwise use default value */
    value = wp_properties_get (config_settings, key);
    if (!value) {
      spec_json = wp_spa_json_new_from_string (spec_str);

      if (!wp_spa_json_is_object (spec_json)) {
        wp_warning_object (self,
            "settings schema spec for %s is not an object: %s", key, spec_str);
        continue;
      }

      if (!wp_spa_json_object_get (spec_json, "default", "J", &def_value,
          NULL)) {
        wp_warning_object (self,
            "settings schema spec for %s does not have default value: %s", key,
            spec_str);
        continue;
      }

      value = wp_spa_json_get_data (def_value);
    }

    /* Add setting in the metadata */
    wp_debug_object (self, "adding setting to %s metadata: %s = %s",
        self->metadata_name, key, value);
    wp_metadata_set (m, 0, key, "Spa:String:JSON", value);
  }

  wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
on_persistent_metadata_activated (WpMetadata * m, GAsyncResult * res,
    gpointer user_data)
{
  WpTransition *transition = WP_TRANSITION (user_data);
  WpSettingsPlugin *self = wp_transition_get_source_object (transition);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_autoptr (GError) error = NULL;
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;

  if (!wp_object_activate_finish (WP_OBJECT (m), res, &error)) {
    g_prefix_error (&error, "Failed to activate \"%s\": "
        "Metadata object ", self->metadata_persistent_name);
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  /* Load the persistent settings */
  self->state = wp_state_new (NAME);
  self->persistent_settings = wp_state_load (self->state);

  /* Set persistent settings in persistent metadata */
  for (it = wp_properties_new_iterator (self->persistent_settings);
      wp_iterator_next (it, &item);
      g_value_unset (&item)) {
    WpPropertiesItem *pi = g_value_get_boxed (&item);
    const gchar *key = wp_properties_item_get_key (pi);
    const gchar *value = wp_properties_item_get_value (pi);

    wp_debug_object (self, "adding persistent setting to %s metadata: %s = %s",
        self->metadata_persistent_name, key, value);
    wp_metadata_set (m, 0, key, "Spa:String:JSON", value);
  }

  /* monitor changes in persistent metadata */
  g_signal_connect_object (m, "changed",
      G_CALLBACK (on_persistent_metadata_changed), self, 0);

  /* create metadata object */
  self->impl_metadata = wp_impl_metadata_new_full (core, self->metadata_name,
      NULL);
  wp_object_activate (WP_OBJECT (self->impl_metadata),
      WP_OBJECT_FEATURES_ALL,
      NULL,
      (GAsyncReadyCallback)on_metadata_activated,
      transition);
}

static void
on_schema_metadata_activated (WpMetadata * m, GAsyncResult * res,
    gpointer user_data)
{
  WpTransition *transition = WP_TRANSITION (user_data);
  WpSettingsPlugin *self = wp_transition_get_source_object (transition);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_autoptr (WpConf) conf = wp_core_get_conf (core);
  g_autoptr (GError) error = NULL;
  g_autoptr (WpSpaJson) schema_json = NULL;

  if (!wp_object_activate_finish (WP_OBJECT (m), res, &error)) {
    wp_transition_return_error (transition, g_error_new (
        WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "Failed to activate metadata object %s", self->metadata_schema_name));
    return;
  }

  /* Load the schema into metadata if any */
  schema_json = wp_conf_get_section (conf, "wireplumber.settings.schema");
  if (schema_json) {
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) item = G_VALUE_INIT;

    if (!wp_spa_json_is_object (schema_json)) {
      wp_transition_return_error (transition, g_error_new (
          WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
          "Settings schema is not a JSON object: %s",
          wp_spa_json_get_data (schema_json)));
      return;
    }

    it = wp_spa_json_new_iterator (schema_json);
    while (wp_iterator_next (it, &item)) {
      WpSpaJson *j = g_value_get_boxed (&item);
      g_autofree gchar *key = wp_spa_json_parse_string (j);
      g_autofree gchar *value = NULL;

      g_value_unset (&item);
      if (!wp_iterator_next (it, &item)) {
        wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
            WP_LIBRARY_ERROR_INVARIANT, "Malformed settings schema"));
        return;
      }
      j = g_value_get_boxed (&item);
      value = wp_spa_json_to_string (j);
      g_value_unset (&item);

      wp_debug_object (self, "adding schema setting to %s metadata: %s = %s",
          self->metadata_schema_name, key, value);
      wp_metadata_set (m, 0, key, "Spa:String:JSON", value);
    }
  } else {
    wp_warning_object (self, "settings schema not found in configuration");
  }

  /* create persistent metadata object */
  self->persistent_impl_metadata = wp_impl_metadata_new_full (core,
      self->metadata_persistent_name, NULL);
  wp_object_activate (WP_OBJECT (self->persistent_impl_metadata),
      WP_OBJECT_FEATURES_ALL,
      NULL,
      (GAsyncReadyCallback)on_persistent_metadata_activated,
      transition);
}

static void
wp_settings_plugin_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpSettingsPlugin * self = WP_SETTINGS_PLUGIN (plugin);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));

  /* create schema metadata object */
  self->schema_impl_metadata = wp_impl_metadata_new_full (core,
      self->metadata_schema_name, NULL);
  wp_object_activate (WP_OBJECT (self->schema_impl_metadata),
      WP_OBJECT_FEATURES_ALL,
      NULL,
      (GAsyncReadyCallback)on_schema_metadata_activated,
      transition);
}

static void
wp_settings_plugin_disable (WpPlugin * plugin)
{
  WpSettingsPlugin * self = WP_SETTINGS_PLUGIN (plugin);

  g_clear_object (&self->impl_metadata);
  g_clear_object (&self->persistent_impl_metadata);
  g_clear_pointer (&self->persistent_settings, wp_properties_unref);
  g_clear_object (&self->state);

  g_clear_pointer (&self->metadata_name, g_free);
  g_clear_pointer (&self->metadata_schema_name, g_free);
  g_clear_pointer (&self->metadata_persistent_name, g_free);
}

static void
wp_settings_plugin_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpSettingsPlugin *self = WP_SETTINGS_PLUGIN (object);

  switch (property_id) {
  case PROP_METADATA_NAME:
    self->metadata_name = g_value_dup_string (value);
    self->metadata_schema_name = g_strdup_printf (
        WP_SETTINGS_SCHEMA_METADATA_NAME_PREFIX "%s", self->metadata_name);
    self->metadata_persistent_name = g_strdup_printf (
        WP_SETTINGS_PERSISTENT_METADATA_NAME_PREFIX "%s", self->metadata_name);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_settings_plugin_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpSettingsPlugin *self = WP_SETTINGS_PLUGIN (object);

  switch (property_id) {
  case PROP_METADATA_NAME:
    g_value_set_string (value, self->metadata_name);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_settings_plugin_class_init (WpSettingsPluginClass * klass)
{
  WpPluginClass *plugin_class = (WpPluginClass *) klass;
  GObjectClass *object_class = (GObjectClass *) klass;

  plugin_class->enable = wp_settings_plugin_enable;
  plugin_class->disable = wp_settings_plugin_disable;

  object_class->set_property = wp_settings_plugin_set_property;
  object_class->get_property = wp_settings_plugin_get_property;

  g_object_class_install_property (object_class, PROP_METADATA_NAME,
      g_param_spec_string ("metadata-name", "metadata-name",
          "The metadata object to look after", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  g_autofree gchar *metadata_name = NULL;
  if (args)
    wp_spa_json_object_get (args, "metadata.name", "s", &metadata_name, NULL);

  return G_OBJECT (g_object_new (wp_settings_plugin_get_type (),
      "name", "settings",
      "core", core,
      "metadata-name", metadata_name ? metadata_name : "sm-settings",
      NULL));
}
 070701000000FB000081A400000000000000000000000165F86304000066D4000000000000000000000000000000000000003400000000wireplumber-0.5.0/modules/module-si-audio-adapter.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <pipewire/keys.h>
#include <pipewire/properties.h>

#include <spa/param/format.h>
#include <spa/param/audio/raw.h>
#include <spa/param/audio/format-utils.h>
#include <spa/param/param.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-si-audio-adapter")

#define SI_FACTORY_NAME "si-audio-adapter"

struct _WpSiAudioAdapter
{
  WpSessionItem parent;

  /* configuration */
  WpNode *node;
  WpPort *port;  /* only used for passthrough or convert mode */
  gboolean no_format;
  gboolean control_port;
  gboolean monitor;
  gboolean disable_dsp;
  WpDirection portconfig_direction;
  gboolean is_device;
  gboolean dont_remix;
  gboolean is_autoconnect;
  gboolean have_encoded;
  gboolean encoded_only;
  gboolean is_unpositioned;
  struct spa_audio_info_raw raw_format;

  gulong ports_changed_sigid;
  gulong params_changed_sigid;

  WpSpaPod *format;
  gchar mode[32];
  GTask *format_task;
  WpSiAdapterPortsState ports_state;
};

static void si_audio_adapter_linkable_init (WpSiLinkableInterface * iface);
static void si_audio_adapter_adapter_init (WpSiAdapterInterface * iface);

G_DECLARE_FINAL_TYPE(WpSiAudioAdapter, si_audio_adapter, WP, SI_AUDIO_ADAPTER,
    WpSessionItem)
G_DEFINE_TYPE_WITH_CODE (WpSiAudioAdapter, si_audio_adapter,
    WP_TYPE_SESSION_ITEM,
    G_IMPLEMENT_INTERFACE (WP_TYPE_SI_LINKABLE, si_audio_adapter_linkable_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_SI_ADAPTER, si_audio_adapter_adapter_init))

static void
si_audio_adapter_init (WpSiAudioAdapter * self)
{
}

static void
si_audio_adapter_set_ports_state (WpSiAudioAdapter *self, WpSiAdapterPortsState
    new_state)
{
  if (self->ports_state != new_state) {
    WpSiAdapterPortsState old_state = self->ports_state;
    self->ports_state = new_state;
    g_signal_emit_by_name (self, "adapter-ports-state-changed", old_state,
        new_state);
  }
}

static void
si_audio_adapter_soft_reset (WpSiAudioAdapter *self)
{
  /* deactivate first */
  wp_object_deactivate (WP_OBJECT (self), WP_SESSION_ITEM_FEATURE_ACTIVE);

  /* reset back to the initial "configured" state */
  if (self->format_task) {
    g_task_return_new_error (self->format_task, WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_OPERATION_FAILED,
        "item deactivated before format was set");
    g_clear_object (&self->format_task);
  }
  g_clear_pointer (&self->format, wp_spa_pod_unref);
  self->mode[0] = '\0';
  si_audio_adapter_set_ports_state (self, WP_SI_ADAPTER_PORTS_STATE_NONE);
}

static void
si_audio_adapter_reset (WpSessionItem * item)
{
  WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item);

  si_audio_adapter_soft_reset (self);

  /* reset */
  g_clear_object (&self->node);
  g_clear_object (&self->port);
  self->no_format = FALSE;
  self->control_port = FALSE;
  self->monitor = FALSE;
  self->disable_dsp = FALSE;
  self->portconfig_direction = WP_DIRECTION_INPUT;
  self->is_device = FALSE;
  self->dont_remix = FALSE;
  self->is_autoconnect = FALSE;
  self->have_encoded = FALSE;
  self->encoded_only = FALSE;
  spa_memzero (&self->raw_format, sizeof(struct spa_audio_info_raw));

  WP_SESSION_ITEM_CLASS (si_audio_adapter_parent_class)->reset (item);
}

static guint
si_audio_adapter_get_default_clock_rate (WpSiAudioAdapter * self)
{
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_autoptr (WpProperties) props = NULL;
  const gchar *rate_str = NULL;
  g_return_val_if_fail (core, 48000);
  props = wp_core_get_remote_properties (core);
  g_return_val_if_fail (props, 48000);
  rate_str = wp_properties_get (props, "default.clock.rate");
  return rate_str ? atoi (rate_str) : 48000;
}

static gboolean
is_unpositioned (struct spa_audio_info_raw *info)
{
  uint32_t i;
  if (SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED))
    return TRUE;
  for (i = 0; i < info->channels; i++)
    if (info->position[i] >= SPA_AUDIO_CHANNEL_START_Aux &&
        info->position[i] <= SPA_AUDIO_CHANNEL_LAST_Aux)
      return TRUE;
  return FALSE;
}

static gboolean
si_audio_adapter_find_format (WpSiAudioAdapter * self, WpNode * node)
{
  g_autoptr (WpIterator) formats = NULL;
  g_auto (GValue) value = G_VALUE_INIT;
  gboolean have_format = FALSE;

  formats = wp_pipewire_object_enum_params_sync (WP_PIPEWIRE_OBJECT (node),
      "EnumFormat", NULL);
  if (!formats)
    return FALSE;

  for (; wp_iterator_next (formats, &value); g_value_unset (&value)) {
    WpSpaPod *pod = g_value_get_boxed (&value);
    uint32_t mtype, msubtype;

    if (!wp_spa_pod_is_object (pod)) {
      wp_warning_object (self,
          "non-object POD appeared on formats list; this node is buggy");
      continue;
    }

    if (!wp_spa_pod_get_object (pod, NULL,
        "mediaType", "I", &mtype,
        "mediaSubtype", "I", &msubtype,
        NULL)) {
      wp_warning_object (self, "format does not have media type / subtype");
      continue;
    }

    if (mtype != SPA_MEDIA_TYPE_audio)
      continue;

    switch (msubtype) {
    case SPA_MEDIA_SUBTYPE_raw: {
      struct spa_audio_info_raw raw_format;
      struct spa_pod *position = NULL;
      wp_spa_pod_fixate (pod);

      spa_zero(raw_format);
      if (spa_pod_parse_object(wp_spa_pod_get_spa_pod (pod),
                               SPA_TYPE_OBJECT_Format, NULL,
                               SPA_FORMAT_AUDIO_format,   SPA_POD_OPT_Id(&raw_format.format),
                               SPA_FORMAT_AUDIO_rate,     SPA_POD_OPT_Int(&raw_format.rate),
                               SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&raw_format.channels),
                               SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position)) < 0)
        continue;

      if (position == NULL ||
          !spa_pod_copy_array(position, SPA_TYPE_Id, raw_format.position, SPA_AUDIO_MAX_CHANNELS))
        SPA_FLAG_SET(raw_format.flags, SPA_AUDIO_FLAG_UNPOSITIONED);

      if (self->raw_format.channels < raw_format.channels) {
        self->raw_format = raw_format;
        if (is_unpositioned(&raw_format))
          self->is_unpositioned = TRUE;
      }
      have_format = TRUE;
      break;
    }
    case SPA_MEDIA_SUBTYPE_iec958:
    case SPA_MEDIA_SUBTYPE_dsd:
      wp_info_object (self, "passthrough IEC958/DSD node %d found",
          wp_proxy_get_bound_id (WP_PROXY (node)));
      self->have_encoded = TRUE;
      break;
    default: {
      enum spa_audio_format audio_format;
      if (spa_pod_parse_object(wp_spa_pod_get_spa_pod (pod),
                               SPA_TYPE_OBJECT_Format, NULL,
                               SPA_FORMAT_AUDIO_format,   SPA_POD_OPT_Id(&audio_format)) >= 0) {
        self->have_encoded = (audio_format == SPA_AUDIO_FORMAT_ENCODED);
      }
      break;
    }
    }
  }
  if (!have_format && self->have_encoded) {
    wp_info_object (self, ".. passthrough IEC958/DSD/encoded only");
    self->encoded_only = TRUE;
    have_format = TRUE;
  }

  return have_format;
}

static void
on_proxy_destroyed (WpNode * proxy, WpSiAudioAdapter * self)
{
  if (self->node == proxy) {
    wp_object_abort_activation (WP_OBJECT (self), "proxy destroyed");
    si_audio_adapter_soft_reset (self);
  }
}

static gboolean
si_audio_adapter_configure (WpSessionItem * item, WpProperties *p)
{
  WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item);
  g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p);
  WpNode *node = NULL;
  const gchar *str;

  /* reset previous config */
  si_audio_adapter_reset (item);

  str = wp_properties_get (si_props, "item.node");
  if (!str || sscanf(str, "%p", &node) != 1 || !WP_IS_NODE (node))
    return FALSE;

  str = wp_properties_get (si_props, PW_KEY_MEDIA_CLASS);
  if (!str)
    return FALSE;
  if ((strstr (str, "Source") || strstr (str, "Output"))
        && !strstr (str, "Virtual")) {
    self->portconfig_direction = WP_DIRECTION_OUTPUT;
  }

  str = wp_properties_get (si_props, "item.features.no-format");
  self->no_format = str && pw_properties_parse_bool (str);
  if (!self->no_format && !si_audio_adapter_find_format (self, node)) {
    wp_notice_object (item, "no usable format found for node %d",
        wp_proxy_get_bound_id (WP_PROXY (node)));
    return FALSE;
  }

  str = wp_properties_get (si_props, "item.features.control-port");
  self->control_port = str && pw_properties_parse_bool (str);

  str = wp_properties_get (si_props, "item.features.monitor");
  self->monitor = str && pw_properties_parse_bool (str);

  str = wp_properties_get (si_props, "item.features.no-dsp");
  self->disable_dsp = str && pw_properties_parse_bool (str);

  str = wp_properties_get (si_props, "item.node.type");
  self->is_device = !g_strcmp0 (str, "device");

  str = wp_properties_get (si_props, PW_KEY_STREAM_DONT_REMIX);
  self->dont_remix = str && pw_properties_parse_bool (str);

  str = wp_properties_get (si_props, PW_KEY_NODE_AUTOCONNECT);
  self->is_autoconnect = str && pw_properties_parse_bool (str);

  self->node = g_object_ref (node);
  g_signal_connect_object (self->node, "pw-proxy-destroyed",
      G_CALLBACK (on_proxy_destroyed), self, 0);

  wp_properties_set (si_props, "item.node.supports-encoded-fmts",
      self->have_encoded ? "true" : "false");

  wp_properties_set (si_props, "item.node.encoded-only",
      self->encoded_only ? "true" : "false");

  wp_properties_set (si_props, "item.node.unpositioned",
      self->is_unpositioned ? "true" : "false");

  wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME);
  wp_session_item_set_properties (item, g_steal_pointer (&si_props));
  return TRUE;
}

static gpointer
si_audio_adapter_get_associated_proxy (WpSessionItem * item, GType proxy_type)
{
  WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item);

  if (proxy_type == WP_TYPE_NODE)
    return self->node ? g_object_ref (self->node) : NULL;

  return NULL;
}

static WpSpaPod *
format_audio_raw_build (const struct spa_audio_info_raw *info)
{
  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_object (
      "Spa:Pod:Object:Param:Format", "Format");
  wp_spa_pod_builder_add (builder,
      "mediaType",    "K", "audio",
      "mediaSubtype", "K", "raw",
      "format",       "I", info->format,
      "rate",         "i", info->rate,
      "channels",     "i", info->channels,
      NULL);

   if (!SPA_FLAG_IS_SET (info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) {
     /* Build the position array spa pod */
     g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
     for (guint i = 0; i < info->channels; i++)
       wp_spa_pod_builder_add_id (position_builder, info->position[i]);

     /* Add the position property */
     wp_spa_pod_builder_add_property (builder, "position");
     g_autoptr (WpSpaPod) position = wp_spa_pod_builder_end (position_builder);
     wp_spa_pod_builder_add_pod (builder, position);
   }

   return wp_spa_pod_builder_end (builder);
}

static gboolean
parse_adapter_format (WpSpaPod *format, gint *channels,
   WpSpaPod **position)
{
  g_autoptr (WpSpaPodParser) parser = NULL;
  guint32 t = 0, s = 0, f = 0;
  gint r = 0, c = 0;
  g_autoptr (WpSpaPod) p = NULL;

  g_return_val_if_fail (format, FALSE);
  parser = wp_spa_pod_parser_new_object (format, NULL);
  g_return_val_if_fail (parser, FALSE);

  if (!wp_spa_pod_parser_get (parser, "mediaType", "I", &t, NULL) ||
      !wp_spa_pod_parser_get (parser, "mediaSubtype", "I", &s, NULL) ||
      !wp_spa_pod_parser_get (parser, "format", "I", &f, NULL) ||
      !wp_spa_pod_parser_get (parser, "rate", "i", &r, NULL) ||
      !wp_spa_pod_parser_get (parser, "channels", "i", &c, NULL))
    return FALSE;

  /* position is optional */
  wp_spa_pod_parser_get (parser, "position", "P", &p, NULL);

  if (channels && c != 0)
    *channels = c;
  if (position)
    *position = p ? wp_spa_pod_ref (p) : NULL;

  return TRUE;
}

static WpSpaPod *
build_adapter_format (WpSiAudioAdapter * self, guint32 format, gint channels,
    WpSpaPod *pos)
{
  g_autoptr (WpSpaPod) position = pos;
  g_autoptr (WpSpaPodBuilder) b = NULL;

  g_return_val_if_fail (channels > 0, NULL);

  /* build the position array if not given */
  if (!position) {
    switch (channels) {
    case 1: {
      g_autoptr (WpSpaPodBuilder) pos_b = wp_spa_pod_builder_new_array ();
      wp_spa_pod_builder_add_id (pos_b, SPA_AUDIO_CHANNEL_MONO);
      position = wp_spa_pod_builder_end (pos_b);
      break;
    }
    case 2: {
      g_autoptr (WpSpaPodBuilder) pos_b = wp_spa_pod_builder_new_array ();
      wp_spa_pod_builder_add_id (pos_b, SPA_AUDIO_CHANNEL_FL);
      wp_spa_pod_builder_add_id (pos_b, SPA_AUDIO_CHANNEL_FR);
      position = wp_spa_pod_builder_end (pos_b);
      break;
    }
    default:
      break;
    }
  }

  /* build the format */
  b = wp_spa_pod_builder_new_object ("Spa:Pod:Object:Param:Format", "Format");
  wp_spa_pod_builder_add_property (b, "mediaType");
  wp_spa_pod_builder_add_id (b, SPA_MEDIA_TYPE_audio);
  wp_spa_pod_builder_add_property (b, "mediaSubtype");
  wp_spa_pod_builder_add_id (b, SPA_MEDIA_SUBTYPE_raw);
  wp_spa_pod_builder_add_property (b, "format");
  wp_spa_pod_builder_add_id (b, format);
  wp_spa_pod_builder_add_property (b, "rate");
  wp_spa_pod_builder_add_int (b, si_audio_adapter_get_default_clock_rate (self));
  wp_spa_pod_builder_add_property (b, "channels");
  wp_spa_pod_builder_add_int (b, channels);
  if (position) {
    wp_spa_pod_builder_add_property (b, "position");
    wp_spa_pod_builder_add_pod (b, position);
  }
  return wp_spa_pod_builder_end (b);
}

static WpSpaPod *
build_adapter_dsp_format (WpSiAudioAdapter * self, WpSpaPod *dev_format)
{
  g_autoptr (WpSpaPod) position = NULL;
  gint channels = 2;

  /* parse device format */
  if (dev_format && !parse_adapter_format (dev_format, &channels, &position))
    return NULL;

  /* build F32P with same channels and position as device format */
  return build_adapter_format (self, SPA_AUDIO_FORMAT_F32P, channels,
      g_steal_pointer (&position));
}

static WpSpaPod *
build_adapter_default_format (WpSiAudioAdapter * self, const gchar *mode)
{
  guint32 format = SPA_AUDIO_FORMAT_F32;

  /* if dsp, use plannar format */
  if (!mode || g_strcmp0 (mode, "dsp") == 0)
    format = SPA_AUDIO_FORMAT_F32P;

  return build_adapter_format (self, format, 2, NULL);
}

static void
on_format_set (GObject *obj, GAsyncResult * res, gpointer p)
{
  g_autoptr(WpTransition) transition = p;
  WpSiAudioAdapter *self = wp_transition_get_source_object (transition);
  g_autoptr (GError) error = NULL;

  if (wp_transition_get_completed (transition))
    return;

  wp_si_adapter_set_ports_format_finish (WP_SI_ADAPTER (self), res, &error);
  if (error) {
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  wp_object_update_features (WP_OBJECT (self),
      WP_SESSION_ITEM_FEATURE_ACTIVE, 0);
}

static void
si_audio_adapter_configure_node (WpSiAudioAdapter *self,
    WpTransition * transition)
{
  g_autoptr (WpSpaPod) format = NULL;
  g_autoptr (WpSpaPod) ports_format = NULL;
  const gchar *mode = NULL;

  /* set the chosen format on the node */
  format = format_audio_raw_build (&self->raw_format);
  wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (self->node), "Format", 0,
      wp_spa_pod_ref (format));

  /* build the ports format */
  if (self->disable_dsp) {
    mode = "passthrough";
    ports_format = g_steal_pointer (&format);
  } else {
    mode = "dsp";
    ports_format = build_adapter_dsp_format (self, format);
    if (!ports_format) {
        wp_transition_return_error (transition,
          g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
              "failed to build ports format"));
      return;
    }
  }

  /* set chosen format in the ports */
  wp_si_adapter_set_ports_format (WP_SI_ADAPTER (self),
      g_steal_pointer (&ports_format), mode, on_format_set, g_object_ref (transition));
}

static void
on_port_param_info (WpPipewireObject * port, GParamSpec * param,
    WpSiAudioAdapter *self)
{
  /* finish the task started by _set_ports_format() */
  if (self->format_task) {
    g_autoptr (GTask) t = g_steal_pointer (&self->format_task);
    si_audio_adapter_set_ports_state (self,
        WP_SI_ADAPTER_PORTS_STATE_CONFIGURED);
    g_task_return_boolean (t, TRUE);
  }
}

static void
on_node_ports_changed (WpObject * node, WpSiAudioAdapter *self)
{
  /* clear port and handler */
  if (self->port) {
    g_signal_handlers_disconnect_by_func (self->port, on_port_param_info, self);
    g_clear_object (&self->port);
  }

  if (wp_node_get_n_ports (self->node) > 0) {
    /* if non DSP mode, listen for param-info on the single port in order to
     * be notified of format changed events */
    if (g_strcmp0 (self->mode, "dsp") != 0) {
      self->port = wp_node_lookup_port (self->node,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.direction", "=s",
          self->portconfig_direction == WP_DIRECTION_INPUT ? "in" : "out",
          NULL);
      if (self->port)
        g_signal_connect_object (self->port, "notify::param-info",
            G_CALLBACK (on_port_param_info), self, 0);
    }

    /* finish the task started by _set_ports_format() */
    if (self->format_task) {
      g_autoptr (GTask) t = g_steal_pointer (&self->format_task);
      si_audio_adapter_set_ports_state (self,
          WP_SI_ADAPTER_PORTS_STATE_CONFIGURED);
      g_task_return_boolean (t, TRUE);
    }
  }
}

static void
on_node_params_changed (WpPipewireObject * proxy, const gchar *param_name,
    WpSiAudioAdapter *self)
{
  /* Only handle Props param that has been emitted when setting ports format */
  if (!g_str_equal (param_name, "Props") || !self->format_task)
    return;

  /* If ports are already configured, finish task. This is the case for already
   * loaded nodes such as virtual filters */
  if (wp_node_get_n_ports (self->node) > 0)
    on_node_ports_changed (WP_OBJECT (self->node), self);
}

static void
si_audio_adapter_enable_active (WpSessionItem *si, WpTransition *transition)
{
  WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (si);

  if (!wp_session_item_is_configured (si)) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
        "si-audio-adapter: item is not configured"));
    return;
  }

  if (!(wp_object_test_active_features (WP_OBJECT (self->node),
        WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL))) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
        "si-audio-adapter: node minimal feature not enabled"));
    return;
  }

  self->ports_changed_sigid = g_signal_connect_object (self->node,
      "ports-changed", (GCallback) on_node_ports_changed, self, 0);
  self->params_changed_sigid = g_signal_connect_object (self->node,
      "params-changed", (GCallback) on_node_params_changed, self, 0);

  /* If device node, enum available formats and set one of them */
  if (!self->no_format && (self->is_device || self->dont_remix ||
      !self->is_autoconnect || self->disable_dsp || self->is_unpositioned))
    si_audio_adapter_configure_node (self, transition);

  /* Otherwise just finish activating */
  else
    wp_object_update_features (WP_OBJECT (self),
          WP_SESSION_ITEM_FEATURE_ACTIVE, 0);
}

static void
si_audio_adapter_disable_active (WpSessionItem *si)
{
  WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (si);

  if (self->ports_changed_sigid) {
    g_signal_handler_disconnect (self->node, self->ports_changed_sigid);
    self->ports_changed_sigid = 0;
  }
  if (self->params_changed_sigid) {
    g_signal_handler_disconnect (self->node, self->params_changed_sigid);
    self->params_changed_sigid = 0;
  }

  wp_object_update_features (WP_OBJECT (self), 0,
      WP_SESSION_ITEM_FEATURE_ACTIVE);
}

static WpObjectFeatures
si_audio_adapter_get_supported_features (WpObject * self)
{
  return WP_SESSION_ITEM_FEATURE_ACTIVE;
}

static void
si_audio_adapter_class_init (WpSiAudioAdapterClass * klass)
{
  WpObjectClass * wpobject_class = (WpObjectClass *) klass;
  WpSessionItemClass *si_class = (WpSessionItemClass *) klass;

  wpobject_class->get_supported_features =
      si_audio_adapter_get_supported_features;

  si_class->reset = si_audio_adapter_reset;
  si_class->configure = si_audio_adapter_configure;
  si_class->get_associated_proxy = si_audio_adapter_get_associated_proxy;
  si_class->disable_active = si_audio_adapter_disable_active;
  si_class->enable_active = si_audio_adapter_enable_active;
}

static WpSiAdapterPortsState
si_audio_adapter_get_ports_state (WpSiAdapter * item)
{
  WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item);
  return self->ports_state;
}

static WpSpaPod *
si_audio_adapter_get_ports_format (WpSiAdapter * item, const gchar **mode)
{
  WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item);
  if (mode)
    *mode = self->mode;
  return self->format ? wp_spa_pod_ref (self->format) : NULL;
}

static void
si_audio_adapter_set_ports_format (WpSiAdapter * item, WpSpaPod *f,
    const gchar *mode, GAsyncReadyCallback callback, gpointer data)
{
  WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item);
  g_autoptr (WpSpaPod) format = f;
  g_autoptr (GTask) task = g_task_new (self, NULL, callback, data);

  /* cancel previous task if any */
  if (self->format_task) {
    g_autoptr (GTask) t = g_steal_pointer (&self->format_task);
    g_task_return_new_error (t, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
        "setting new format before previous done");
  }

  /* build default format if NULL was given */
  if (!format && !g_strcmp0 (mode, "dsp")) {
    format = build_adapter_default_format (self, mode);
    if (!format) {
      g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
          WP_LIBRARY_ERROR_OPERATION_FAILED,
          "failed to build default format, aborting set format operation");
      return;
    }
  }

  /* make sure the node has WP_NODE_FEATURE_PORTS */
  if (G_UNLIKELY (!(wp_object_test_active_features (WP_OBJECT (self->node),
        WP_NODE_FEATURE_PORTS)))) {
    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_OPERATION_FAILED,
        "node feature ports is not enabled, aborting set format operation");
    return;
  }

  /* skip reconfiguring if the same mode & format are requested */
  if (!g_strcmp0 (mode, self->mode) &&
      ((format == NULL && self->format == NULL) ||
        wp_spa_pod_equal (format, self->format))) {
    g_task_return_boolean (task, TRUE);
    return;
  }

  /* ensure the node is suspended */
  if (wp_node_get_state (self->node, NULL) >= WP_NODE_STATE_IDLE)
    wp_node_send_command (self->node, "Suspend");

  /* set task, format and mode */
  self->format_task = g_steal_pointer (&task);
  g_clear_pointer (&self->format, wp_spa_pod_unref);
  self->format = g_steal_pointer (&format);
  strncpy (self->mode, mode ? mode : "dsp", sizeof (self->mode) - 1);

  si_audio_adapter_set_ports_state (self,
      WP_SI_ADAPTER_PORTS_STATE_CONFIGURING);

  /* configure DSP with chosen format */
  wp_pipewire_object_set_param (WP_PIPEWIRE_OBJECT (self->node),
      "PortConfig", 0, wp_spa_pod_new_object (
          "Spa:Pod:Object:Param:PortConfig", "PortConfig",
          "direction",  "I", self->portconfig_direction,
          "mode",       "K", self->mode,
          "monitor",    "b", self->monitor,
          "control",    "b", self->control_port,
          "format",     "P", self->format,
          NULL));

  /* the task finishes with new ports being emitted -> on_node_ports_changed */
}

static gboolean
si_audio_adapter_set_ports_format_finish (WpSiAdapter * item,
    GAsyncResult * res, GError ** error)
{
  return g_task_propagate_boolean (G_TASK (res), error);
}

static void
si_audio_adapter_adapter_init (WpSiAdapterInterface * iface)
{
  iface->get_ports_state = si_audio_adapter_get_ports_state;
  iface->get_ports_format = si_audio_adapter_get_ports_format;
  iface->set_ports_format = si_audio_adapter_set_ports_format;
  iface->set_ports_format_finish = si_audio_adapter_set_ports_format_finish;
}

static GVariant *
si_audio_adapter_get_ports (WpSiLinkable * item, const gchar * context)
{
  WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item);
  g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY);
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) val = G_VALUE_INIT;
  WpDirection direction;
  guint32 node_id;

  if (!g_strcmp0 (context, "output")) {
    direction = WP_DIRECTION_OUTPUT;
  }
  else if (!g_strcmp0 (context, "input")) {
    direction = WP_DIRECTION_INPUT;
  }
  else {
    /* on any other context, return an empty list of ports */
    return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0);
  }

  g_variant_builder_init (&b, G_VARIANT_TYPE ("a(uuu)"));
  node_id = wp_proxy_get_bound_id (WP_PROXY (self->node));

  for (it = wp_node_new_ports_iterator (self->node);
       wp_iterator_next (it, &val);
       g_value_unset (&val))
  {
    WpPort *port = g_value_get_object (&val);
    g_autoptr (WpProperties) props = NULL;
    const gchar *channel;
    guint32 port_id, channel_id = 0;

    if (wp_port_get_direction (port) != direction)
      continue;

    port_id = wp_proxy_get_bound_id (WP_PROXY (port));
    props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (port));

    /* skip control ports for now */
    if (spa_atob (wp_properties_get (props, PW_KEY_PORT_CONTROL)))
      continue;

    /* try to find the audio channel; if channel is NULL, this will silently
       leave the channel_id to its default value, 0 */
    channel = wp_properties_get (props, PW_KEY_AUDIO_CHANNEL);
    if (channel) {
      WpSpaIdValue idval = wp_spa_id_value_from_short_name (
          "Spa:Enum:AudioChannel", channel);
      if (idval)
        channel_id = wp_spa_id_value_number (idval);
    }

    g_variant_builder_add (&b, "(uuu)", node_id, port_id, channel_id);
  }

  return g_variant_builder_end (&b);
}

static void
si_audio_adapter_linkable_init (WpSiLinkableInterface * iface)
{
  iface->get_ports = si_audio_adapter_get_ports;
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (wp_si_factory_new_simple (SI_FACTORY_NAME,
      si_audio_adapter_get_type ()));
}
070701000000FC000081A400000000000000000000000165F8630400002ECE000000000000000000000000000000000000003400000000wireplumber-0.5.0/modules/module-si-audio-virtual.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <spa/param/format.h>
#include <spa/param/audio/raw.h>
#include <spa/param/param.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-si-audio-virtual")

#define SI_FACTORY_NAME "si-audio-virtual"

struct _WpSiAudioVirtual
{
  WpSessionItem parent;

  /* configuration */
  gchar name[96];
  gchar media_class[32];
  WpDirection direction;
  gchar role[32];
  guint priority;
  gboolean disable_dsp;

  /* activation */
  WpNode *node;
  WpSiAdapter *adapter;
};

static void si_audio_virtual_linkable_init (WpSiLinkableInterface * iface);
static void si_audio_virtual_adapter_init (WpSiAdapterInterface * iface);

G_DECLARE_FINAL_TYPE(WpSiAudioVirtual, si_audio_virtual, WP,
    SI_AUDIO_VIRTUAL, WpSessionItem)
G_DEFINE_TYPE_WITH_CODE (WpSiAudioVirtual, si_audio_virtual,
    WP_TYPE_SESSION_ITEM,
    G_IMPLEMENT_INTERFACE (WP_TYPE_SI_LINKABLE,
        si_audio_virtual_linkable_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_SI_ADAPTER, si_audio_virtual_adapter_init))

static void
si_audio_virtual_init (WpSiAudioVirtual * self)
{
}

static void
si_audio_virtual_reset (WpSessionItem * item)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);

  /* deactivate first */
  wp_object_deactivate (WP_OBJECT (self),
      WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED);

  /* reset */
  self->name[0] = '\0';
  self->media_class[0] = '\0';
  self->direction = WP_DIRECTION_INPUT;
  self->role[0] = '\0';
  self->priority = 0;
  self->disable_dsp = FALSE;

  WP_SESSION_ITEM_CLASS (si_audio_virtual_parent_class)->reset (item);
}

static gboolean
si_audio_virtual_configure (WpSessionItem * item, WpProperties *p)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
  g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p);
  const gchar *str;

  /* reset previous config */
  si_audio_virtual_reset (item);

  str = wp_properties_get (si_props, "name");
  if (!str)
    return FALSE;
  strncpy (self->name, str, sizeof (self->name) - 1);

  str = wp_properties_get (si_props, "media.class");
  if (!str)
    return FALSE;
  strncpy (self->media_class, str, sizeof (self->media_class) - 1);

  if (strstr (self->media_class, "Source") ||
      strstr (self->media_class, "Output"))
    self->direction = WP_DIRECTION_OUTPUT;
  wp_properties_set (si_props, "item.node.direction",
      self->direction == WP_DIRECTION_OUTPUT ? "output" : "input");

  str = wp_properties_get (si_props, "role");
  if (str) {
    strncpy (self->role, str, sizeof (self->role) - 1);
  } else {
    strncpy (self->role, "Unknown", sizeof (self->role) - 1);
    wp_properties_set (si_props, "role", self->role);
  }

  str = wp_properties_get (si_props, "priority");
  if (str && sscanf(str, "%u", &self->priority) != 1)
    return FALSE;
  if (!str)
    wp_properties_setf (si_props, "priority", "%u", self->priority);

  str = wp_properties_get (si_props, "item.features.no-dsp");
  self->disable_dsp = str && pw_properties_parse_bool (str);

  /* We always want virtual sources to autoconnect */
  wp_properties_set (si_props, PW_KEY_NODE_AUTOCONNECT, "true");
  wp_properties_set (si_props, "media.type", "Audio");

  wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME);
  wp_session_item_set_properties (WP_SESSION_ITEM (self),
      g_steal_pointer (&si_props));
  return TRUE;
}

static gpointer
si_audio_virtual_get_associated_proxy (WpSessionItem * item, GType proxy_type)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);

  return wp_session_item_get_associated_proxy (
      WP_SESSION_ITEM (self->adapter), proxy_type);
}

static void
si_audio_virtual_disable_active (WpSessionItem *si)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (si);

  g_clear_object (&self->adapter);
  g_clear_object (&self->node);
  wp_object_update_features (WP_OBJECT (self), 0,
      WP_SESSION_ITEM_FEATURE_ACTIVE);
}

static void
si_audio_virtual_disable_exported (WpSessionItem *si)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (si);

  wp_object_update_features (WP_OBJECT (self), 0,
      WP_SESSION_ITEM_FEATURE_EXPORTED);
}

static void
on_adapter_activate_done (WpObject * adapter, GAsyncResult * res,
    WpTransition * transition)
{
  WpSiAudioVirtual *self = wp_transition_get_source_object (transition);
  g_autoptr (GError) error = NULL;

  if (!wp_object_activate_finish (adapter, res, &error)) {
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  wp_object_update_features (WP_OBJECT (self),
      WP_SESSION_ITEM_FEATURE_ACTIVE, 0);
}

static void
on_adapter_port_state_changed (WpSiAdapter *item,
    WpSiAdapterPortsState old_state, WpSiAdapterPortsState new_state,
    WpSiAudioVirtual *self)
{
  g_signal_emit_by_name (self, "adapter-ports-state-changed", old_state,
      new_state);
}

static void
on_node_activate_done (WpObject * node, GAsyncResult * res,
    WpTransition * transition)
{
  WpSiAudioVirtual *self = wp_transition_get_source_object (transition);
  g_autoptr (GError) error = NULL;
  g_autoptr (WpCore) core = NULL;
  g_autoptr (WpProperties) props = NULL;

  if (!wp_object_activate_finish (node, res, &error)) {
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  /* create adapter */
  core = wp_object_get_core (WP_OBJECT (self));
  self->adapter = WP_SI_ADAPTER (wp_session_item_make (core,
      "si-audio-adapter"));
  if (!self->adapter) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "si-audio-virtual: could not create si-audio-adapter"));
  }

  /* Set node.id and node.name properties in this session item */
  {
    g_autoptr (WpProperties) si_props = wp_session_item_get_properties (
        WP_SESSION_ITEM (self));
    g_autoptr (WpProperties) new_props = wp_properties_new_empty ();
    guint32 node_id = wp_proxy_get_bound_id (WP_PROXY (node));
    wp_properties_setf (new_props, "node.id", "%u", node_id);
    wp_properties_set (new_props, "node.name",
        wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (node),
        PW_KEY_NODE_NAME));
    wp_properties_update (si_props, new_props);
    wp_session_item_set_properties (WP_SESSION_ITEM (self),
        g_steal_pointer (&si_props));
  }

  /* Forward adapter-ports-state-changed signal */
  g_signal_connect_object (self->adapter, "adapter-ports-state-changed",
      G_CALLBACK (on_adapter_port_state_changed), self, 0);

  /* configure adapter */
  props = wp_properties_new_empty ();
  wp_properties_setf (props, "item.node", "%p", node);
  wp_properties_set (props, "name", self->name);
  wp_properties_set (props, "media.class", "Audio/Sink");
  wp_properties_set (props, "item.features.no-format", "true");
  wp_properties_set (props, "item.features.monitor", "true");
  if (self->disable_dsp)
    wp_properties_set (props, "item.features.no-dsp", "true");
  if (!wp_session_item_configure (WP_SESSION_ITEM (self->adapter),
      g_steal_pointer (&props))) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "si-audio-virtual: could not configure si-audio-adapter"));
  }

  /* activate adapter */
  wp_object_activate (WP_OBJECT (self->adapter), WP_SESSION_ITEM_FEATURE_ACTIVE,
      NULL, (GAsyncReadyCallback) on_adapter_activate_done, transition);
}

static void
si_audio_virtual_enable_active (WpSessionItem *si, WpTransition *transition)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (si);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_autofree gchar *name = g_strdup_printf ("control.%s", self->name);
  g_autofree gchar *desc = g_strdup_printf ("%s %s Virtual", self->role,
      (self->direction == WP_DIRECTION_OUTPUT) ? "Capture" : "Playback");
  g_autofree gchar *media = g_strdup_printf ("Audio/%s",
      (self->direction == WP_DIRECTION_OUTPUT) ? "Source" : "Sink");
  const gchar *passive =
      (self->direction == WP_DIRECTION_OUTPUT) ? "in" : "out";

  if (!wp_session_item_is_configured (si)) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "si-audio-virtual: item is not configured"));
    return;
  }

  /* create the node */
  self->node = wp_node_new_from_factory (core, "adapter",
      wp_properties_new (
          PW_KEY_NODE_NAME, name,
          PW_KEY_MEDIA_CLASS, media,
          PW_KEY_FACTORY_NAME, "support.null-audio-sink",
          PW_KEY_NODE_DESCRIPTION, desc,
          PW_KEY_NODE_AUTOCONNECT, "true",
          PW_KEY_NODE_PASSIVE, passive,
          "monitor.channel-volumes", "true",
          "wireplumber.is-virtual", "true",
          NULL));
  if (!self->node) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "si-audio-virtual: could not create null-audio-sink node"));
    return;
  }

  /* activate node */
  wp_object_activate (WP_OBJECT (self->node),
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL | WP_NODE_FEATURE_PORTS, NULL,
      (GAsyncReadyCallback) on_node_activate_done, transition);
}

static void
si_audio_virtual_enable_exported (WpSessionItem *si, WpTransition *transition)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (si);

  wp_object_update_features (WP_OBJECT (self),
      WP_SESSION_ITEM_FEATURE_EXPORTED, 0);
}

static void
si_audio_virtual_class_init (WpSiAudioVirtualClass * klass)
{
  WpSessionItemClass *si_class = (WpSessionItemClass *) klass;

  si_class->reset = si_audio_virtual_reset;
  si_class->configure = si_audio_virtual_configure;
  si_class->get_associated_proxy = si_audio_virtual_get_associated_proxy;
  si_class->disable_active = si_audio_virtual_disable_active;
  si_class->disable_exported = si_audio_virtual_disable_exported;
  si_class->enable_active = si_audio_virtual_enable_active;
  si_class->enable_exported = si_audio_virtual_enable_exported;
}

static GVariant *
si_audio_virtual_get_ports (WpSiLinkable * item, const gchar * context)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
  return wp_si_linkable_get_ports (WP_SI_LINKABLE (self->adapter), context);
}

static void
si_audio_virtual_linkable_init (WpSiLinkableInterface * iface)
{
  iface->get_ports = si_audio_virtual_get_ports;
}

static WpSiAdapterPortsState
si_audio_virtual_get_ports_state (WpSiAdapter * item)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
  return wp_si_adapter_get_ports_state (self->adapter);
}

static WpSpaPod *
si_audio_virtual_get_ports_format (WpSiAdapter * item, const gchar **mode)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
  return wp_si_adapter_get_ports_format (self->adapter, mode);
}

static void
si_audio_virtual_set_ports_format (WpSiAdapter * item, WpSpaPod *f,
    const gchar *mode, GAsyncReadyCallback callback, gpointer data)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
  wp_si_adapter_set_ports_format (self->adapter, f, mode, callback, data);
}

static gboolean
si_audio_virtual_set_ports_format_finish (WpSiAdapter * item,
    GAsyncResult * res, GError ** error)
{
  WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
  return wp_si_adapter_set_ports_format_finish (self->adapter, res, error);
}

static void
si_audio_virtual_adapter_init (WpSiAdapterInterface * iface)
{
  iface->get_ports_state = si_audio_virtual_get_ports_state;
  iface->get_ports_format = si_audio_virtual_get_ports_format;
  iface->set_ports_format = si_audio_virtual_set_ports_format;
  iface->set_ports_format_finish = si_audio_virtual_set_ports_format_finish;
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (wp_si_factory_new_simple (SI_FACTORY_NAME,
      si_audio_virtual_get_type ()));
}
  070701000000FD000081A400000000000000000000000165F8630400001884000000000000000000000000000000000000002B00000000wireplumber-0.5.0/modules/module-si-node.c    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <pipewire/keys.h>
#include <pipewire/properties.h>
#include <pipewire/extensions/session-manager/keys.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-si-node")

#define SI_FACTORY_NAME "si-node"

struct _WpSiNode
{
  WpSessionItem parent;

  /* configuration */
  WpNode *node;
};

static void si_node_linkable_init (WpSiLinkableInterface * iface);

G_DECLARE_FINAL_TYPE(WpSiNode, si_node, WP, SI_NODE, WpSessionItem)
G_DEFINE_TYPE_WITH_CODE (WpSiNode, si_node, WP_TYPE_SESSION_ITEM,
    G_IMPLEMENT_INTERFACE (WP_TYPE_SI_LINKABLE, si_node_linkable_init))

static void
si_node_init (WpSiNode * self)
{
}

static void
si_node_soft_reset (WpSiNode * self)
{
  wp_object_deactivate (WP_OBJECT (self), WP_SESSION_ITEM_FEATURE_ACTIVE);
}

static void
si_node_reset (WpSessionItem * item)
{
  WpSiNode *self = WP_SI_NODE (item);

  si_node_soft_reset (self);

  /* reset */
  g_clear_object (&self->node);

  WP_SESSION_ITEM_CLASS (si_node_parent_class)->reset (item);
}

static void
on_proxy_destroyed (WpNode * proxy, WpSiNode * self)
{
  if (self->node == proxy) {
    wp_object_abort_activation (WP_OBJECT (self), "proxy destroyed");
    si_node_soft_reset (self);
  }
}

static gboolean
si_node_configure (WpSessionItem * item, WpProperties *p)
{
  WpSiNode *self = WP_SI_NODE (item);
  g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p);
  WpNode *node = NULL;
  const gchar *str;

  /* reset previous config */
  si_node_reset (item);

  str = wp_properties_get (si_props, "item.node");
  if (!str || sscanf(str, "%p", &node) != 1 || !WP_IS_NODE (node))
    return FALSE;

  self->node = g_object_ref (node);
  g_signal_connect_object (self->node, "pw-proxy-destroyed",
      G_CALLBACK (on_proxy_destroyed), self, 0);

  wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME);
  wp_session_item_set_properties (WP_SESSION_ITEM (self),
      g_steal_pointer (&si_props));
  return TRUE;
}

static gpointer
si_node_get_associated_proxy (WpSessionItem * item, GType proxy_type)
{
  WpSiNode *self = WP_SI_NODE (item);

  if (proxy_type == WP_TYPE_NODE)
    return self->node ? g_object_ref (self->node) : NULL;

  return NULL;
}

static void
si_node_disable_active (WpSessionItem *si)
{
  WpSiNode *self = WP_SI_NODE (si);

  wp_object_update_features (WP_OBJECT (self), 0,
      WP_SESSION_ITEM_FEATURE_ACTIVE);
}

static void
on_node_activated (WpObject * node, GAsyncResult * res,
    WpTransition * transition)
{
  WpSiNode *self = wp_transition_get_source_object (transition);
  g_autoptr (GError) error = NULL;

  if (!wp_object_activate_finish (node, res, &error)) {
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  wp_object_update_features (WP_OBJECT (self),
      WP_SESSION_ITEM_FEATURE_ACTIVE, 0);
}

static void
si_node_enable_active (WpSessionItem *si, WpTransition *transition)
{
  WpSiNode *self = WP_SI_NODE (si);

  if (!wp_session_item_is_configured (si)) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "si-node: item is not configured"));
    return;
  }

  wp_object_activate (WP_OBJECT (self->node),
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL | WP_NODE_FEATURE_PORTS,
      NULL, (GAsyncReadyCallback) on_node_activated, transition);
}

static WpObjectFeatures
si_node_get_supported_features (WpObject * self)
{
  return WP_SESSION_ITEM_FEATURE_ACTIVE;
}

static void
si_node_class_init (WpSiNodeClass * klass)
{
  WpObjectClass * wpobject_class = (WpObjectClass *) klass;
  WpSessionItemClass *si_class = (WpSessionItemClass *) klass;

  wpobject_class->get_supported_features = si_node_get_supported_features;

  si_class->reset = si_node_reset;
  si_class->configure = si_node_configure;
  si_class->get_associated_proxy = si_node_get_associated_proxy;
  si_class->disable_active = si_node_disable_active;
  si_class->enable_active = si_node_enable_active;
}

static GVariant *
si_node_get_ports (WpSiLinkable * item, const gchar * context)
{
  WpSiNode *self = WP_SI_NODE (item);
  g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY);
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) val = G_VALUE_INIT;
  WpDirection direction;
  guint32 node_id;

  if (!g_strcmp0 (context, "output")) {
    direction = WP_DIRECTION_OUTPUT;
  }
  else if (!g_strcmp0 (context, "input")) {
    direction = WP_DIRECTION_INPUT;
  }
  else {
    /* on any other context, return an empty list of ports */
    return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0);
  }

  g_variant_builder_init (&b, G_VARIANT_TYPE ("a(uuu)"));
  node_id = wp_proxy_get_bound_id (WP_PROXY (self->node));

  for (it = wp_node_new_ports_iterator (self->node);
       wp_iterator_next (it, &val);
       g_value_unset (&val))
  {
    WpPort *port = g_value_get_object (&val);
    g_autoptr (WpProperties) props = NULL;
    const gchar *channel;
    guint32 port_id, channel_id = 0;

    if (wp_port_get_direction (port) != direction)
      continue;

    port_id = wp_proxy_get_bound_id (WP_PROXY (port));
    props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (port));

    /* skip control ports for now */
    if (spa_atob (wp_properties_get (props, PW_KEY_PORT_CONTROL)))
      continue;

    /* try to find the audio channel; if channel is NULL, this will silently
       leave the channel_id to its default value, 0 */
    channel = wp_properties_get (props, PW_KEY_AUDIO_CHANNEL);
    if (channel) {
      WpSpaIdValue idval = wp_spa_id_value_from_short_name (
          "Spa:Enum:AudioChannel", channel);
      if (idval)
        channel_id = wp_spa_id_value_number (idval);
    }

    g_variant_builder_add (&b, "(uuu)", node_id, port_id, channel_id);
  }

  return g_variant_builder_end (&b);
}

static void
si_node_linkable_init (WpSiLinkableInterface * iface)
{
  iface->get_ports = si_node_get_ports;
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (wp_si_factory_new_simple (SI_FACTORY_NAME,
      si_node_get_type ()));
}
070701000000FE000081A400000000000000000000000165F86304000065A9000000000000000000000000000000000000003400000000wireplumber-0.5.0/modules/module-si-standard-link.c   /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <spa/debug/types.h>
#include <spa/param/audio/type-info.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-si-standard-link")

#define SI_FACTORY_NAME "si-standard-link"

struct _WpSiStandardLink
{
  WpSessionItem parent;

  /* configuration */
  GWeakRef out_item;
  GWeakRef in_item;
  const gchar *out_item_port_context;
  const gchar *in_item_port_context;
  gboolean passthrough;

  /* activate */
  GPtrArray *node_links;
  guint n_active_links;
  guint n_failed_links;
  guint n_async_ops_wait;
};

enum {
  SIGNAL_LINK_ERROR,
  LAST_SIGNAL,
};

static guint signals[LAST_SIGNAL] = { 0 };

static void si_standard_link_link_init (WpSiLinkInterface * iface);

G_DECLARE_FINAL_TYPE (WpSiStandardLink, si_standard_link, WP, SI_STANDARD_LINK,
    WpSessionItem)
G_DEFINE_TYPE_WITH_CODE (WpSiStandardLink, si_standard_link,
    WP_TYPE_SESSION_ITEM,
    G_IMPLEMENT_INTERFACE (WP_TYPE_SI_LINK, si_standard_link_link_init))

static void
si_standard_link_init (WpSiStandardLink * self)
{
  g_weak_ref_init (&self->out_item, NULL);
  g_weak_ref_init (&self->in_item, NULL);
}

static void
si_standard_link_reset (WpSessionItem * item)
{
  WpSiStandardLink *self = WP_SI_STANDARD_LINK (item);

  /* deactivate first */
  wp_object_deactivate (WP_OBJECT (self),
      WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED);

  /* reset */
  g_weak_ref_set (&self->out_item, NULL);
  g_weak_ref_set (&self->in_item, NULL);
  self->out_item_port_context = NULL;
  self->in_item_port_context = NULL;
  self->passthrough = FALSE;

  WP_SESSION_ITEM_CLASS (si_standard_link_parent_class)->reset (item);
}

static WpSessionItem *
get_and_validate_item (WpProperties * props, const gchar *key)
{
  WpSessionItem *res = NULL;
  const gchar *str = NULL;

  str = wp_properties_get (props, key);
  if (!str || sscanf(str, "%p", &res) != 1 || !WP_IS_SI_LINKABLE (res) ||
      !(wp_object_test_active_features (WP_OBJECT (res),
          WP_SESSION_ITEM_FEATURE_ACTIVE)))
    return NULL;

  return res;
}

static gboolean
si_standard_link_configure (WpSessionItem * item, WpProperties * p)
{
  WpSiStandardLink *self = WP_SI_STANDARD_LINK (item);
  g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p);
  WpSessionItem *out_item, *in_item;
  const gchar *str;

  /* reset previous config */
  si_standard_link_reset (item);

  out_item = get_and_validate_item (si_props, "out.item");
  if (!out_item)
    return FALSE;
  wp_properties_setf (si_props, "out.item.id", "%u",
      wp_object_get_id (WP_OBJECT (out_item)));

  in_item = get_and_validate_item (si_props, "in.item");
  if (!in_item)
    return FALSE;
  wp_properties_setf (si_props, "in.item.id", "%u",
      wp_object_get_id (WP_OBJECT (in_item)));

  self->out_item_port_context = wp_properties_get (si_props,
      "out.item.port.context");

  self->in_item_port_context = wp_properties_get (si_props,
      "in.item.port.context");

  str = wp_properties_get (si_props, "passthrough");
  self->passthrough = str && pw_properties_parse_bool (str);

  g_weak_ref_set(&self->out_item, out_item);
  g_weak_ref_set(&self->in_item, in_item);

  wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME);
  wp_session_item_set_properties (WP_SESSION_ITEM (self),
      g_steal_pointer (&si_props));
  return TRUE;
}

static gpointer
si_standard_link_get_associated_proxy (WpSessionItem * item, GType proxy_type)
{
  return NULL;
}

static void
request_destroy_link (gpointer data, gpointer user_data)
{
  WpLink *link = WP_LINK (data);

  wp_global_proxy_request_destroy (WP_GLOBAL_PROXY (link));
}

static void
clear_node_links (GPtrArray **node_links_p)
{
  /*
   * Something else (eg. object managers) may be keeping the WpLink
   * objects alive. Deactive the links now, to destroy the PW objects.
   */
  if (*node_links_p)
    g_ptr_array_foreach (*node_links_p, request_destroy_link, NULL);

  g_clear_pointer (node_links_p, g_ptr_array_unref);
}

static void
si_standard_link_disable_active (WpSessionItem *si)
{
  WpSiStandardLink *self = WP_SI_STANDARD_LINK (si);
  g_autoptr (WpSessionItem) si_out = g_weak_ref_get (&self->out_item);
  g_autoptr (WpSessionItem) si_in = g_weak_ref_get (&self->in_item);
  WpSiAcquisition *out_acquisition, *in_acquisition;

  if (si_out) {
    out_acquisition = wp_si_linkable_get_acquisition (
        WP_SI_LINKABLE (si_out));
    if (out_acquisition)
      wp_si_acquisition_release (out_acquisition, WP_SI_LINK (self),
          WP_SI_LINKABLE (si_out));
  }
  if (si_in) {
    in_acquisition = wp_si_linkable_get_acquisition (WP_SI_LINKABLE (si_in));
    if (in_acquisition)
      wp_si_acquisition_release (in_acquisition, WP_SI_LINK (self),
          WP_SI_LINKABLE (si_in));
  }

  clear_node_links (&self->node_links);

  self->n_active_links = 0;
  self->n_failed_links = 0;
  self->n_async_ops_wait = 0;

  wp_object_update_features (WP_OBJECT (self), 0,
      WP_SESSION_ITEM_FEATURE_ACTIVE);
}

static void
on_link_activated (WpObject * proxy, GAsyncResult * res,
    WpTransition * transition)
{
  WpSiStandardLink *self = wp_transition_get_source_object (transition);
  guint len = self->node_links ? self->node_links->len : 0;

  /* Count the number of failed and active links */
  if (wp_object_activate_finish (proxy, res, NULL))
    self->n_active_links++;
  else
    self->n_failed_links++;

  /* Wait for all links to finish activation */
  if (self->n_failed_links + self->n_active_links != len)
    return;

  /* We only active feature if all links activated successfully */
  if (self->n_failed_links > 0) {
    clear_node_links (&self->node_links);
    wp_transition_return_error (transition, g_error_new (
        WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "%d of %d PipeWire links failed to activate",
        self->n_failed_links, len));
  } else {
    wp_object_update_features (WP_OBJECT (self),
        WP_SESSION_ITEM_FEATURE_ACTIVE, 0);
  }
}

static void
on_link_state_changed (WpLink *link, WpLinkState old_state,
  WpLinkState new_state, WpSiStandardLink * self)
{
  if (new_state == WP_LINK_STATE_ERROR) {
    const gchar *error_msg;
    wp_link_get_state (link, &error_msg);
    g_signal_emit_by_name (self, "link-error", error_msg);
  }
}

struct port
{
  guint32 node_id;
  guint32 port_id;
  guint32 channel;
  gboolean visited;
};

static inline bool
channel_is_aux(guint32 channel)
{
  return channel >= SPA_AUDIO_CHANNEL_START_Aux &&
    channel <= SPA_AUDIO_CHANNEL_LAST_Aux;
}

static inline int
score_ports(struct port *out, struct port *in)
{
  int score = 0;

  if (out->channel == in->channel)
    score += 100;
  else if ((out->channel == SPA_AUDIO_CHANNEL_SL && in->channel == SPA_AUDIO_CHANNEL_RL) ||
            (out->channel == SPA_AUDIO_CHANNEL_RL && in->channel == SPA_AUDIO_CHANNEL_SL) ||
            (out->channel == SPA_AUDIO_CHANNEL_SR && in->channel == SPA_AUDIO_CHANNEL_RR) ||
            (out->channel == SPA_AUDIO_CHANNEL_RR && in->channel == SPA_AUDIO_CHANNEL_SR))
    score += 60;
  else if ((out->channel == SPA_AUDIO_CHANNEL_FC && in->channel == SPA_AUDIO_CHANNEL_MONO) ||
            (out->channel == SPA_AUDIO_CHANNEL_MONO && in->channel == SPA_AUDIO_CHANNEL_FC))
    score += 50;
  else if (in->channel == SPA_AUDIO_CHANNEL_UNKNOWN ||
            in->channel == SPA_AUDIO_CHANNEL_MONO ||
            out->channel == SPA_AUDIO_CHANNEL_UNKNOWN ||
            out->channel == SPA_AUDIO_CHANNEL_MONO)
    score += 10;
  else if (channel_is_aux(in->channel) != channel_is_aux(out->channel))
    score += 7;
  if (score > 0 && !in->visited)
    score += 5;
  if (score <= 10)
    score = 0;
  return score;
}

static gboolean
create_links (WpSiStandardLink * self, WpTransition * transition,
    GVariant * out_ports, GVariant * in_ports)
{
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
  g_autoptr (GArray) in_ports_arr = NULL;
  struct port out_port = {0};
  struct port *in_port;
  GVariantIter *iter = NULL;
  guint i;

  /* Clear old links if any */
  self->n_active_links = 0;
  self->n_failed_links = 0;
  clear_node_links (&self->node_links);

  /* tuple format:
      uint32 node_id;
      uint32 port_id;
      uint32 channel;  // enum spa_audio_channel
   */
  if (!g_variant_is_of_type (out_ports, G_VARIANT_TYPE("a(uuu)")))
    return FALSE;
  if (!g_variant_is_of_type (in_ports, G_VARIANT_TYPE("a(uuu)")))
    return FALSE;

  i = g_variant_n_children (in_ports);
  if (i == 0)
    return FALSE;

  self->node_links = g_ptr_array_new_with_free_func (g_object_unref);

  /* transfer the in ports to an array so that we can
     mark them when they are linked */
  in_ports_arr = g_array_sized_new (FALSE, TRUE, sizeof (struct port), i + 1);
  g_array_set_size (in_ports_arr, i + 1);
  g_variant_get (in_ports, "a(uuu)", &iter);
  i = 0;
  do {
    in_port = &g_array_index (in_ports_arr, struct port, i++);
  } while (g_variant_iter_loop (iter, "(uuu)", &in_port->node_id,
              &in_port->port_id, &in_port->channel));
  g_variant_iter_free (iter);

  /* now loop over the out ports and figure out where they should be linked */
  g_variant_get (out_ports, "a(uuu)", &iter);
  while (g_variant_iter_loop (iter, "(uuu)", &out_port.node_id,
              &out_port.port_id, &out_port.channel))
  {
    int best_score = 0;
    struct port *best_port = NULL;
    WpProperties *props = NULL;
    WpLink *link;

    for (i = 0; i < in_ports_arr->len - 1; i++) {
      in_port = &g_array_index (in_ports_arr, struct port, i);
      int score = score_ports (&out_port, in_port);
      if (score > best_score) {
        best_score = score;
        best_port = in_port;
      }
    }

    /* not all output ports have to be linked ... */
    if (!best_port || best_port->visited)
      continue;

    best_port->visited = TRUE;

    /* Create the properties */
    props = wp_properties_new_empty ();
    wp_properties_setf (props, PW_KEY_LINK_OUTPUT_NODE, "%u", out_port.node_id);
    wp_properties_setf (props, PW_KEY_LINK_OUTPUT_PORT, "%u", out_port.port_id);
    wp_properties_setf (props, PW_KEY_LINK_INPUT_NODE, "%u", best_port->node_id);
    wp_properties_setf (props, PW_KEY_LINK_INPUT_PORT, "%u", best_port->port_id);

    wp_debug_object (self, "create pw link: %u:%u (%s) -> %u:%u (%s)",
        out_port.node_id, out_port.port_id,
        spa_debug_type_find_name (spa_type_audio_channel, out_port.channel),
        best_port->node_id, best_port->port_id,
        spa_debug_type_find_name (spa_type_audio_channel, best_port->channel));

    /* create the link */
    link = wp_link_new_from_factory (core, "link-factory", props);
    g_ptr_array_add (self->node_links, link);

    /* activate to ensure it is created without errors */
    wp_object_activate_closure (WP_OBJECT (link),
        WP_OBJECT_FEATURES_ALL, NULL,
        g_cclosure_new_object (
            (GCallback) on_link_activated, G_OBJECT (transition)));

    g_signal_connect_object (link, "state-changed",
      G_CALLBACK (on_link_state_changed), self, 0);
  }
  g_variant_iter_free (iter);
  return self->node_links->len > 0;
}

static void
get_ports_and_create_links (WpSiStandardLink *self, WpTransition *transition)
{
  g_autoptr (WpSiLinkable) si_out = NULL;
  g_autoptr (WpSiLinkable) si_in = NULL;
  g_autoptr (GVariant) out_ports = NULL;
  g_autoptr (GVariant) in_ports = NULL;

  si_out = WP_SI_LINKABLE (g_weak_ref_get (&self->out_item));
  si_in = WP_SI_LINKABLE (g_weak_ref_get (&self->in_item));

  if (!si_out || !si_in ||
      !wp_object_test_active_features (WP_OBJECT (si_out), WP_SESSION_ITEM_FEATURE_ACTIVE) ||
      !wp_object_test_active_features (WP_OBJECT (si_in), WP_SESSION_ITEM_FEATURE_ACTIVE)) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "si-standard-link: in/out items are not valid anymore"));
    return;
  }

  out_ports = wp_si_linkable_get_ports (si_out, self->out_item_port_context);
  in_ports = wp_si_linkable_get_ports (si_in, self->in_item_port_context);
  if (!out_ports || !in_ports) {
    wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
          WP_LIBRARY_ERROR_INVARIANT,
          "Failed to create links because one of the nodes has no ports"));
    return;
  }

  if (!create_links (self, transition, out_ports, in_ports))
      wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
          WP_LIBRARY_ERROR_INVARIANT,
          "Failed to create links because of wrong ports"));
}

static void
on_adapters_ready (GObject *obj, GAsyncResult * res, gpointer p)
{
  WpTransition *transition = p;
  WpSiStandardLink *self = wp_transition_get_source_object (transition);
  g_autoptr (GError) error = NULL;

  wp_si_adapter_set_ports_format_finish (WP_SI_ADAPTER (obj), res, &error);
  if (error) {
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  /* create links */
  get_ports_and_create_links (self, transition);
}

struct adapter
{
  WpSiAdapter *si;
  gboolean is_device;
  gboolean dont_remix;
  gboolean unpositioned;
  gboolean no_dsp;
  WpSpaPod *fmt;
  const gchar *mode;
};

static void
adapter_free (struct adapter *a)
{
  g_clear_object (&a->si);
  g_clear_pointer (&a->fmt, wp_spa_pod_unref);
  g_slice_free (struct adapter, a);
}

static void
configure_adapter (WpSiStandardLink *self, WpTransition *transition,
    struct adapter *main, struct adapter *other)
{
  /* configure other to have the same format with main, if necessary */
  if (!main->no_dsp && !other->dont_remix && !other->unpositioned && !main->unpositioned) {
    /* if formats are the same, no need to reconfigure */
    if (other->fmt && !g_strcmp0 (main->mode, other->mode)
        && wp_spa_pod_equal (main->fmt, other->fmt))
      get_ports_and_create_links (self, transition);
    else
      wp_si_adapter_set_ports_format (other->si, wp_spa_pod_ref (main->fmt),
          "dsp", on_adapters_ready, transition);
  } else if (main->no_dsp) {
    /* if formats are the same, no need to reconfigure */
    if (other->fmt && !g_strcmp0 (other->mode, "convert")
        && wp_spa_pod_equal (main->fmt, other->fmt))
      get_ports_and_create_links (self, transition);
    else
      wp_si_adapter_set_ports_format (other->si, wp_spa_pod_ref (main->fmt),
          "convert", on_adapters_ready, transition);
  } else {
    /* dont_remix or unpositioned case */
    if (other->fmt)
      get_ports_and_create_links (self, transition);
    else
      wp_si_adapter_set_ports_format (other->si, NULL,
          "dsp", on_adapters_ready, transition);
  }
}

static void
on_main_adapter_ready (GObject *obj, GAsyncResult * res, gpointer p)
{
  WpTransition *transition = p;
  WpSiStandardLink *self = wp_transition_get_source_object (transition);
  g_autoptr (GError) error = NULL;
  struct adapter *main, *other;

  wp_si_adapter_set_ports_format_finish (WP_SI_ADAPTER (obj), res, &error);
  if (error) {
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  main = g_object_get_data (G_OBJECT (transition), "adapter_main");
  other = g_object_get_data (G_OBJECT (transition), "adapter_other");

  if (!wp_object_test_active_features (WP_OBJECT (main->si), WP_SESSION_ITEM_FEATURE_ACTIVE) ||
      !wp_object_test_active_features (WP_OBJECT (other->si), WP_SESSION_ITEM_FEATURE_ACTIVE)) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "si-standard-link: in/out items are not valid anymore"));
    return;
  }

  if (self->passthrough) {
    wp_si_adapter_set_ports_format (other->si, NULL, "passthrough",
        on_adapters_ready, transition);
  } else {
    /* get the up-to-date formats */
    g_clear_pointer (&main->fmt, wp_spa_pod_unref);
    g_clear_pointer (&other->fmt, wp_spa_pod_unref);
    main->fmt = wp_si_adapter_get_ports_format (main->si, &main->mode);
    other->fmt = wp_si_adapter_get_ports_format (other->si, &other->mode);

    /* now configure other based on main */
    configure_adapter (self, transition, main, other);
  }
}

static void
configure_and_link_adapters (WpSiStandardLink *self, WpTransition *transition)
{
  g_autoptr (WpSiAdapter) si_out =
      WP_SI_ADAPTER (g_weak_ref_get (&self->out_item));
  g_autoptr (WpSiAdapter) si_in =
      WP_SI_ADAPTER (g_weak_ref_get (&self->in_item));
  struct adapter *out, *in, *main, *other;
  const gchar *str = NULL;

  if (!si_out || !si_in ||
      !wp_object_test_active_features (WP_OBJECT (si_out), WP_SESSION_ITEM_FEATURE_ACTIVE) ||
      !wp_object_test_active_features (WP_OBJECT (si_in), WP_SESSION_ITEM_FEATURE_ACTIVE)) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "si-standard-link: in/out items are not valid anymore"));
    return;
  }

  out = g_slice_new0 (struct adapter);
  in = g_slice_new0 (struct adapter);
  out->si = g_steal_pointer (&si_out);
  in->si = g_steal_pointer (&si_in);

  str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.node.type");
  out->is_device = !g_strcmp0 (str, "device");
  str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.node.type");
  in->is_device = !g_strcmp0 (str, "device");

  str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.factory.name");
  out->is_device = (str && !g_strcmp0 (str, "si-audio-virtual") && !in->is_device)
      || out->is_device;
  str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.factory.name");
  in->is_device = (str && !g_strcmp0 (str, "si-audio-virtual") && !out->is_device)
      || in->is_device;

  str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "stream.dont-remix");
  out->dont_remix = str && pw_properties_parse_bool (str);
  str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "stream.dont-remix");
  in->dont_remix = str && pw_properties_parse_bool (str);

  str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.node.unpositioned");
  out->unpositioned = str && pw_properties_parse_bool (str);
  str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.node.unpositioned");
  in->unpositioned = str && pw_properties_parse_bool (str);

  str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.features.no-dsp");
  out->no_dsp = str && pw_properties_parse_bool (str);
  str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.features.no-dsp");
  in->no_dsp = str && pw_properties_parse_bool (str);

  wp_debug_object (self, "out [device:%d, dont_remix %d, unpos %d], "
      "in: [device %d, dont_remix %d, unpos %d]",
      out->is_device, out->dont_remix, out->unpositioned,
      in->is_device, in->dont_remix, in->unpositioned);

  /* we always use out->si format, unless in->si is device */
  if (!out->is_device && in->is_device) {
    main = in;
    other = out;
  } else {
    main = out;
    other = in;
  }

  /* always configure both adapters in passthrough mode
     if this is a passthrough link */
  if (self->passthrough) {
    g_object_set_data_full (G_OBJECT (transition), "adapter_main", main,
        (GDestroyNotify) adapter_free);
    g_object_set_data_full (G_OBJECT (transition), "adapter_other", other,
        (GDestroyNotify) adapter_free);
    wp_si_adapter_set_ports_format (main->si, NULL, "passthrough",
        on_main_adapter_ready, transition);
    return;
  }

  main->fmt = wp_si_adapter_get_ports_format (main->si, &main->mode);
  other->fmt = wp_si_adapter_get_ports_format (other->si, &other->mode);

  if (main->fmt)
    /* ideally, configure other based on main */
    configure_adapter (self, transition, main, other);
  else if (other->fmt)
    /* if main is not configured but other is, do it the other way around */
    configure_adapter (self, transition, other, main);
  else {
    /* no adapter configured, let's configure main first */
    g_object_set_data_full (G_OBJECT (transition), "adapter_main", main,
        (GDestroyNotify) adapter_free);
    g_object_set_data_full (G_OBJECT (transition), "adapter_other", other,
        (GDestroyNotify) adapter_free);
    wp_si_adapter_set_ports_format (main->si, NULL,
        main->no_dsp ? "passthrough" : "dsp", on_main_adapter_ready, transition);
    return;
  }

  adapter_free (main);
  adapter_free (other);
}

static void
si_standard_link_do_link (WpSiStandardLink *self, WpTransition *transition)
{
  g_autoptr (WpSessionItem) si_out = g_weak_ref_get (&self->out_item);
  g_autoptr (WpSessionItem) si_in = g_weak_ref_get (&self->in_item);

  if (!si_out || !si_in ||
      !wp_object_test_active_features ((WP_OBJECT (si_out)), WP_SESSION_ITEM_FEATURE_ACTIVE) ||
      !wp_object_test_active_features ((WP_OBJECT (si_in)), WP_SESSION_ITEM_FEATURE_ACTIVE)) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "si-standard-link: in/out items are not valid anymore"));
    return;
  }

  if (WP_IS_SI_ADAPTER (si_out) && WP_IS_SI_ADAPTER (si_in))
    configure_and_link_adapters (self, transition);
  else if (!WP_IS_SI_ADAPTER (si_out) && !WP_IS_SI_ADAPTER (si_in))
    get_ports_and_create_links (self, transition);
  else
    wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
          WP_LIBRARY_ERROR_INVARIANT,
          "Adapters cannot be linked with non-adapters"));
}

static void
on_item_acquired (WpSiAcquisition * acq, GAsyncResult * res,
    WpTransition * transition)
{
  WpSiStandardLink *self = wp_transition_get_source_object (transition);
  g_autoptr (GError) error = NULL;

  if (!wp_si_acquisition_acquire_finish (acq, res, &error)) {
    wp_transition_return_error (transition, g_steal_pointer (&error));
    return;
  }

  self->n_async_ops_wait--;
  if (self->n_async_ops_wait == 0)
    si_standard_link_do_link (self, transition);
}

static void
si_standard_link_enable_active (WpSessionItem *si, WpTransition *transition)
{
  WpSiStandardLink *self = WP_SI_STANDARD_LINK (si);
  g_autoptr (WpSessionItem) si_out = NULL;
  g_autoptr (WpSessionItem) si_in = NULL;
  WpSiAcquisition *out_acquisition = NULL, *in_acquisition = NULL;

  if (!wp_session_item_is_configured (si)) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "si-standard-link: item is not configured"));
    return;
  }

  /* make sure in/out items are valid */
  si_out = g_weak_ref_get (&self->out_item);
  si_in = g_weak_ref_get (&self->in_item);
  if (!si_out || !si_in ||
      !wp_object_test_active_features ((WP_OBJECT (si_out)), WP_SESSION_ITEM_FEATURE_ACTIVE) ||
      !wp_object_test_active_features ((WP_OBJECT (si_in)), WP_SESSION_ITEM_FEATURE_ACTIVE)) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "si-standard-link: in/out items are not valid anymore"));
    return;
  }

  /* acquire */
  out_acquisition = wp_si_linkable_get_acquisition (WP_SI_LINKABLE (si_out));
  in_acquisition = wp_si_linkable_get_acquisition (WP_SI_LINKABLE (si_in));
  if (out_acquisition && in_acquisition)
    self->n_async_ops_wait = 2;
  else if (out_acquisition || in_acquisition)
    self->n_async_ops_wait = 1;
  else {
    self->n_async_ops_wait = 0;
    si_standard_link_do_link (self, transition);
    return;
  }

  if (out_acquisition) {
    wp_si_acquisition_acquire (out_acquisition, WP_SI_LINK (self),
        WP_SI_LINKABLE (si_out), (GAsyncReadyCallback) on_item_acquired,
        transition);
  }
  if (in_acquisition) {
    wp_si_acquisition_acquire (in_acquisition, WP_SI_LINK (self),
        WP_SI_LINKABLE (si_in), (GAsyncReadyCallback) on_item_acquired,
        transition);
  }
}

static void
si_standard_link_finalize (GObject * object)
{
  WpSiStandardLink *self = WP_SI_STANDARD_LINK (object);

  g_weak_ref_clear (&self->out_item);
  g_weak_ref_clear (&self->in_item);

  G_OBJECT_CLASS (si_standard_link_parent_class)->finalize (object);
}

static void
si_standard_link_class_init (WpSiStandardLinkClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpSessionItemClass *si_class = (WpSessionItemClass *) klass;

  object_class->finalize = si_standard_link_finalize;

  si_class->reset = si_standard_link_reset;
  si_class->configure = si_standard_link_configure;
  si_class->get_associated_proxy = si_standard_link_get_associated_proxy;
  si_class->disable_active = si_standard_link_disable_active;
  si_class->enable_active = si_standard_link_enable_active;

  signals[SIGNAL_LINK_ERROR] = g_signal_new (
      "link-error", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
}

static GVariant *
si_standard_link_get_registration_info (WpSiLink * item)
{
  GVariantBuilder b;
  g_variant_builder_init (&b, G_VARIANT_TYPE ("a{ss}"));
  return g_variant_builder_end (&b);
}

static WpSiLinkable *
si_standard_link_get_out_item (WpSiLink * item)
{
  WpSiStandardLink *self = WP_SI_STANDARD_LINK (item);
  return WP_SI_LINKABLE (g_weak_ref_get (&self->out_item));
}

static WpSiLinkable *
si_standard_link_get_in_item (WpSiLink * item)
{
  WpSiStandardLink *self = WP_SI_STANDARD_LINK (item);
  return WP_SI_LINKABLE (g_weak_ref_get (&self->in_item));
}

static void
si_standard_link_link_init (WpSiLinkInterface * iface)
{
  iface->get_registration_info = si_standard_link_get_registration_info;
  iface->get_out_item = si_standard_link_get_out_item;
  iface->get_in_item = si_standard_link_get_in_item;
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (wp_si_factory_new_simple (SI_FACTORY_NAME,
      si_standard_link_get_type ()));
}
   070701000000FF000081A400000000000000000000000165F8630400003DB0000000000000000000000000000000000000003900000000wireplumber-0.5.0/modules/module-standard-event-source.c  /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("m-standard-event-source")

/*
 * Module subscribes for certain object manager events to and pushes them as
 * events on to the Event Stack.
 */

enum {
  ACTION_GET_OBJECT_MANAGER,
  ACTION_CREATE_EVENT,
  ACTION_PUSH_EVENT,
  ACTION_SCHEDULE_RESCAN,
  N_SIGNALS
};

typedef enum {
  OBJECT_TYPE_PORT,
  OBJECT_TYPE_LINK,
  OBJECT_TYPE_NODE,
  OBJECT_TYPE_SESSION_ITEM,
  OBJECT_TYPE_CLIENT,
  OBJECT_TYPE_DEVICE,
  OBJECT_TYPE_METADATA,
  N_OBJECT_TYPES,
  OBJECT_TYPE_INVALID = N_OBJECT_TYPES
} ObjectType;

typedef enum {
  RESCAN_CONTEXT_LINKING,
  RESCAN_CONTEXT_DEFAULT_NODES,
  N_RESCAN_CONTEXTS,
} RescanContext;

static GType
rescan_context_get_type (void)
{
  static gsize gtype_id = 0;
  static const GEnumValue values[] = {
    { RESCAN_CONTEXT_LINKING, "RESCAN_CONTEXT_LINKING", "linking" },
    { RESCAN_CONTEXT_DEFAULT_NODES, "RESCAN_CONTEXT_DEFAULT_NODES", "default-nodes" },
    { 0, NULL, NULL }
  };
  if (g_once_init_enter (&gtype_id)) {
      GType new_type = g_enum_register_static (
          g_intern_static_string ("WpStandardEventSource_RescanContext"),
          values);
      g_once_init_leave (&gtype_id, new_type);
  }
  return (GType) gtype_id;
}
#define TYPE_RESCAN_CONTEXT (rescan_context_get_type ())

struct _WpStandardEventSource
{
  WpPlugin parent;
  WpObjectManager *oms[N_OBJECT_TYPES];
  WpEventHook *rescan_done_hook;
  gboolean rescan_scheduled[N_RESCAN_CONTEXTS];
  gint n_oms_installed;
};

static guint signals[N_SIGNALS] = {0};

G_DECLARE_FINAL_TYPE (WpStandardEventSource, wp_standard_event_source,
                      WP, STANDARD_EVENT_SOURCE, WpPlugin)
G_DEFINE_TYPE (WpStandardEventSource, wp_standard_event_source, WP_TYPE_PLUGIN)

static void
wp_standard_event_source_init (WpStandardEventSource * self)
{
}

static GType
object_type_to_gtype (ObjectType type)
{
  switch (type) {
    case OBJECT_TYPE_PORT: return WP_TYPE_PORT;
    case OBJECT_TYPE_LINK: return WP_TYPE_LINK;
    case OBJECT_TYPE_NODE: return WP_TYPE_NODE;
    case OBJECT_TYPE_SESSION_ITEM: return WP_TYPE_SESSION_ITEM;
    case OBJECT_TYPE_CLIENT: return WP_TYPE_CLIENT;
    case OBJECT_TYPE_DEVICE: return WP_TYPE_DEVICE;
    case OBJECT_TYPE_METADATA: return WP_TYPE_METADATA;
    default:
      g_assert_not_reached ();
  }
}

static ObjectType
type_str_to_object_type (const gchar * type_str)
{
  if (!g_strcmp0 (type_str, "port"))
    return OBJECT_TYPE_PORT;
  else if (!g_strcmp0 (type_str, "link"))
    return OBJECT_TYPE_LINK;
  else if (!g_strcmp0 (type_str, "node"))
    return OBJECT_TYPE_NODE;
  else if (!g_strcmp0 (type_str, "session-item"))
    return OBJECT_TYPE_SESSION_ITEM;
  else if (!g_strcmp0 (type_str, "client"))
    return OBJECT_TYPE_CLIENT;
  else if (!g_strcmp0 (type_str, "device"))
    return OBJECT_TYPE_DEVICE;
  else if (!g_strcmp0 (type_str, "metadata"))
    return OBJECT_TYPE_METADATA;
  else
    return OBJECT_TYPE_INVALID;
}

static const gchar *
get_object_type (gpointer obj, WpProperties **properties)
{
  /* keeping these sorted by the frequency of events related to these objects */
  if (WP_IS_PORT (obj))
    return "port";
  else if (WP_IS_LINK (obj))
    return "link";
  else if (WP_IS_NODE (obj))
    return "node";
  else if (WP_IS_SESSION_ITEM (obj)) {
    if (*properties == NULL)
      *properties = wp_properties_new_empty();

    if (WP_IS_SI_LINKABLE (obj)) {
      wp_properties_set (*properties,
          "event.session-item.interface", "linkable");
    } else if (WP_IS_SI_LINK (obj)) {
      wp_properties_set (*properties,
          "event.session-item.interface", "link");
    }
    return "session-item";
  }
  else if (WP_IS_CLIENT (obj))
    return "client";
  else if (WP_IS_DEVICE (obj))
    return "device";
  else if (WP_IS_METADATA (obj))
    return "metadata";

  wp_debug_object (obj, "Unknown global proxy type");
  return G_OBJECT_TYPE_NAME (obj);
}

static gint
get_default_event_priority (const gchar *event_type)
{
  if (g_str_has_prefix(event_type, "select-") ||
        g_str_has_prefix(event_type, "create-"))
    return 500;
  else if (!g_strcmp0 (event_type, "rescan-for-default-nodes"))
    return -490;
  else if (!g_strcmp0 (event_type, "rescan-for-linking"))
    return -500;
  else if (!g_strcmp0 (event_type, "node-state-changed"))
    return 50;
  else if (!g_strcmp0 (event_type, "metadata-changed"))
    return 50;
  else if (g_str_has_suffix (event_type, "-params-changed"))
    return 50;
  else if (g_str_has_prefix (event_type, "client-"))
    return 200;
  else if (g_str_has_prefix (event_type, "device-"))
    return 170;
  else if (g_str_has_prefix (event_type, "port-"))
    return 150;
  else if (g_str_has_prefix (event_type, "node-"))
    return 130;
  else if (g_str_has_prefix (event_type, "session-item-"))
    return 110;
  else if (g_str_has_suffix (event_type, "-added") ||
           g_str_has_suffix (event_type, "-removed"))
    return 20;

  wp_debug ("Unknown event type: %s, using priority 0", event_type);
  return 0;
}

static WpObjectManager *
wp_standard_event_get_object_manager (WpStandardEventSource *self,
    const gchar * type_str)
{
  ObjectType type = type_str_to_object_type (type_str);

  if (G_UNLIKELY (type == OBJECT_TYPE_INVALID)) {
    wp_critical_object (self, "object type '%s' is not valid", type_str);
    return NULL;
  }

  g_return_val_if_fail (self->oms[type], NULL);
  return g_object_ref (self->oms[type]);
}

/* Local Events are events created by WirePlumber. */
static gboolean
is_it_local_event (const gchar *event_type)
{
  if (g_str_has_prefix(event_type, "select-") ||
    g_str_has_prefix(event_type, "create-"))
    return TRUE;

  return FALSE;
}

static WpEvent *
wp_standard_event_source_create_event (WpStandardEventSource *self,
    const gchar *event_type, gpointer subject, WpProperties *misc_properties)
{
  g_autoptr (WpEvent) event = NULL;
  g_autoptr (WpProperties) properties = wp_properties_new_empty ();
  g_autofree gchar *full_event_type = NULL;

  const gchar *subject_type =
      subject ? get_object_type (subject, &properties) : NULL;

  if (subject_type) {
    wp_properties_set (properties, "event.subject.type", subject_type);

    /* prefix the event with the subject type, unless it is a local event */
    if (!is_it_local_event (event_type)) {
      full_event_type = g_strdup_printf ("%s-%s", subject_type, event_type);
      event_type = full_event_type;
    }
  }
  if (misc_properties)
    wp_properties_add (properties, misc_properties);

  gint priority = get_default_event_priority (event_type);

  wp_debug_object (self,
      "pushing event '%s', prio %d, subject " WP_OBJECT_FORMAT " (%s)",
      event_type, priority, WP_OBJECT_ARGS (subject), subject_type);

  event = wp_event_new (event_type, priority, g_steal_pointer (&properties),
      G_OBJECT (self), G_OBJECT (subject));

  /* watch for subject pw-proxy-destroyed and cancel event,
     unless this is a "removed" event, in which case we expect the proxy
     to be destroyed and the event should still go through */
  if (subject && !g_str_has_suffix (event_type, "-removed")
        && g_type_is_a (G_OBJECT_TYPE (subject), WP_TYPE_PROXY)) {
    g_signal_connect_object (subject, "pw-proxy-destroyed",
        (GCallback) g_cancellable_cancel, wp_event_get_cancellable (event),
        G_CONNECT_SWAPPED);
  }

  return g_steal_pointer (&event);
}

static void
wp_standard_event_source_push_event (WpStandardEventSource *self,
    const gchar *event_type, gpointer subject, WpProperties *misc_properties)
{

  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));

  /* this can happen during the core dispose sequence; the weak ref to the
     core is invalidated before the registered objects are destroyed */
  if (!core)
    return;

  g_autoptr (WpEventDispatcher) dispatcher =
      wp_event_dispatcher_get_instance (core);
  g_return_if_fail (dispatcher);

  wp_event_dispatcher_push_event (dispatcher,
      wp_standard_event_source_create_event (
      self, event_type, subject, misc_properties));
}

static void
wp_standard_event_source_schedule_rescan (WpStandardEventSource *self,
    RescanContext context)
{
  if (!self->rescan_scheduled[context]) {
    g_autoptr (GEnumClass) klass = g_type_class_ref (TYPE_RESCAN_CONTEXT);
    GEnumValue *value = g_enum_get_value (klass, context);
    g_autofree gchar *event_type = g_strdup_printf ("rescan-for-%s",
        value->value_nick);
    wp_standard_event_source_push_event (self, event_type, NULL, NULL);
    self->rescan_scheduled[context] = TRUE;
  }
}

static void
on_metadata_changed (WpMetadata *obj, guint32 subject,
    const gchar *key, const gchar *spa_type, const gchar *value,
    WpStandardEventSource *self)
{
  g_autoptr (WpProperties) properties = wp_properties_new_empty ();
  wp_properties_setf (properties, "event.subject.id", "%u", subject);
  wp_properties_set (properties, "event.subject.key", key);
  wp_properties_set (properties, "event.subject.spa_type", spa_type);
  wp_properties_set (properties, "event.subject.value", value);

  wp_standard_event_source_push_event (self, "changed", obj, properties);
}

static void
on_params_changed (WpPipewireObject *obj, const gchar *id,
    WpStandardEventSource *self)
{
  g_autoptr (WpProperties) properties = wp_properties_new_empty ();
  wp_properties_set (properties, "event.subject.param-id", id);

  wp_standard_event_source_push_event (self, "params-changed", obj, properties);
}

static void
on_node_state_changed (WpNode *obj, WpNodeState old_state,
    WpNodeState new_state, WpStandardEventSource *self)
{
  g_autoptr (GEnumClass) klass = g_type_class_ref (WP_TYPE_NODE_STATE);
  GEnumValue *old_value = g_enum_get_value (klass, old_state);
  GEnumValue *new_value = g_enum_get_value (klass, new_state);

  g_autoptr (WpProperties) properties = wp_properties_new (
      "event.subject.old-state", old_value ? old_value->value_nick : NULL,
      "event.subject.new-state", new_value ? new_value->value_nick : NULL,
      NULL);
  wp_standard_event_source_push_event (self, "state-changed", obj,
      properties);
}

static void
on_object_added (WpObjectManager *om, WpObject *obj, WpStandardEventSource *self)
{
  wp_standard_event_source_push_event (self, "added", obj, NULL);

  if (WP_IS_PIPEWIRE_OBJECT (obj)) {
    g_signal_connect_object (obj, "params-changed",
        G_CALLBACK (on_params_changed), self, 0);
  }

  if (WP_IS_NODE (obj)) {
    g_signal_connect_object (obj, "state-changed",
        G_CALLBACK (on_node_state_changed), self, 0);
  }
  else if (WP_IS_METADATA (obj)) {
    g_signal_connect_object (obj, "changed",
        G_CALLBACK (on_metadata_changed), self, 0);
  }
}

static void
on_object_removed (WpObjectManager *om, WpObject *obj, WpStandardEventSource *self)
{
  wp_standard_event_source_push_event (self, "removed", obj, NULL);
}

static void
on_om_installed (WpObjectManager * om, WpStandardEventSource * self)
{
  if (++self->n_oms_installed == N_OBJECT_TYPES)
    wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}

static void
on_rescan_done (WpEvent * event, WpStandardEventSource * self)
{
  g_autoptr (WpProperties) properties = wp_event_get_properties (event);
  const gchar *event_type = wp_properties_get (properties, "event.type");

  /* the event type is "rescan-for-<context>" and the enum nickname is just
     "<context>", so we get the substring from the 12th character onwards */
  g_autoptr (GEnumClass) klass = g_type_class_ref (TYPE_RESCAN_CONTEXT);
  GEnumValue *value = g_enum_get_value_by_nick (klass, &event_type[11]);

  g_return_if_fail (value != NULL && value->value_nick != NULL);
  self->rescan_scheduled[value->value] = FALSE;
}

static void
wp_standard_event_source_enable (WpPlugin * plugin, WpTransition * transition)
{
  WpStandardEventSource * self = WP_STANDARD_EVENT_SOURCE (plugin);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
  g_return_if_fail (core);
  g_autoptr (WpEventDispatcher) dispatcher =
      wp_event_dispatcher_get_instance (core);
  g_return_if_fail (dispatcher);

  /* install object managers */
  self->n_oms_installed = 0;
  for (gint i = 0; i < N_OBJECT_TYPES; i++) {
    GType gtype = object_type_to_gtype (i);
    self->oms[i] = wp_object_manager_new ();
    wp_object_manager_add_interest (self->oms[i], gtype, NULL);
    wp_object_manager_request_object_features (self->oms[i],
        gtype, WP_OBJECT_FEATURES_ALL);
    g_signal_connect_object (self->oms[i], "object-added",
        G_CALLBACK (on_object_added), self, 0);
    g_signal_connect_object (self->oms[i], "object-removed",
        G_CALLBACK (on_object_removed), self, 0);
    g_signal_connect_object (self->oms[i], "installed",
        G_CALLBACK (on_om_installed), self, 0);
    wp_core_install_object_manager (core, self->oms[i]);
  }

  /* install hook to restore the rescan_scheduled state just before rescanning */
  self->rescan_done_hook = wp_simple_event_hook_new (
      "m-standard-event-source/rescan-done", (const gchar *[]) { "*", NULL }, NULL,
      g_cclosure_new_object ((GCallback) on_rescan_done, G_OBJECT (self)));
  wp_interest_event_hook_add_interest (
      WP_INTEREST_EVENT_HOOK (self->rescan_done_hook),
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "#s", "rescan-for-*",
      NULL);
  wp_event_dispatcher_register_hook (dispatcher, self->rescan_done_hook);
}

static void
wp_standard_event_source_disable (WpPlugin * plugin)
{
  WpStandardEventSource * self = WP_STANDARD_EVENT_SOURCE (plugin);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
  g_autoptr (WpEventDispatcher) dispatcher = core ?
      wp_event_dispatcher_get_instance (core) : NULL;

  for (gint i = 0; i < N_OBJECT_TYPES; i++)
    g_clear_object (&self->oms[i]);

  if (dispatcher)
    wp_event_dispatcher_unregister_hook (dispatcher, self->rescan_done_hook);
  g_clear_object (&self->rescan_done_hook);
}

static void
wp_standard_event_source_class_init (WpStandardEventSourceClass * klass)
{
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  plugin_class->enable = wp_standard_event_source_enable;
  plugin_class->disable = wp_standard_event_source_disable;

  signals[ACTION_GET_OBJECT_MANAGER] = g_signal_new_class_handler (
      "get-object-manager", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_standard_event_get_object_manager,
      NULL, NULL, NULL, WP_TYPE_OBJECT_MANAGER, 1, G_TYPE_STRING);

  signals[ACTION_CREATE_EVENT] = g_signal_new_class_handler (
      "create-event", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_standard_event_source_create_event,
      NULL, NULL, NULL, WP_TYPE_EVENT, 3,
      G_TYPE_STRING, WP_TYPE_OBJECT, WP_TYPE_PROPERTIES);

  signals[ACTION_PUSH_EVENT] = g_signal_new_class_handler (
      "push-event", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_standard_event_source_push_event,
      NULL, NULL, NULL, G_TYPE_NONE, 3,
      G_TYPE_STRING, WP_TYPE_OBJECT, WP_TYPE_PROPERTIES);

  signals[ACTION_SCHEDULE_RESCAN] = g_signal_new_class_handler (
      "schedule-rescan", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_standard_event_source_schedule_rescan,
      NULL, NULL, NULL, G_TYPE_NONE, 1, TYPE_RESCAN_CONTEXT);
}

WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
  return G_OBJECT (g_object_new (wp_standard_event_source_get_type (),
      "name", "standard-event-source",
      "core", core,
      NULL));
}
07070100000100000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001500000000wireplumber-0.5.0/po  07070100000101000081A400000000000000000000000165F86304000000B7000000000000000000000000000000000000001D00000000wireplumber-0.5.0/po/LINGUAS  af
as
be
bg
bn_IN
ca
cs
da
de
de_CH
el
eo
es
fa
fi
fr
gl
gu
he
hi
hr
hu
id
it
ja
ka
kk
kn
ko
lt
ml
mr
my
nl
nn
oc
or
pa
pl
pt
pt_BR
ro
ru
si
sk
sr
sr@latin
sv
ta
te
tr
uk
zh_CN
zh_TW
 07070100000102000081A400000000000000000000000165F863040000006F000000000000000000000000000000000000002100000000wireplumber-0.5.0/po/POTFILES.in  src/scripts/monitors/alsa.lua
src/scripts/monitors/bluez-midi.lua
src/scripts/monitors/libcamera/name-node.lua
 07070100000103000081A400000000000000000000000165F86304000000A6000000000000000000000000000000000000002300000000wireplumber-0.5.0/po/POTFILES.skip    src/systemd/system/wireplumber.service.in
src/systemd/system/wireplumber@.service.in
src/systemd/user/wireplumber.service.in
src/systemd/user/wireplumber@.service.in
  07070100000104000081A400000000000000000000000165F8630400000758000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/af.po    # Afrikaans translation of PipeWire.
# This file is distributed under the same license as the PipeWire package.
# F Wolff <friedel@translate.org.za>, 2019.
msgid ""
msgstr ""
"Project-Id-Version: master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2019-01-09 12:17+0200\n"
"Last-Translator: F Wolff <friedel@translate.org.za>\n"
"Language-Team: translate-discuss-af@lists.sourceforge.net\n"
"Language: af\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Virtaal 0.7.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Ingeboude oudio"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
07070100000105000081A400000000000000000000000165F86304000007CD000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/as.po    # translation of pipewire.master-tx.as.po to Assamese
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Amitakhya Phukan <aphukan@fedoraproject.org>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx.as\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:52+0000\n"
"Last-Translator: Amitakhya Phukan <aphukan@fedoraproject.org>\n"
"Language-Team: Assamese <>\n"
"Language: as\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Lokalize 0.2\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "আভ্যন্তৰীণ অ'ডিঅ'"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "মোডেম"
   07070100000106000081A400000000000000000000000165F8630400000FB3000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/be.po    # Belarusian tanslation of PipeWire
# Copyright (C) 2016 PipeWire's COPYRIGHT HOLDER
# This file is distributed under the same license as the PipeWire package.
# Viktar Vaŭčkievič <victorenator@gmail.com>, 2016-2023.
#
msgid ""
msgstr ""
"Project-Id-Version: PipeWire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2023-03-11 14:28+0300\n"
"PO-Revision-Date: 2023-03-11 14:31+0300\n"
"Last-Translator: Viktar Vaŭčkievič <victorenator@gmail.com>\n"
"Language-Team: Belarusian <>\n"
"Language: be\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Gtranslator 42.0\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:236
msgid "Loopback"
msgstr "Пятля"

#: src/scripts/monitors/alsa.lua:238
msgid "Built-in Audio"
msgstr "Убудаванае аўдыя"

#: src/scripts/monitors/alsa.lua:240
msgid "Modem"
msgstr "Мадэм"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "Убудаваная пярэдняя камера"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "Убудаваная задняя камера"
 07070100000107000081A400000000000000000000000165F863040000070F000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/bg.po    # Valentin Laskov <laskov@festa.bg>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2020-10-15 21:30+0000\n"
"Last-Translator: Emanuil Novachev <em.novachev@gmail.com>\n"
"Language-Team: Bulgarian <https://translate.fedoraproject.org/projects/"
"pipewire/pipewire/bg/>\n"
"Language: bg\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.2.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr ""

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr ""
 07070100000108000081A400000000000000000000000165F86304000007E7000000000000000000000000000000000000001E00000000wireplumber-0.5.0/po/bn_IN.po # translation of pipewire.master-tx.bn_IN.po to Bengali INDIA
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Runa Bhattacharjee <runab@redhat.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx.bn_IN\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:52+0000\n"
"Last-Translator: Runa Bhattacharjee <runab@redhat.com>\n"
"Language-Team: Bengali INDIA <anubad@lists.ankur.org.in>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: KBabel 1.11.4\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "অভ্যন্তরীণ অডিও"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "মোডেম"
 07070100000109000081A400000000000000000000000165F86304000012A1000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/ca.po    # Catalan translation of pipewire by Softcatalà
# Copyright (C) 2008 Free Software Foundation
# This file is distributed under the same license as the pipewire package.
#
# Xavier Conde Rueda <xavi.conde@gmail.com>, 2008.
# Agustí Grau <fletxa@gmail.com>, 2009.
# Judith Pintó Subirada <judithp@gmail.com>
#
# This file is translated according to the glossary and style guide of
# Softcatalà. If you plan to modify this file, please read first the page
# of the Catalan translation team for the Fedora project at:
# http://www.softcatala.org/projectes/fedora/
# and contact the previous translator.
#
# Aquest fitxer s'ha de traduir d'acord amb el recull de termes i la guia
# d'estil de Softcatalà. Si voleu modificar aquest fitxer, llegiu si
# us plau la pàgina de catalanització del projecte Fedora a:
# http://www.softcatala.org/projectes/fedora/
# i contacteu l'anterior traductor/a.
# Josep Torné Llavall <josep.torne@gmail.com>, 2009, 2012.
# Robert Antoni Buj Gelonch <rbuj@fedoraproject.org>, 2016. #zanata
# Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata
# Robert Antoni Buj Gelonch <rbuj@fedoraproject.org>, 2017. #zanata
# Robert Antoni Buj Gelonch <rbuj@fedoraproject.org>, 2019. #zanata
# Jordi Mas i Hernàndez <jmas@softcatala.org>, 2022
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues\n"
"POT-Creation-Date: 2022-07-08 03:32+0000\n"
"PO-Revision-Date: 2022-09-12 09:52+0000\n"
"Last-Translator: Jordi Mas i Hernàndez <jmas@softcatala.org>\n"
"Language-Team: Catalan <fedora@softcatala.net>\n"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#. WirePlumber
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:228
msgid "Built-in Audio"
msgstr "Àudio intern"

#: src/scripts/monitors/alsa.lua:230
msgid "Modem"
msgstr "Mòdem"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device
#. name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "Càmera frontal integrada"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "Càmera posterior integrada"
   0707010000010A000081A400000000000000000000000165F8630400000F5B000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/cs.po    # Czech translation of pipewire.
# Copyright (C) 2008, 2009 the author(s) of pipewire.
# This file is distributed under the same license as the pipewire package.
# Petr Kovar <pknbe@volny.cz>, 2008, 2009, 2012.
# Daniel Rusek <mail@asciiwolf.com>, 2018.
# Marek Černocký <marek@manet.cz>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2022-07-08 03:32+0000\n"
"PO-Revision-Date: 2022-10-19 20:52+0200\n"
"Last-Translator: Daniel Rusek <mail@asciiwolf.com>\n"
"Language-Team: čeština <gnome-cs-list@gnome.org>\n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Poedit 3.1.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:228
msgid "Built-in Audio"
msgstr "Vnitřní zvukový systém"

#: src/scripts/monitors/alsa.lua:230
msgid "Modem"
msgstr "Modem"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "Vestavěná přední kamera"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "Vestavěná zadní kamera"
 0707010000010B000081A400000000000000000000000165F8630400000727000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/da.po    # Danish translation for PipeWire.
# Copyright (C) 2019 PipeWire's COPYRIGHT HOLDER
# This file is distributed under the same license as the PipeWire package.
# scootergrisen, 2019, 2021.
msgid ""
msgstr ""
"Project-Id-Version: PipeWire master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-04-19 20:26+0200\n"
"Last-Translator: scootergrisen\n"
"Language-Team: Danish\n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Indbygget lyd"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
 0707010000010C000081A400000000000000000000000165F863040000100C000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/de.po    # translation of WirePlumber
# German translation of WirePlumber
# Copyright (C) 2008 WirePlumber
# This file is distributed under the same license as the WirePlumber package.
#
#
# Fabian Affolter <fab@fedoraproject.org>, 2008-2009.
# Micha Pietsch <barney@fedoraproject.org>, 2008, 2009.
# Hedda Peters <hpeters@redhat.com>, 2009, 2012.
# Mario Blättermann <mario.blaettermann@gmail.com>, 2016.
# Jürgen Benvenuti <gastornis@posteo.org>, 2023.
#
msgid ""
msgstr ""
"Project-Id-Version: WirePlumber\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2023-01-30 15:31+0000\n"
"PO-Revision-Date: 2023-01-28 20:55+0100\n"
"Last-Translator: Jürgen Benvenuti <gastornis@posteo.org>\n"
"Language-Team: German <gnome-de@gnome.org>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Poedit 3.1.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:234
msgid "Loopback"
msgstr "Schleifenschaltung"

#: src/scripts/monitors/alsa.lua:236
msgid "Built-in Audio"
msgstr "Internes Audio"

#: src/scripts/monitors/alsa.lua:238
msgid "Modem"
msgstr "Modem"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "Interne vordere Kamera"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "Interne hintere Kamera"
0707010000010D000081A400000000000000000000000165F86304000007BC000000000000000000000000000000000000001E00000000wireplumber-0.5.0/po/de_CH.po # German translation of pipewire
# Copyright (C) 2008 pipewire
# This file is distributed under the same license as the pipewire package.
#
# Micha Pietsch <barney@fedoraproject.org>, 2008, 2009.
# Fabian Affolter <fab@fedoraproject.org>, 2008-2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:53+0000\n"
"Last-Translator: Fabian Affolter <fab@fedoraproject.org>\n"
"Language-Team: German <fedora-trans-de@redhat.com>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-Language: German\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Internes Audio"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
0707010000010E000081A400000000000000000000000165F8630400000813000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/el.po    # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Dimitris Glezos <dimitris@glezos.com>, 2008.
# Thalia Papoutsaki <saliyath@gmail.com>, 2009, 2012.
# Dimitris Spingos (Δημήτρης Σπίγγος) <dmtrs32@gmail.com>, 2013, 2014.
msgid ""
msgstr ""
"Project-Id-Version: el\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2014-05-08 09:53+0300\n"
"Last-Translator: Dimitris Spingos (Δημήτρης Σπίγγος) <dmtrs32@gmail.com>\n"
"Language-Team: team@lists.gnome.gr\n"
"Language: el\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Virtaal 0.7.0\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Εσωτερικός ήχος"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Μόντεμ"
 0707010000010F000081A400000000000000000000000165F86304000007A9000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/eo.po    # SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the pipewire package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-02-05 01:40+0000\n"
"Last-Translator: Carmen Bianca Bakker <carmen@carmenbianca.eu>\n"
"Language-Team: Esperanto <https://translate.fedoraproject.org/projects/"
"pipewire/pipewire/eo/>\n"
"Language: eo\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.4.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Integrita sono"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr ""
   07070100000110000081A400000000000000000000000165F863040000091B000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/es.po    # Fedora Spanish translation of PipeWire.
# This file is distributed under the same license as the PipeWire Package.
#
# Domingo Becker <domingobecker@gmail.com>, 2009.
# Héctor Daniel Cabrera <h.daniel.cabrera@gmail.com>, 2009.
# Fernando Gonzalez Blanco <fgonz@fedoraproject.org>, 2009, 2012.
# Alberto Castillo <acg@albertocg.com>, 2016. #zanata
# Gladys Guerrero Lozano <gguerrer@redhat.com>, 2016. #zanata
# Máximo Castañeda Riloba <mcrcctm@gmail.com>, 2016. #zanata
# Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2020-10-01 15:30+0000\n"
"Last-Translator: Emilio Herrera <ehespinosa57@gmail.com>\n"
"Language-Team: Spanish <https://translate.fedoraproject.org/projects/"
"pipewire/pipewire/es/>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.2.2\n"
"X-Poedit-Language: Spanish\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Audio Interno"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Módem"
 07070100000111000081A400000000000000000000000165F8630400000F17000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/fa.po    # Persian translation for WirePlumber.
# Copyright (C) 2022 WirePlumber's COPYRIGHT HOLDER
# This file is distributed under the same license as the WirePlumber package.
# Danial Behzadi <dani.behzi@ubuntu.com>, 2022-2023.
#
msgid ""
msgstr ""
"Project-Id-Version: WirePlumber master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2023-10-06 03:31+0000\n"
"PO-Revision-Date: 2023-10-06 16:22+0330\n"
"Last-Translator: Danial Behzadi <dani.behzi@ubuntu.com>\n"
"Language-Team: Persian <fa@li.org>\n"
"Language: fa\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.3.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:236
msgid "Loopback"
msgstr "حلقهٔ معکوس"

#: src/scripts/monitors/alsa.lua:238
msgid "Built-in Audio"
msgstr "صدای توکار"

#: src/scripts/monitors/alsa.lua:240
msgid "Modem"
msgstr "مودم"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "دوربین جلوی توکار"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "دوربین عقب توکار"
 07070100000112000081A400000000000000000000000165F86304000007EA000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/fi.po    # pipewire translation to Finnish (fi).
# Copyright (C) 2008 Timo Jyrinki
# This file is distributed under the same license as the pipewire package.
# Timo Jyrinki <timo.jyrinki@iki.fi>, 2008.
# Ville-Pekka Vainio <vpivaini@cs.helsinki.fi>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: git trunk\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-04-18 16:38+0300\n"
"Last-Translator: Ricky Tigg <ricky.tigg@gmail.com>\n"
"Language-Team: Finnish <https://translate.fedoraproject.org/projects/"
"pipewire/pipewire/fi/>\n"
"Language: fi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Sisäinen äänentoisto"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modeemi"
  07070100000113000081A400000000000000000000000165F8630400000940000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/fr.po    # French translation of pipewire.
# Copyright (C) 2006-2008 Lennart Poettering
# This file is distributed under the same license as the pipewire package.
#
#
# Robert-André Mauchin <zebob.m@pengzone.org>, 2008.
# Michaël Ughetto <telimektar esraonline com>, 2008.
# Pablo Martin-Gomez <pablo.martin-gomez@laposte.net>, 2008.
# Corentin Perard <corentin.perard@gmail.com>, 2009.
# Thomas Canniot <mrtom@fedoraproject.org>, 2009, 2012.
# Sam Friedmann <sfriedma@redhat.com>, 2016. #zanata
# Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata
# Edouard Duliege <edouard.duliege@gmail.com>, 2017. #zanata
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2020-12-13 17:35+0000\n"
"Last-Translator: Julien Humbert <julroy67@gmail.com>\n"
"Language-Team: French <https://translate.fedoraproject.org/projects/pipewire/"
"pipewire/fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.3.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Audio interne"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
07070100000114000081A400000000000000000000000165F86304000007FB000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/gl.po    # SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Translators:
# bassball93 <bassball93@gmail.com>, 2011.
# mbouzada <mbouzada@gmail.com>, 2011.
# Fran Dieguez <frandieguez@gnome.org>, 2012, 2019.
# Marcos Lans <marcoslansgarza@gmail.com>, 2018.
msgid ""
msgstr ""
"Project-Id-Version: PipeWire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2019-02-20 01:36+0200\n"
"Last-Translator: Fran Dieguez <frandieguez@gnome.org>\n"
"Language-Team: Galician\n"
"Language: gl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Virtaal 0.7.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Audio interno"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Módem"
 07070100000115000081A400000000000000000000000165F8630400000792000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/gu.po    # translation of PipeWire.po to Gujarati
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Sweta Kothari <swkothar@redhat.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: PipeWire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:53+0000\n"
"Last-Translator: Sweta Kothari <swkothar@redhat.com>\n"
"Language-Team: Gujarati\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: KBabel 1.11.4\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "આંતરિક ઓડિયો"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "મોડેમ"
  07070100000116000081A400000000000000000000000165F8630400000EC5000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/he.po    #
# Elad <el.il@doom.co.il>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2023-03-04 13:34+0000\n"
"PO-Revision-Date: 2023-12-06 10:07+0200\n"
"Last-Translator: Yaron Shahrabani <sh.yaron@gmail.com>\n"
"Language-Team: Hebrew <https://translate.fedoraproject.org/projects/pipewire/"
"pipewire/he/>\n"
"Language: he\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.4.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:236
msgid "Loopback"
msgstr "לולאה חוזרת"

#: src/scripts/monitors/alsa.lua:238
msgid "Built-in Audio"
msgstr "צליל מובנה"

#: src/scripts/monitors/alsa.lua:240
msgid "Modem"
msgstr "מודם"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "מצלמה פנימית מובנית"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "מצלמה אחורית מובנית"
   07070100000117000081A400000000000000000000000165F863040000058D000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/hi.po    # translation of Wireplumber.master to Hindi
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Rajesh Ranjan <rajesh672@gmail.com>, 2009, 2012.
# Hemish <hemish04082005@gmail.com>, 2022

msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2022-07-08 03:32+0000\n"
"PO-Revision-Date: 2022-12-04 12:27+0530\n"
"Last-Translator: Hemish <hemish04082005@gmail.com>\n"
"Language-Team: Hindi <indlinux-hindi@lists.sourceforge.net> https://indlinux.org/hindi\n"
"Language: hi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT

#: src/scripts/monitors/alsa.lua:228
msgid "Built-in Audio"
msgstr "आंतरिक ऑडियो"

#: src/scripts/monitors/alsa.lua:230
msgid "Modem"
msgstr "मॉडेम"

#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "आंतरिक आगे का कैमरा"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "आंतरिक पीछे का कैमरा"
   07070100000118000081A400000000000000000000000165F863040000082E000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/hr.po    # Croatian translation for pipewire
# Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010
# This file is distributed under the same license as the pipewire package.
# gogo <trebelnik2@gmail.com>, 2017.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2022-04-03 12:57+0200\n"
"Last-Translator: gogo <trebelnik2@gmail.com>\n"
"Language-Team: Croatian <https://translate.fedoraproject.org/projects/"
"pipewire/pipewire/hr/>\n"
"Language: hr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Poedit 2.3\n"
"X-Launchpad-Export-Date: 2017-04-20 21:04+0000\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Ugrađeni zvuk"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
  07070100000119000081A400000000000000000000000165F8630400000FE0000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/hu.po    # Hungarian translation for PipeWire.
# Copyright (C) 2012, 2016. Free Software Foundation, Inc.
# This file is distributed under the same license as the PipeWire package.
#
# KAMI <kami911 at gmail dot com>, 2012.
# Gabor Kelemen <kelemeng at ubuntu dot com>, 2016.
# Balázs Úr <ur.balazs at fsf dot hu>, 2016, 2022.
msgid ""
msgstr ""
"Project-Id-Version: PipeWire master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/is"
"sues\n"
"POT-Creation-Date: 2022-07-08 03:32+0000\n"
"PO-Revision-Date: 2022-09-21 07:31+0200\n"
"Last-Translator: Balázs Úr <ur.balazs at fsf dot hu>\n"
"Language-Team: Hungarian <https://translate.fedoraproject.org/projects/pipewir"
"e/pipewire/hu/>\n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Lokalize 19.12.3\n"
"X-Poedit-Language: Hungarian\n"
"X-Poedit-Country: HUNGARY\n"
"X-Poedit-SourceCharset: utf-8\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:228
msgid "Built-in Audio"
msgstr "Belső hangforrás"

#: src/scripts/monitors/alsa.lua:230
msgid "Modem"
msgstr "Modem"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "Beépített elülső kamera"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "Beépített hátsó kamera"
0707010000011A000081A400000000000000000000000165F8630400000797000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/id.po    # Indonesian translation of pipewire
# Copyright (C) 2011 THE pipewire'S COPYRIGHT HOLDER
# This file is distributed under the same license as the pipewire package.
#
# Translators:
# Andika Triwidada <andika@gmail.com>, 2011, 2012, 2018.
msgid ""
msgstr ""
"Project-Id-Version: PipeWire master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-08-11 10:50+0700\n"
"Last-Translator: Andika Triwidada <andika@gmail.com>\n"
"Language-Team: Indonesia <id@li.org>\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
"X-Generator: Poedit 2.2.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Audio Bawaan"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
 0707010000011B000081A400000000000000000000000165F8630400000861000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/it.po    # Italian translation for PipeWire.
# Copyright (C) 2008, 2009, 2012, 2015, 2019 The Free Software Foundation, Inc
# This file is distributed under the same license as the pipewire package.
#
# Luca Ferretti <elle.uca@libero.it>, 2008, 2009.
# mario_santagiuliana <mario at marionline.it>, 2009.
# Milo Casagrande <milo@ubuntu.com>, 2009, 2012, 2015, 2019.
# Albano Battistella <albano_battistella@hotmail.com>,2021
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-05-09 13:29+0100\n"
"Last-Translator: Albano Battistella <albano_battistella@hotmail.com>\n"
"Language-Team: Italian <>pipewire/pipewire/it/>\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.2.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Audio interno"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
   0707010000011C000081A400000000000000000000000165F8630400000867000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/ja.po    # translation of ja.po to Japanese
# PipeWire
# Copyright (C) 2009.
# This file is distributed under the same license as the PACKAGE package.
#
# Hyu_gabaru Ryu_ichi <hyu_gabaru@yahoo.co.jp>, 2009.
# Kiyoto Hashida <khashida@redhat.com>, 2009, 2012.
# Kenzo Moriguchi <kmoriguc@redhat.com>, 2016. #zanata
# Ooyama Yosiyuki <qqke6wd9k@apricot.ocn.ne.jp>, 2016. #zanata
# Wim Taymans <wim.taymans@gmail.com>, 2016. #zanata
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2016-03-15 08:25+0000\n"
"Last-Translator: Kenzo Moriguchi <kmoriguc@redhat.com>\n"
"Language-Team: Japanese <jp@li.org>\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Zanata 4.6.2\n"
"Plural-Forms: Plural-Forms: nplurals=1; plural=0;\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "内部オーディオ"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "モデム"
 0707010000011D000081A400000000000000000000000165F8630400000F41000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/ka.po    # Georgian translation for pipewire.
# Copyright © 2008-2022 Free Software Foundation, Inc.
# This file is distributed under the same license as the pipewire package.
# Temuri Doghonadze <temuri.doghonadze@gmail.com>, 2022.
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2022-06-15 15:30+0000\n"
"PO-Revision-Date: 2022-07-25 13:53+0200\n"
"Last-Translator: Temuri Doghonadze <temuri.doghonadze@gmail.com>\n"
"Language-Team: Georgian <(nothing)>\n"
"Language: ka\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.1.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:220
msgid "Built-in Audio"
msgstr "ჩაშენებული აუდიო"

#: src/scripts/monitors/alsa.lua:222
msgid "Modem"
msgstr "მოდემი"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "ჩაშენებული წინა კამერა"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "ჩაშენებული უკანა კამერა"
   0707010000011E000081A400000000000000000000000165F8630400000782000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/kk.po    # Kazakh translation of pipewire.
# Copyright (C) 2020 The pipewire authors.
# This file is distributed under the same license as the pipewire package.
# Baurzhan Muftakhidinov <baurthefirst@gmail.com>, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2020-06-30 08:04+0500\n"
"Last-Translator: Baurzhan Muftakhidinov <baurthefirst@gmail.com>\n"
"Language-Team: \n"
"Language: kk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.3.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Құрамындағы аудио"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Модем"
  0707010000011F000081A400000000000000000000000165F86304000007C9000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/kn.po    # translation of pipewire.master-tx.kn.po to Kannada
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Shankar Prasad <svenkate@redhat.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx.kn\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:54+0000\n"
"Last-Translator: Shankar Prasad <svenkate@redhat.com>\n"
"Language-Team: Kannada <kde-l10n-kn@kde.org>\n"
"Language: kn\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Lokalize 1.0\n"
"Plural-Forms:  nplurals=2; plural=(n != 1);\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "ಆಂತರಿಕ ಆಡಿಯೊ"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "ಮಾಡೆಮ್"
   07070100000120000081A400000000000000000000000165F8630400000722000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/ko.po    # eukim <eukim@redhat.com>, 2013. #zanata
# KimJeongYeon <jeongyeon.kim@samsung.com>, 2017.
# Sangchul Lee <sc11.lee@samsung.com>, 2018.
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2018-06-21 15:10+0900\n"
"Last-Translator: Sangchul Lee <sc11.lee@samsung.com>\n"
"Language-Team: Korean\n"
"Language: ko\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.7.1\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "내장 오디오 "

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "모뎀 "
  07070100000121000081A400000000000000000000000165F863040000075D000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/lt.po    # SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Moo, 2017-2019
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2019-09-01 16:15+0300\n"
"Last-Translator: Moo\n"
"Language-Team: \n"
"Language: lt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.2.1\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2);\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Įtaisytas garsas"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modemas"
   07070100000122000081A400000000000000000000000165F8630400000190000000000000000000000000000000000000002100000000wireplumber-0.5.0/po/meson.build  i18n = import('i18n')

i18n.gettext(
  meson.project_name(),
  preset: 'glib',
  # Page width is set to 90 characters in order to avoid bad wrapping of the
  # bug reporting address.
  args: ['--msgid-bugs-address=https://gitlab.freedesktop.org/pipewire/wireplumber/issues/new',
         '--width=90', '--keyword=I18n.gettext:1', '--keyword=I18n.ngettext:1,2']
)

po_dir = meson.current_source_dir()
07070100000123000081A400000000000000000000000165F863040000069D000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/ml.po    #
#  <>, YEAR, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx.ml\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:41+0000\n"
"Last-Translator: \n"
"Language-Team:  <en@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: KBabel 1.11.4\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "ഇന്റേര്‍ണല്‍ ഓഡിയോ"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "മോഡം"
   07070100000124000081A400000000000000000000000165F86304000007C6000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/mr.po    # translation of pipewire.master-tx.po to Marathi
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Sandeep Shedmake <sshedmak@redhat.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:54+0000\n"
"Last-Translator: Sandeep Shedmake <sshedmak@redhat.com>\n"
"Language-Team: Marathi <fedora-trans-mr@redhat.com>\n"
"Language: mr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: KBabel 1.11.4\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "आंतरीक ऑडिओ"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "मोडेम"
  07070100000125000081A400000000000000000000000165F86304000007CE000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/my.po    # SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire-master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-08-26 21:52+0630\n"
"Last-Translator: zayar lwin <lw1nzayar@yandex.com>\n"
"Language-Team: lw1nzayar@yandex.com\n"
"Language: my\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 2.4.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "နဂိုတည်းကထည့်သွင်းထားသည်ံ့ အသံကရိယာ"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "ဆ.သ.ရ-စက်"
  07070100000126000081A400000000000000000000000165F8630400000812000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/nl.po    # Dutch translation of pipewire.master-tx.
# Copyright (C) 2009 THE pipewire.master-tx'S COPYRIGHT HOLDER
# This file is distributed under the same license as the pipewire.master-tx package.
# Geert Warrink <geert.warrink@onsnet.nu>, 2009.
# Reinout van Schouwen <reinout@gmail.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2020-07-11 20:27+0000\n"
"Last-Translator: Geert Warrink <geert.warrink@onsnet.nu>\n"
"Language-Team: Dutch <https://translate.fedoraproject.org/projects/pipewire/"
"pipewire/nl/>\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.1.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Intern geluid"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
  07070100000127000081A400000000000000000000000165F86304000007C2000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/nn.po    # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Karl Ove Hufthammer <karl@huftis.org>, 2017.
# Nicolai Syvertsen <saivert@saivert.com> 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-02-07 15:40+0000\n"
"Last-Translator: Karl Ove Hufthammer <karl@huftis.org>\n"
"Language-Team: Norwegian Nynorsk <https://translate.fedoraproject.org/"
"projects/pipewire/pipewire/nn/>\n"
"Language: nn\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.4.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Innebygd lyd"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
  07070100000128000081A400000000000000000000000165F8630400000909000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/oc.po    # French translation of pipewire.
# Copyright (C) 2006-2008 Lennart Poettering
# This file is distributed under the same license as the pipewire package.
# Robert-André Mauchin <zebob.m@pengzone.org>, 2008.
# Michaël Ughetto <telimektar esraonline com>, 2008.
# Pablo Martin-Gomez <pablo.martin-gomez@laposte.net>, 2008.
# Corentin Perard <corentin.perard@gmail.com>, 2009.
# Thomas Canniot <mrtom@fedoraproject.org>, 2009, 2012.
# Cédric Valmary (Tot en Òc) <cvalmary@yahoo.fr>, 2015.
# Cédric Valmary (totenoc.eu) <cvalmary@yahoo.fr>, 2016.
msgid ""
msgstr ""
"Project-Id-Version: pipewire trunk\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2016-10-12 22:20+0200\n"
"Last-Translator: Cédric Valmary (totenoc.eu) <cvalmary@yahoo.fr>\n"
"Language-Team: Tot En Òc\n"
"Language: oc\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Virtaal 0.7.1\n"
"X-Launchpad-Export-Date: 2016-10-12 20:12+0000\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Àudio integrat"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modèm"
   07070100000129000081A400000000000000000000000165F863040000081E000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/or.po    # translation of pipewire.master-tx.or.po to Oriya
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Manoj Kumar Giri <mgiri@redhat.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx.or\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:55+0000\n"
"Last-Translator: Manoj Kumar Giri <mgiri@redhat.com>\n"
"Language-Team: Oriya <oriya-it@googlegroups.com>\n"
"Language: or\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: KBabel 1.11.4\n"
"Plural-Forms:  nplurals=2; plural=(n!=1);\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "ଆଭ୍ୟନ୍ତରୀଣ ଧ୍ୱନି"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "ମଡେମ"
  0707010000012A000081A400000000000000000000000165F8630400000822000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/pa.po    # translation of pipewire.master-tx.pa.po to Punjabi
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
# Amanpreet Singh Alam <aalam@users.sf.net>, 2008.
# A S Alam <aalam@users.sf.net>, 2009.
# Jaswinder Singh <jsingh@redhat.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx.pa\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:55+0000\n"
"Last-Translator: Jaswinder Singh <jsingh@redhat.com>\n"
"Language-Team: Punjabi/Panjabi <kde-i18n-doc@kde.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Lokalize 1.0\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "ਅੰਦਰੂਨੀ ਆਡੀਓ"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "ਮਾਡਮ"
  0707010000012B000081A400000000000000000000000165F86304000010B8000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/pl.po    # Polish translation for wireplumber.
# Copyright © 2008-2024 the wireplumber authors.
# This file is distributed under the same license as the wireplumber package.
# Piotr Drąg <piotrdrag@gmail.com>, 2008, 2012-2024.
#
msgid ""
msgstr ""
"Project-Id-Version: wireplumber\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2024-01-08 15:36+0000\n"
"PO-Revision-Date: 2024-03-03 14:45+0100\n"
"Last-Translator: Piotr Drąg <piotrdrag@gmail.com>\n"
"Language-Team: Polish <community-poland@mozilla.org>\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. add vm.type for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:211
msgid "Loopback"
msgstr "Urządzenie zwrotne"

#: src/scripts/monitors/alsa.lua:213
msgid "Built-in Audio"
msgstr "Wbudowany dźwięk"

#: src/scripts/monitors/alsa.lua:215
msgid "Modem"
msgstr "Modem"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2022 Pauli Virtanen
#. @author Pauli Virtanen
#.
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. set the node description
#. sanitize description, replace ':' with ' '
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. apply properties from bluetooth.conf
#. create the node
#. it doesn't necessarily need to be a local node,
#. the other Bluetooth parts run in the local process,
#. so it's consistent to have also this here
#. reset the name tables to make sure names are recycled
#: src/scripts/monitors/bluez-midi.lua:113
#, lua-format
msgid "BLE MIDI %d"
msgstr "MIDI BLE %d"

#. if logind support is enabled, activate
#. the monitor only when the seat is active
#. WirePlumber
#.
#. Copyright © 2023 Collabora Ltd.
#. @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera/name-node.lua:61
msgid "Built-in Front Camera"
msgstr "Wbudowana przednia kamera"

#: src/scripts/monitors/libcamera/name-node.lua:63
msgid "Built-in Back Camera"
msgstr "Wbudowana tylna kamera"
0707010000012C000081A400000000000000000000000165F8630400000875000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/pt.po    # Portuguese translation for pipewire.
# Copyright © 2008-2022 the pipewire authors.
# This file is distributed under the same license as the pipewire package.
# Rui Gouveia <rui.gouveia@globaltek.pt>, 2012.
# Rui Gouveia <rui.gouveia@globaltek.pt>, 2012, 2015.
# Pedro Albuquerque <palbuquerque73@openmailbox.com>, 2015.
# Juliano de Souza Camargo <julianosc@pm.me>, 2020.
# Hugo Carvalho <hugokarvalho@hotmail.com>, 2021 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2022-03-30 19:37+0100\n"
"Last-Translator: Hugo Carvalho <hugokarvalho@hotmail.com>\n"
"Language-Team: Portuguese <https://l10n.gnome.org/teams/pt/>\n"
"Language: pt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.0.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Áudio interno"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
   0707010000012D000081A400000000000000000000000165F8630400000825000000000000000000000000000000000000001E00000000wireplumber-0.5.0/po/pt_BR.po # Brazilian Portuguese translation for pipewire
# Copyright (C) 2021 Rafael Fontenelle <rafaelff@gnome.org>
# This file is distributed under the same license as the pipewire package.
# Fabian Affolter <fab@fedoraproject.org>, 2008.
# Igor Pires Soares <igor@projetofedora.org>, 2009, 2012.
# Rafael Fontenelle <rafaelff@gnome.org>, 2013-2021.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-08-01 17:02-0300\n"
"Last-Translator: Rafael Fontenelle <rafaelff@gnome.org>\n"
"Language-Team: Brazilian Portuguese <gnome-pt_br-list@gnome.org>\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
"X-Generator: Gtranslator 40.0\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Áudio interno"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
   0707010000012E000081A400000000000000000000000165F8630400000773000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/ro.po    # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the pipewire package.
#
# Sergiu Bivol <sergiu@cip.md>, 2022.
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2022-02-05 12:06+0000\n"
"Last-Translator: Sergiu Bivol <sergiu@cip.md>\n"
"Language-Team: Romanian\n"
"Language: ro\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < "
"20)) ? 1 : 2;\n"
"X-Generator: Lokalize 21.12.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Audio încorporat"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
 0707010000012F000081A400000000000000000000000165F8630400000FF3000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/ru.po    # Russian translation of pipewire.
# Copyright (C) 2010 pipewire
# This file is distributed under the same license as the pipewire package.
#
# Leonid Kurbatov <llenchikk@rambler.ru>, 2010, 2012.
# Alexander Potashev <aspotashev@gmail.com>, 2014, 2019.
# Victor Ryzhykh <victorr2007@yandex.ru>, 2021.
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2023-04-06 03:34+0000\n"
"PO-Revision-Date: 2023-04-06 11:08+0300\n"
"Last-Translator: Aleksandr Melman <Alexmelman88@gmail.com>\n"
"Language-Team: ru\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 3.2.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:236
msgid "Loopback"
msgstr "Петля"

#: src/scripts/monitors/alsa.lua:238
msgid "Built-in Audio"
msgstr "Встроенное аудио"

#: src/scripts/monitors/alsa.lua:240
msgid "Modem"
msgstr "Модем"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "Встроенная фронтальная камера"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "Встроенная задняя камера"
 07070100000130000081A400000000000000000000000165F8630400000718000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/si.po    # SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the pipewire package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: si\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr ""

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr ""
07070100000131000081A400000000000000000000000165F8630400000F1C000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/sk.po    # Slovak translation for PipeWire.
# Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER
# This file is distributed under the same license as the PipeWire package.
# Dušan Kazik <prescott66@gmail.com>, 2014-2022.
#
msgid ""
msgstr ""
"Project-Id-Version: PipeWire master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2022-07-08 03:32+0000\n"
"PO-Revision-Date: 2022-11-05 11:45+0100\n"
"Last-Translator: Dušan Kazik <prescott66@gmail.com>\n"
"Language-Team: Slovak <https://translate.fedoraproject.org/projects/pipewire/"
"pipewire/sk/>\n"
"Language: sk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 1 : (n>=2 && n<=4) ? 2 : 0;\n"
"X-Generator: Poedit 3.1.1\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:228
msgid "Built-in Audio"
msgstr "Vstavaný zvuk"

#: src/scripts/monitors/alsa.lua:230
msgid "Modem"
msgstr "Modem"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "Vstavaná predná kamera"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "Vstavaná zadná kamera"
07070100000132000081A400000000000000000000000165F863040000081A000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/sr.po    # Serbian translations for pipewire
# Copyright (C) 2006 Lennart Poettering
# This file is distributed under the same license as the pipewire package.
# Igor Miletic (Игор Милетић) <grejigl-gnomeprevod@yahoo.ca>, 2009.
# Miloš Komarčević <kmilos@gmail.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:55+0000\n"
"Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n"
"Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Унутрашњи звук"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Модем"
  07070100000133000081A400000000000000000000000165F8630400000807000000000000000000000000000000000000002100000000wireplumber-0.5.0/po/sr@latin.po  # Serbian(Latin) translations for pipewire
# Copyright (C) 2006 Lennart Poettering
# This file is distributed under the same license as the pipewire package.
# Igor Miletic (Igor Miletić) <grejigl-gnomeprevod@yahoo.ca>, 2009.
# Miloš Komarčević <kmilos@gmail.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:55+0000\n"
"Last-Translator: Miloš Komarčević <kmilos@gmail.com>\n"
"Language-Team: Serbian (sr) <fedora-trans-sr@redhat.com>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Unutrašnji zvuk"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
 07070100000134000081A400000000000000000000000165F8630400001238000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/sv.po    # Swedish translation for pipewire.
# Copyright © 2008-2024 Free Software Foundation, Inc.
# This file is distributed under the same license as the pipewire package.
# Daniel Nylander <po@danielnylander.se>, 2008, 2012.
# Josef Andersson <josef.andersson@fripost.org>, 2014, 2017.
# Anders Jonsson <anders.jonsson@norsjovallen.se>, 2021, 2022, 2024.
#
# Termer:
# input/output: ingång/utgång (det handlar om ljud)
# latency: latens
# delay: fördröjning
# boost: öka
# gain: förstärkning
# channel map: kanalmappning
# passthrough: genomströmning
# och en hel del termer som inte översätts inom ljuddomänen, ex. surround.
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/"
"issues\n"
"POT-Creation-Date: 2024-03-11 15:33+0000\n"
"PO-Revision-Date: 2024-01-11 01:08+0100\n"
"Last-Translator: Anders Jonsson <anders.jonsson@norsjovallen.se>\n"
"Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n"
"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Poedit 3.4.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. add vm.type for rule matching purposes
#. apply properties from rules defined in JSON .conf file
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:214
msgid "Loopback"
msgstr "Loopback"

#: src/scripts/monitors/alsa.lua:216
msgid "Built-in Audio"
msgstr "Inbyggt ljud"

#: src/scripts/monitors/alsa.lua:218
msgid "Modem"
msgstr "Modem"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from rules defined in JSON .conf file
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2022 Pauli Virtanen
#. @author Pauli Virtanen
#.
#. SPDX-License-Identifier: MIT
#. unique device/node name tables
#. set the node description
#. sanitize description, replace ':' with ' '
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. apply properties from the rules in the configuration file
#. create the node
#. it doesn't necessarily need to be a local node,
#. the other Bluetooth parts run in the local process,
#. so it's consistent to have also this here
#. reset the name tables to make sure names are recycled
#: src/scripts/monitors/bluez-midi.lua:114
#, lua-format
msgid "BLE MIDI %d"
msgstr "BLE MIDI %d"

#. if logind support is enabled, activate
#. the monitor only when the seat is active
#. WirePlumber
#.
#. Copyright © 2023 Collabora Ltd.
#. @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera/name-node.lua:61
msgid "Built-in Front Camera"
msgstr "Inbyggd främre kamera"

#: src/scripts/monitors/libcamera/name-node.lua:63
msgid "Built-in Back Camera"
msgstr "Inbyggd bakre kamera"
07070100000135000081A400000000000000000000000165F863040000082B000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/ta.po    # translation of pipewire.master-tx.ta.po to Tamil
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# I. Felix <ifelix@redhat.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx.ta\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:56+0000\n"
"Last-Translator: I. Felix <ifelix@redhat.com>\n"
"Language-Team: Tamil <fedora-trans-ta@redhat.com>\n"
"Language: ta\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: KBabel 1.11.4\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\\n\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "உட்புற ஆடியோ"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "மாதிரி"
 07070100000136000081A400000000000000000000000165F86304000007E6000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/te.po    # translation of pipewire.master-tx.te.po to Telugu
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Krishna Babu K <kkrothap@redhat.com>, 2009, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx.te\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2012-01-30 09:56+0000\n"
"Last-Translator: Krishna Babu K <kkrothap@redhat.com>\n"
"Language-Team: Telugu <en@li.org>\n"
"Language: te\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: KBabel 1.11.4\n"
"Plural-Forms: nplurals=2; plural=(n!=1);\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"
"\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "అంతర్గత ఆడియో"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "మోడెమ్"
  07070100000137000081A400000000000000000000000165F863040000080B000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/tr.po    # Turkish translation for PipeWire.
# Copyright (C) 2014 PipeWire's COPYRIGHT HOLDER
# This file is distributed under the same license as the PipeWire package.
# Necdet Yücel <necdetyucel@gmail.com>, 2014.
# Kaan Özdinçer <kaanozdincer@gmail.com>, 2014.
# Muhammet Kara <muhammetk@gmail.com>, 2015, 2016, 2017.
# Oğuz Ersen <oguzersen@protonmail.com>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: PipeWire master\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-12-06 21:31+0300\n"
"Last-Translator: Oğuz Ersen <oguzersen@protonmail.com>\n"
"Language-Team: Turkish <tr>\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.4.2\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "Dahili Ses"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "Modem"
 07070100000138000081A400000000000000000000000165F8630400000F97000000000000000000000000000000000000001B00000000wireplumber-0.5.0/po/uk.po    # Copyright (C) 2009 Free Software Foundation, Inc.
# This file is distributed under the same license as the pipewire package.
#
# Yuri Chornoivan <yurchor@ukr.net>, 2009-2021, 2022, 2023.
msgid ""
msgstr ""
"Project-Id-Version: pipewire\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/-/i"
"ssues\n"
"POT-Creation-Date: 2022-12-13 15:30+0000\n"
"PO-Revision-Date: 2023-02-15 22:44+0200\n"
"Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n"
"Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<"
"=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Lokalize 20.12.0\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. unique device/node name tables
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply VM overrides
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:234
msgid "Loopback"
msgstr "Петля"

#: src/scripts/monitors/alsa.lua:236
msgid "Built-in Audio"
msgstr "Вбудоване аудіо"

#: src/scripts/monitors/alsa.lua:238
msgid "Modem"
msgstr "Модем"

#. ensure the device has a nick
#. set the icon name
#. form factor -> icon
#. apply properties from config.rules
#. override the device factory to use ACP
#. use device reservation, if available
#. unlike pipewire-media-session, this logic here keeps the device
#. acquired at all times and destroys it if someone else acquires
#. create the device
#. attempt to acquire again
#. destroy the device
#. TODO enable the jack device
#. TODO disable the jack device
#. create the device
#. handle create-object to prepare device
#. handle object-removed to destroy device reservations and recycle device name
#. reset the name tables to make sure names are recycled
#. activate monitor
#. create the JACK device (for PipeWire to act as client to a JACK server)
#. enable device reservation if requested
#. if the reserve-device plugin is enabled, at the point of script execution
#. it is expected to be connected. if it is not, assume the d-bus connection
#. has failed and continue without it
#. handle rd_plugin state changes to destroy and re-create the ALSA monitor in
#. case D-Bus service is restarted
#. create the monitor
#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. set the node name
#. sanitize name
#. deduplicate nodes with the same name
#. set the node description
#: src/scripts/monitors/libcamera.lua:88
msgid "Built-in Front Camera"
msgstr "Вбудована передня камера"

#: src/scripts/monitors/libcamera.lua:90
msgid "Built-in Back Camera"
msgstr "Вбудована задня камера"
 07070100000139000081A400000000000000000000000165F8630400000880000000000000000000000000000000000000001E00000000wireplumber-0.5.0/po/zh_CN.po # Simplified Chinese translation for PipeWire.
# Copyright (C) 2008 PULSEAUDIO COPYRIGHT HOLDER
# This file is distributed under the same license as the pipewire package.
# 闫丰刚 <sainry@gmail.com>, 2008, 2009.
# Leah Liu <lliu@redhat.com>, 2009, 2012.
# Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012.
# Frank Hill <hxf.prc@gmail.com>, 2015.
# Mingye Wang (Arthur2e5) <arthur200126@gmail.com>, 2015.
#
msgid ""
msgstr ""
"Project-Id-Version: pipewire.master-tx\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2021-04-18 10:56+0800\n"
"Last-Translator: Huang-Huang Bao <i@eh5.me>\n"
"Language-Team: Huang-Huang Bao <i@eh5.me>\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2016-03-22 13:23+0000\n"
"X-Generator: Poedit 2.4.1\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "内置音频"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "调制解调器"
0707010000013A000081A400000000000000000000000165F86304000007CC000000000000000000000000000000000000001E00000000wireplumber-0.5.0/po/zh_TW.po # Chinese (Taiwan) translation for pipewire.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
# Cheng-Chia Tseng <pswo10680@gmail.com>, 2010, 2012.
# pan93412 <pan93412@gmail.com>, 2020.
msgid ""
msgstr ""
"Project-Id-Version: PipeWire Volume Control\n"
"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/wireplumber/"
"issues/new\n"
"POT-Creation-Date: 2022-04-09 15:19+0300\n"
"PO-Revision-Date: 2020-01-11 13:49+0800\n"
"Last-Translator: pan93412 <pan93412@gmail.com>\n"
"Language-Team: Chinese <zh-l10n@lists.linux.org.tw>\n"
"Language: zh_TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Lokalize 19.12.0\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#. WirePlumber
#.
#. Copyright © 2021 Collabora Ltd.
#. @author George Kiagiadakis <george.kiagiadakis@collabora.com>
#.
#. SPDX-License-Identifier: MIT
#. Receive script arguments from config.lua
#. ensure config.properties is not nil
#. preprocess rules and create Interest objects
#. applies properties from config.rules when asked to
#. set the device id and spa factory name; REQUIRED, do not change
#. set the default pause-on-idle setting
#. try to negotiate the max ammount of channels
#. set priority
#. ensure the node has a media class
#. ensure the node has a name
#. sanitize name
#. deduplicate nodes with the same name
#. and a nick
#. also sanitize nick, replace ':' with ' '
#. ensure the node has a description
#. also sanitize description, replace ':' with ' '
#. add api.alsa.card.* properties for rule matching purposes
#. apply properties from config.rules
#. create the node
#. ensure the device has an appropriate name
#. deduplicate devices with the same name
#. ensure the device has a description
#: src/scripts/monitors/alsa.lua:222
msgid "Built-in Audio"
msgstr "內部音效"

#: src/scripts/monitors/alsa.lua:224
msgid "Modem"
msgstr "數據機"
0707010000013B000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001600000000wireplumber-0.5.0/src 0707010000013C000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001D00000000wireplumber-0.5.0/src/config  0707010000013D000081A400000000000000000000000165F8630400000121000000000000000000000000000000000000002900000000wireplumber-0.5.0/src/config/meson.build  install_data('wireplumber.conf', install_dir : wireplumber_data_dir)
install_subdir('wireplumber.conf.d', install_dir : wireplumber_data_dir)
install_subdir('wireplumber.conf.d.examples',
  install_dir : wireplumber_doc_dir / 'examples' / 'wireplumber.conf.d',
  strip_directory : true,
)
   0707010000013E000081A400000000000000000000000165F86304000057F6000000000000000000000000000000000000002E00000000wireplumber-0.5.0/src/config/wireplumber.conf ## The WirePlumber configuration

context.spa-libs = {
  ## SPA factory name to library mappings
  ## Used to find SPA factory names. It maps a SPA factory name regular
  ## expression to a library name that should contain that factory.
  ##
  ## Syntax:
  ## <factory-name regex> = <library-name>

  api.alsa.*      = alsa/libspa-alsa
  api.bluez5.*    = bluez5/libspa-bluez5
  api.v4l2.*      = v4l2/libspa-v4l2
  api.libcamera.* = libcamera/libspa-libcamera
  audio.convert.* = audioconvert/libspa-audioconvert
  support.*       = support/libspa-support
}

context.modules = [
  ## PipeWire modules to load.
  ## These modules are loaded before a connection to pipewire is attempted.
  ## This section should be kept minimal and load only the modules that are
  ## necessary for the protocol to work.
  ##
  ## If ifexists is given, the module is ignored when it is not found.
  ## If nofail is given, module initialization failures are ignored.
  ##
  ## Syntax:
  ## {
  ##    name = <module-name>
  ##    [ args = { <key> = <value> ... } ]
  ##    [ flags = [ ifexists | nofail ] ]
  ## }

  # Uses RTKit to boost the data thread priority. Also allows clamping
  # of utilisation when using the Completely Fair Scheduler on Linux.
  {
    name = libpipewire-module-rt
    args = {
        nice.level   = -11
        # rt.prio      = 88
        # rt.time.soft = -1
        # rt.time.hard = -1
        # uclamp.min = 0
        # uclamp.max = 1024
    }
    flags = [ ifexists, nofail ]
  }

  ## The native communication protocol.
  { name = libpipewire-module-protocol-native }

  ## Support for metadata objects
  { name = libpipewire-module-metadata }
]

wireplumber.profiles = {
  ## Syntax:
  ## <profile> = {
  ##   # optional is the default
  ##   <feature name> = [ required | optional | disabled ]
  ##   ...
  ## }

  main = {
    check.no-media-session = required
    metadata.sm-settings = required
    support.settings = required
    support.log-settings = required
    metadata.sm-objects = required
    hardware.audio = required
    hardware.bluetooth = required
    hardware.video-capture = required
    policy.standard = required
    #policy.role-priority-system = optional
  }
}

wireplumber.components = [
  ## WirePlumber components to load.
  ## These components are loaded after a connection to pipewire is established.
  ## type is mandatory; rest of the tags are optional
  ##
  ## Syntax:
  ## {
  ##   name = <component-name>
  ##   type = <component-type>
  ##   arguments = { <json object> }
  ##
  ##   # Feature that this component provides
  ##   provides = <feature>
  ##
  ##   # List of features that must be provided before this component is loaded
  ##   requires = [ <features> ]
  ##
  ##   # List of features that would offer additional functionality if provided
  ##   # but are not strictly required
  ##   wants = [ <features> ]
  ## }

  ## Check to avoid loading together with media-session
  {
    name = ensure-no-media-session, type = built-in
    provides = check.no-media-session
  }

  ## Makes a secondary connection to PipeWire for exporting objects
  {
    name = export-core, type = built-in
    provides = support.export-core
  }

  ## Enables creating local nodes that are exported to pipewire
  ## This is needed for LocalNode() / WpImplNode
  ## This should be used with the export-core to avoid protocol deadlocks,
  ## unless you know what you are doing
  {
    name = libpipewire-module-client-node, type = pw-module
    provides = pw.client-node
    wants = [ support.export-core ]
  }

  ## Enables creating local devices that are exported to pipewire
  ## This is needed for SpaDevice() / WpSpaDevice
  ## This should be used with the export-core to avoid protocol deadlocks,
  ## unless you know what you are doing
  {
    name = libpipewire-module-client-device, type = pw-module
    provides = pw.client-device
    wants = [ support.export-core ]
  }

  # Provides a node factory to create SPA nodes
  # You need this to use LocalNode("spa-node-factory", ...)
  {
    name = libpipewire-module-spa-node-factory, type = pw-module
    provides = pw.node-factory.spa
    requires = [ pw.client-node ]
  }

  ## Provides a node factory to create SPA nodes wrapped in an adapter
  ## You need this to use LocalNode("adapter", ...)
  {
    name = libpipewire-module-adapter, type = pw-module
    provides = pw.node-factory.adapter
    requires = [ pw.client-node ]
  }

  ## Provides the "sm-settings" metadata object
  {
    name = libwireplumber-module-settings, type = module
    arguments = { metadata.name = sm-settings }
    provides = metadata.sm-settings
  }

  ## Activates a global WpSettings instance, providing settings from
  ## the sm-settings metadata object
  {
    name = settings-instance, type = built-in
    arguments = { metadata.name = sm-settings }
    provides = support.settings
  }

  ## Log level settings
  {
    name = libwireplumber-module-log-settings, type = module
    provides = support.log-settings
  }

  ## The lua scripting engine
  {
    name = libwireplumber-module-lua-scripting, type = module
    provides = support.lua-scripting
  }

  ## Module listening for pipewire objects to push events
  {
    name = libwireplumber-module-standard-event-source, type = module
    provides = support.standard-event-source
  }

  ## The shared D-Bus connection
  {
    name = libwireplumber-module-dbus-connection, type = module
    provides = support.dbus
  }

  ## Module managing the portal permissions
  {
    name = libwireplumber-module-portal-permissionstore, type = module
    provides = support.portal-permissionstore
    requires = [ support.dbus ]
  }

  ## Needed for device reservation to work
  {
    name = libwireplumber-module-reserve-device, type = module
    provides = support.reserve-device
    requires = [ support.dbus ]
  }

  ## logind integration to enable certain functionality only on the active seat
  {
    name = libwireplumber-module-logind, type = module
    provides = support.logind
  }

  ## Session item factories
  {
    name = libwireplumber-module-si-node, type = module
    provides = si.node
  }
  {
    name = libwireplumber-module-si-audio-adapter, type = module
    provides = si.audio-adapter
  }
  {
    name = libwireplumber-module-si-standard-link, type = module
    provides = si.standard-link
  }
  {
    name = libwireplumber-module-si-audio-virtual, type = module
    provides = si.audio-virtual
  }

  ## API to access default nodes from scripts
  {
    name = libwireplumber-module-default-nodes-api, type = module
    provides = api.default-nodes
  }

  ## API to access mixer controls
  {
    name = libwireplumber-module-mixer-api, type = module
    provides = api.mixer
  }

  ## API to get notified about file changes
  {
    name = libwireplumber-module-file-monitor-api, type = module
    provides = api.file-monitor
  }

  ## Provide the "default" pw_metadata
  {
    name = metadata.lua, type = script/lua
    arguments = { metadata.name = default }
    provides = metadata.default
  }

  ## Provide the "filters" pw_metadata
  {
    name = metadata.lua, type = script/lua
    arguments = { metadata.name = filters }
    provides = metadata.filters
  }

  ## Provide the "sm-objects" pw_metadata, supporting dynamic loadable objects
  {
    name = sm-objects.lua, type = script/lua
    provides = metadata.sm-objects
  }

  ## Device monitors' optional features
  {
    type = virtual, provides = monitor.alsa.reserve-device,
    requires = [ support.reserve-device ]
  }
  {
    type = virtual, provides = monitor.alsa-midi.monitoring,
    requires = [ api.file-monitor ]
  }
  {
    type = virtual, provides = monitor.bluez.seat-monitoring,
    requires = [ support.logind ]
  }

  ## Device monitors
  {
    name = monitors/alsa.lua, type = script/lua
    provides = monitor.alsa
    requires = [ support.export-core, pw.client-device ]
    wants = [ monitor.alsa.reserve-device ]
  }
  {
    name = monitors/bluez.lua, type = script/lua
    provides = monitor.bluez
    requires = [ support.export-core,
                 pw.client-device,
                 pw.client-node,
                 pw.node-factory.adapter ]
    wants = [ monitor.bluez.seat-monitoring ]
  }
  {
    name = monitors/bluez-midi.lua, type = script/lua
    provides = monitor.bluez-midi
    requires = [ support.export-core,
                 pw.client-device,
                 pw.client-node,
                 pw.node-factory.spa ]
    wants = [ monitor.bluez.seat-monitoring ]
  }
  {
    name = monitors/alsa-midi.lua, type = script/lua
    provides = monitor.alsa-midi
    wants = [ monitor.alsa-midi.monitoring ]
  }
  ## v4l2 monitor hooks
  {
    name = monitors/v4l2/name-device.lua, type = script/lua
    provides = hooks.monitor.v4l2-name-device
    requires = [ support.export-core,
                 support.standard-event-source ]
  }
  {
    name = monitors/v4l2/create-device.lua, type = script/lua
    provides = hooks.monitor.v4l2-create-device
    requires = [ support.export-core,
                 pw.client-device,
                 support.standard-event-source ]
  }
  {
    name = monitors/v4l2/name-node.lua, type = script/lua
    provides = hooks.monitor.v4l2-name-node
    requires = [ support.export-core,
                 support.standard-event-source ]
  }
  {
    name = monitors/v4l2/create-node.lua, type = script/lua
    provides = hooks.monitor.v4l2-create-node
    requires = [ support.export-core,
                 support.standard-event-source ]
  }
  {
    type = virtual, provides = monitor.v4l2.hooks
    wants = [ hooks.monitor.v4l2-name-device,
              hooks.monitor.v4l2-create-device,
              hooks.monitor.v4l2-name-node,
              hooks.monitor.v4l2-create-node ]
  }
  # enumerate-device.lua needs rest of the monitor hooks to be loaded first.
  {
    name = monitors/v4l2/enumerate-device.lua, type = script/lua
    provides = hooks.monitor.v4l2-enumerate-device
    requires = [ support.export-core,
                 pw.client-device,
                 support.standard-event-source,
                 monitor.v4l2.hooks ]
  }
  {
    type = virtual, provides = monitor.v4l2
    wants = [ hooks.monitor.v4l2-enumerate-device,
              monitor.v4l2.hooks ]
  }

  ## libcamera monitor hooks
  {
    name = monitors/libcamera/name-device.lua, type = script/lua
    provides = hooks.monitor.libcamera-name-device
    requires = [ support.export-core,
                 support.standard-event-source ]
  }
  {
    name = monitors/libcamera/create-device.lua, type = script/lua
    provides = hooks.monitor.libcamera-create-device
    requires = [ support.export-core,
                 pw.client-device,
                 support.standard-event-source ]
  }
  {
    name = monitors/libcamera/name-node.lua, type = script/lua
    provides = hooks.monitor.libcamera-name-node
    requires = [ support.export-core,
                 support.standard-event-source ]
  }
  {
    name = monitors/libcamera/create-node.lua, type = script/lua
    provides = hooks.monitor.libcamera-create-node
    requires = [ support.export-core,
                 support.standard-event-source ]
  }
  {
    type = virtual, provides = monitor.libcamera.hooks
    wants = [ hooks.monitor.libcamera-name-device,
              hooks.monitor.libcamera-create-device,
              hooks.monitor.libcamera-name-node,
              hooks.monitor.libcamera-create-node ]
  }
  # enumerate-device.lua needs rest of the monitor hooks to be loaded first.
  {
    name = monitors/libcamera/enumerate-device.lua, type = script/lua
    provides = hooks.monitor.libcamera-enumerate-device
    requires = [ support.export-core,
                 pw.client-device,
                 support.standard-event-source,
                 monitor.libcamera.hooks ]
  }
  {
    type = virtual, provides = monitor.libcamera
    wants = [ hooks.monitor.libcamera-enumerate-device,
              monitor.libcamera.hooks ]
  }

  ## Client access configuration hooks
  {
    name = client/access-default.lua, type = script/lua
    provides = script.client.access-default
  }
  {
    name = client/access-portal.lua, type = script/lua
    provides = script.client.access-portal
    requires = [ support.portal-permissionstore ]
  }
  {
    name = client/access-snap.lua, type = script/lua
    provides = script.client.access-snap
  }
  {
    type = virtual, provides = policy.client.access
    wants = [ script.client.access-default,
              script.client.access-portal,
              script.client.access-snap ]
  }

  ## Device profile selection hooks
  {
    name = device/select-profile.lua, type = script/lua
    provides = hooks.device.profile.select
  }
  {
    name = device/find-preferred-profile.lua, type = script/lua
    provides = hooks.device.profile.find-preferred
  }
  {
    name = device/find-best-profile.lua, type = script/lua
    provides = hooks.device.profile.find-best
  }
  {
    name = device/state-profile.lua, type = script/lua
    provides = hooks.device.profile.state
  }
  {
    name = device/apply-profile.lua, type = script/lua
    provides = hooks.device.profile.apply
  }
  {
    name = device/autoswitch-bluetooth-profile.lua, type = script/lua
    provides = hooks.device.profile.autoswitch-bluetooth
  }
  {
    type = virtual, provides = policy.device.profile
    requires = [ hooks.device.profile.select,
                 hooks.device.profile.autoswitch-bluetooth,
                 hooks.device.profile.apply ]
    wants = [ hooks.device.profile.find-best, hooks.device.profile.find-preferred,
              hooks.device.profile.state ]
  }

  # Device route selection hooks
  {
    name = device/select-routes.lua, type = script/lua
    provides = hooks.device.routes.select
  }
  {
    name = device/find-best-routes.lua, type = script/lua
    provides = hooks.device.routes.find-best
  }
  {
    name = device/state-routes.lua, type = script/lua
    provides = hooks.device.routes.state
  }
  {
    name = device/apply-routes.lua, type = script/lua
    provides = hooks.device.routes.apply
  }
  {
    type = virtual, provides = policy.device.routes
    requires = [ hooks.device.routes.select,
                 hooks.device.routes.apply ]
    wants = [ hooks.device.routes.find-best,
              hooks.device.routes.state ]
  }

  ## Default nodes selection hooks
  {
    name = default-nodes/rescan.lua, type = script/lua
    provides = hooks.default-nodes.rescan
  }
  {
    name = default-nodes/find-selected-default-node.lua, type = script/lua
    provides = hooks.default-nodes.find-selected
    requires = [ metadata.default ]
  }
  {
    name = default-nodes/find-best-default-node.lua, type = script/lua
    provides = hooks.default-nodes.find-best
  }
  {
    name = default-nodes/state-default-nodes.lua, type = script/lua
    provides = hooks.default-nodes.state
    requires = [ metadata.default ]
  }
  {
    name = default-nodes/apply-default-node.lua, type = script/lua,
    provides = hooks.default-nodes.apply
    requires = [ metadata.default ]
  }
  {
    type = virtual, provides = policy.default-nodes
    requires = [ hooks.default-nodes.rescan,
                 hooks.default-nodes.apply ]
    wants = [ hooks.default-nodes.find-selected,
              hooks.default-nodes.find-best,
              hooks.default-nodes.state ]
  }

  ## Node configuration hooks
  {
    name = node/create-item.lua, type = script/lua
    provides = hooks.node.create-session-item
    requires = [ si.audio-adapter, si.node ]
  }
  {
    name = node/suspend-node.lua, type = script/lua
    provides = hooks.node.suspend
  }
  {
    name = node/state-stream.lua, type = script/lua
    provides = hooks.stream.state
  }
  {
    name = node/filter-forward-format.lua, type = script/lua
    provides = hooks.filter.forward-format
  }
  {
    name = node/create-virtual-item.lua, type = script/lua
    provides = script.create-role-items
    requires = [ si.audio-virtual ]
  }
  {
    type = virtual, provides = policy.node
    requires = [ hooks.node.create-session-item ]
    wants = [ hooks.node.suspend
              hooks.stream.state
              hooks.filter.forward-format ]
  }

  ## Linking hooks
  {
    name = linking/rescan.lua, type = script/lua
    provides = hooks.linking.rescan
  }
  {
    name = linking/find-defined-target.lua, type = script/lua
    provides = hooks.linking.target.find-defined
  }
  {
    name = linking/find-filter-target.lua, type = script/lua
    provides = hooks.linking.target.find-filter
    requires = [ metadata.filters ]
  }
  {
    name = linking/find-default-target.lua, type = script/lua
    provides = hooks.linking.target.find-default
    requires = [ api.default-nodes ]
  }
  {
    name = linking/find-best-target.lua, type = script/lua
    provides = hooks.linking.target.find-best
  }
  {
    name = linking/get-filter-from-target.lua, type = script/lua
    provides = hooks.linking.target.get-filter-from
    requires = [ metadata.filters ]
  }
  {
    name = linking/prepare-link.lua, type = script/lua
    provides = hooks.linking.target.prepare-link
    requires = [ api.default-nodes ]
  }
  {
    name = linking/link-target.lua, type = script/lua
    provides = hooks.linking.target.link
    requires = [ si.standard-link ]
  }
  {
    type = virtual, provides = policy.linking.standard
    requires = [ hooks.linking.rescan,
                 hooks.linking.target.prepare-link,
                 hooks.linking.target.link ]
    wants = [ hooks.linking.target.find-defined,
              hooks.linking.target.find-filter,
              hooks.linking.target.find-default,
              hooks.linking.target.find-best,
              hooks.linking.target.get-filter-from ]
  }

  ## Linking: Role-based priority system
  {
    name = linking/rescan-virtual-links.lua, type = script/lua
    provides = hooks.linking.role-priority-system.links.rescan
    requires = [ api.mixer ]
  }
  {
    name = linking/find-virtual-target.lua, type = script/lua
    provides = hooks.linking.role-priority-system.target.find
  }
  {
    type = virtual, provides = policy.linking.role-priority-system
    requires = [ policy.linking.standard,
                 hooks.linking.role-priority-system.links.rescan,
                 hooks.linking.role-priority-system.target.find ]
  }
  {
    type = virtual, provides = policy.standard
    requires = [ policy.client.access
                 policy.device.profile
                 policy.device.routes
                 policy.default-nodes
                 policy.linking.standard
                 policy.node
                 support.standard-event-source ]
  }
  {
    type = virtual, provides = policy.role-priority-system
    requires = [ policy.standard,
                 script.create-role-items,
                 policy.linking.role-priority-system ]
  }
  ## Load targets
  {
    type = virtual, provides = hardware.audio
    wants = [ monitor.alsa, monitor.alsa-midi ]
  }
  {
    type = virtual, provides = hardware.bluetooth
    wants = [ monitor.bluez, monitor.bluez-midi ]
  }
  {
    type = virtual, provides = hardware.video-capture
    wants = [ monitor.v4l2, monitor.libcamera ]
  }
]

wireplumber.components.rules = [
  ## Rules to apply on top of wireplumber.components
  ## Syntax:
  ## {
  ##   matches = [
  ##     {
  ##       [ <key> = <value> ... ]
  ##     }
  ##     ...
  ##   ]
  ##   actions = {
  ##     <override|merge> = {
  ##       [ <key> = <value> ... ]
  ##     }
  ##     ...
  ##   }
  ## }

  {
    matches = [
      {
        type = "script/lua"
      }
    ]
    actions = {
      merge = {
        requires = [ support.lua-scripting ]
      }
    }
  }
]

wireplumber.settings.schema = {
  ## Bluetooth
  bluetooth.use-persistent-storage = {
    description = "Whether to use persistent BT storage or not"
    type = "bool"
    default = true
  }
  bluetooth.autoswitch-to-headset-profile = {
    description = "Whether to autoswitch to BT headset profile or not"
    type = "bool"
    default = true
  }

  ## Device
  device.restore-profile = {
    description = "Whether to restore device profile or not"
    type = "bool"
    default = true
  }
  device.restore-routes = {
    description = "Whether to restore device routes or not"
    type = "bool"
    default = true
  }
  device.routes.default-sink-volume = {
    description = "The default volume for sink devices"
    type = "float"
    default = 0.064
    min = 0.0
    max = 1.0
  }
  device.routes.default-source-volume = {
    description = "The default volume for source devices"
    type = "float"
    default = 1.0
    min = 0.0
    max = 1.0
  }

  ## Linking
  linking.allow-moving-streams = {
    description = "Whether to allow metadata to move streams at runtime or not"
    type = "bool"
    default = true
  }
  linking.follow-default-target = {
    description = "Whether to allow streams follow the default device or not"
    type = "bool"
    default = true
  }

  ## Monitor
  monitor.camera-discovery-timeout = {
    description = "The camera discovery timeout in milliseconds"
    type = "int"
    default = 100
    min = 0
    max = 60000
  }

  ## Node
  node.features.audio.no-dsp = {
    description = "Whether to never convert audio to F32 format or not"
    type = "bool"
    default = false
  }
  node.features.audio.monitor-ports = {
    description = "Whether to enable monitor ports on audio nodes or not"
    type = "bool"
    default = true
  }
  node.features.audio.control-port = {
    description = "Whether to enable control ports on audio nodes or not"
    type = "bool"
    default = false
  }
  node.stream.restore-props = {
    description = "Whether to restore properties on stream nodes or not"
    type = "bool"
    default = true
  }
  node.stream.restore-target = {
    description = "Whether to restore target on stream nodes or not"
    type = "bool"
    default = true
  }
  node.stream.default-playback-volume = {
    description = "The default volume for playback nodes"
    type = "float"
    default = 1.0
    min = 0.0
    max = 1.0
  }
  node.stream.default-capture-volume = {
    description = "The default volume for capture nodes"
    type = "float"
    default = 1.0
    min = 0.0
    max = 1.0
  }
  node.filter.forward-format = {
    description = "Whether to forward format on filter nodes or not"
    type = "bool"
    default = false
  }
  node.restore-default-targets = {
    description = "Whether to restore default targets or not"
    type = "bool"
    default = true
  }
}
  0707010000013F000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000003000000000wireplumber-0.5.0/src/config/wireplumber.conf.d   07070100000140000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000003900000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples  07070100000141000081A400000000000000000000000165F86304000003D9000000000000000000000000000000000000004500000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/access.conf  ## The WirePlumber access configuration

access.rules = [
  # The list of access rules

  # The following are the default rules applied if none overrides them.
  # {
  #   matches = [
  #     {
  #       access = "flatpak"
  #       media.category = "Manager"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       access = "flatpak-manager"
  #       default_permissions = "all",
  #     }
  #   }
  # }

  # {
  #   matches = [
  #     {
  #       access = "flatpak"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       default_permissions = "rx"
  #     }
  #   }
  # }

  # {
  #   matches = [
  #     {
  #       access = "restricted"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       default_permissions = "rx"
  #     }
  #   }
  # }

  # {
  #   matches = [
  #     {
  #       access = "default"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       default_permissions = "all"
  #     }
  #   }
  # }
]
   07070100000142000081A400000000000000000000000165F8630400001392000000000000000000000000000000000000004300000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/alsa.conf    ## The WirePlumber ALSA configuration

wireplumber.settings = {
  ## The priority for device reservation
  # monitor.alsa.reserve-priority = -20

  ## The application name for device reservation
  # monitor.alsa.reserve-application-name = WirePlumber
}

monitor.alsa.properties = {
  ## The properties used when constructing the 'api.alsa.enum.udev' plugin
}

monitor.alsa-midi.properties = {
  ## MIDI bridge node properties

  ## Name set for the node with ALSA MIDI ports
  # node.name = "Midi-Bridge"

  ## Removes longname/number from MIDI port names
  # api.alsa.disable-longname = false
}

monitor.alsa.rules = [
  ## The list of monitor rules

  ## The following are the default rules applied if none overrides them.
  # {
  #   matches = [
  #     {
  #       device.name = "~alsa_card.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       api.alsa.use-acp  = true
  #       api.acp.auto-port = false
  #       device.disabled   = false
  #     }
  #   }
  # }

  ## This rule example allows changing properties on all ALSA devices.
  # {
  #   matches = [
  #     {
  #       ## This matches all cards.
  #       device.name = "~alsa_card.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       ## Use ALSA-Card-Profile devices. They use UCM or the profile
  #       ## configuration to configure the device and mixer settings.
  #       api.alsa.use-acp = false
  #
  #       ## Use UCM instead of profile when available. Can be
  #       ## disabled to skip trying to use the UCM profile.
  #       api.alsa.use-ucm = false
  #
  #       ## Don't use the hardware mixer for volume control. It
  #       ## will only use software volume. The mixer is still used
  #       ## to mute unused paths based on the selected port.
  #       api.alsa.soft-mixer = false
  #
  #       ## Ignore decibel settings of the driver. Can be used to
  #       ## work around buggy drivers that report wrong values.
  #       api.alsa.ignore-dB = false
  #
  #       ## The profile set to use for the device. Usually this is
  #       ## "default.conf" but can be changed with a udev rule or here.
  #       device.profile-set = "profileset-name"
  #
  #       ## The default active profile. Is by default set to "Off".
  #       device.profile = "default profile name"
  #
  #       ## Automatically select the best profile. This is the
  #       ## highest priority available profile. This is disabled
  #       ## here and instead implemented in the session manager
  #       ## where it can save and load previous preferences.
  #       api.acp.auto-profile = false
  #
  #       ## Automatically switch to the highest priority available port.
  #       ## This is disabled here and implemented in the session manager instead.
  #       api.acp.auto-port = true
  #
  #       ## Other properties can be set here.
  #       device.nick = "My Device"
  #
  #       ## Whether to disable the device or not
  #       device.disabled = false
  #     }
  #   }
  # }

  ## This rule example allows changing properties on all ALSA nodes.
  # {
  #   matches = [
  #     {
  #       ## Matches all sources.
  #       node.name = "~alsa_input.*"
  #     }
  #     {
  #       ## Matches all sinks.
  #       node.name = "~alsa_output.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       node.nick              = "My Node"
  #       node.description       = "My Node Description"
  #       priority.driver        = 100
  #       priority.session       = 100
  #       node.pause-on-idle     = false
  #       monitor.channel-volumes = false
  #       resample.quality       = 4
  #       resample.disable       = false
  #       channelmix.normalize   = false
  #       channelmix.mix-lfe     = false
  #       channelmix.upmix       = true
  #
  #       ## only "psd", "none" or "simple" values are accepted
  #       channelmix.upmix-method = "psd"
  #
  #       channelmix.lfe-cutoff  = 150
  #       channelmix.fc-cutoff   = 12000
  #       channelmix.rear-delay  = 12.0
  #       channelmix.stereo-widen = 0.0
  #       channelmix.hilbert-taps = 0
  #       channelmix.disable     = false
  #       dither.noise           = 0
  #       dither.method          = "none"
  #       audio.channels         = 2
  #       audio.format           = "S16LE"
  #       audio.rate             = 44100
  #       audio.allowed-rates    = [32000, 96000]
  #       audio.position         = [FL, FR]
  #       api.alsa.period-size   = 1024
  #       api.alsa.period-num    = 2
  #       api.alsa.headroom      = 0
  #       api.alsa.start-delay   = 0
  #       api.alsa.disable-mmap  = false
  #       api.alsa.disable-batch = false
  #       api.alsa.use-chmap     = false
  #       api.alsa.multirate     = true
  #       latency.internal.rate  = 0
  #       latency.internal.ns    = 0
  #       clock.name             = "api.alsa.0"
  #
  #       ## 0 disables suspend
  #       session.suspend-timeout-seconds = 5
  #       node.disabled = false
  #     }
  #   }
  # }
]
  07070100000143000081A400000000000000000000000165F8630400001986000000000000000000000000000000000000004800000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/bluetooth.conf   ## Template WirePlumber Bluetooth configuration

wireplumber.settings = {
  ## Whether to store state on the filesystem.
  # bluetooth.use-persistent-storage = true

  ## Whether to use headset profile in the presence of an input stream.
  # bluetooth.autoswitch-to-headset-profile = true
}

monitor.bluez.properties = {
  ## The properties used when constructing the 'api.bluez5.enum.dbus' plugin

  ## Enabled roles (default: [ a2dp_sink a2dp_source bap_sink bap_source hfp_hf hfp_ag ])
  ##
  ## Currently some headsets (Sony WH-1000XM3) are not working with
  ## both hsp_ag and hfp_ag enabled, so by default we enable only HFP.
  ##
  ## Supported roles: hsp_hs (HSP Headset),
  ##                  hsp_ag (HSP Audio Gateway),
  ##                  hfp_hf (HFP Hands-Free),
  ##                  hfp_ag (HFP Audio Gateway)
  ##                  a2dp_sink (A2DP Audio Sink)
  ##                  a2dp_source (A2DP Audio Source)
  ##                  bap_sink (LE Audio Basic Audio Profile Sink)
  ##                  bap_source (LE Audio Basic Audio Profile Source)
  # bluez5.roles = [ a2dp_sink a2dp_source bap_sink bap_source hsp_hs hsp_ag hfp_hf hfp_ag ]

  ## Enabled A2DP codecs (default: all).
  # bluez5.codecs = [ sbc sbc_xq aac ldac aptx aptx_hd aptx_ll aptx_ll_duplex faststream faststream_duplex ]

  ## These features do not work on all headsets, so they are enabled
  ## by default based on the hardware database. They can also be
  ## forced on/off for all devices by the following options:
  # bluez5.enable-sbc-xq = true
  # bluez5.enable-msbc = true
  # bluez5.enable-hw-volume = true

  ## HFP/HSP backend (default: native).
  ## Available values: any, none, hsphfpd, ofono, native
  # bluez5.hfphsp-backend = "native"

  ## HFP/HSP native backend modem (default: none).
  ## Available values: none, any or the modem device string as found in
  ## 'Device' property of org.freedesktop.ModemManager1.Modem interface
  # bluez5.hfphsp-backend-native-modem = "none"

  ## HFP/HSP hardware offload SCO support (default: false).
  ## Using this feature requires a custom WirePlumber script that
  ## handles audio routing in a platform-specific way. See
  ## tests/examples/bt-pinephone.lua for an example.
  # bluez5.hw-offload-sco = false

  ## Properties for the A2DP codec configuration
  # bluez5.default.rate = 48000
  # bluez5.default.channels = 2

  ## Register dummy AVRCP player. Some devices have wrongly functioning
  ## volume or playback controls if this is not enabled. Default: false
  # bluez5.dummy-avrcp-player = true

  ## Opus Pro Audio mode settings
  # bluez5.a2dp.opus.pro.channels = 3
  # bluez5.a2dp.opus.pro.coupled-streams = 1
  # bluez5.a2dp.opus.pro.locations = [ FL,FR,LFE ]
  # bluez5.a2dp.opus.pro.max-bitrate = 600000
  # bluez5.a2dp.opus.pro.frame-dms = 50
  # bluez5.a2dp.opus.pro.bidi.channels = 1
  # bluez5.a2dp.opus.pro.bidi.coupled-streams = 0
  # bluez5.a2dp.opus.pro.bidi.locations = [ FC ]
  # bluez5.a2dp.opus.pro.bidi.max-bitrate = 160000
  # bluez5.a2dp.opus.pro.bidi.frame-dms = 400
}

monitor.bluez-midi.properties = {
  ## The properties used when constructing the 'api.bluez5.midi.enum' plugin
}

monitor.bluez-midi.servers = [
  ## List of MIDI server node names. Each node name given will create a new instance
  ## of a BLE MIDI service. Typical BLE MIDI instruments have on service instance,
  ## so adding more than one here may confuse some clients. The node property matching
  ## rules below apply also to these servers.

  # "bluez_midi.server"
]

monitor.bluez.rules = [
  ## The list of monitor rules

  ## This rule example allows changing properties on all Bluetooth devices.
  # {
  #   matches = [
  #     {
  #       ## This matches all bluetooth devices.
  #       device.name = "~bluez_card.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       ## Auto-connect device profiles on start up or when only partial
  #       ## profiles have connected. Disabled by default if the property
  #       ## is not specified.
  #       bluez5.auto-connect = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
  #
  #       ## Hardware volume control (default: [ hfp_ag hsp_ag a2dp_source ])
  #       bluez5.hw-volume = [ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]
  #
  #       ## LDAC encoding quality
  #       ## Available values: auto (Adaptive Bitrate, default)
  #       ##                   hq   (High Quality, 990/909kbps)
  #       ##                   sq   (Standard Quality, 660/606kbps)
  #       ##                   mq   (Mobile use Quality, 330/303kbps)
  #       bluez5.a2dp.ldac.quality = "auto"
  #
  #       ## AAC variable bitrate mode
  #       ## Available values: 0 (cbr, default), 1-5 (quality level)
  #       bluez5.a2dp.aac.bitratemode = 0
  #
  #       ## Opus Pro Audio encoding mode: audio, voip, lowdelay
  #       bluez5.a2dp.opus.pro.application = "audio"
  #       bluez5.a2dp.opus.pro.bidi.application = "audio"
  #     }
  #   }
  # }

  ## This rule example allows changing properties on all Bluetooth nodes.
  # {
  #   matches = [
  #     {
  #       ## Matches all sources.
  #       node.name = "~bluez_input.*"
  #     }
  #     {
  #       ## Matches all sinks.
  #       node.name = "~bluez_output.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       node.nick              = "My Node"
  #       priority.driver        = 100
  #       priority.session       = 100
  #       node.pause-on-idle     = false
  #       resample.quality       = 4
  #       channelmix.normalize   = false
  #       channelmix.mix-lfe     = false
  #       session.suspend-timeout-seconds = 5
  #       monitor.channel-volumes = false
  #
  #       ## Media source role, "input" or "playback"
  #       ## Defaults to "playback", playing stream to speakers
  #       ## Set to "input" to use as an input for apps
  #       bluez5.media-source-role = "input"
  #     }
  #   }
  # }
]

monitor.bluez-midi.rules = [
  ## The list of MIDI monitor rules

  ## This rule example allows changing properties on all Bluetooth MIDI nodes.
  # {
  #   matches = [
  #     ## Matches all bluez midi nodes.
  #     {
  #       ## Matches all sources.
  #       node.name = "~bluez_midi.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       node.nick = "My Node"
  #       priority.driver = 100
  #       priority.session = 100
  #       node.pause-on-idle = false
  #       session.suspend-timeout-seconds = 5
  #       node.latency-offset-msec = 0
  #     }
  #   }
  # }
]
  07070100000144000081A400000000000000000000000165F86304000004AD000000000000000000000000000000000000004500000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/device.conf  ## The WirePlumber device configuration

wireplumber.settings = {
  ## Enables storing/restoring preferences to the file system at startup;
  ## when set to false, default nodes and routes are selected based on
  ## their priorities and any runtime changes do not persist after restart
  # device.restore-profile = true

  # device.restore-routes = true

  ## device default volume level
  # device.routes.default-sink-volume = 0.064

  ## device default input volume
  # device.routes.default-source-volume = 1.0
}

# # This rule example shows how to prioritize between available profiles(codecs)
# # of a bluetooth device.
# device.profile.priority.rules = [
#   # The following are the default rules applied if none overrides them.
#   {
#     matches = [
#       {
#         device.name = "~bluez_card.*"
#       }
#     ]
#     actions = {
#       update-props = {
#         # lower the index higher the priority
#         priorities = [
#           "a2dp-sink-aptx_ll",
#           "a2dp-sink-aac",
#           "a2dp-sink-sbc",
#           "a2dp-sink-aptx",
#           "a2dp-sink-aptx_hd",
#           "a2dp-sink-ldac",
#           "a2dp-sink-sbc_xq",
#         ]
#       }
#     }
#   }
]
   07070100000145000081A400000000000000000000000165F8630400000478000000000000000000000000000000000000004800000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/libcamera.conf   ## The WirePlumber libcamera configuration

monitor.libcamera.properties = {
  ## The properties used when constructing the 'api.libcamera.enum.manager' plugin
}

monitor.libcamera.rules = [
  ## The list of monitor rules

  ## This rule example allows changing properties on all libcamera devices.
  # {
  #   matches = [
  #     {
  #       ## This matches all cards.
  #       device.name = "~libcamera_device.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       device.nick       = "My Device"
  #       device.disabled   = false
  #     }
  #   }
  # }

  ## This rule example allows changing properties on all libcamera nodes.
  # {
  #   matches = [
  #     {
  #       ## Matches all sources.
  #       node.name = "~libcamera_input.*"
  #     }
  #     {
  #       ## Matches all sinks.
  #       node.name = "~libcamera_output.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       node.nick              = "My Node"
  #       priority.driver        = 100
  #       priority.session       = 100
  #       node.pause-on-idle     = false
  #       node.disabled = false
  #     }
  #   }
  # }
]
07070100000146000081A400000000000000000000000165F863040000016E000000000000000000000000000000000000004600000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/linking.conf ## The WirePlumber linking configuration

wireplumber.settings = {
  ## Moves session items when metadata ``target.object`` changes. Also responds to
  ##`target.node` key. But `target.object` is the canonical key.
  # linking.allow-moving-streams = true

  ## Moves session items to the default device when it has changed
  # linking.follow-default-target = true
}
  07070100000147000081A400000000000000000000000165F8630400000188000000000000000000000000000000000000004200000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/log.conf context.properties = {
  ## The default log level (single letter, one of F(atal), E(rror), W(arn),
  ## N(otice), I(nfo), D(ebug), T(race)). This may also contain a category name,
  ## separated by a colon, to set the log level for a specific category.
  ## For example, set the log level to W for all categories and I for wp-core:
  ##   log.level = "W,wp-core:I"
  ##
  # log.level = "N"
}
07070100000148000081A400000000000000000000000165F86304000005D5000000000000000000000000000000000000004500000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/stream.conf  ## The WirePlumber stream configuration

wireplumber.settings = {
  ## WirePlumber recognizes the client/app from which the stream is originating and
  ## always stores the stream priorities like volume, channel volumes, mute status
  ## and channel map. If this config is true, WirePlumber will restore the
  ## previously stored stream properties.

  ## When set to `false`, the above stream properties will be initialized to
  ## default values irrespective of the previous values.

  # node.stream.restore-props = true

  ## WirePlumber recognizes the client/app from which the stream is originating and
  ## always stores the target to which the stream is linked and when the stream
  ## shows up for the second time. WirePlumber will try to link it to the this
  ## stored target.

  # node.stream.restore-target = true

  ## The default channel volume for new streams whose props were never saved
  ## previously. This is only used if "node.stream.restore-props" is set to true.
  # node.stream.default-playback-volume = 1.0
  # node.stream.default-capture-volume = 1.0
}

stream.rules = [
  ## The list of stream rules

  ## This rule example allows setting properties on the "pw-play" stream.
  # {
  #   matches = [
  #       ## Matches all devices
  #       { application.name = "pw-play" }
  #   ]
  #   actions = {
  #     update-props = {
  #       state.restore-props = false
  #       state.restore-target = false
  #       state.default-volume = 1.0
  #     }
  #   }
  # }
]
   07070100000149000081A400000000000000000000000165F8630400000443000000000000000000000000000000000000004300000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/v4l2.conf    ## The WirePlumber V4L2 configuration

monitor.v4l2.properties = {
  ## The properties used when constructing the 'api.v4l2.enum.udev' plugin
}

monitor.v4l2.rules = [
  ## The list of monitor rules.

  ## This rule example allows changing properties on all V4L2 devices.
  # {
  #   matches = [
  #     {
  #       ## This matches all cards.
  #       device.name = "~v4l2_device.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       device.nick = "My Device"
  #       device.disabled   = false
  #     }
  #   }
  # }

  ## This rule example allows changing properties on all V4L2 nodes.
  # {
  #   matches = [
  #     {
  #       ## Matches all sources.
  #       node.name = "~v4l2_input.*"
  #     }
  #     {
  #       ## Matches all sinks.
  #       node.name = "~v4l2_output.*"
  #     }
  #   ]
  #   actions = {
  #     update-props = {
  #       node.nick              = "My Node"
  #       priority.driver        = 100
  #       priority.session       = 100
  #       node.pause-on-idle     = false
  #       node.disabled = false
  #     }
  #   }
  # }
]
 0707010000014A000081A400000000000000000000000165F8630400000867000000000000000000000000000000000000004600000000wireplumber-0.5.0/src/config/wireplumber.conf.d.examples/virtual.conf ## The WirePlumber virtual item configuration

virtual-items = {
  ## The list of virtual items to create

  # virtual-item.capture = {
  #   media.class = "Audio/Source"
  #   role = "Capture"
  # }
  # virtual-item.multimedia = {
  #   media.class = "Audio/Sink"
  #   role = "Multimedia"
  # }
  # virtual-item.speech_low = {
  #   media.class = "Audio/Sink"
  #   role = "Speech-Low"
  # }
  # virtual-item.custom_low = {
  #   media.class = "Audio/Sink"
  #   role = "Custom-Low"
  # }
  # virtual-item.navigation = {
  #   media.class = "Audio/Sink"
  #   role = "Navigation"
  # }
  # virtual-item.speech_high = {
  #   media.class = "Audio/Sink"
  #   role = "Speech-High"
  # }
  # virtual-item.custom_high = {
  #   media.class = "Audio/Sink"
  #   role = "Custom-High"
  # }
  # virtual-item.communication = {
  #   media.class = "Audio/Sink"
  #   role = "Communication"
  # }
  # virtual-item.emergency = {
  #   media.class = "Audio/Sink"
  #   role = "Emergency"
  # }
}

virtual-item-roles = {
  ## The list of virtual item roles to use

  # Capture = {
  #   alias = [ "Multimedia", "Music", "Voice", "Capture" ]
  #   priority = 25
  #   action.default = "cork"
  #   action.capture = "mix"
  #   media.class = "Audio/Source"
  # }
  # Multimedia = {
  #   alias = [ "Movie" "Music" "Game" ]
  #   priority = 25
  #   action.default = "cork"
  # }
  # Speech-Low = {
  #   priority = 30
  #   action.default = "cork"
  #   action.Speech-Low = "mix"
  # }
  # Custom-Low = {
  #   priority = 35
  #   action.default = "cork"
  #   action.Custom-Low = "mix"
  # }
  # Navigation = {
  #   priority = 50
  #   action.default = "duck"
  #   action.Navigation = "mix"
  # }
  # Speech-High = {
  #   priority = 60
  #   action.default = "cork"
  #   action.Speech-High = "mix"
  # }
  # Custom-High = {
  #   priority = 65
  #   action.default = "cork"
  #   action.Custom-High = "mix"
  # }
  # Communication = {
  #   priority = 75
  #   action.default = "cork"
  #   action.Communication = "mix"
  # }
  # Emergency = {
  #   alias = [ "Alert" ]
  #   priority = 99
  #   action.default = "cork"
  #   action.Emergency = "mix"
  # }
}
 0707010000014B000081A400000000000000000000000165F86304000001C3000000000000000000000000000000000000003D00000000wireplumber-0.5.0/src/config/wireplumber.conf.d/alsa-vm.conf  # ALSA node property overrides for virtual machine hardware

monitor.alsa.rules = [
  # Generic PCI cards on any VM type
  {
    matches = [
      {
        node.name = "~alsa_input.pci.*"
        cpu.vm.name = "~.*"
      }
      {
        node.name = "~alsa_output.pci.*"
        cpu.vm.name = "~.*"
      }
    ]
    actions = {
      update-props = {
        api.alsa.period-size   = 1024
        api.alsa.headroom      = 2048
      }
    }
  }
]
 0707010000014C000081A400000000000000000000000165F8630400001374000000000000000000000000000000000000001D00000000wireplumber-0.5.0/src/main.c  /* WirePlumber
 *
 * Copyright © 2019-2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <glib-unix.h>
#include <pipewire/pipewire.h>
#include <locale.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wireplumber")

enum WpExitCode
{
  /* based on sysexits.h */
  WP_EXIT_OK = 0,
  WP_EXIT_USAGE = 64,       /* command line usage error */
  WP_EXIT_UNAVAILABLE = 69, /* service unavailable */
  WP_EXIT_SOFTWARE = 70,    /* internal software error */
  WP_EXIT_CONFIG = 78,      /* configuration error */
};

static gboolean show_version = FALSE;
static gchar * config_file = NULL;
static gchar * profile = NULL;

static GOptionEntry entries[] =
{
  { "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &show_version,
    "Show version", NULL },
  { "config-file", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &config_file,
    "The configuration file to use", NULL },
  { "profile", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &profile,
    "The profile to load", NULL },
  { NULL }
};

/*** WpDaemon ***/

typedef struct
{
  WpCore *core;
  GMainLoop *loop;
  gint exit_code;
} WpDaemon;

static void
daemon_clear (WpDaemon * self)
{
  g_clear_pointer (&self->loop, g_main_loop_unref);
  g_clear_object (&self->core);
}

G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpDaemon, daemon_clear)

static void
daemon_exit (WpDaemon * d, gint code)
{
  /* replace OK with an error, but do not replace error with OK */
  if (d->exit_code == WP_EXIT_OK)
    d->exit_code = code;
  g_main_loop_quit (d->loop);
}

static void
on_disconnected (WpCore *core, WpDaemon * d)
{
  wp_notice ("disconnected from pipewire");
  daemon_exit (d, WP_EXIT_OK);
}

static gboolean
signal_handler (int signal, gpointer data)
{
  WpDaemon *d = data;
  wp_notice ("stopped by signal: %s", strsignal (signal));
  daemon_exit (d, WP_EXIT_OK);
  return G_SOURCE_CONTINUE;
}

static gboolean
signal_handler_int (gpointer data)
{
  return signal_handler (SIGINT, data);
}

static gboolean
signal_handler_hup (gpointer data)
{
  return signal_handler (SIGHUP, data);
}

static gboolean
signal_handler_term (gpointer data)
{
  return signal_handler (SIGTERM, data);
}

static void
on_core_activated (WpObject * core, GAsyncResult * res, WpDaemon * d)
{
  g_autoptr (GError) error = NULL;

  if (!wp_object_activate_finish (core, res, &error)) {
    fprintf (stderr, "%s\n", error->message);

    switch (error->code) {
      case WP_LIBRARY_ERROR_SERVICE_UNAVAILABLE:
        daemon_exit (d, WP_EXIT_UNAVAILABLE);
        break;

      case WP_LIBRARY_ERROR_INVALID_ARGUMENT:
        daemon_exit (d, WP_EXIT_CONFIG);
        break;

      default:
        daemon_exit (d, WP_EXIT_SOFTWARE);
    }
  }
}

gint
main (gint argc, gchar **argv)
{
  g_auto (WpDaemon) d = {0};
  g_autoptr (GOptionContext) context = NULL;
  g_autoptr (GError) error = NULL;
  g_autoptr (WpProperties) properties = NULL;
  g_autoptr (WpConf) conf = NULL;

  setlocale (LC_ALL, "");
  setlocale (LC_NUMERIC, "C");
  wp_init (WP_INIT_ALL);

  context = g_option_context_new ("- PipeWire Session/Policy Manager");
  g_option_context_add_main_entries (context, entries, NULL);
  if (!g_option_context_parse (context, &argc, &argv, &error)) {
    fprintf (stderr, "%s\n", error->message);
    return WP_EXIT_USAGE;
  }

  if (show_version) {
    g_print ("%s\n"
        "Compiled with libwireplumber %s\n"
        "Linked with libwireplumber %s\n",
        argv[0],
        WIREPLUMBER_VERSION,
        wp_get_library_version());
    return WP_EXIT_OK;
  }

  if (!config_file)
    config_file = "wireplumber.conf";
  if (!profile)
    profile = "main";

  /* load configuration */
  conf = wp_conf_new_open (config_file, NULL, &error);
  if (!conf) {
    fprintf (stderr, "Failed to load configuration: %s\n", error->message);
    return WP_EXIT_CONFIG;
  }

  /* prepare core properties */
  properties = wp_properties_new (
      PW_KEY_APP_NAME, "WirePlumber",
      PW_KEY_APP_VERSION, WIREPLUMBER_VERSION,
      "wireplumber.daemon", "true",
      "wireplumber.profile", profile,
      NULL);

  /* prefer manager socket */
  if (pw_check_library_version(0, 3, 84))
    wp_properties_set (properties, PW_KEY_REMOTE_NAME,
        ("[" PW_DEFAULT_REMOTE "-manager," PW_DEFAULT_REMOTE "]"));

  /* init wireplumber daemon */
  d.loop = g_main_loop_new (NULL, FALSE);
  d.core = wp_core_new (NULL, g_steal_pointer (&conf),
      g_steal_pointer (&properties));
  g_signal_connect (d.core, "disconnected", G_CALLBACK (on_disconnected), &d);

  /* watch for exit signals */
  g_unix_signal_add (SIGINT, signal_handler_int, &d);
  g_unix_signal_add (SIGTERM, signal_handler_term, &d);
  g_unix_signal_add (SIGHUP, signal_handler_hup, &d);

  wp_object_activate (WP_OBJECT (d.core), WP_OBJECT_FEATURES_ALL, NULL,
      (GAsyncReadyCallback) on_core_activated, &d);

  /* run */
  g_main_loop_run (d.loop);
  wp_core_disconnect (d.core);
  return d.exit_code;
}
0707010000014D000081A400000000000000000000000165F86304000002B0000000000000000000000000000000000000002200000000wireplumber-0.5.0/src/meson.build scripts_doc_files_relative = [
  'default-nodes'/'README.rst',
  'device'/'README.rst',
  'lib'/'SETTINGS.rst',
  'linking'/'README.rst',
]

scripts_doc_files = []
foreach f : scripts_doc_files_relative
  scripts_doc_files += files('scripts'/f)
endforeach

if build_tools
  subdir('tools')
endif

if build_daemon
  subdir('config')
  subdir('systemd')

  install_subdir('scripts',
    install_dir: wireplumber_data_dir,
    exclude_files: scripts_doc_files_relative,
    strip_directory : false
  )

  wp_sources = [
    'main.c',
  ]

  wireplumber = executable('wireplumber',
    wp_sources,
    install: true,
    dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep],
  )
endif
0707010000014E000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001E00000000wireplumber-0.5.0/src/scripts 0707010000014F000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002500000000wireplumber-0.5.0/src/scripts/client  07070100000150000081A400000000000000000000000165F863040000093A000000000000000000000000000000000000003800000000wireplumber-0.5.0/src/scripts/client/access-default.lua   -- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT

log = Log.open_topic ("s-client")

config = {}
config.rules = Conf.get_section_as_json ("access.rules")

function getAccess (properties)
  local access = properties["pipewire.access"]
  local client_access = properties["pipewire.client.access"]
  local is_flatpak = properties["pipewire.sec.flatpak"]

  if is_flatpak then
    client_access = "flatpak"
  end

  if client_access == nil then
    return access
  elseif access == "unrestricted" or access == "default" then
    if client_access ~= "unrestricted" then
      return client_access
    end
  end

  return access
end

function getDefaultPermissions (properties)
  local access = properties["access"]
  local media_category = properties["media.category"]

  if access == "flatpak" and media_category == "Manager" then
    return "all", "flatpak-manager"
  elseif access == "flatpak" or access == "restricted" then
    return "rx", access
  elseif access == "default" then
    return "all", access
  end

  return nil, nil
end

function getPermissions (properties)
  if config.rules then
    local mprops, matched = JsonUtils.match_rules_update_properties (
        config.rules, properties)
    if (matched > 0 and mprops["default_permissions"]) then
      return mprops["default_permissions"], mprops["access"]
    end
  end

  return nil, nil
end

clients_om = ObjectManager {
  Interest { type = "client" }
}

clients_om:connect("object-added", function (om, client)
  local id = client["bound-id"]
  local properties = client["properties"]
  local access = getAccess (properties)

  properties["access"] = access

  local perms, effective_access = getPermissions (properties)
  if perms == nil then
    perms, effective_access = getDefaultPermissions (properties)
  end
  if effective_access == nil then
    effective_access = access
  end

  if perms ~= nil then
    log:info(client, "Granting permissions to client " .. id .. " (access " ..
      effective_access .. "): " .. perms)
    client:update_permissions { ["any"] = perms }
    client:update_properties { ["pipewire.access.effective"] = effective_access }
  else
    log:debug(client, "No rule for client " .. id .. " (access " .. access .. ")")
  end
end)

clients_om:activate()
  07070100000151000081A400000000000000000000000165F863040000104A000000000000000000000000000000000000003700000000wireplumber-0.5.0/src/scripts/client/access-portal.lua    MEDIA_ROLE_NONE = 0
MEDIA_ROLE_CAMERA = 1 << 0

log = Log.open_topic ("s-client")

function hasPermission (permissions, app_id, lookup)
  if permissions then
    for key, values in pairs(permissions) do
      if key == app_id then
        for _, v in pairs(values) do
          if v == lookup then
            return true
          end
        end
      end
    end
  end
  return false
end

function parseMediaRoles (media_roles_str)
  local media_roles = MEDIA_ROLE_NONE
  for role in media_roles_str:gmatch('[^,%s]+') do
    if role == "Camera" then
      media_roles = media_roles | MEDIA_ROLE_CAMERA
    end
  end
  return media_roles
end

function setPermissions (client, allow_client, allow_nodes)
  local client_id = client["bound-id"]
  log:info(client, "Granting ALL access to client " .. client_id)

  -- Update permissions on client
  client:update_permissions { [client_id] = allow_client and "all" or "-" }

  -- Update permissions on camera source nodes
  for node in nodes_om:iterate() do
    local node_id = node["bound-id"]
    client:update_permissions { [node_id] = allow_nodes and "all" or "-" }
  end
end

function updateClientPermissions (client, permissions)
  local client_id = client["bound-id"]
  local str_prop = nil
  local app_id = nil
  local media_roles = nil
  local allowed = false

  -- Make sure the client is not the portal itself
  str_prop = client.properties["pipewire.access.portal.is_portal"]
  if str_prop == "yes" then
    log:info (client, "client is the portal itself")
    return
  end

  -- Make sure the client has a portal app Id
  str_prop = client.properties["pipewire.access.portal.app_id"]
  if str_prop == nil then
    log:info (client, "Portal managed client did not set app_id")
    return
  end
  if str_prop == "" then
    log:info (client, "Ignoring portal check for non-sandboxed client")
    setPermissions (client, true, true)
    return
  end
  app_id = str_prop

  -- Make sure the client has portal media roles
  str_prop = client.properties["pipewire.access.portal.media_roles"]
  if str_prop == nil then
  log:info (client, "Portal managed client did not set media_roles")
    return
  end
  media_roles = parseMediaRoles (str_prop)
  if (media_roles & MEDIA_ROLE_CAMERA) == 0 then
    log:info (client, "Ignoring portal check for clients without camera role")
    return
  end

  -- Update permissions
  allowed = hasPermission (permissions, app_id, "yes")

  log:info (client, "setting permissions: " .. tostring(allowed))
  setPermissions (client, allowed, allowed)
end

-- Create portal clients object manager
clients_om = ObjectManager {
  Interest {
    type = "client",
    Constraint { "pipewire.access", "=", "portal" },
  }
}

-- Set permissions to portal clients from the permission store if loaded
pps_plugin = Plugin.find("portal-permissionstore")
if pps_plugin then
  nodes_om = ObjectManager {
    Interest {
      type = "node",
      Constraint { "media.role", "=", "Camera" },
      Constraint { "media.class", "=", "Video/Source" },
    }
  }
  nodes_om:activate()

  clients_om:connect("object-added", function (om, client)
    local new_perms = pps_plugin:call("lookup", "devices", "camera");
    updateClientPermissions (client, new_perms)
  end)

  nodes_om:connect("object-added", function (om, node)
    local new_perms = pps_plugin:call("lookup", "devices", "camera");
    for client in clients_om:iterate() do
      updateClientPermissions (client, new_perms)
    end
  end)

  pps_plugin:connect("changed", function (p, table, id, deleted, permissions)
    if table == "devices" or id == "camera" then
      for app_id, _ in pairs(permissions) do
        for client in clients_om:iterate {
            Constraint { "pipewire.access.portal.app_id", "=", app_id }
        } do
          updateClientPermissions (client, permissions)
        end
      end
    end
  end)
else
  -- Otherwise, just set all permissions to all portal clients
  clients_om:connect("object-added", function (om, client)
    local id = client["bound-id"]
    log:info(client, "Granting ALL access to client " .. id)
    client:update_permissions { ["any"] = "all" }
  end)
end

clients_om:activate()
  07070100000152000081A400000000000000000000000165F86304000009DF000000000000000000000000000000000000003500000000wireplumber-0.5.0/src/scripts/client/access-snap.lua  -- Manage snap audio permissions
--
-- Copyright © 2023 Canonical Ltd.
--    @author Sergio Costas Rodriguez <sergio.costas@canonical.com>
--
-- SPDX-License-Identifier: MIT

function removeClientPermissionsForOtherClients (client)
  -- Remove access to any other clients, but allow all the process of the
  -- same snap to access their elements
  local client_id = client.properties["pipewire.snap.id"]
  for snap_client in clients_snap:iterate() do
    local snap_client_id = snap_client.properties["pipewire.snap.id"]
    if snap_client_id ~= client_id then
      client:update_permissions { [snap_client["bound-id"]] = "-" }
    end
  end
  for no_snap_client in clients_no_snap:iterate() do
    client:update_permissions { [no_snap_client["bound-id"]] = "-" }
  end
end

function updateClientPermissions (client)
  -- Remove access to Audio/Sources and Audio/Sinks based on snap permissions
  for node in nodes_om:iterate() do
    local node_id = node["bound-id"]
    local property = "pipewire.snap.audio.playback"

    if node.properties["media.class"] == "Audio/Source" then
      property = "pipewire.snap.audio.record"
    end

    if client.properties[property] ~= "true" then
      client:update_permissions { [node_id] = "-" }
    end
  end
end

clients_snap = ObjectManager {
  Interest {
    type = "client",
    Constraint { "pipewire.snap.id", "+", type = "pw"},
  }
}

clients_no_snap = ObjectManager {
  Interest {
    type = "client",
    Constraint { "pipewire.snap.id", "-", type = "pw"},
  }
}

nodes_om = ObjectManager {
  Interest {
    type = "node",
    Constraint { "media.class", "matches", "Audio/*"}
  }
}

clients_snap:connect("object-added", function (om, client)
  -- If a new snap client is added, adjust its permissions
  updateClientPermissions (client)
  removeClientPermissionsForOtherClients (client)
end)

clients_no_snap:connect("object-added", function (om, client)
  -- If a new, non-snap client is added,
  -- remove access to it from other snaps
  client_id = client["bound-id"]
  for snap_client in clients_snap:iterate() do
    if client.properties["pipewire.snap.id"] ~= nil then
      snap_client:update_permissions { [client_id] = "-" }
    end
  end
end)

nodes_om:connect("object-added", function (om, node)
  -- If a new Audio/Sink or Audio/Source node is added,
  -- adjust the permissions in the snap clients
  for client in clients_snap:iterate() do
    updateClientPermissions (client)
  end
end)

clients_snap:activate()
clients_no_snap:activate()
nodes_om:activate() 07070100000153000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002C00000000wireplumber-0.5.0/src/scripts/default-nodes   07070100000154000081A400000000000000000000000165F86304000010B8000000000000000000000000000000000000003700000000wireplumber-0.5.0/src/scripts/default-nodes/README.rst    Default Nodes Scripts
=====================

These scripts contain logic to select default source and sink nodes and also
manage user preferences regarding those.

Hooks
-----

Management of the default source and sink nodes is implemented by scanning all
the available nodes in the graph and assigning them a priority based on certain
logic. The node with the highest priority in each category becomes the default.

Scanning is implemented using a "rescan-for-default-nodes" event.
The "default-nodes/rescan-trigger" hook is the one that monitors graph changes
and schedules "rescan-for-default-nodes". Then, the "default-nodes/rescan"
hook is executed for the "rescan-for-default-nodes" event and it pushes a
"select-default-node" event for each one of the categories where a default node
is required:

 - Audio sink
 - Audio source
 - Video source

.. list-table:: Hooks triggered by changes in the graph
   :header-rows: 1
   :width: 100%
   :widths: 25 15 30 30

   * - Hook name
     - File
     - Triggered by
     - Action

   * - default-nodes/rescan-trigger
     - rescan.lua
     - linkables added/removed or default.configured.* metadata changed
     - schedule rescan-for-default-nodes

   * - default-nodes/store-configured-default-nodes
     - state-default-nodes.lua
     - default.configured.* metadata changed
     - stores user selections in the state file

   * - default-nodes/metadata-added
     - state-default-nodes.lua
     - metadata object created
     - restores default.configured.* values from the state file

.. list-table:: Hooks for the rescan-for-default-nodes event, in order of execution
   :header-rows: 1
   :width: 100%
   :widths: 25 25 50

   * - Hook name
     - File
     - Description

   * - m-standard-event-source/rescan-done
     - module-standard-event-source.c
     - clears the rescan_scheduled flag

   * - default-nodes/rescan
     - rescan.lua
     - schedules select-default-node for each category

.. list-table:: Hooks for the select-default-node event, in order of execution
   :header-rows: 1
   :width: 100%
   :widths: 25 25 50

   * - Hook name
     - File
     - Description

   * - default-nodes/find-best-default-node
     - find-best-default-node.lua
     - prioritizes nodes based on their priority.session property

   * - default-nodes/find-selected-default-node
     - find-selected-default-node.lua
     - prioritizes the current default.configured.* node, i.e. the current user selection

   * - default-nodes/find-stored-default-node
     - state-default-nodes.lua
     - prioritizes past user selections from the state file

   * - default-nodes/apply-default-node
     - apply-default-node.lua
     - sets the highest priority selected node as the default in the metadata

.. note::

   The actual order of the "default-nodes/find-\*" hooks is not defined and doesn't matter.
   The only thing that matters is that "default-nodes/apply-default-node" is the last hook.

select-default-node event
-------------------------

High priority event to select the default node for a given category
(media.type & direction combination).

In this event, each hook is tasked to find the highest priority node of the
category it runs for. In order to do that, a list of available nodes is
calculated in advance, by the "default-nodes/rescan" hook, and passed on to
each of the "select-default-node" hooks via event data. Each hook then has
to go through this list and select a node, placing it in the "selected-node"
event data together with its priority number in "selected-node-priority".
The next hook, then, may override the "selected-node" and "selected-node-priority"
with something else, but only if the new priority is higher than the old one.

.. list-table:: Event properties
   :header-rows: 1

   * - Property name
     - Description

   * - default-node.type
     - the suffix of the metadata keys related to this default node (audio.sink, audio.source or video.source)

.. list-table:: Exchanged event data
   :header-rows: 1

   * - Name
     - Description

   * - available-nodes
     - JSON array of all selectable nodes, with each element containing all node properties

   * - selected-node
     - the selected node's node.name (string)

   * - selected-node-priority
     - the priority (integer)
07070100000155000081A400000000000000000000000165F86304000004AC000000000000000000000000000000000000004300000000wireplumber-0.5.0/src/scripts/default-nodes/apply-default-node.lua    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT

log = Log.open_topic ("s-default-nodes")

SimpleEventHook {
  name = "default-nodes/apply-default-node",
  after = { "default-nodes/find-best-default-node",
            "default-nodes/find-selected-default-node",
            "default-nodes/find-stored-default-node" },
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-default-node" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    local props = event:get_properties ()
    local def_node_type = props ["default-node.type"]
    local selected_node = event:get_data ("selected-node")

    local om = source:call ("get-object-manager", "metadata")
    local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }

    if selected_node then
      local key = "default." .. def_node_type

      log:info ("set default node for " .. key .. " " .. selected_node)

      metadata:set (0, key, "Spa:String:JSON",
          Json.Object { ["name"] = selected_node }:to_string ())
    else
      metadata:set (0, "default." .. def_node_type, nil, nil)
    end
  end
}:register ()
07070100000156000081A400000000000000000000000165F863040000047A000000000000000000000000000000000000004700000000wireplumber-0.5.0/src/scripts/default-nodes/find-best-default-node.lua    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT

log = Log.open_topic ("s-default-nodes")

SimpleEventHook {
  name = "default-nodes/find-best-default-node",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-default-node" },
    },
  },
  execute = function (event)
    local available_nodes = event:get_data ("available-nodes")
    local selected_prio = event:get_data ("selected-node-priority") or 0
    local selected_node = event:get_data ("selected-node")

    available_nodes = available_nodes and available_nodes:parse ()
    if not available_nodes then
      return
    end

    for _, node_props in ipairs (available_nodes) do
      -- Highest priority node wins
      local priority = node_props ["priority.session"]
      priority = math.tointeger (priority) or 0

      if priority > selected_prio or selected_node == nil then
        selected_prio = priority
        selected_node = node_props ["node.name"]
      end
    end

    event:set_data ("selected-node-priority", selected_prio)
    event:set_data ("selected-node", selected_node)
  end
}:register ()
  07070100000157000081A400000000000000000000000165F863040000081E000000000000000000000000000000000000004B00000000wireplumber-0.5.0/src/scripts/default-nodes/find-selected-default-node.lua    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT

-- hook to make sure the user prefered device(default.configured.*) in other
-- words currently selected device is given higher priority

-- state-default-nodes.lua also does find out the default node out of the user
-- preferences(current and past), however it doesnt give any higher priority to
-- the currently selected device.

log = Log.open_topic ("s-default-nodes")

SimpleEventHook {
  name = "default-nodes/find-selected-default-node",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-default-node" },
    },
  },
  execute = function (event)
    local available_nodes = event:get_data ("available-nodes")

    available_nodes = available_nodes and available_nodes:parse ()
    if not available_nodes then
      return
    end

    local selected_prio = event:get_data ("selected-node-priority") or 0
    local selected_node = event:get_data ("selected-node")

    local source = event:get_source ()
    local props = event:get_properties ()
    local def_node_type = props ["default-node.type"]
    local metadata_om = source:call ("get-object-manager", "metadata")
    local metadata = metadata_om:lookup { Constraint { "metadata.name", "=", "default" } }
    local obj = metadata:find (0, "default.configured." .. def_node_type)

    if not obj then
      return
    end

    local json = Json.Raw (obj)
    local current_configured_node = json:parse ().name

    for _, node_props in ipairs (available_nodes) do
      local name = node_props ["node.name"]
      local priority = node_props ["priority.session"]
      priority = math.tointeger (priority) or 0

      if current_configured_node == name then
        priority = 30000 + priority

        if priority > selected_prio then

          selected_prio = priority
          selected_node = name

          event:set_data ("selected-node-priority", selected_prio)
          event:set_data ("selected-node", selected_node)
        end

        break
      end
    end
  end
}:register ()
  07070100000158000081A400000000000000000000000165F863040000163A000000000000000000000000000000000000003700000000wireplumber-0.5.0/src/scripts/default-nodes/rescan.lua    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT

-- looks for changes in user-preferences and devices added/removed and schedules
-- rescan and pushes "select-default-node" event for each of the media_classes

log = Log.open_topic ("s-default-nodes")

-- looks for changes in user-preferences and devices added/removed and schedules
-- rescan
SimpleEventHook {
  name = "default-nodes/rescan-trigger",
  interests = {
    EventInterest {
      Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "media.class", "#", "Audio/*" },
    },
    EventInterest {
      Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "media.class", "#", "Video/*" },
    },
    EventInterest {
      Constraint { "event.type", "=", "metadata-changed" },
      Constraint { "metadata.name", "=", "default" },
      Constraint { "event.subject.key", "c", "default.configured.audio.sink",
          "default.configured.audio.source", "default.configured.video.source"
      },
    },
    EventInterest {
      Constraint { "event.type", "=", "device-params-changed"},
      Constraint { "event.subject.param-id", "c", "Route", "EnumRoute"},
    },
  },
  execute = function (event)
    local source = event:get_source ()
    source:call ("schedule-rescan", "default-nodes")
  end
}:register ()

-- pushes "select-default-node" event for each of the media_classes
SimpleEventHook {
  name = "default-nodes/rescan",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "rescan-for-default-nodes" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    local si_om = source:call ("get-object-manager", "session-item")
    local devices_om = source:call ("get-object-manager", "device")

    log:trace ("re-evaluating default nodes")

    -- Audio Sink
    pushSelectDefaultNodeEvent (source, si_om, devices_om, "audio.sink", "in", {
      "Audio/Sink", "Audio/Duplex"
    })

    -- Audio Source
    pushSelectDefaultNodeEvent (source, si_om, devices_om, "audio.source", "out", {
      "Audio/Source", "Audio/Source/Virtual", "Audio/Duplex", "Audio/Sink"
    })

    -- Video Source
    pushSelectDefaultNodeEvent (source, si_om, devices_om, "video.source", "out", {
      "Video/Source", "Video/Source/Virtual"
    })
  end
}:register ()

function pushSelectDefaultNodeEvent (source, si_om, devices_om, def_node_type,
                                     port_direction, media_classes)
  local nodes =
      collectAvailableNodes (si_om, devices_om, port_direction, media_classes)
  local event = source:call ("create-event", "select-default-node", nil, {
      ["default-node.type"] = def_node_type,
  })
  event:set_data ("available-nodes", Json.Array (nodes))
  EventDispatcher.push_event (event)
end

-- Return an array table where each element is another table containing all the
-- node properties of all the nodes that can be selected for a given media class
-- set and direction
function collectAvailableNodes (si_om, devices_om, port_direction, media_classes)
  local collected = {}

  for linkable in si_om:iterate {
    type = "SiLinkable",
    Constraint { "media.class", "c", table.unpack (media_classes) },
  } do
    local linkable_props = linkable.properties
    local node = linkable:get_associated_proxy ("node")

    -- check that the node has ports in the requested direction
    if not node:lookup_port {
      Constraint { "port.direction", "=", port_direction }
    } then
      goto next_linkable
    end

    -- check that the node has available routes,
    -- if it is associated to a real device
    if not nodeHasAvailableRoutes (node, devices_om) then
      goto next_linkable
    end

    table.insert (collected, Json.Object (node.properties))

    ::next_linkable::
  end

  return collected
end

-- If the node has an associated device, verify that it has an available
-- route. Some UCM profiles expose all paths (headphones, HDMI, etc) as nodes,
-- even though they may not be connected... See #145
function nodeHasAvailableRoutes (node, devices_om)
  local properties = node.properties
  local device_id = properties ["device.id"]
  local cpd = properties ["card.profile.device"]

  if not device_id or not cpd then
    return true
  end

  -- Get the device
  local device = devices_om:lookup {
    Constraint { "bound-id", "=", device_id, type = "gobject" }
  }
  if not device then
    return true
  end

  --  Check if the current device route supports the node card device profile
  for r in device:iterate_params ("Route") do
    local route = r:parse ()
    local route_props = route.properties
    if route_props.device == tonumber (cpd) then
      if route_props.available == "no" then
        return false
      else
        return true
      end
    end
  end

  -- Check if available routes support the node card device profile
  local found = 0
  for r in device:iterate_params ("EnumRoute") do
    local route = r:parse ()
    local route_props = route.properties
    if type (route_props.devices) == "table" then
      for _, i in ipairs (route_props.devices) do
        if i == tonumber (cpd) then
          found = found + 1
          if route_props.available ~= "no" then
            return true
          end
        end
      end
    end
  end

  -- The node is part of a profile without routes so we assume it
  -- is available. This can happen for Pro Audio profiles
  if found == 0 then
    return true
  end

  return false
end
  07070100000159000081A400000000000000000000000165F86304000016C8000000000000000000000000000000000000004400000000wireplumber-0.5.0/src/scripts/default-nodes/state-default-nodes.lua   -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT

-- the script states the default nodes from the user preferences, it has hooks
-- which stores the user preferences(it stores not just the current preference
-- but all the previous preferences) in to the state file, retrives them from
-- state file during the bootup, finally it has a hook which finds a default
-- node out of the user preferences

log = Log.open_topic ("s-default-nodes")

-- the state storage
state = nil
state_table = nil

find_stored_default_node_hook = SimpleEventHook {
  name = "default-nodes/find-stored-default-node",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-default-node" },
    },
  },
  execute = function (event)
    local props = event:get_properties ()
    local available_nodes = event:get_data ("available-nodes")
    local selected_prio = event:get_data ("selected-node-priority") or 0
    local selected_node = event:get_data ("selected-node")

    available_nodes = available_nodes and available_nodes:parse ()
    if not available_nodes then
      return
    end

    local stored = collectStored (props ["default-node.type"])

    -- Check if any of the available nodes matches any of the configured
    for _, node_props in ipairs (available_nodes) do
      local name = node_props ["node.name"]

      for i, v in ipairs (stored) do
        if name == v then
          local priority = node_props ["priority.session"]
          priority = math.tointeger (priority) or 0
          priority = priority + 20001 - i

          if priority > selected_prio then
            selected_prio = priority
            selected_node = name
          end

          break
        end
      end
    end

    if selected_node then
      event:set_data ("selected-node-priority", selected_prio)
      event:set_data ("selected-node", selected_node)
    end
  end
}

store_configured_default_nodes_hook = SimpleEventHook {
  name = "default-nodes/store-configured-default-nodes",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "metadata-changed" },
      Constraint { "metadata.name", "=", "default" },
      Constraint { "event.subject.key", "c", "default.configured.audio.sink",
          "default.configured.audio.source", "default.configured.video.source"
      },
    },
  },
  execute = function (event)
    local props = event:get_properties ()
    -- get the part after "default.configured." (= 19 chars)
    local def_node_type = props ["event.subject.key"]:sub (20)
    local new_value = props ["event.subject.value"]
    local new_stored = {}

    if new_value then
      new_value = Json.Raw (new_value):parse () ["name"]
    end

    if new_value then
      local stored = collectStored (def_node_type)
      local pos = #stored + 1

      -- find if the curent configured value is already in the stack
      for i, v in ipairs (stored) do
        if v == new_value then
          pos = i
          break
        end
      end

      -- insert at the top and shift the remaining to fill the gap
      new_stored [1] = new_value
      if pos > 1 then
        table.move (stored, 1, pos-1, 2, new_stored)
      end
      if pos < #stored then
        table.move (stored, pos+1, #stored, pos+1, new_stored)
      end
    end

    updateStored (def_node_type, new_stored)
  end
}

-- set initial values
metadata_added_hook = SimpleEventHook {
  name = "default-nodes/metadata-added",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "metadata-added" },
      Constraint { "metadata.name", "=", "default" },
    },
  },
  execute = function (event)
    local types = { "audio.sink", "audio.source", "video.source" }
    local source = event:get_source ()
    local om = source:call ("get-object-manager", "metadata")
    local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }

    for _, t in ipairs (types) do
      local v = state_table ["default.configured." .. t]
      if v then
        metadata:set (0, "default.configured." .. t, "Spa:String:JSON",
                      Json.Object { ["name"] = v }:to_string ())
      end
    end
  end
}

-- Collect all the previously configured node names from the state file
function collectStored (def_node_type)
  local stored = {}
  local key_base = "default.configured." .. def_node_type
  local key = key_base

  local index = 0
  repeat
    local v = state_table [key]
    table.insert (stored, v)
    key = key_base .. "." .. tostring (index)
    index = index + 1
  until v == nil

  return stored
end

-- Store the given node names in the state file
function updateStored (def_node_type, stored)
  local key_base = "default.configured." .. def_node_type
  local key = key_base

  local index = 0
  for _, v in ipairs (stored) do
    state_table [key] = v
    key = key_base .. "." .. tostring (index)
    index = index + 1
  end

  -- erase the rest, if any
  repeat
    local v = state_table [key]
    state_table [key] = nil
    key = key_base .. "." .. tostring (index)
    index = index + 1
  until v == nil

  state:save_after_timeout (state_table)
end

function toggleState (enable)
  if enable and not state then
    state = State ("default-nodes")
    state_table = state:load ()
    find_stored_default_node_hook:register ()
    store_configured_default_nodes_hook:register ()
    metadata_added_hook:register ()
  elseif not enable and state then
    state = nil
    state_table = nil
    find_stored_default_node_hook:remove ()
    store_configured_default_nodes_hook:remove ()
    metadata_added_hook:remove ()
  end
end

Settings.subscribe ("node.restore-default-targets", function ()
  toggleState (Settings.get_boolean ("node.restore-default-targets"))
end)
toggleState (Settings.get_boolean ("node.restore-default-targets"))
0707010000015A000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002500000000wireplumber-0.5.0/src/scripts/device  0707010000015B000081A400000000000000000000000165F8630400000F34000000000000000000000000000000000000003000000000wireplumber-0.5.0/src/scripts/device/README.rst   Device Profile/Route Management Scripts
=======================================

These scripts are tasked to select appropriate profiles and routes for each
device.

Hooks
-----

.. list-table:: Hooks triggered by changes in the graph
   :header-rows: 1
   :width: 100%
   :widths: 20 10 20 50

   * - Hook name
     - File
     - Triggered by
     - Action

   * - device/select-profile
     - select-profile.lua
     - device added or EnumProfiles changed
     - schedules a 'select-profile' event

   * - device/select-route
     - select-routes.lua
     - device added or EnumRoute changed
     - updates the device info cache with the latest routes and schedules a 'select-routes' event, if needed

   * - device/store-user-selected-profile
     - select-profile.lua
     - device Profile param changed
     - stores profile into the state file if it was selected by the user (profile.save == true)

   * - device/store-or-restore-routes
     - select-routes.lua
     - device Route param changed
     - stores or restores Route selections based on the current state; may push a 'select-routes' event to update properties

.. list-table:: Hooks for the select-profile event, in order of execution
   :header-rows: 1
   :width: 100%
   :widths: 20 20 60

   * - Hook name
     - File
     - Description

   * - device/find-stored-profile
     - state-profile.lua
     - selects the profile that has been stored in the state file (user's explicit selection)

   * - device/find-best-profile
     - find-best-profile.lua
     - finds the best profile for a device based on profile priorities and availability

   * - device/apply-profile
     - apply-profile.lua
     - applies the selected profile to the device

.. list-table:: Hooks for the select-routes event, in order of execution
   :header-rows: 1
   :width: 100%
   :widths: 20 20 60

   * - Hook name
     - File
     - Description

   * - device/find-stored-routes
     - state-routes.lua
     - restores routes selection for a newly selected profile

   * - device/find-best-routes
     - find-best-routes.lua
     - finds the best routes based on availability and priority

   * - device/apply-route-props
     - state-routes.lua
     - augments the selected routes to include properties stored in the state file (volume, channel map, codecs, ...)"

   * - device/apply-routes
     - apply-routes.lua
     - applies the selected routes to the device

select-profile event
--------------------

High priority event to select a profile for a given device. The event hooks
must also apply the profile.

The event "subject" is the device (`WpDevice`) object.

This event has no special properties.

.. list-table:: Exchanged event data
   :header-rows: 1

   * - Name
     - Description

   * - selected-profile
     - The selected profile to be set:
        - Type: string, containing a JSON object
        - The JSON object should contain the properties of the Profile param

select-routes event
-------------------

High priority event to select routes for a given profile. The event hooks
must also apply the routes.

The event "subject" is the device (`WpDevice`) object.

.. list-table:: Event Properties
   :header-rows: 1

   * - Property name
     - Description

   * - profile.changed
     - true if a new profile has been selected / false if only the available routes changed

   * - profile.name
     - the active profile's name

   * - profile.active-device-ids
     - json array of integers containing the active device IDs for which to select routes


.. list-table:: Exchanged event data
   :header-rows: 1

   * - Name
     - Description

   * - selected-routes
     - The selected routes to be set:
        - Type: map<string, string>
        - The keys are device IDs (as represented in EnumRoute)
        - The values are JSON objects like this: { index: <a route index>, props: { <object with route props> } }
0707010000015C000081A400000000000000000000000165F86304000006E7000000000000000000000000000000000000003700000000wireplumber-0.5.0/src/scripts/device/apply-profile.lua    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT

-- apply the selected profile to the device

cutils = require ("common-utils")
log = Log.open_topic ("s-device")

AsyncEventHook {
  name = "device/apply-profile",
  after = { "device/find-stored-profile", "device/find-preferred-profile", "device/find-best-profile" },
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-profile" },
    },
  },
  steps = {
    start = {
      next = "none",
      execute = function (event, transition)
        local device = event:get_subject ()
        local profile = event:get_data ("selected-profile")
        local dev_name = device.properties ["device.name"]

        if not profile then
          log:info (device, "No profile found to set on " .. dev_name)
          transition:advance ()
          return
        end

        for p in device:iterate_params ("Profile") do
          local active_profile = cutils.parseParam (p, "Profile")
          if active_profile.index == profile.index then
            log:info (device, "Profile " .. profile.name .. " is already set on " .. dev_name)
            transition:advance ()
            return
          end
        end

        local param = Pod.Object {
          "Spa:Pod:Object:Param:Profile", "Profile",
          index = profile.index,
        }
        log:info (device, "Setting profile " .. profile.name .. " on " .. dev_name)
        device:set_param ("Profile", param)

        -- FIXME: add cancellability
        -- sync on the pipewire connection to ensure that the param
        -- has been configured on the remote device object
        Core.sync (function ()
          transition:advance ()
        end)
      end
    },
  }
}:register()
 0707010000015D000081A400000000000000000000000165F8630400000DA9000000000000000000000000000000000000003600000000wireplumber-0.5.0/src/scripts/device/apply-routes.lua -- WirePlumber
--
-- Copyright © 2021-2022 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on default-routes.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT
--
-- Set the Route param as part of the "select-routes" event run

devinfo = require ("device-info-cache")
log = Log.open_topic ("s-device")

AsyncEventHook {
  name = "device/apply-routes",
  after = { "device/find-stored-routes",
            "device/find-best-routes",
            "device/apply-route-props" },
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-routes" },
    },
  },
  steps = {
    start = {
      next = "none",
      execute = function (event, transition)
        local device = event:get_subject ()
        local selected_routes = event:get_data ("selected-routes")

        local dev_info = devinfo:get_device_info (device)
        assert (dev_info)

        if not selected_routes then
          log:info (device, "No routes selected to set on " .. dev_info.name)
          transition:advance ()
          return
        end

        for device_id, route in pairs (selected_routes) do
           -- JSON to lua table
          route = Json.Raw (route):parse ()

           -- steal the props
          local props = route.props or {}

          -- replace with the full route info
          local route_info = devinfo.find_route_info (dev_info, route)
          if not route_info then
            goto skip_route
          end

          -- ensure default values
          local is_input = (route_info.direction == "Input")
          props.mute = props.mute or false
          props.channelVolumes = props.channelVolumes or
              { is_input and Settings.get_float ("device.routes.default-source-volume")
                          or Settings.get_float ("device.routes.default-sink-volume") }

          -- prefix the props with correct IDs to create a Pod.Object
          table.insert (props, 1, "Spa:Pod:Object:Param:Props")
          table.insert (props, 2, "Route")

          -- convert arrays to Spa Pod
          if props.channelVolumes then
            table.insert (props.channelVolumes, 1, "Spa:Float")
            props.channelVolumes = Pod.Array (props.channelVolumes)
          end
          if props.channelMap then
            table.insert (props.channelMap, 1, "Spa:Enum:AudioChannel")
            props.channelMap = Pod.Array (props.channelMap)
          end
          if props.iec958Codecs then
            table.insert (props.iec958Codecs, 1, "Spa:Enum:AudioIEC958Codec")
            props.iec958Codecs = Pod.Array (props.iec958Codecs)
          end

          -- construct Route param
          local param = Pod.Object {
            "Spa:Pod:Object:Param:Route", "Route",
            index = route_info.index,
            device = device_id,
            props = Pod.Object (props),
            save = route_info.save,
          }

          log:debug (param,
            string.format ("setting route(%s) on for device(%s)(%s)",
              route_info.name, dev_info.name, tostring (device)))

          device:set_param ("Route", param)

          ::skip_route::
        end

        -- FIXME: add cancellability
        -- sync on the pipewire connection to ensure that the params
        -- have been configured on the remote device object
        Core.sync (function ()
          transition:advance ()
        end)
      end
    },
  }
}:register()
   0707010000015E000081A400000000000000000000000165F86304000030B9000000000000000000000000000000000000004600000000wireplumber-0.5.0/src/scripts/device/autoswitch-bluetooth-profile.lua -- WirePlumber
--
-- Copyright © 2021 Asymptotic Inc.
--    @author Sanchayan Maity <sanchayan@asymptotic.io>
--
-- Based on bt-profile-switch.lua in tests/examples
-- Copyright © 2021 George Kiagiadakis
--
-- Based on bluez-autoswitch in media-session
-- Copyright © 2021 Pauli Virtanen
--
-- SPDX-License-Identifier: MIT
--
-- This script is charged to automatically change BT profiles on a device. If a
-- client is linked to the device's loopback source node, the associated BT
-- device profile is automatically switched to HSP/HFP. If there is no clients
-- linked to the device's loopback source node, the BT device profile is
-- switched back to A2DP profile.
--
-- We switch to the highest priority profile that has an Input route available.
-- The reason for this is that we may have microphone enabled with non-HFP
-- codecs eg. Faststream.
-- When a stream goes away if the list with which we track the streams above
-- is empty, then we revert back to the old profile.

-- settings file: bluetooth.conf

lutils = require ("linking-utils")
cutils = require ("common-utils")

state = nil
headset_profiles = nil
device_loopback_sources = {}

local profile_restore_timeout_msec = 2000

local INVALID = -1
local timeout_source = {}
local restore_timeout_source = {}

local last_profiles = {}

local active_streams = {}
local previous_streams = {}

function handlePersistentSetting (enable)
  if enable and state == nil then
    -- the state storage
    state = Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile")
        and State ("bluetooth-autoswitch") or nil
    headset_profiles = state and state:load () or {}
  else
    state = nil
    headset_profiles = nil
  end
end

handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
  handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
end)

devices_om = ObjectManager {
  Interest {
    type = "device",
    Constraint { "device.api", "=", "bluez5" },
  }
}

streams_om = ObjectManager {
  Interest {
    type = "node",
    Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
    Constraint { "stream.monitor", "!", "true", type = "pw" },
    Constraint { "bluez5.loopback", "!", "true", type = "pw" }
  }
}

loopback_nodes_om = ObjectManager {
  Interest {
    type = "node",
    Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
    Constraint { "bluez5.loopback", "=", "true", type = "pw" },
  }
}

local function saveHeadsetProfile (device, profile_name)
  local key = "saved-headset-profile:" .. device.properties ["device.name"]
  headset_profiles [key] = profile_name
  state:save_after_timeout (headset_profiles)
end

local function getSavedHeadsetProfile (device)
  local key = "saved-headset-profile:" .. device.properties ["device.name"]
  return headset_profiles [key]
end

local function saveLastProfile (device, profile_name)
  last_profiles [device.properties ["device.name"]] = profile_name
end

local function getSavedLastProfile (device)
  return last_profiles [device.properties ["device.name"]]
end

local function isSwitchedToHeadsetProfile (device)
  return getSavedLastProfile (device) ~= nil
end

local function findProfile (device, index, name)
  for p in device:iterate_params ("EnumProfile") do
    local profile = cutils.parseParam (p, "EnumProfile")
    if not profile then
      goto skip_enum_profile
    end

    Log.debug ("Profile name: " .. profile.name .. ", priority: "
              .. tostring (profile.priority) .. ", index: " .. tostring (profile.index))
    if (index ~= nil and profile.index == index) or
       (name ~= nil and profile.name == name) then
      return profile.priority, profile.index, profile.name
    end

    ::skip_enum_profile::
  end

  return INVALID, INVALID, nil
end

local function getCurrentProfile (device)
  for p in device:iterate_params ("Profile") do
    local profile = cutils.parseParam (p, "Profile")
    if profile then
      return profile.name
    end
  end

  return nil
end

local function highestPrioProfileWithInputRoute (device)
  local profile_priority = INVALID
  local profile_index = INVALID
  local profile_name = nil

  for p in device:iterate_params ("EnumRoute") do
    local route = cutils.parseParam (p, "EnumRoute")
    -- Parse pod
    if not route then
      goto skip_enum_route
    end

    if route.direction ~= "Input" then
      goto skip_enum_route
    end

    Log.debug ("Route with index: " .. tostring (route.index) .. ", direction: "
          .. route.direction .. ", name: " .. route.name .. ", description: "
          .. route.description .. ", priority: " .. route.priority)
    if route.profiles then
      for _, v in pairs (route.profiles) do
        local priority, index, name = findProfile (device, v)
        if priority ~= INVALID then
          if profile_priority < priority then
            profile_priority = priority
            profile_index = index
            profile_name = name
          end
        end
      end
    end

    ::skip_enum_route::
  end

  return profile_priority, profile_index, profile_name
end

local function hasProfileInputRoute (device, profile_index)
  for p in device:iterate_params ("EnumRoute") do
    local route = cutils.parseParam (p, "EnumRoute")
    if route and route.direction == "Input" and route.profiles then
      for _, v in pairs (route.profiles) do
        if v == profile_index then
          return true
        end
      end
    end
  end
  return false
end

local function switchDeviceToHeadsetProfile (dev_id)
  local index
  local name

  -- Find the actual device
  local device = devices_om:lookup {
      Constraint { "bound-id", "=", dev_id, type = "gobject" }
  }
  if device == nil then
    Log.info ("Device with id " .. tostring(dev_id).. " not found")
    return
  end

  if isSwitchedToHeadsetProfile (device) then
    Log.info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
    return
  end

  local cur_profile_name = getCurrentProfile (device)
  _, index, name = findProfile (device, nil, cur_profile_name)
  if hasProfileInputRoute (device, index) then
    Log.info ("Current profile has input route, not switching")
    return
  end

  -- clear restore callback, if any
  if restore_timeout_source[dev_id] ~= nil then
    restore_timeout_source[dev_id]:destroy ()
    restore_timeout_source[dev_id] = nil
  end

  local saved_headset_profile = getSavedHeadsetProfile (device)
  index = INVALID
  if saved_headset_profile then
    _, index, name = findProfile (device, nil, saved_headset_profile)
  end
  if index == INVALID then
    _, index, name = highestPrioProfileWithInputRoute (device)
  end

  if index ~= INVALID then
    local pod = Pod.Object {
      "Spa:Pod:Object:Param:Profile", "Profile",
      index = index
    }

    -- store the current profile (needed when restoring)
    saveLastProfile (device, cur_profile_name)

    -- switch to headset profile
    Log.info ("Setting profile of '"
          .. device.properties ["device.description"]
          .. "' from: " .. cur_profile_name
          .. " to: " .. name)
    device:set_params ("Profile", pod)
  else
    Log.warning ("Got invalid index when switching profile")
  end
end

local function restoreProfile (dev_id)
  -- Find the actual device
  local device = devices_om:lookup {
      Constraint { "bound-id", "=", dev_id, type = "gobject" }
  }
  if device == nil then
    Log.info ("Device with id " .. tostring(dev_id).. " not found")
    return
  end

  if not isSwitchedToHeadsetProfile (device) then
    Log.info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP")
    return
  end

  local profile_name = getSavedLastProfile (device)
  local cur_profile_name = getCurrentProfile (device)

  if cur_profile_name then
    Log.info ("Setting saved headset profile to: " .. cur_profile_name)
    saveHeadsetProfile (device, cur_profile_name)
  end

  if profile_name then
    local _, index, name = findProfile (device, nil, profile_name)

    if index ~= INVALID then
      local pod = Pod.Object {
        "Spa:Pod:Object:Param:Profile", "Profile",
        index = index
      }

      -- clear last profile as we will restore it now
      saveLastProfile (device, nil)

      -- restore previous profile
      Log.info ("Restoring profile of '"
            .. device.properties ["device.description"]
            .. "' from: " .. cur_profile_name
            .. " to: " .. name)
      device:set_params ("Profile", pod)
    else
      Log.warning ("Failed to restore profile")
    end
  end
end

local function triggerRestoreProfile (dev_id)
  -- we never restore the device profiles if there are active streams
  for _, v in pairs (active_streams) do
    if v == dev_id then
      return
    end
  end

  restore_timeout_source[dev_id] = nil
  restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function ()
    restore_timeout_source[dev_id] = nil
    restoreProfile (dev_id)
  end)
end

-- We consider a Stream of interest if it is linked to a bluetooth loopback
-- source filter
local function checkStreamStatus (stream)
  -- check if the stream is linked to a bluetooth loopback source
  local stream_id = tonumber(stream["bound-id"])
  local peer_id = lutils.getNodePeerId (stream_id)
  if peer_id ~= nil then
    local bt_node = loopback_nodes_om:lookup {
        Constraint { "bound-id", "=", peer_id, type = "gobject" }
    }
    if bt_node ~= nil then
      local dev_id = bt_node.properties["device.id"]
      if dev_id ~= nil then
        -- If a stream we previously saw stops running, we consider it
        -- inactive, because some applications (Teams) just cork input
        -- streams, but don't close them.
        if previous_streams [stream.id] == dev_id and
            stream.state ~= "running" then
          return nil
        end

        return dev_id
      end
    end
  end

  return nil
end

local function handleStream (stream)
  if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
    return
  end

  local dev_id = checkStreamStatus (stream)
  if dev_id ~= nil then
    active_streams [stream.id] = dev_id
    previous_streams [stream.id] = dev_id
    switchDeviceToHeadsetProfile (dev_id)
  else
    dev_id = active_streams [stream.id]
    active_streams [stream.id] = nil
    if dev_id ~= nil then
      triggerRestoreProfile (dev_id)
    end
  end
end

local function handleAllStreams ()
  for stream in streams_om:iterate() do
    handleStream (stream)
  end
end

SimpleEventHook {
  name = "node-removed@autoswitch-bluetooth-profile",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "node-removed" },
      Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
      Constraint { "bluez5.loopback", "!", "true", type = "pw" },
    },
  },
  execute = function (event)
    local stream = event:get_subject ()
    local dev_id = active_streams[stream.id]
    active_streams[stream.id] = nil
    previous_streams[stream.id] = nil
    if dev_id ~= nil then
      triggerRestoreProfile (dev_id)
    end
  end
}:register ()

SimpleEventHook {
  name = "link-added@autoswitch-bluetooth-profile",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "link-added" },
    },
  },
  execute = function (event)
    local link = event:get_subject ()
    local p = link.properties
    for stream in streams_om:iterate () do
      local in_id = tonumber(p["link.input.node"])
      local out_id = tonumber(p["link.output.node"])
      local stream_id = tonumber(stream["bound-id"])
      local bt_node = loopback_nodes_om:lookup {
          Constraint { "bound-id", "=", out_id, type = "gobject" }
      }
      if in_id == stream_id and bt_node ~= nil then
        handleStream (stream)
      end
    end
  end
}:register ()

SimpleEventHook {
  name = "bluez-device-added@autoswitch-bluetooth-profile",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "device-added" },
      Constraint { "device.api", "=", "bluez5" },
    },
  },
  execute = function (event)
    local device = event:get_subject ()
    -- Devices are unswitched initially
    saveLastProfile (device, nil)
    handleAllStreams ()
  end
}:register ()

devices_om:activate ()
streams_om:activate ()
loopback_nodes_om:activate()

   0707010000015F000081A400000000000000000000000165F86304000008D6000000000000000000000000000000000000003B00000000wireplumber-0.5.0/src/scripts/device/find-best-profile.lua    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Find the best profile for a device based on profile priorities and
-- availability

cutils = require ("common-utils")
log = Log.open_topic ("s-device")

SimpleEventHook {
  name = "device/find-best-profile",
  after = "device/find-preferred-profile",
  before = "device/apply-profile",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-profile" },
    },
  },
  execute = function (event)
    local selected_profile = event:get_data ("selected-profile")

    -- skip hook if profile is already selected
    if selected_profile then
      return
    end

    local device = event:get_subject ()
    local dev_name = device.properties["device.name"]
    local off_profile = nil
    local best_profile = nil
    local unk_profile = nil
    -- Takes absolute priority if available or unknown
    local profile_prop = device.properties["device.profile"]


    for p in device:iterate_params ("EnumProfile") do
      profile = cutils.parseParam (p, "EnumProfile")
      if profile and profile.name == profile_prop and profile.available ~= "no" then
        selected_profile = profile
        goto profile_set
      elseif profile and profile.name ~= "pro-audio" then
        if profile.name == "off" then
          off_profile = profile
        elseif profile.available == "yes" then
          if best_profile == nil or profile.priority > best_profile.priority then
            best_profile = profile
          end
        elseif profile.available ~= "no" then
          if unk_profile == nil or profile.priority > unk_profile.priority then
            unk_profile = profile
          end
        end
      end
    end

    if best_profile ~= nil then
      selected_profile = best_profile
    elseif unk_profile ~= nil then
      selected_profile = unk_profile
    elseif off_profile ~= nil then
      selected_profile = off_profile
    end

::profile_set::
    if selected_profile then
      log:info (device, string.format (
          "Found best profile '%s' (%d) for device '%s'",
          selected_profile.name, selected_profile.index, dev_name))
      event:set_data ("selected-profile", selected_profile)
    end
  end
}:register()
  07070100000160000081A400000000000000000000000165F86304000009F6000000000000000000000000000000000000003A00000000wireplumber-0.5.0/src/scripts/device/find-best-routes.lua -- WirePlumber
--
-- Copyright © 2021-2022 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on default-routes.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT
--
-- find the best route for a given device_id, based on availability and priority

cutils = require ("common-utils")
devinfo = require ("device-info-cache")
log = Log.open_topic ("s-device")

SimpleEventHook {
  name = "device/find-best-routes",
  after = "device/find-stored-routes",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-routes" },
      Constraint { "profile.active-device-ids", "is-present" },
    },
  },
  execute = function (event)
    local device = event:get_subject ()
    local event_properties = event:get_properties ()
    local active_ids = event_properties ["profile.active-device-ids"]
    local selected_routes = event:get_data ("selected-routes") or {}

    local dev_info = devinfo:get_device_info (device)
    assert (dev_info)

    -- active IDs are exchanged in JSON format
    active_ids = Json.Raw (active_ids):parse ()

    for _, device_id in ipairs (active_ids) do
      -- if a previous hook already selected a route for this device_id, skip it
      if selected_routes [tostring (device_id)] then
        goto next_device_id
      end

      local best_avail = nil
      local best_unk = nil
      for _, ri in pairs (dev_info.route_infos) do
        if cutils.arrayContains (ri.devices, device_id) and
              (ri.profiles == nil or cutils.arrayContains (ri.profiles, dev_info.active_profile)) then
          if ri.available == "yes" or ri.available == "unknown" then
            if ri.direction == "Output" and ri.available ~= ri.prev_available then
              best_avail = ri
              ri.save = true
              break
            elseif ri.available == "yes" then
              if (best_avail == nil or ri.priority > best_avail.priority) then
                best_avail = ri
              end
            elseif best_unk == nil or ri.priority > best_unk.priority then
              best_unk = ri
            end
          end
        end
      end

      local route = best_avail or best_unk
      if route then
        selected_routes [tostring (device_id)] =
            Json.Object { index = route.index }:to_string ()
      end

      ::next_device_id::
    end

    -- save the selected routes for the apply-routes hook
    event:set_data ("selected-routes", selected_routes)
  end
}:register ()
  07070100000161000081A400000000000000000000000165F863040000081D000000000000000000000000000000000000004000000000wireplumber-0.5.0/src/scripts/device/find-preferred-profile.lua   -- WirePlumber
--
-- Copyright © 2024 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Finds the user preferred profile for a device, based on the priorities
-- defined in the "device.profile.priority.rules" section of the configuration.

cutils = require ("common-utils")
log = Log.open_topic ("s-device")

config = {}
config.rules = Conf.get_section_as_json ("device.profile.priority.rules", Json.Array {})

SimpleEventHook {
  name = "device/find-preferred-profile",
  after = "device/find-stored-profile",
  before = "device/find-best-profile",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-profile" },
    },
  },
  execute = function (event)
    local selected_profile = event:get_data ("selected-profile")

    -- skip hook if the profile is already selected for this device.
    if selected_profile then
      return
    end

    local device = event:get_subject ()
    local props = JsonUtils.match_rules_update_properties (
        config.rules, device.properties)
    local p_array = props["priorities"]

    -- skip hook if the profile priorities are NOT defined for this device.
    if not p_array then
      return nil
    end

    local p_json = Json.Raw(p_array)
    local priorities = p_json:parse()

    for _, priority_profile in ipairs(priorities) do
      for p in device:iterate_params("EnumProfile") do
        local device_profile = cutils.parseParam(p, "EnumProfile")
        if device_profile and device_profile.name == priority_profile then
          selected_profile = device_profile
          goto profile_set
        end
      end
    end

::profile_set::
    if selected_profile then
      log:info (device, string.format (
        "Found preferred profile '%s' (%d) for device '%s'",
        selected_profile.name, selected_profile.index, device_name))
      event:set_data ("selected-profile", selected_profile)
    else
      log:info (device, "Profiles listed in 'device.profile.priority.rules'"
        .. " do not match the available ones of device: " .. device_name)
    end

  end
}:register()
   07070100000162000081A400000000000000000000000165F86304000002D0000000000000000000000000000000000000003800000000wireplumber-0.5.0/src/scripts/device/select-profile.lua   -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT

-- look for new devices and raise select-profile event.

cutils = require ("common-utils")
log = Log.open_topic ("s-device")

SimpleEventHook {
  name = "device/select-profile",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "device-added" },
    },
    EventInterest {
      Constraint { "event.type", "=", "device-params-changed" },
      Constraint { "event.subject.param-id", "=", "EnumProfile" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    local device = event:get_subject ()
    source:call ("push-event", "select-profile", device, nil)
  end
}:register()
07070100000163000081A400000000000000000000000165F863040000139D000000000000000000000000000000000000003700000000wireplumber-0.5.0/src/scripts/device/select-routes.lua    -- WirePlumber
--
-- Copyright © 2021-2022 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on default-routes.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT
--
-- Update the device info cache with the latest information from EnumRoute(all
-- the device routes) and trigger a "select-routes" event to select new routes
-- for the given device configuration, if it has changed

cutils = require ("common-utils")
devinfo = require ("device-info-cache")
log = Log.open_topic ("s-device")

SimpleEventHook {
  name = "device/select-route",
  after = "device/select-profile",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "device-added" },
    },
    EventInterest {
      Constraint { "event.type", "=", "device-params-changed" },
      Constraint { "event.subject.param-id", "c", "EnumRoute" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    local device = event:get_subject ()

    local dev_info = devinfo:get_device_info (device)
    if not dev_info then
      return
    end

    local new_route_infos = {}
    local avail_routes_changed = false
    local profile = nil

    -- get current profile
    for p in device:iterate_params ("Profile") do
      profile = cutils.parseParam (p, "Profile")
    end

    -- look at all the routes and update/reset cached information
    for p in device:iterate_params ("EnumRoute") do
      -- parse pod
      local route = cutils.parseParam (p, "EnumRoute")
      if not route then
        goto skip_enum_route
      end

      -- find cached route information
      local route_info = devinfo.find_route_info (dev_info, route, true)

      -- update properties
      route_info.prev_available = route_info.available
      if route_info.available ~= route.available then
        log:info (device, "route " .. route.name .. " available changed " ..
                         route_info.available .. " -> " .. route.available)
        route_info.available = route.available
        if profile and cutils.arrayContains (route.profiles, profile.index) then
          avail_routes_changed = true
        end
      end

      -- store
      new_route_infos [route.index] = route_info

      ::skip_enum_route::
    end

    -- replace old route_infos to lose old routes
    -- that no longer exist on the device
    dev_info.route_infos = new_route_infos
    new_route_infos = nil

    -- restore routes for profile
    if profile then
      local profile_changed = (dev_info.active_profile ~= profile.index)
      dev_info.active_profile = profile.index

      -- if the profile changed, restore routes for that profile
      -- if any of the routes of the current profile changed in availability,
      -- then try to select a new "best" route for each device and ignore
      -- what was stored
      if profile_changed or avail_routes_changed then
        log:info (device,
            string.format ("restore routes for profile(%s) of device(%s)",
            profile.name, dev_info.name))

        -- find the active device IDs for which to select routes
        local active_ids = findActiveDeviceIDs (profile)
        active_ids = Json.Array (active_ids):to_string ()

        -- push select-routes event and let the hooks select the appropriate routes
        local props = {
          ["profile.changed"] = profile_changed,
          ["profile.name"] = profile.name,
          ["profile.active-device-ids"] = active_ids,
        }
        source:call ("push-event", "select-routes", device, props)
      end
    end
  end
}:register()

-- These device ids are like routes(speaker, mic, headset etc) or sub-devices or
-- paths with in the pipewire devices/soundcards.
function findActiveDeviceIDs (profile)
  -- parses the classes from the profile and returns the device IDs
  ----- sample structure, should return { 0, 8 } -----
  -- classes:
  --  1: 2
  --  2:
  --    1: Audio/Source
  --    2: 1
  --    3: card.profile.devices
  --    4:
  --      1: 0
  --      pod_type: Array
  --      value_type: Spa:Int
  --    pod_type: Struct
  --  3:
  --    1: Audio/Sink
  --    2: 1
  --    3: card.profile.devices
  --    4:
  --      1: 8
  --      pod_type: Array
  --      value_type: Spa:Int
  --    pod_type: Struct
  --  pod_type: Struct
  local active_ids = {}
  if type (profile.classes) == "table" and profile.classes.pod_type == "Struct" then
    for _, p in ipairs (profile.classes) do
      if type (p) == "table" and p.pod_type == "Struct" then
        local i = 1
        while true do
          local k, v = p [i], p [i+1]
          i = i + 2
          if not k or not v then
            break
          end
          if k == "card.profile.devices" and
              type (v) == "table" and v.pod_type == "Array" then
            for _, dev_id in ipairs (v) do
              table.insert (active_ids, dev_id)
            end
          end
        end
      end
    end
  end
  return active_ids
end
   07070100000164000081A400000000000000000000000165F8630400001053000000000000000000000000000000000000003700000000wireplumber-0.5.0/src/scripts/device/state-profile.lua    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- This file contains all the logic related to saving device profiles
-- to a state file and restoring them later on.

-- A devices profile needs to be selected for any new device. the script selects
-- the device profile from the user preferences, as well as store the user
-- selected device profile to state file

cutils = require ("common-utils")
log = Log.open_topic ("s-device")

-- the state storage
state = nil
state_table = nil

find_stored_profile_hook = SimpleEventHook {
  name = "device/find-stored-profile",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-profile" },
    },
  },
  execute = function (event)
    local selected_profile = event:get_data ("selected-profile")

    -- skip hook if profile is already selected
    if selected_profile then
      return
    end

    local device = event:get_subject ()
    local dev_name = device.properties["device.name"]
    if not dev_name then
      log:critical (device, "invalid device.name")
      return
    end

    local profile_name = state_table[dev_name]

    if profile_name then
      for p in device:iterate_params ("EnumProfile") do
        local profile = cutils.parseParam (p, "EnumProfile")
        if profile.name == profile_name then
          selected_profile = profile
          break
        end
      end
    end

    if selected_profile then
      log:info (device, string.format (
          "Found stored profile '%s' (%d) for device '%s'",
          selected_profile.name, selected_profile.index, dev_name))
      event:set_data ("selected-profile", selected_profile)
    end
  end
}

store_user_selected_profile_hook = SimpleEventHook {
  name = "device/store-user-selected-profile",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "device-params-changed" },
      Constraint { "event.subject.param-id", "=", "Profile" },
    },
  },
  execute = function (event)
    local device = event:get_subject ()

    for p in device:iterate_params ("Profile") do
      local profile = cutils.parseParam (p, "Profile")
      if profile.save then
        -- store only if this was a user-generated action (save == true)
        updateStoredProfile (device, profile)
      end
    end
  end
}

function updateStoredProfile (device, profile)
  local dev_name = device.properties["device.name"]
  local index = nil

  if not dev_name then
    log:critical (device, "invalid device.name")
    return
  end

  log:debug (device, string.format (
      "update stored profile to '%s' (%d) for device '%s'",
      profile.name, profile.index, dev_name))

  -- check if the new profile is the same as the current one
  if state_table[dev_name] == profile.name then
    log:debug (device, " ... profile is already stored")
    return
  end

  -- find the full profile from EnumProfile, making also sure that the
  -- user / client application has actually set an existing profile
  for p in device:iterate_params ("EnumProfile") do
    local enum_profile = cutils.parseParam (p, "EnumProfile")
    if enum_profile.name == profile.name then
      index = enum_profile.index
    end
  end

  if not index then
    log:info (device, string.format (
        "profile '%s' (%d) is not valid on device '%s'",
        profile.name, profile.index, dev_name))
    return
  end

  state_table[dev_name] = profile.name
  state:save_after_timeout (state_table)

  log:info (device, string.format (
      "stored profile '%s' (%d) for device '%s'",
      profile.name, index, dev_name))
end

function toggleState (enable)
  if enable and not state then
    state = State ("default-profile")
    state_table = state:load ()
    find_stored_profile_hook:register ()
    store_user_selected_profile_hook:register ()
  elseif not enable and state then
    state = nil
    state_table = nil
    find_stored_profile_hook:remove ()
    store_user_selected_profile_hook:remove ()
  end
end

Settings.subscribe ("device.restore-profile", function ()
  toggleState (Settings.get_boolean ("device.restore-profile"))
end)
toggleState (Settings.get_boolean ("device.restore-profile"))
 07070100000165000081A400000000000000000000000165F86304000029FF000000000000000000000000000000000000003600000000wireplumber-0.5.0/src/scripts/device/state-routes.lua -- WirePlumber
--
-- Copyright © 2021-2022 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on default-routes.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT
--
-- This file contains all the logic related to saving device routes and their
-- properties to a state file and restoring both the routes selection and
-- the properties of routes later on.
--

cutils = require ("common-utils")
devinfo = require ("device-info-cache")
log = Log.open_topic ("s-device")

-- the state storage
state = nil
state_table = nil

-- hook to restore routes selection for a newly selected profile
find_stored_routes_hook = SimpleEventHook {
  name = "device/find-stored-routes",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-routes" },
      Constraint { "profile.changed", "=", "true" },
      Constraint { "profile.active-device-ids", "is-present" },
    },
  },
  execute = function (event)
    local device = event:get_subject ()
    local event_properties = event:get_properties ()
    local profile_name = event_properties ["profile.name"]
    local active_ids = event_properties ["profile.active-device-ids"]
    local selected_routes = event:get_data ("selected-routes") or {}

    local dev_info = devinfo:get_device_info (device)
    assert (dev_info)

    -- get the stored routes for this profile
    -- skip the hook if there are no stored routes, there is no point
    local spr = getStoredProfileRoutes (dev_info, profile_name)
    if #spr == 0 then
      return
    end

    -- active IDs are exchanged in JSON format
    active_ids = Json.Raw (active_ids):parse ()

    for _, device_id in ipairs (active_ids) do
      -- if a previous hook already selected a route for this device_id, skip it
      if selected_routes [tostring (device_id)] then
        goto next_device_id
      end

      log:info (device, "restoring route for device ID " .. tostring (device_id));

      local route_info = nil

      -- find a route that was previously stored for a device_id
      for _, ri in pairs (dev_info.route_infos) do
        if cutils.arrayContains (ri.devices, tonumber (device_id)) and
            (ri.profiles == nil or cutils.arrayContains (ri.profiles, dev_info.active_profile)) and
            cutils.arrayContains (spr, ri.name) then
          route_info = ri
          break
        end
      end

      if route_info then
        -- we found a stored route
        if route_info.available == "no" then
          log:info (device, "stored route '" .. route_info.name .. "' not available")
          -- not available, try to find next best
          route_info = nil
        else
          log:info (device, "found stored route: " .. route_info.name)
          -- make sure we save it again
          route_info.save = true
        end
      end

      if route_info then
        selected_routes [tostring (device_id)] =
            Json.Object { index = route_info.index }:to_string ()
      end

      ::next_device_id::
    end

    -- save the selected routes for the apply-routes hook
    event:set_data ("selected-routes", selected_routes)
  end
}

-- extract the "selected-routes" event data and augment it to include
-- the route properties, as they were stored in the state file;
-- this is the last step before applying the routes
apply_route_props_hook = SimpleEventHook {
  name = "device/apply-route-props",
  after = { "device/find-stored-routes", "device/find-best-routes" },
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-routes" },
    },
  },
  execute = function (event)
    local device = event:get_subject ()
    local selected_routes = event:get_data ("selected-routes") or {}
    local new_selected_routes = {}

    local dev_info = devinfo:get_device_info (device)
    assert (dev_info)

    if next (selected_routes) == nil then
      log:info (device, "No routes selected to set on " .. dev_info.name)
      return
    end

    for device_id, route in pairs (selected_routes) do
       -- JSON to lua table
      route = Json.Raw (route):parse ()

      local route_info = devinfo.find_route_info (dev_info, route, false)
      local props = getStoredRouteProps (dev_info, route_info)

      -- convert arrays to Json
      if props.channelVolumes then
        props.channelVolumes = Json.Array (props.channelVolumes)
      end
      if props.channelMap then
        props.channelMap = Json.Array (props.channelMap)
      end
      if props.iec958Codecs then
        props.iec958Codecs = Json.Array (props.iec958Codecs)
      end

      local json = Json.Object {
        index = route_info.index,
        props = Json.Object (props),
      }
      new_selected_routes [device_id] = json:to_string ()
    end

    -- save the selected routes for the apply-routes hook
    event:set_data ("selected-routes", new_selected_routes)
  end
}

store_or_restore_routes_hook = SimpleEventHook {
  name = "device/store-or-restore-routes",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "device-params-changed" },
      Constraint { "event.subject.param-id", "=", "Route" },
    },
  },
  execute = function (event)
    local device = event:get_subject ()
    local source = event:get_source ()
    local selected_routes = {}
    local push_select_routes = false

    local dev_info = devinfo:get_device_info (device)
    if not dev_info then
      return
    end

    local new_route_infos = {}

    -- look at all the routes and update/reset cached information
    for p in device:iterate_params ("EnumRoute") do
      -- parse pod
      local route = cutils.parseParam (p, "EnumRoute")
      if not route then
        goto skip_enum_route
      end

      -- find cached route information
      local route_info = devinfo.find_route_info (dev_info, route, true)
      if not route_info then
        goto skip_enum_route
      end

      -- update properties
      route_info.prev_active = route_info.active
      route_info.active = false
      route_info.save = false

      -- store
      new_route_infos [route.index] = route_info

      ::skip_enum_route::
    end

    -- update route_infos with new prev_active, active and save changes
    dev_info.route_infos = new_route_infos
    new_route_infos = nil

    -- check for changes in the active routes
    for p in device:iterate_params ("Route") do
      local route = cutils.parseParam (p, "Route")
      if not route then
        goto skip_route
      end

      -- get cached route info and at the same time
      -- ensure that the route is also in EnumRoute
      local route_info = devinfo.find_route_info (dev_info, route, false)
      if not route_info then
        goto skip_route
      end

      -- update route_info state
      route_info.active = true
      route_info.save = route.save

      if not route_info.prev_active then
        -- a new route is now active, restore the volume and
        -- make sure we save this as a preferred route
        log:info (device,
            string.format ("new active route(%s) found of device(%s)",
                route.name, dev_info.name))
        route_info.prev_active = true
        route_info.active = true

        selected_routes [tostring (route.device)] =
            Json.Object { index = route_info.index }:to_string ()
        push_select_routes = true

      elseif route.save and route.props then
        -- just save route properties
        log:info (device,
            string.format ("storing route(%s) props of device(%s)",
              route.name, dev_info.name))

        saveRouteProps (dev_info, route)
      end

      ::skip_route::
    end

    -- save selected routes for the active profile
    for p in device:iterate_params ("Profile") do
      local profile = cutils.parseParam (p, "Profile")
      saveProfileRoutes (dev_info, profile.name)
    end

    -- push a select-routes event to re-apply the routes with new properties
    if push_select_routes then
      local e = source:call ("create-event", "select-routes", device, nil)
      e:set_data ("selected-routes", selected_routes)
      EventDispatcher.push_event (e)
    end
  end
}

function saveRouteProps (dev_info, route)
  local props = route.props.properties
  local key = dev_info.name .. ":" ..
                   route.direction:lower () .. ":" ..
                   route.name

  state_table [key] = Json.Object {
    volume = props.volume,
    mute = props.mute,
    channelVolumes = props.channelVolumes and Json.Array (props.channelVolumes),
    channelMap = props.channelMap and Json.Array (props.channelMap),
    latencyOffsetNsec = props.latencyOffsetNsec,
    iec958Codecs = props.iec958Codecs and Json.Array (props.iec958Codecs),
  }:to_string ()

  state:save_after_timeout (state_table)
end

function getStoredRouteProps (dev_info, route)
  local key = dev_info.name .. ":" ..
                   route.direction:lower () .. ":" ..
                   route.name
  local value = state_table [key]
  if value then
    local json = Json.Raw (value)
    if json and json:is_object () then
      return json:parse ()
    end
  end
  return {}
end

-- stores an array with the route names that are selected
-- for the given device and profile
function saveProfileRoutes (dev_info, profile_name)
  -- select only routes with save == true
  local routes = {}
  for idx, ri in pairs (dev_info.route_infos) do
    if ri.save then
      table.insert (routes, ri.name)
    end
  end

  if #routes > 0 then
    local key = dev_info.name .. ":profile:" .. profile_name
    state_table [key] = Json.Array (routes):to_string()
    state:save_after_timeout (state_table)
  end
end

-- returns an array of the route names that were previously selected
-- for the given device and profile
function getStoredProfileRoutes (dev_info, profile_name)
  local key = dev_info.name .. ":profile:" .. profile_name
  local value = state_table [key]
  if value then
    local json = Json.Raw (value)
    if json and json:is_array () then
      return json:parse ()
    end
  end
  return {}
end

function toggleState (enable)
  if enable and not state then
    state = State ("default-routes")
    state_table = state:load ()
    find_stored_routes_hook:register ()
    apply_route_props_hook:register ()
    store_or_restore_routes_hook:register ()
  elseif not enable and state then
    state = nil
    state_table = nil
    find_stored_routes_hook:remove ()
    apply_route_props_hook:remove ()
    store_or_restore_routes_hook:remove ()
  end
end

Settings.subscribe ("device.restore-routes", function ()
  toggleState (Settings.get_boolean ("device.restore-routes"))
end)
toggleState (Settings.get_boolean ("device.restore-routes"))
 07070100000166000081A400000000000000000000000165F8630400000943000000000000000000000000000000000000003000000000wireplumber-0.5.0/src/scripts/fallback-sink.lua   -- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author Frédéric Danis <frederic.danis@collabora.com>
--
-- SPDX-License-Identifier: MIT

sink_ids = {}
fallback_node = nil

node_om = ObjectManager {
  Interest {
    type = "node",
    Constraint { "media.class", "matches", "Audio/Sink", type = "pw-global" },
    -- Do not consider virtual items created by WirePlumber
    Constraint { "wireplumber.is-virtual", "!", true, type = "pw" },
    -- or the fallback sink itself
    Constraint { "wireplumber.is-fallback", "!", true, type = "pw" },
  }
}

function createFallbackSink()
  if fallback_node then
    return
  end

  Log.info("Create fallback sink")

  local properties = {}

  properties["node.name"] = "auto_null"
  properties["node.description"] = "Dummy Output"

  properties["audio.rate"] = 48000
  properties["audio.channels"] = 2
  properties["audio.position"] = "FL,FR"

  properties["media.class"] = "Audio/Sink"
  properties["factory.name"] = "support.null-audio-sink"
  properties["node.virtual"] = "true"
  properties["monitor.channel-volumes"] = "true"

  properties["wireplumber.is-fallback"] = "true"
  properties["priority.session"] = 500

  fallback_node = LocalNode("adapter", properties)
  fallback_node:activate(Feature.Proxy.BOUND)
end

function checkSinks()
  local sink_ids_items = 0
  for _ in pairs(sink_ids) do sink_ids_items = sink_ids_items + 1 end

  if sink_ids_items > 0 then
    if fallback_node then
      Log.info("Remove fallback sink")
      fallback_node = nil
    end
  elseif not fallback_node then
    createFallbackSink()
  end
end

function checkSinksAfterTimeout()
  if timeout_source then
    timeout_source:destroy()
  end
  timeout_source = Core.timeout_add(1000, function ()
    checkSinks()
    timeout_source = nil
  end)
end

node_om:connect("object-added", function (_, node)
  Log.debug("object added: " .. node.properties["object.id"] .. " " ..
      tostring(node.properties["node.name"]))

  sink_ids[node.properties["object.id"]] = node.properties["node.name"]

  checkSinksAfterTimeout()
end)

node_om:connect("object-removed", function (_, node)
  Log.debug("object removed: " .. node.properties["object.id"] .. " " ..
      tostring(node.properties["node.name"]))

  sink_ids[node.properties["object.id"]] = nil
  checkSinksAfterTimeout()
end)

node_om:activate()

checkSinksAfterTimeout()
 07070100000167000081A400000000000000000000000165F8630400000819000000000000000000000000000000000000003100000000wireplumber-0.5.0/src/scripts/intended-roles.lua  -- WirePlumber
--
-- Copyright © 2021 Asymptotic
--    @author Arun Raghavan <arun@asymptotic.io>
--
-- SPDX-License-Identifier: MIT
--
-- Route streams of a given role (media.role property) to devices that are
-- intended for that role (device.intended-roles property)

metadata_om = ObjectManager {
  Interest {
    type = "metadata",
    Constraint { "metadata.name", "=", "default" },
  }
}

devices_om = ObjectManager {
  Interest {
    type = "node",
    Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
    Constraint { "device.intended-roles", "is-present", type = "pw" },
  }
}

streams_om = ObjectManager {
  Interest {
    type = "node",
    Constraint { "media.class", "matches", "Stream/*/Audio", type = "pw-global" },
    Constraint { "media.role", "is-present", type = "pw-global" }
  }
}

function routeUsingIntendedRole(stream, dev)
  local stream_role = stream.properties["media.role"]
  local is_input = stream.properties["media.class"]:find("Input") ~= nil

  local is_source = dev.properties["media.class"]:find("Source") ~= nil
  local dev_roles = dev.properties["device.intended-roles"]

  -- Make sure the stream and device direction match
  if is_input ~= is_source then
    return
  end

  for role in dev_roles:gmatch("(%a+)") do
    if role == stream_role then
      Log.info(stream,
        string.format("Routing stream '%s' (%d) with role '%s' to '%s' (%d)",
          stream.properties["node.name"], stream["bound-id"], stream_role,
          dev.properties["node.name"], dev["bound-id"])
      )

      local metadata = metadata_om:lookup()
      metadata:set(stream["bound-id"], "target.node", "Spa:Id", dev["bound-id"])
    end
  end
end

streams_om:connect("object-added", function (streams_om, stream)
  for dev in devices_om:iterate() do
    routeUsingIntendedRole(stream, dev)
  end
end)

devices_om:connect("object-added", function (devices_om, dev)
  for stream in streams_om:iterate() do
    routeUsingIntendedRole(stream, dev)
  end
end)

metadata_om:activate()
devices_om:activate()
streams_om:activate()
   07070100000168000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002200000000wireplumber-0.5.0/src/scripts/lib 07070100000169000081A400000000000000000000000165F86304000020ED000000000000000000000000000000000000002F00000000wireplumber-0.5.0/src/scripts/lib/SETTINGS.rst    
.. describe:: device.restore-profile

   When a device profile is changed manually (e.g. via pavucontrol), WirePlumber
   stores the selected profile and restores it when the device appears again
   (e.g. after a reboot). If this setting is disabled, WirePlumber will always
   pick the best profile for the device based on profile priorities and
   availability (or custom rules, if any).

   :Default value: ``true``

.. describe:: device.restore-routes

   When a device route is changed manually (e.g. via pavucontrol), WirePlumber
   stores the selected route and restores it when the same profile is
   selected for this device. If this setting is disabled, WirePlumber will
   always pick the best route for this device profile based on route priorities
   and availability (or custom rules, if any).

   This setting also enables WirePlumber to restore properties of the device
   route when the route is restored. This includes the volume levels of sources
   and sinks, as well as the IEC958 codecs selected (for routes that support
   encoded streams, such as HDMI).

   :Default value: ``true``

.. describe:: device.routes.default-sink-volume

   This option allows to set the default volume for sinks that are part of a
   device route (e.g. ALSA PCM sinks). This is used when the route is restored
   and the sink does not have a previously stored volume.

   :Default value: ``0.4 ^ 3`` (40% on the cubic scale)

.. describe:: device.routes.default-source-volume

   This option allows to set the default volume for sources that are part of a
   device route (e.g. ALSA PCM sources). This is used when the route is restored
   and the source does not have a previously stored volume.

   :Default value: ``1.0`` (100%)

.. describe:: linking.allow-moving-streams

   This option allows moving streams by overriding their target via metadata.
   When enabled, WirePlumber monitors the "default" metadata for changes in the
   ``target.object`` key of streams and if this key is set to a valid node name
   (``node.name``) or serial (``object.serial``), the stream is moved to that
   target node.

   This is used by applications such as pavucontrol and is recommended for
   compatibility with PulseAudio.

    .. note::

       On the metadata, the ``target.node`` key is also supported for
       compatibility with older versions of PipeWire, but it is deprecated.
       Please use the ``target.object`` key instead.

   :Default value: ``true``
   :See also: ``node.stream.restore-target``

.. describe:: linking.follow-default-target

   When a stream was started with the ``target.object`` property, WirePlumber
   normally links that stream to that target node and ignores the "default"
   target for that direction. However, if this option is enabled, WirePlumber
   will check if the designated target node *is* the "default" target and if so,
   it will act as if the stream did not have that property.

   In practice, this means that if the "default" target changes at runtime,
   the stream will be moved to the new "default" target.

   This is what Pulseaudio does and is implemented here for compatibility
   with some applications that do start with a ``target.object`` property
   set to the "default" target and expect the stream to be moved when the
   "default" target changes.

   Note that this logic is only applied on client (i.e. application) streams
   and *not* on filters.

   :Default value: ``true``

.. describe:: node.features.audio.no-dsp

   When this option is set to ``true``, audio nodes will not be configured
   in dsp mode, meaning that their channels will *not* be split into separate
   ports and that the audio data will *not* be converted to the float 32 format
   (F32P). Instead, devices will be configured in passthrough mode and streams
   will be configured in convert mode, so that their audio data is converted
   directly to the format that the device is expecting.

   This may be useful if you are trying to minimize audio processing for an
   embedded system, but it is not recommended for general use.

   .. warning::

      This option **will break** compatibility with JACK applications
      and may also break certain patchbay applications. Do not enable, unless
      you understand what you are doing.

   :Default value: ``false``

.. describe:: node.features.audio.monitor-ports

   This enables the creation of "monitor" ports for audio nodes. Monitor ports
   are created on nodes that have input ports (i.e. sinks and capture streams)
   and allow monitoring of the audio data that is being sent to the node.

   This is mostly used by monitoring applications, such as pavucontrol.

   :Default value: ``true``

.. describe:: node.features.audio.control-port

   This enables the creation of a "control" port for audio nodes. Control ports
   allow sending MIDI data to the node, allowing for control of certain node's
   parameters (such as volume) via external controllers.

   :Default value: ``false``

.. describe:: node.stream.restore-props

   WirePlumber stores stream parameters such as volume and mute status for each
   client (i.e. application) stream. If this setting is enabled, WirePlumber
   will restore the previously stored stream parameters when the stream is
   activated. If it is disabled, stream parameters will be initialized to their
   default values.

   :Default value: ``true``

.. describe:: node.stream.restore-target

   When a client (i.e. application) stream is manually moved to a different
   target node (e.g. via pavucontrol), the target node is stored by WirePlumber.
   If this setting is enabled, WirePlumber will restore the previously stored
   target node when the stream is activated.

   .. note::

      This does not restore manual links made by patchbay applications. This
      is only meant to restore the ``target.object`` property in the "default"
      metadata, which is manipulated by applications such as pavucontrol when
      a stream is moved to a different target.

   :Default value: ``true``
   :See also: ``linking.allow-moving-streams``

.. describe:: node.stream.default-playback-volume

   The default volume for playback streams to be applied when the stream is
   activated. This is only applied when ``node.stream.restore-props`` is
   ``true`` and the stream does not have a previously stored volume.

   :Default value: ``1.0``
   :Range: ``0.0`` to ``1.0``

.. describe:: node.stream.default-capture-volume

   The default volume for capture streams to be applied when the stream is
   activated. This is only applied when ``node.stream.restore-props`` is
   ``true`` and the stream does not have a previously stored volume.

   :Default value: ``1.0``
   :Range: ``0.0`` to ``1.0``

.. describe:: node.filter.forward-format

   When a "filter" pair of nodes (such as echo-cancel or filter-chain) is
   linked to a device node that has a different channel map than the filter
   nodes, this option allows the channel map of the filter nodes to be changed
   to match the channel map of the device node. The change is applied to both
   ends of the "filter", so that any streams linked to the filter are also
   reconfigured to match the target channel map.

   This is useful, for instance, to make sure that an application will be
   properly configured to output surround audio to a surround device, even
   when going through a filter that was not explicitly configured to have
   a surround channel map.

   :Default value: ``false``

.. describe:: node.restore-default-targets

   This setting enables WirePlumber to store and restore the "default" source
   and sink targets of the graph. In PulseAudio terminology, this is also known
   as the "fallback" source and sink.

   When this setting is enabled, WirePlumber will store the "default" source
   and sink targets when they are changed manually (e.g. via pavucontrol) and
   restore them when the available nodes change or after a reload/restart.
   It will also store a history of past selected "default" targets and restore
   previously selected ones if the currently selected are not available.

   If this is disabled, WirePlumber will pick the best available source
   and sink targets based on their priorities, but it will also respect
   manual user selections that are done at runtime - it will just not remember
   them so that it can restore them at a later time.

   :Default value: ``true``
   0707010000016A000081A400000000000000000000000165F8630400000A09000000000000000000000000000000000000003300000000wireplumber-0.5.0/src/scripts/lib/common-utils.lua    -- WirePlumber

-- Copyright © 2022 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>

-- SPDX-License-Identifier: MIT

-- Script is a Lua Module of common Lua utility functions

local cutils = {}

function cutils.parseBool (var)
  return var and (var:lower () == "true" or var == "1")
end

function cutils.parseParam (param, id)
  local props = param:parse ()
  if props.pod_type == "Object" and props.object_id == id then
    return props.properties
  else
    return nil
  end
end

function cutils.mediaClassToDirection (media_class)
  if media_class:find ("Sink") or
      media_class:find ("Input") or
      media_class:find ("Duplex") then
    return "input"
  elseif media_class:find ("Source") or media_class:find ("Output") then
    return "output"
  else
    return nil
  end
end

function cutils.getTargetDirection (properties)
  local target_direction = nil

  -- retrun same direction for si-audio-virtual session items
  if properties ["item.factory.name"] == "si-audio-virtual" then
    return properties ["item.node.direction"]
  end

  if properties ["item.node.direction"] == "output" or
      (properties ["item.node.direction"] == "input" and
          cutils.parseBool (properties ["stream.capture.sink"])) then
    target_direction = "input"
  else
    target_direction = "output"
  end
  return target_direction
end

local default_nodes = Plugin.find ("default-nodes-api")

function cutils.getDefaultNode (properties, target_direction)
  local target_media_class =
  properties ["media.type"] ..
      (target_direction == "input" and "/Sink" or "/Source")

  if not default_nodes then
    default_nodes = Plugin.find ("default-nodes-api")
  end

  return default_nodes:call ("get-default-node", target_media_class)
end

cutils.source_plugin = nil
cutils.object_managers = {}

function cutils.get_object_manager (name)
  cutils.source_plugin = cutils.source_plugin or
      Plugin.find ("standard-event-source")
  cutils.object_managers [name] = cutils.object_managers [name] or
      cutils.source_plugin:call ("get-object-manager", name)
  return cutils.object_managers [name]
end

function cutils.get_default_metadata_object ()
  return cutils.get_object_manager ("metadata"):lookup {
    Constraint { "metadata.name", "=", "default" },
  }
end

function cutils.arrayContains (a, value)
  for _, v in ipairs (a) do
    if v == value then
      return true
    end
  end
  return false
end

function cutils.get_application_name ()
  return Core.get_properties()["application.name"] or "WirePlumber"
end

return cutils
   0707010000016B000081A400000000000000000000000165F863040000071D000000000000000000000000000000000000003800000000wireplumber-0.5.0/src/scripts/lib/device-info-cache.lua   -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT

local module = {
  -- table of device info
  dev_infos = {},
}

SimpleEventHook {
  name = "lib/device-info-cache/cleanup",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "device-removed" },
    },
  },
  execute = function (event)
    local props = event:get_properties ()
    local device_id = props ["object.serial"]
    Log.trace ("cleaning up dev_info for object.serial = " .. device_id)
    module.dev_infos [device_id] = nil
  end
}:register()

function module.get_device_info (self, device)
  local device_properties = device.properties
  local device_id = device_properties ["object.serial"]
  local dev_info = self.dev_infos [device_id]

  -- new device
  if not dev_info then
    local device_name = device_properties ["device.name"]
    if not device_name then
      Log.critical (device, "invalid device.name")
      return nil
    end

    Log.trace (device, string.format (
        "create dev_info for '%s', object.serial = %s", device_name, device_id))

    dev_info = {
      name = device_name,
      active_profile = -1,
      route_infos = {},
    }
    self.dev_infos [device_id] = dev_info
  end

  return dev_info
end

function module.find_route_info (dev_info, route, return_new)
  local ri = dev_info.route_infos [route.index]
  if not ri and return_new then
    ri = {
      index = route.index,
      name = route.name,
      direction = route.direction,
      devices = route.devices or {},
      profiles = route.profiles,
      priority = route.priority or 0,
      available = route.available or "unknown",
      prev_available = route.available or "unknown",
      active = false,
      prev_active = false,
      save = false,
    }
  end
  return ri
end

return module
   0707010000016C000081A400000000000000000000000165F8630400002F2A000000000000000000000000000000000000003300000000wireplumber-0.5.0/src/scripts/lib/filter-utils.lua    -- WirePlumber

-- Copyright © 2023 Collabora Ltd.
--    @author Julian Bouzas <julian.bouzas@collabora.com>

-- SPDX-License-Identifier: MIT

-- Script is a Lua Module of filter Lua utility functions

local cutils = require ("common-utils")

local module = {
  metadata = nil,
  filters = {},
}

local function getFilterSmart (metadata, node)
  -- Check metadata
  if metadata ~= nil then
    local id = node["bound-id"]
    local value_str = metadata:find (id, "filter.smart")
    if value_str ~= nil then
      local json = Json.Raw (value_str)
      if json:is_boolean() then
        return json:parse()
      end
    end
  end

  -- Check node properties
  local prop_str = node.properties ["filter.smart"]
  if prop_str ~= nil then
    return cutils.parseBool (prop_str)
  end

  -- Otherwise consider the filter not smart by default
  return false
end

local function getFilterSmartName (metadata, node)
  -- Check metadata
  if metadata ~= nil then
    local id = node["bound-id"]
    local value_str = metadata:find (id, "filter.smart.name")
    if value_str ~= nil then
      local json = Json.Raw (value_str)
      if json:is_string() then
        return json:parse()
      end
    end
  end

  -- Check node properties
  local prop_str = node.properties ["filter.smart.name"]
  if prop_str ~= nil then
    return prop_str
  end

  -- Otherwise use link group as name
  return node.properties ["node.link-group"]
end

local function getFilterSmartDisabled (metadata, node)
  -- Check metadata
  if metadata ~= nil then
    local id = node["bound-id"]
    local value_str = metadata:find (id, "filter.smart.disabled")
    if value_str ~= nil then
      local json = Json.Raw (value_str)
      if json:is_boolean() then
        return json:parse()
      end
    end
  end

  -- Check node properties
  local prop_str = node.properties ["filter.smart.disabled"]
  if prop_str ~= nil then
    return cutils.parseBool (prop_str)
  end

  -- Otherwise consider the filter not disabled by default
  return false
end

local function getFilterSmartTargetable (metadata, node)
  -- Check metadata
  if metadata ~= nil then
    local id = node["bound-id"]
    local value_str = metadata:find (id, "filter.smart.targetable")
    if value_str ~= nil then
      local json = Json.Raw (value_str)
      if json:is_boolean() then
        return json:parse()
      end
    end
  end

  -- Check node properties
  local prop_str = node.properties ["filter.smart.targetable"]
  if prop_str ~= nil then
    return cutils.parseBool (prop_str)
  end

  -- Otherwise consider the filter not targetable by default
  return false
end

local function getFilterSmartTarget (metadata, node, om)
  -- Check metadata and fallback to properties
  local id = node["bound-id"]
  local value_str = nil
  if metadata ~= nil then
    value_str = metadata:find (id, "filter.smart.target")
  end
  if value_str == nil then
    value_str = node.properties ["filter.smart.target"]
    if value_str == nil then
      return nil
    end
  end

  -- Parse match rules
  local match_rules_json = Json.Raw (value_str)
  if not match_rules_json:is_object () then
    return nil
  end
  local match_rules = match_rules_json:parse ()

  -- Find target
  local target = nil
  for si_target in om:iterate { type = "SiLinkable" } do
    local n_target = si_target:get_associated_proxy ("node")
    if n_target == nil then
      goto skip_target
    end

    -- Target nodes are only meant to be device nodes, without link-group
    if n_target.properties ["node.link-group"] ~= nil then
      goto skip_target
    end

    -- Make sure the target node properties match all rules
    for key, val in pairs(match_rules) do
      if n_target.properties[key] ~= tostring (val) then
        goto skip_target
      end
    end

    -- Target found
    target = si_target
    break;

    ::skip_target::
  end

  return target
end

local function getFilterSmartBefore (metadata, node)
  -- Check metadata and fallback to properties
  local id = node["bound-id"]
  local value_str = nil
  if metadata ~= nil then
    value_str = metadata:find (id, "filter.smart.before")
  end
  if value_str == nil then
    value_str = node.properties ["filter.smart.before"]
    if value_str == nil then
      return nil
    end
  end

  -- Parse
  local before_json = Json.Raw (value_str)
  if not before_json:is_array() then
    return nil
  end
  return before_json:parse ()
end

local function getFilterSmartAfter (metadata, node)
  -- Check metadata and fallback to properties
  local id = node["bound-id"]
  local value_str = nil
  if metadata ~= nil then
    value_str = metadata:find (id, "filter.smart.after")
  end
  if value_str == nil then
    value_str = node.properties ["filter.smart.after"]
    if value_str == nil then
      return nil
    end
  end

  -- Parse
  local after_json = Json.Raw (value_str)
  if not after_json:is_array() then
    return nil
  end
  return after_json:parse ()
end

local function insertFilterSorted (curr_filters, filter)
  local before_filters = {}
  local after_filters = {}
  local new_filters = {}

  -- Check if the current filters need to be inserted before or after
  for i, v in ipairs(curr_filters) do
    local insert_before = true
    local insert_after = false

    if v.before ~= nil then
      for j, b in ipairs(v.before) do
        if filter.name == b then
          insert_after = false
          break
        end
      end
    end

    if v.after ~= nil then
      for j, b in ipairs(v.after) do
        if filter.name == b then
          insert_before = false
          break
        end
      end
    end

    if filter.before ~= nil then
      for j, b in ipairs(filter.before) do
        if v.name == b then
          insert_after = true
        end
      end
    end

    if filter.after ~= nil then
      for j, b in ipairs(filter.after) do
        if v.name == b then
          insert_before = true
        end
      end
    end

    if insert_before then
      if insert_after then
        Log.warning ("cyclic before/after found in filters " .. v.name .. " and " .. filter.name)
      end
      table.insert (before_filters, v)
    else
      table.insert (after_filters, v)
    end

  end

  -- Add the filters to the new table stored
  for i, v in ipairs(before_filters) do
    table.insert (new_filters, v)
  end
  table.insert (new_filters, filter)
  for i, v in ipairs(after_filters) do
    table.insert (new_filters, v)
  end

  return new_filters
end

local function rescanFilters (om, metadata_om)
  local metadata =
        metadata_om:lookup { Constraint { "metadata.name", "=", "filters" } }

  -- Always clear all filters data on rescan
  module.filters = {}

  Log.info ("rescanning filters...")

  for si in om:iterate { type = "SiLinkable" } do
    local filter = {}

    local n = si:get_associated_proxy ("node")
    if n == nil then
      goto skip_linkable
    end

    -- Only handle nodes with link group (filters)
    filter.link_group = n.properties ["node.link-group"]
    if filter.link_group == nil then
      goto skip_linkable
    end

    -- Only handle the main filter nodes
    filter.media_class = n.properties ["media.class"]
    if filter.media_class ~= "Audio/Sink" and
        filter.media_class ~= "Audio/Source" and
        filter.media_class ~= "Video/Source" then
      goto skip_linkable
    end

    -- Filter direction
    if filter.media_class == "Audio/Sink" then
      filter.direction = "input"
    else
      filter.direction = "output"
    end

    -- Get filter properties
    filter.smart = getFilterSmart (metadata, n)
    filter.name = getFilterSmartName (metadata, n)
    filter.disabled = getFilterSmartDisabled (metadata, n)
    filter.targetable = getFilterSmartTargetable (metadata, n)
    filter.target = getFilterSmartTarget (metadata, n, om)
    filter.before = getFilterSmartBefore (metadata, n)
    filter.after = getFilterSmartAfter (metadata, n)

    -- Add the main and stream session items
    filter.main_si = si
    filter.stream_si = om:lookup {
      type = "SiLinkable",
      Constraint { "node.link-group", "=", filter.link_group },
      Constraint { "media.class", "#", "Stream/*", type = "pw-global" }
    }

    -- Add the filter to the list sorted by before and after
    module.filters = insertFilterSorted (module.filters, filter)

  ::skip_linkable::
  end

end

SimpleEventHook {
  name = "lib/filter-utils/rescan",
  before = "linking/rescan",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "rescan-for-linking" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    local om = source:call ("get-object-manager", "session-item")
    local metadata_om = source:call ("get-object-manager", "metadata")

    rescanFilters (om, metadata_om)
  end
}:register ()

function module.is_filter_smart (direction, link_group)
  -- Make sure direction and link_group is valid
  if direction == nil or link_group == nil then
    return false
  end

  for i, v in ipairs(module.filters) do
    if v.direction == direction and v.link_group == link_group then
      return v.smart
    end
  end

  return false
end

function module.is_filter_disabled (direction, link_group)
  -- Make sure direction and link_group is valid
  if direction == nil or link_group == nil then
    return false
  end

  for i, v in ipairs(module.filters) do
    if v.direction == direction and v.link_group == link_group then
      return v.disabled
    end
  end

  return false
end

function module.is_filter_targetable (direction, link_group)
  -- Make sure direction and link_group is valid
  if direction == nil or link_group == nil then
    return false
  end

  for i, v in ipairs(module.filters) do
    if v.direction == direction and v.link_group == link_group then
      return v.targetable
    end
  end

  return false
end

function module.get_filter_target (direction, link_group)
  -- Make sure direction and link_group are valid
  if direction == nil or link_group == nil then
    return nil
  end

  -- Find the current filter
  local filter = nil
  local index = nil
  for i, v in ipairs(module.filters) do
    if v.direction == direction and
        v.link_group == link_group and
        not v.disabled and
        v.smart then
      filter = v
      index = i
      break
    end
  end
  if filter == nil then
    return nil
  end

  -- Return the next filter with matching target
  for i, v in ipairs(module.filters) do
    if v.direction == direction and
        v.name ~= filter.name and
        v.link_group ~= link_group and
        not v.disabled and
        v.smart and
        ((v.target == nil and filter.target == nil) or
            (v.target ~= nil and filter.target ~= nil and v.target.id == filter.target.id)) and
        i > index then
      return v.main_si
    end
  end

  -- Otherwise return the filter destination target
  return filter.target
end

function module.get_filter_from_target (direction, si_target)
  local target = si_target

  -- Make sure direction is valid
  if direction == nil then
    return nil
  end

  -- If si_target is a filter, find it and use its target
  if si_target then
    local target_node = si_target:get_associated_proxy ("node")
    local target_link_group = target_node.properties ["node.link-group"]
    if target_link_group ~= nil then
      local filter = nil
      for i, v in ipairs(module.filters) do
        if v.direction == direction and
            v.link_group == target_link_group and
            not v.disabled and
            v.smart then
          filter = v
          break
        end
      end
      if filter == nil then
        return nil
      end
      target = filter.target
    end
  end


  -- Find the first filter matching target
  for i, v in ipairs(module.filters) do
    if v.direction == direction and
        not v.disabled and
        v.smart and
        ((v.target ~= nil and target ~= nil and v.target.id == target.id) or
            (target == nil and v.target == nil)) then
      return v.main_si
    end
  end

  return nil
end

return module
  0707010000016D000081A400000000000000000000000165F8630400002883000000000000000000000000000000000000003400000000wireplumber-0.5.0/src/scripts/lib/linking-utils.lua   -- WirePlumber

-- Copyright © 2022 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>

-- SPDX-License-Identifier: MIT

-- Script is a Lua Module of linking Lua utility functions

local cutils = require ("common-utils")

local lutils = {
  si_flags = {},
}

function lutils.get_flags (self, si_id)
  if not self.si_flags [si_id] then
    self.si_flags [si_id] = {}
  end

  return self.si_flags [si_id]
end

function lutils.clear_flags (self, si_id)
  self.si_flags [si_id] = nil
end

function lutils.unwrap_select_target_event (self, event)
  local source = event:get_source ()
  local si = event:get_subject ()
  local target = event:get_data ("target")
  local om = source:call ("get-object-manager", "session-item")
  local si_id = si.id

  return source, om, si, si.properties, self:get_flags (si_id), target
end

function lutils.canPassthrough (si, si_target)
  local props = si.properties
  local tprops = si_target.properties
  -- both nodes must support encoded formats
  if not cutils.parseBool (props ["item.node.supports-encoded-fmts"])
      or not cutils.parseBool (tprops ["item.node.supports-encoded-fmts"]) then
    return false
  end

  -- make sure that the nodes have at least one common non-raw format
  local n1 = si:get_associated_proxy ("node")
  local n2 = si_target:get_associated_proxy ("node")
  for p1 in n1:iterate_params ("EnumFormat") do
    local p1p = p1:parse ()
    if p1p.properties.mediaSubtype ~= "raw" then
      for p2 in n2:iterate_params ("EnumFormat") do
        if p1:filter (p2) then
          return true
        end
      end
    end
  end
  return false
end

function lutils.checkFollowDefault (si, si_target)
  -- If it got linked to the default target that is defined by node
  -- props but not metadata, start ignoring the node prop from now on.
  -- This is what Pulseaudio does.
  --
  -- Pulseaudio skips here filter streams (i->origin_sink and
  -- o->destination_source set in PA). Pipewire does not have a flag
  -- explicitly for this, but we can use presence of node.link-group.
  local si_props = si.properties
  local target_props = si_target.properties
  local reconnect = not cutils.parseBool (si_props ["node.dont-reconnect"])
  local is_filter = (si_props ["node.link-group"] ~= nil)

  if reconnect and not is_filter then
    local def_id = cutils.getDefaultNode (si_props,
      cutils.getTargetDirection (si_props))

    if target_props ["node.id"] == tostring (def_id) then
      local metadata = cutils.get_default_metadata_object ()
      -- Set target.node, for backward compatibility
      metadata:set (tonumber
        (si_props ["node.id"]), "target.node", "Spa:Id", "-1")
      Log.info (si, "... set metadata to follow default")
    end
  end
end

function lutils.lookupLink (si_id, si_target_id)
  local link = cutils.get_object_manager ("session-item"):lookup {
    type = "SiLink",
    Constraint { "out.item.id", "=", si_id },
    Constraint { "in.item.id", "=", si_target_id }
  }
  if not link then
    link = cutils.get_object_manager ("session-item"):lookup {
      type = "SiLink",
      Constraint { "in.item.id", "=", si_id },
      Constraint { "out.item.id", "=", si_target_id }
    }
  end
  return link
end

function lutils.isLinked (si_target)
  local target_id = si_target.id
  local linked = false
  local exclusive = false

  for l in cutils.get_object_manager ("session-item"):iterate {
    type = "SiLink",
  } do
    local p = l.properties
    local out_id = tonumber (p ["out.item.id"])
    local in_id = tonumber (p ["in.item.id"])
    linked = (out_id == target_id) or (in_id == target_id)
    if linked then
      exclusive = cutils.parseBool (p ["exclusive"]) or cutils.parseBool (p ["passthrough"])
      break
    end
  end
  return linked, exclusive
end

function lutils.getNodePeerId (node_id)
  for l in cutils.get_object_manager ("link"):iterate() do
    local p = l.properties
    local in_id = tonumber(p["link.input.node"])
    local out_id = tonumber(p["link.output.node"])
    if in_id == node_id then
      return out_id
    elseif out_id == node_id then
      return in_id
    end
  end
  return nil
end

function lutils.canLink (properties, si_target)
  local target_props = si_target.properties

  -- nodes must have the same media type
  if properties ["media.type"] ~= target_props ["media.type"] then
    return false
  end

  local function isMonitor(properties)
    return properties ["item.node.direction"] == "input" and
        cutils.parseBool (properties ["item.features.monitor"]) and
        not cutils.parseBool (properties ["item.features.no-dsp"]) and
        properties ["item.factory.name"] == "si-audio-adapter"
  end

  if properties ["item.factory.name"] == "si-audio-virtual" then
    -- virtual nodes must have the same direction, unless the target is monitor
    if properties ["item.node.direction"] ~= target_props ["item.node.direction"]
        and not isMonitor (target_props) then
      return false
    end
  else
    -- nodes must have opposite direction, or otherwise they must be both input
    -- and the target must have a monitor (so the target will be used as a source)
    if properties ["item.node.direction"] == target_props ["item.node.direction"]
        and not isMonitor (target_props) then
      return false
    end
  end

  -- check link group
  local function canLinkGroupCheck(link_group, si_target, hops)
    local target_props = si_target.properties
    local target_link_group = target_props ["node.link-group"]

    if hops == 8 then
      return false
    end

    -- allow linking if target has no link-group property
    if not target_link_group then
      return true
    end

    -- do not allow linking if target has the same link-group
    if link_group == target_link_group then
      return false
    end

    -- make sure target is not linked with another node with same link group
    -- start by locating other nodes in the target's link-group, in opposite direction
    for n in cutils.get_object_manager ("session-item"):iterate {
      type = "SiLinkable",
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
      Constraint { "id", "!", si_target.id, type = "gobject" },
      Constraint { "item.node.direction", "!", target_props ["item.node.direction"] },
      Constraint { "node.link-group", "=", target_link_group },
    } do
      -- iterate their peers and return false if one of them cannot link
      for silink in cutils.get_object_manager ("session-item"):iterate {
        type = "SiLink",
      } do
        local out_id = tonumber (silink.properties ["out.item.id"])
        local in_id = tonumber (silink.properties ["in.item.id"])
        if out_id == n.id or in_id == n.id then
          local peer_id = (out_id == n.id) and in_id or out_id
          local peer = cutils.get_object_manager ("session-item"):lookup {
            type = "SiLinkable",
            Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
            Constraint { "id", "=", peer_id, type = "gobject" },
          }
          if peer and not canLinkGroupCheck (link_group, peer, hops + 1) then
            return false
          end
        end
      end
    end
    return true
  end

  local link_group = properties ["node.link-group"]
  if link_group then
    return canLinkGroupCheck (link_group, si_target, 0)
  end
  return true
end

function lutils.findDefaultLinkable (si)
  local si_props = si.properties
  local target_direction = cutils.getTargetDirection (si_props)
  local def_node_id = cutils.getDefaultNode (si_props, target_direction)
  return cutils.get_object_manager ("session-item"):lookup {
    type = "SiLinkable",
    Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    Constraint { "node.id", "=", tostring (def_node_id) }
  }
end

function lutils.checkPassthroughCompatibility (si, si_target)
  local si_must_passthrough =
      cutils.parseBool (si.properties ["item.node.encoded-only"])
  local si_target_must_passthrough =
      cutils.parseBool (si_target.properties ["item.node.encoded-only"])
  local can_passthrough = lutils.canPassthrough (si, si_target)
  if (si_must_passthrough or si_target_must_passthrough)
      and not can_passthrough then
    return false, can_passthrough
  end
  return true, can_passthrough
end

-- Does the target device have any active/available paths/routes to
-- the physical device(spkr/mic/cam)?
function lutils.haveAvailableRoutes (si_props)
  local card_profile_device = si_props ["card.profile.device"]
  local device_id = si_props ["device.id"]
  local device = device_id and cutils.get_object_manager ("device"):lookup {
    Constraint { "bound-id", "=", device_id, type = "gobject" },
  }

  if not card_profile_device or not device then
    return true
  end

  local found = 0
  local avail = 0

  -- First check "SPA_PARAM_Route" if there are any active devices
  -- in an active profile.
  for p in device:iterate_params ("Route") do
    local route = cutils.parseParam (p, "Route")
    if not route then
      goto skip_route
    end

    if (route.device ~= tonumber (card_profile_device)) then
      goto skip_route
    end

    if (route.available == "no") then
      return false
    end

    do return true end

    ::skip_route::
  end

  -- Second check "SPA_PARAM_EnumRoute" if there is any route that
  -- is available if not active.
  for p in device:iterate_params ("EnumRoute") do
    local route = cutils.parseParam (p, "EnumRoute")
    if not route then
      goto skip_enum_route
    end

    if not cutils.arrayContains
          (route.devices, tonumber (card_profile_device)) then
      goto skip_enum_route
    end
    found = found + 1;
    if (route.available ~= "no") then
      avail = avail + 1
    end
    ::skip_enum_route::
  end

  if found == 0 then
    return true
  end
  if avail > 0 then
    return true
  end

  return false
end

function lutils.sendClientError (event, node, message)
  local source = event:get_source ()
  local client_id = node.properties ["client.id"]
  if client_id then
    local clients_om = source:call ("get-object-manager", "client")
    local client = clients_om:lookup {
        Constraint { "bound-id", "=", client_id, type = "gobject" }
    }
    if client then
      client:send_error (node ["bound-id"], -2, message)
    end
  end
end

return lutils
 0707010000016E000081A400000000000000000000000165F8630400000EBE000000000000000000000000000000000000003400000000wireplumber-0.5.0/src/scripts/lib/monitor-utils.lua   -- WirePlumber

-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>

-- SPDX-License-Identifier: MIT

-- Script is a Lua Module of monitor Lua utility functions

log = Log.open_topic ("s-monitors-utils")

local mutils = {
  cam_data = {}
}

-- finds out if any of the managed objects(nodes of a device or devices of
-- device enumerator) has duplicate values
function mutils.find_duplicate (parent, id, property, value)
  for i = 0, id - 1, 1 do
    local obj = parent:get_managed_object (i)
    if obj and obj.properties[property] == value then
      return true
    end
  end
  return false
end

function mutils.get_cam_data (self, dev_string)
  local dev_num = tonumber (dev_string)
  if not dev_num then
    return
  end

  if not self.cam_data[dev_num] then
    self.cam_data[dev_num] = {}
    self.cam_data[dev_num]["libcamera"] = {}
    self.cam_data[dev_num]["v4l2"] = {}
  end

  return self.cam_data[dev_num], dev_num
end

function mutils.create_cam_node (self, dev_num)
  local api = nil
  local cam_data = self:get_cam_data (dev_num)

  if cam_data["v4l2"].enum_status and cam_data["libcamera"].enum_status then
    if cam_data.is_device_uvc then
      api = "v4l2"
    else
      api = "libcamera"
    end
  else
    api = cam_data["v4l2"].enum_status and "v4l2" or "libcamera"
  end

  log:info (string.format ("create \"%s\" node for device:%s%s", api,
    cam_data.dev_path, (cam_data.is_device_uvc and "(uvc)" or "")))

  source = source or Plugin.find ("standard-event-source")
  local e = source:call ("create-event", "create-" .. api .. "-device-node",
    cam_data[api].parent, nil)
  e:set_data ("factory", cam_data[api].factory)
  e:set_data ("node-properties", cam_data[api].properties)
  e:set_data ("node-sub-id", cam_data[api].id)

  EventDispatcher.push_event (e)
end

-- arbitrates between v4l2 and libcamera on who gets to create the device node
-- for a device, logic is based on the device number of the device given by both
-- the parties.
function mutils.register_cam_node (self, parent, id, factory, properties)
  local cam_data, dev_num = self:get_cam_data (properties["device.devids"])
  local api = properties["device.api"]

  if not cam_data then
    log:notice (string.format ("device number invalid for %s device:%s",
      api, properties["device.name"]))
    return false
  end

  -- only v4l2 can give this info
  if properties["api.v4l2.cap.driver"] == "uvcvideo" then
    cam_data.is_device_uvc = true
  end

  -- only v4l2 can give this info
  if properties["api.v4l2.path"] then
    cam_data.dev_path = properties["api.v4l2.path"]
  end

  cam_api_data = cam_data[api]
  cam_api_data.enum_status = true

  -- cache info, it comes handy when creating node
  cam_api_data.parent = parent
  cam_api_data.id = id
  cam_api_data.factory = factory
  cam_api_data.properties = properties

  local other_api = api == "v4l2" and "libcamera" or "v4l2"
  if cam_api_data.enum_status and not cam_data[other_api].enum_status then
    log:trace (string.format ("\"%s\" armed a timer for %d", api, dev_num))
    cam_data.source = Core.timeout_add (
        Settings.get_int ("monitor.camera-discovery-timeout"), function()
      log:trace (string.format ("\"%s\" armed timer expired for %d", api, dev_num))
      self:create_cam_node (dev_num)
      cam_data.source = nil
    end)
  elseif cam_data.source then
    log:trace (string.format ("\"%s\" disarmed timer for %d", api, dev_num))
    cam_data.source:destroy ()
    cam_data.source = nil
    self:create_cam_node (dev_num)
  else
    log:notice (string.format ("\"%s\" calling after timer expiry for %d:%s%s",
      api, dev_num, cam_data.dev_path,
      (cam_data.is_device_uvc and "(uvc)" or "")))
  end

  return true
end

return mutils
  0707010000016F000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002600000000wireplumber-0.5.0/src/scripts/linking 07070100000170000081A400000000000000000000000165F8630400000E06000000000000000000000000000000000000003100000000wireplumber-0.5.0/src/scripts/linking/README.rst  Linking Scripts
===============

These scripts contain all the logic for creating links between nodes.
This involves, to a large extent, deciding which links to create.

Hooks
-----

The hooks in this section are organized in 3 sub-categories. The first category
includes hooks that are triggered by changes in the graph. Some of them are tasked
to schedule a "rescan-for-linking" event, which is the lowest priority event and
its purpose is to scan through all the linkable session items and link them
to a particular target. The "rescan-for-linking" event is always scheduled to run
once for all the graph changes in a cycle. This is achieved by flagging the event
as already scheduled in the module-standard-event-source; this flag is then cleared
by a hook that runs on this event.

Selecting a target for each linkable and linking to it is deferred to another
set of hooks by pushing a "select-target" event for each linkable. This event
is the highest priority event and therefore no other changes in the graph are
processed while targets are being selected.

.. list-table:: Hooks triggered by changes in the graph
   :header-rows: 1

   * - Hook name
     - File
     - Triggered by
     - Action

   * - linking/rescan-trigger
     - rescan.lua
     - linkable SI added|removed or metadata-changed
     - schedules rescan-for-linking event

   * - linking/linkable-removed
     - rescan.lua
     - linkable SI removed
     - destroys links related to the removed linkable

   * - linking/follow
     - move-follow.lua
     - metadata-changed
     - schedules rescan-for-linking when the configured default sources/sinks are changed by the user

   * - linking/move
     - move-follow.lua
     - metadata-changed
     - schedules rescan-for-linking when node target metadata properties are changed

   * - linking/rescan-virtual-links
     - rescan-virtual-links.lua
     - link SI added, removed or metadata-changed
     -

.. list-table:: rescan-for-linking hooks, in order of execution
   :header-rows: 1
   :width: 100%
   :widths: 20 20 60

   * - Hook name
     - File
     - Description

   * - m-standard-event-source/rescan-done
     - module-standard-event-source.c
     - clears the rescan_scheduled flag

   * - linking/rescan
     - rescan.lua
     - schedules select-target for each linkable session item

.. list-table:: select-target hooks, in order of execution
   :header-rows: 1
   :width: 100%
   :widths: 20 20 60

   * - Hook name
     - File
     - Description

   * - linking/find-virtual-target
     - find-virtual-target.lua
     -

   * - linking/find-defined-target
     - find-defined-target.lua
     - Select the target that has been defined explicitly by the 'target.object' property or metadata

   * - linking/find-filter-target
     - find-filter-target.lua
     - Select the target of a filter node, if the subject is a filter node

   * - linking/find-default-target
     - find-default-target.lua
     - Select the default source/sink as target

   * - linking/find-best-target
     - find-best-target.lua
     - Select target based on priority.session

   * - linking/get-filter-from-target
     - get-filter-from-target.lua
     - Translate the found target to a filter target that should be linked instead

   * - linking/prepare-link
     - prepare-link.lua
     - Break existing link if needed, check if the target is available for linking; send error to the client if needed

   * - linking/link-target
     - link-target.lua
     - Create si-standard-link session item to create links between the subject linkable and the selected target
  07070100000171000081A400000000000000000000000165F8630400000E98000000000000000000000000000000000000003B00000000wireplumber-0.5.0/src/scripts/linking/find-best-target.lua    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Traverse through all the possible targets to pick up target node.

lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-linking")

SimpleEventHook {
  name = "linking/find-best-target",
  after = "linking/find-default-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        lutils:unwrap_select_target_event (event)

    -- bypass the hook if the target is already picked up
    if target then
      return
    end

    local target_direction = cutils.getTargetDirection (si_props)
    local target_picked = nil
    local target_can_passthrough = false
    local target_priority = 0
    local target_plugged = 0

    log:info (si, string.format ("handling item: %s (%s)",
        tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))

    for target in om:iterate {
      type = "SiLinkable",
      Constraint { "item.node.type", "=", "device" },
      Constraint { "item.node.direction", "=", target_direction },
      Constraint { "media.type", "=", si_props ["media.type"] },
    } do
      local target_props = target.properties
      local target_node_id = target_props ["node.id"]
      local si_target_node = target:get_associated_proxy ("node")
      local si_target_link_group = si_target_node.properties ["node.link-group"]
      local priority = tonumber (target_props ["priority.session"]) or 0

      log:debug (string.format ("Looking at: %s (%s)",
        tostring (target_props ["node.name"]),
        tostring (target_node_id)))

      if si_target_link_group ~= nil then
        Log.debug ("... ignoring filter as best target")
        goto skip_linkable
      end

      if not lutils.canLink (si_props, target) then
        log:debug ("... cannot link, skip linkable")
        goto skip_linkable
      end

      if not lutils.haveAvailableRoutes (target_props) then
        log:debug ("... does not have routes, skip linkable")
        goto skip_linkable
      end

      local passthrough_compatible, can_passthrough =
      lutils.checkPassthroughCompatibility (si, target)
      if not passthrough_compatible then
        log:debug ("... passthrough is not compatible, skip linkable")
        goto skip_linkable
      end

      local plugged = tonumber (target_props ["item.plugged.usec"]) or 0

      log:debug ("... priority:" .. tostring (priority) .. ", plugged:" .. tostring (plugged))

      -- (target_picked == NULL) --> make sure atleast one target is picked.
      -- (priority > target_priority) --> pick the highest priority linkable(node)
      -- target.
      -- (priority == target_priority and plugged > target_plugged) --> pick the
      -- latest connected/plugged(in time) linkable(node) target.
      if (target_picked == nil or
          priority > target_priority or
          (priority == target_priority and plugged > target_plugged)) then
        log:debug ("... picked")
        target_picked = target
        target_can_passthrough = can_passthrough
        target_priority = priority
        target_plugged = plugged
      end
      ::skip_linkable::
    end

    if target_picked then
      log:info (si,
        string.format ("... best target picked: %s (%s), can_passthrough:%s",
          tostring (target_picked.properties ["node.name"]),
          tostring (target_picked.properties ["node.id"]),
          tostring (target_can_passthrough)))
      si_flags.can_passthrough = target_can_passthrough
      event:set_data ("target", target_picked)
    end
  end
}:register ()
07070100000172000081A400000000000000000000000165F863040000061A000000000000000000000000000000000000003E00000000wireplumber-0.5.0/src/scripts/linking/find-default-target.lua -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Check if default nodes can be picked up as target node.

lutils = require ("linking-utils")
log = Log.open_topic ("s-linking")

SimpleEventHook {
  name = "linking/find-default-target",
  after = "linking/find-filter-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        lutils:unwrap_select_target_event (event)

    -- bypass the hook if the target is already picked up
    if target then
      return
    end

    local target_picked = false

    log:info (si, string.format ("handling item: %s (%s)",
        tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))

    target = lutils.findDefaultLinkable (si)

    local can_passthrough, passthrough_compatible
    if target then
      passthrough_compatible, can_passthrough =
      lutils.checkPassthroughCompatibility (si, target)
      if lutils.canLink (si_props, target) and passthrough_compatible then
        target_picked = true;
      end
    end

    if target_picked then
      log:info (si,
        string.format ("... default target picked: %s (%s), can_passthrough:%s",
          tostring (target.properties ["node.name"]),
          tostring (target.properties ["node.id"]),
          tostring (can_passthrough)))
      si_flags.can_passthrough = can_passthrough
      event:set_data ("target", target)
    end
  end
}:register ()
  07070100000173000081A400000000000000000000000165F8630400001103000000000000000000000000000000000000003E00000000wireplumber-0.5.0/src/scripts/linking/find-defined-target.lua -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Check if the target node is defined explicitly.
-- This defination can be done in two ways.
-- 1. "node.target"/"target.object" in the node properties
-- 2. "target.node"/"target.object" in the default metadata

lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-linking")

SimpleEventHook {
  name = "linking/find-defined-target",
  after = "linking/find-virtual-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        lutils:unwrap_select_target_event (event)

    -- bypass the hook if the target is already picked up
    if target then
      return
    end

    log:info (si, string.format ("handling item %d: %s (%s)", si.id,
        tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))

    local metadata = Settings.get_boolean ("linking.allow-moving-streams") and
        cutils.get_default_metadata_object ()
    local dont_fallback = cutils.parseBool (si_props ["node.dont-fallback"])
    local dont_move = cutils.parseBool (si_props ["node.dont-move"])
    local target_key
    local target_value = nil
    local node_defined = false
    local target_picked = nil

    if si_props ["target.object"] ~= nil then
      target_value = si_props ["target.object"]
      target_key = "object.serial"
      node_defined = true
    elseif si_props ["node.target"] ~= nil then
      target_value = si_props ["node.target"]
      target_key = "node.id"
      node_defined = true
    end

    if metadata and not dont_move then
      local id = metadata:find (si_props ["node.id"], "target.object")
      if id ~= nil then
        target_value = id
        target_key = "object.serial"
        node_defined = false
      else
        id = metadata:find (si_props ["node.id"], "target.node")
        if id ~= nil then
          target_value = id
          target_key = "node.id"
          node_defined = false
        end
      end
    end

    if target_value == "-1" then
      target_picked = false
      target = nil
    elseif target_value and tonumber (target_value) then
      target = om:lookup {
        type = "SiLinkable",
        Constraint { target_key, "=", target_value },
      }
      if target and lutils.canLink (si_props, target) then
        target_picked = true
      end
    elseif target_value then
      for lnkbl in om:iterate { type = "SiLinkable" } do
        local target_props = lnkbl.properties
        if (target_props ["node.name"] == target_value or
            target_props ["object.path"] == target_value) and
            target_props ["item.node.direction"] == cutils.getTargetDirection (si_props) and
            lutils.canLink (si_props, lnkbl) then
          target_picked = true
          target = lnkbl
          break
        end
      end
    end

    local can_passthrough, passthrough_compatible
    if target then
      passthrough_compatible, can_passthrough =
      lutils.checkPassthroughCompatibility (si, target)
      if not passthrough_compatible then
        target = nil
      end
    end

    si_flags.has_defined_target = false
    if target_picked and target then
      log:info (si,
        string.format ("... defined target picked: %s (%s), can_passthrough:%s",
          tostring (target.properties ["node.name"]),
          tostring (target.properties ["node.id"]),
          tostring (can_passthrough)))
      si_flags.has_node_defined_target = node_defined
      si_flags.can_passthrough = can_passthrough
      si_flags.has_defined_target = true
      event:set_data ("target", target)
    elseif target_value and dont_fallback then
      -- send error to client and destroy node if linger is not set
      local linger = cutils.parseBool (si_props ["node.linger"])
      if not linger then
        local node = si:get_associated_proxy ("node")
        lutils.sendClientError (event, node, "defined target not found")
        node:request_destroy ()
        log:info(si, "... destroyed node as defined target was not found")
      else
        log:info(si, "... waiting for defined target as dont-fallback is set")
      end
      event:stop_processing ()
    end

  end
}:register ()
 07070100000174000081A400000000000000000000000165F8630400000B70000000000000000000000000000000000000003D00000000wireplumber-0.5.0/src/scripts/linking/find-filter-target.lua  -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Check if the target node is a filter target.

lutils = require ("linking-utils")
cutils = require ("common-utils")
futils = require ("filter-utils")
log = Log.open_topic ("s-linking")

function findFilterTarget (si, om)
  local node = si:get_associated_proxy ("node")
  local link_group = node.properties ["node.link-group"]
  local target_id = -1

  -- return nil if session item is not a filter node
  if link_group == nil then
    return nil, false
  end

  -- return nil if filter is not smart
  local direction = cutils.getTargetDirection (si.properties)
  if not futils.is_filter_smart (direction, link_group) then
    return nil, false
  end

  -- get the filter target
  return futils.get_filter_target (direction, link_group), true
end

SimpleEventHook {
  name = "linking/find-filter-target",
  after = "linking/find-defined-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        lutils:unwrap_select_target_event (event)

    -- bypass the hook if the target is already picked up
    if target then
      return
    end

    local dont_fallback = cutils.parseBool (si_props ["node.dont-fallback"])
    local target_picked = false
    local allow_fallback

    log:info (si, string.format ("handling item %d: %s (%s)", si.id,
        tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))

    target, is_smart_filter = findFilterTarget (si, om)

    local can_passthrough, passthrough_compatible
    if target then
      passthrough_compatible, can_passthrough =
          lutils.checkPassthroughCompatibility (si, target)
      if lutils.canLink (si_props, target) and passthrough_compatible then
        target_picked = true
      end
    end

    if target_picked and target then
      log:info (si,
        string.format ("... filter target picked: %s (%s), can_passthrough:%s",
          tostring (target.properties ["node.name"]),
          tostring (target.properties ["node.id"]),
          tostring (can_passthrough)))
      si_flags.can_passthrough = can_passthrough
      event:set_data ("target", target)
    elseif is_smart_filter and dont_fallback then
      -- send error to client and destroy node if linger is not set
      local linger = cutils.parseBool (si_props ["node.linger"])
      if not linger then
        local node = si:get_associated_proxy ("node")
        lutils.sendClientError (event, node, "smart filter defined target not found")
        node:request_destroy ()
        log:info(si, "... destroyed node as smart filter defined target was not found")
      else
        log:info(si, "... waiting for smart filter defined target as dont-fallback is set")
      end
      event:stop_processing ()
    end
  end
}:register ()
07070100000175000081A400000000000000000000000165F863040000039D000000000000000000000000000000000000004300000000wireplumber-0.5.0/src/scripts/linking/find-user-target.lua.example    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- example of a user injectible hook to link a node to a custom target

lutils = require ("linking-utils")
log = Log.open_topic ("s-linking")

SimpleEventHook {
  name = "linking/sample-find-user-target",
  before = "linking/find-defined-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        lutils:unwrap_select_target_event (event)

    -- bypass the hook if the target is already picked up
    if target then
      return
    end

    log:info (si, "in find-user-target")

    -- implement logic here to find a suitable target

    -- store the found target on the event,
    -- the next hooks will take care of linking
    event:set_data ("target", target)
  end
}:register ()
   07070100000176000081A400000000000000000000000165F8630400000DBB000000000000000000000000000000000000003E00000000wireplumber-0.5.0/src/scripts/linking/find-virtual-target.lua -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Select the virtual target based on roles

lutils = require ("linking-utils")
log = Log.open_topic ("s-linking")

config = {}
config.roles = Conf.get_section_as_object ("virtual-item-roles")

function findRole(role, tmc)
  if role and not config.roles[role] then
    -- find the role with matching alias
    for r, p in pairs(config.roles) do
      -- default media class can be overridden in the role config data
      mc = p["media.class"] or "Audio/Sink"
      if (type(p.alias) == "table" and tmc == mc) then
        for i = 1, #(p.alias), 1 do
          if role == p.alias[i] then
            return r
          end
        end
      end
    end

    -- otherwise get the lowest priority role
    local lowest_priority_p = nil
    local lowest_priority_r = nil
    for r, p in pairs(config.roles) do
      mc = p["media.class"] or "Audio/Sink"
      if tmc == mc and (lowest_priority_p == nil or
          p.priority < lowest_priority_p.priority) then
        lowest_priority_p = p
        lowest_priority_r = r
      end
    end
    return lowest_priority_r
  end
  return role
end

SimpleEventHook {
  name = "linking/find-virtual-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        lutils:unwrap_select_target_event (event)
    local target_class_assoc = {
      ["Stream/Input/Audio"] = "Audio/Source",
      ["Stream/Output/Audio"] = "Audio/Sink",
      ["Stream/Input/Video"] = "Video/Source",
    }
    local node = si:get_associated_proxy ("node")
    local highest_priority = -1
    local target = nil
    local role = node.properties["media.role"] or "Default"

    -- bypass the hook if the target is already picked up
    if target then
      return
    end

    -- dont use virtual target for any si-audio-virtual
    if si_props ["item.factory.name"] == "si-audio-virtual" then
      return
    end

    log:info (si, string.format ("handling item %d: %s (%s)", si.id,
        tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))

    -- get target media class
    local target_media_class = target_class_assoc[si_props ["media.class"]]
    if not target_media_class then
      log:info (si, "target media class not found")
      return
    end

    -- find highest priority virtual by role
    local media_role = findRole (role, target_media_class)
    if media_role == nil then
      log:info (si, "media role not found")
      return
    end

    for si_virtual in om:iterate {
      Constraint { "role", "=", media_role, type = "pw-global" },
      Constraint { "media.class", "=", target_media_class, type = "pw-global" },
      Constraint { "item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
    } do
      local priority = tonumber(si_virtual.properties["priority"])
      if priority > highest_priority then
        highest_priority = priority
        target = si_virtual
      end
    end

    local can_passthrough, passthrough_compatible
    if target then
      passthrough_compatible, can_passthrough =
      lutils.checkPassthroughCompatibility (si, target)

      if not passthrough_compatible then
        target = nil
      end
    end

    -- set target
    if target ~= nil then
      si_flags.can_passthrough = can_passthrough
      event:set_data ("target", target)
    end
  end
}:register ()
 07070100000177000081A400000000000000000000000165F8630400000B7F000000000000000000000000000000000000004100000000wireplumber-0.5.0/src/scripts/linking/get-filter-from-target.lua  -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Check if the target node is a filter target.

lutils = require ("linking-utils")
cutils = require ("common-utils")
futils = require ("filter-utils")
log = Log.open_topic ("s-linking")

SimpleEventHook {
  name = "linking/get-filter-from-target",
  after = "linking/find-best-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        lutils:unwrap_select_target_event (event)

    -- bypass the hook if the target was not found
    if target == nil then
      return
    end

    -- bypass the hook if the session item is a filter
    local node = si:get_associated_proxy ("node")
    local link_group = node.properties ["node.link-group"]
    if link_group ~= nil then
      return
    end

    -- bypass the hook if target is defined, is a filter and is targetable
    local target_node = target:get_associated_proxy ("node")
    local target_link_group = target_node.properties ["node.link-group"]
    local target_direction = cutils.getTargetDirection (si.properties)
    if target_link_group ~= nil and si_flags.has_defined_target then
      if futils.is_filter_smart (target_direction, target_link_group) and
          not futils.is_filter_disabled (target_direction, target_link_group) and
          futils.is_filter_targetable (target_direction, target_link_group) then
        return
      end
    end

    -- Get the filter from the given target if it exists, otherwise get the
    -- default filter, but only if target was not defined
    local target_direction = cutils.getTargetDirection (si.properties)
    local filter_target = futils.get_filter_from_target (target_direction, target)
    if filter_target ~= nil then
      target = filter_target
      log:info (si, "... got filter for given target")
    elseif filter_target == nil and not si_flags.has_defined_target then
      filter_target = futils.get_filter_from_target (target_direction, nil)
      if filter_target ~= nil then
        target = filter_target
        log:info (si, "... got default filter for given target")
      end
    end

    local can_passthrough, passthrough_compatible
    if target ~= nil then
      passthrough_compatible, can_passthrough =
      lutils.checkPassthroughCompatibility (si, target)
      if lutils.canLink (si_props, target) and passthrough_compatible then
        target_picked = true;
      end
    end

    if target_picked then
      log:info (si,
        string.format ("... target picked: %s (%s), can_passthrough:%s",
          tostring (target.properties ["node.name"]),
          tostring (target.properties ["node.id"]),
          tostring (can_passthrough)))
      si_flags.can_passthrough = can_passthrough
      event:set_data ("target", target)
    end
  end
}:register ()
 07070100000178000081A400000000000000000000000165F86304000016A0000000000000000000000000000000000000003600000000wireplumber-0.5.0/src/scripts/linking/link-target.lua -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Links a session item to the target that has been previously selected.
-- This is meant to be the last hook in the select-target chain.

lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-linking")

AsyncEventHook {
  name = "linking/link-target",
  after = "linking/prepare-link",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  steps = {
    start = {
      next = "none",
      execute = function (event, transition)
        local source, om, si, si_props, si_flags, target =
            lutils:unwrap_select_target_event (event)

        if not target then
          -- bypass the hook, nothing to link to.
          transition:advance ()
          return
        end

        local target_props = target.properties
        local out_item = nil
        local in_item = nil
        local si_link = nil
        local passthrough = si_flags.can_passthrough

        log:info (si, string.format ("handling item %d: %s (%s)", si.id,
            tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))

        local exclusive = cutils.parseBool (si_props ["node.exclusive"])

        -- break rescan if tried more than 5 times with same target
        if si_flags.failed_peer_id ~= nil and
            si_flags.failed_peer_id == target.id and
            si_flags.failed_count ~= nil and
            si_flags.failed_count > 5 then
          transition:return_error ("tried to link on last rescan, not retrying "
              .. tostring (si_link))
        end

        if si_props ["item.factory.name"] == "si-audio-virtual" then
          if si_props ["item.node.direction"] == "output" then
            -- playback
            out_item = target
            in_item = si
          else
            -- capture
            in_item = target
            out_item = si
          end
        else
          if si_props ["item.node.direction"] == "output" then
            -- playback
            out_item = si
            in_item = target
          else
            -- capture
            in_item = si
            out_item = target
          end
        end

        local is_virtual_client_link = target_props ["item.factory.name"] == "si-audio-virtual"

        log:info (si,
          string.format ("link %s <-> %s passthrough:%s, exclusive:%s, virtual-client:%s",
            tostring (si_props ["node.name"]),
            tostring (target_props ["node.name"]),
            tostring (passthrough),
            tostring (exclusive),
            tostring (is_virtual_client_link)))

        -- create and configure link
        si_link = SessionItem ("si-standard-link")
        if not si_link:configure {
          ["out.item"] = out_item,
          ["in.item"] = in_item,
          ["passthrough"] = passthrough,
          ["exclusive"] = exclusive,
          ["out.item.port.context"] = "output",
          ["in.item.port.context"] = "input",
          ["media.role"] = target_props["role"],
          ["target.media.class"] = target_props["media.class"],
          ["is.virtual.client.link"] = is_virtual_client_link,
          ["main.item.id"] = si.id,
          ["target.item.id"] = target.id,
        } then
          transition:return_error ("failed to configure si-standard-link "
            .. tostring (si_link))
          return
        end

        si_link:connect("link-error", function (_, error_msg)
          local ids = {si.id, si_flags.peer_id}

          for _, id in ipairs (ids) do
            local si = om:lookup {
              Constraint { "id", "=", id, type = "gobject" },
            }
            if si then
              local node = si:get_associated_proxy ("node")
              local client_id = node.properties["client.id"]
              if client_id then
                local client = om:lookup {
                  Constraint { "bound-id", "=", client_id, type = "gobject" }
                }
                if client then
                  log:info (node, "sending client error: " .. error_msg)
                  client:send_error (node["bound-id"], -32, error_msg)
                end
              end
            end
          end
        end)

        -- register
        si_flags.was_handled = true
        si_flags.peer_id = target.id
        si_flags.failed_peer_id = target.id
        if si_flags.failed_count ~= nil then
          si_flags.failed_count = si_flags.failed_count + 1
        else
          si_flags.failed_count = 1
        end
        si_link:register ()

        log:info (si_link, "registered virtual si-standard-link between "
                .. tostring (si).." and ".. tostring(target))

        -- only activate non virtual links because virtual links activation is
        -- handled by rescan-virtual-links.lua
        if not is_virtual_client_link then
          si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
            if e then
              transition:return_error ("failed to activate si-standard-link: "
                  .. tostring (si) .. " error:" .. tostring (e))
              if si_flags ~= nil then
                si_flags.peer_id = nil
              end
              l:remove ()
            else
              si_flags.failed_peer_id = nil
              if si_flags.peer_id == nil then
                si_flags.peer_id = target.id
              end
              si_flags.failed_count = 0

              log:info (l, "activated si-standard-link between "
                .. tostring (si).." and ".. tostring(target))

              transition:advance ()
            end
          end)
        else
          transition:advance ()
        end
      end,
    },
  },
}:register ()
07070100000179000081A400000000000000000000000165F8630400000F4E000000000000000000000000000000000000003700000000wireplumber-0.5.0/src/scripts/linking/prepare-link.lua    -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- remove the existing link if needed, check the properties of target, which
-- indicate it is not available for linking. If no target is available, send
-- down an error to the corresponding client.

lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-linking")

SimpleEventHook {
  name = "linking/prepare-link",
  after = "linking/get-filter-from-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, _, si, si_props, si_flags, target =
        lutils:unwrap_select_target_event (event)

    local si_id = si.id
    local reconnect = not cutils.parseBool (si_props ["node.dont-reconnect"])
    local exclusive = cutils.parseBool (si_props ["node.exclusive"])
    local si_must_passthrough = cutils.parseBool (si_props ["item.node.encoded-only"])

    log:info (si, string.format ("handling item %d: %s (%s)", si_id,
        tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))

    -- Check if item is linked to proper target, otherwise re-link
    if si_flags.peer_id then
      if target and si_flags.peer_id == target.id then
        log:info (si, "... already linked to proper target")

        -- Check this also here, in case in default targets changed
        if Settings.get_boolean ("linking.follow-default-target") and
            si_flags.has_node_defined_target then
          lutils.checkFollowDefault (si, target)
        end

        target = nil
        goto done
      end

      local link = lutils.lookupLink (si_id, si_flags.peer_id)
      if reconnect then
        if link ~= nil then
          -- remove old link
          if ((link:get_active_features () & Feature.SessionItem.ACTIVE) == 0)
          then
            -- remove also not yet activated links: they might never become
            -- active, and we need not wait for it to become active
            log:warning (link, "Link was not activated before removing")
          end
          si_flags.peer_id = nil
          link:remove ()
          log:info (si, "... moving to new target")
        end
      else
        if link ~= nil then
          log:info (si, "... dont-reconnect, not moving")
          goto done
        end
      end
    end

    -- if the stream has dont-reconnect and was already linked before,
    -- don't link it to a new target
    if not reconnect and si_flags.was_handled then
      target = nil
      goto done
    end

    -- check target's availability
    if target then
      local target_is_linked, target_is_exclusive = lutils.isLinked (target)
      if target_is_exclusive then
        log:info (si, "... target is linked exclusively")
        target = nil
      end

      if target_is_linked then
        if exclusive or si_must_passthrough then
          log:info (si, "... target is already linked, cannot link exclusively")
          target = nil
        else
          -- disable passthrough, we can live without it
          si_flags.can_passthrough = false
        end
      end
    end

    if not target then
      log:info (si, "... target not found, reconnect:" .. tostring (reconnect))

      local node = si:get_associated_proxy ("node")
      if reconnect and si_flags.was_handled then
        log:info (si, "... waiting reconnect")
        return
      end

      local linger = cutils.parseBool (si_props ["node.linger"])

      if linger then
        log:info (si, "... node linger")
        return
      end

      lutils.sendClientError (event, node,
          reconnect and "no target node available" or "target not found")

      if not reconnect then
        log:info (si, "... destroy node")
        node:request_destroy ()
      end
    end

    ::done::
    event:set_data ("target", target)
  end
}:register ()
  0707010000017A000081A400000000000000000000000165F8630400001F86000000000000000000000000000000000000003F00000000wireplumber-0.5.0/src/scripts/linking/rescan-virtual-links.lua    -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Julian Bouzas <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT

lutils = require ("linking-utils")
log = Log.open_topic ("s-linking")

defaults = {}
defaults.duck_level = 0.3

config = {}
config.duck_level = defaults.duck_level -- FIXME
config.roles = Conf.get_section_as_object ("virtual-item-roles")

-- enable ducking if mixer-api is loaded
mixer_api = Plugin.find("mixer-api")

function findRole (role)
  if role and not config.roles[role] then
    for r, p in pairs(config.roles) do
      if type(p.alias) == "table" then
        for i = 1, #(p.alias), 1 do
          if role == p.alias[i] then
            return r
          end
        end
      end
    end
  end
  return role
end

function getRolePriority (role)
  local r = role and config.roles[role] or nil
  return r and r.priority or 0
end

function getAction (dominant_role, other_role)
  -- default to "mix" if the role is not configured
  if not dominant_role or not config.roles[dominant_role] then
    return "mix"
  end

  local role_config = config.roles[dominant_role]
  return role_config["action." .. other_role]
      or role_config["action.default"]
      or "mix"
end

function restoreVolume (om, role, media_class)
  if not mixer_api then return end

  local si_v = om:lookup {
    type = "SiLinkable",
    Constraint { "item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
    Constraint { "media.role", "=", role, type = "pw" },
    Constraint { "media.class", "=", media_class, type = "pw" },
  }

  if si_v then
    local n = si_v:get_associated_proxy ("node")
    if n then
      log:debug(si_v, "restore role " .. role)
      mixer_api:call("set-volume", n["bound-id"], {
        monitorVolume = 1.0,
      })
    end
  end
end

function duckVolume (om, role, media_class)
  if not mixer_api then return end

  local si_v = om:lookup {
    type = "SiLinkable",
    Constraint { "item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
    Constraint { "media.role", "=", role, type = "pw" },
    Constraint { "media.class", "=", media_class, type = "pw" },
  }

  if si_v then
    local n = si_v:get_associated_proxy ("node")
    if n then
      log:debug(si_v, "duck role " .. role)
      mixer_api:call("set-volume", n["bound-id"], {
        monitorVolume = config.duck_level,
      })
    end
  end
end

function getSuspendPlaybackFromMetadata (om)
  local suspend = false
  local metadata = om:lookup {
    type = "metadata",
    Constraint { "metadata.name", "=", "default" },
  }
  if metadata then
    local value = metadata:find(0, "suspend.playback")
    if value then
      suspend = value == "1" and true or false
    end
  end
  return suspend
end

AsyncEventHook {
  name = "linking/rescan-virtual-links",
  interests = {
    EventInterest {
      -- on virtual client link added and removed
      Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
      Constraint { "event.session-item.interface", "=", "link" },
      Constraint { "is.virtual.client.link", "=", true },
    },
    EventInterest {
      -- on default metadata suspend.playback changed
      Constraint { "event.type", "=", "metadata-changed" },
      Constraint { "metadata.name", "=", "default" },
      Constraint { "event.subject.key", "=", "suspend.playback" },
    }
  },
  steps = {
    start = {
      next = "none",
      execute = function (event, transition)
        local source = event:get_source ()
        local om = source:call ("get-object-manager", "session-item")
        local metadata_om = source:call ("get-object-manager", "metadata")
        local suspend = getSuspendPlaybackFromMetadata (metadata_om)
        local pending_activations = 0
        local links = {
          ["Audio/Source"] = {},
          ["Audio/Sink"] = {},
          ["Video/Source"] = {},
        }

        -- gather info about links
        log:info ("Rescanning virtual si-standard-link links...")
        for silink in om:iterate {
            type = "SiLink",
            Constraint { "is.virtual.client.link", "=", true },
          } do

          -- deactivate all links if suspend playback metadata is present
          if suspend then
            silink:deactivate (Feature.SessionItem.ACTIVE)
          end

          local props = silink.properties
          local role = props["media.role"]
          local target_class = props["target.media.class"]
          local plugged = props["item.plugged.usec"]
          local active = ((silink:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
          if links[target_class] then
            table.insert(links[target_class], {
              silink = silink,
              role = findRole (role),
              active = active,
              priority = getRolePriority (role),
              plugged = plugged and tonumber(plugged) or 0
            })
          end
        end

        local function onVirtualLinkActivated (l, e)
          local si_id = tonumber (l.properties ["main.item.id"])
          local target_id = tonumber (l.properties ["target.item.id"])
          local si_flags = lutils:get_flags (si_id)

          if e then
            log:warning (l, "failed to activate virtual si-standard-link: " .. e)
            if si_flags ~= nil then
              si_flags.peer_id = nil
            end
            l:remove ()
          else
            log:info (l, "virtual si-standard-link activated successfully")
            si_flags.failed_peer_id = nil
            if si_flags.peer_id == nil then
              si_flags.peer_id = target_id
            end
            si_flags.failed_count = 0
          end

          -- advance only when all pending activations are completed
          pending_activations = pending_activations - 1
          if pending_activations <= 0 then
            log:info ("All virtual si-standard-links activated")
            transition:advance ()
          end
        end

        local function compareLinks(l1, l2)
          return (l1.priority > l2.priority) or
              ((l1.priority == l2.priority) and (l1.plugged > l2.plugged))
        end

        for media_class, v in pairs(links) do
          -- sort on priority and stream creation time
          table.sort(v, compareLinks)

          -- apply actions
          local first_link = v[1]
          if first_link then
            for i = 2, #v, 1 do
              local action = getAction(first_link.role, v[i].role)
              if action == "cork" then
                if v[i].active then
                  v[i].silink:deactivate(Feature.SessionItem.ACTIVE)
                end
              elseif action == "mix" then
                if not v[i].active and not suspend then
                  pending_activations = pending_activations + 1
                  v[i].silink:activate (Feature.SessionItem.ACTIVE,
                      onVirtualLinkActivated)
                end
                restoreVolume(om, v[i].role, media_class)
              elseif action == "duck" then
                if not v[i].active and not suspend then
                  pending_activations = pending_activations + 1
                  v[i].silink:activate (Feature.SessionItem.ACTIVE,
                      onVirtualLinkActivated)
                end
                duckVolume (om, v[i].role, media_class)
              else
                log:warning("Unknown action: " .. action)
              end
            end

            if not first_link.active and not suspend then
              pending_activations = pending_activations + 1
              first_link.silink:activate(Feature.SessionItem.ACTIVE,
                 onVirtualLinkActivated)
            end
            restoreVolume (om, first_link.role, media_class)
          end
        end

        -- just advance transition if no pending activations are needed
        if pending_activations <= 0 then
          log:info ("All virtual si-standard-links rescanned")
          transition:advance ()
        end
      end,
    },
  },
}:register ()
  0707010000017B000081A400000000000000000000000165F8630400001BC0000000000000000000000000000000000000003100000000wireplumber-0.5.0/src/scripts/linking/rescan.lua  -- WirePlumber
--
-- Copyright © 2020-2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Handle new linkables and trigger rescanning of the graph.
-- Rescan the graph by pushing new select-target events for
-- all linkables that need to be linked
-- Cleanup links when the linkables they are associated with are removed.
-- Also, cleanup flags attached to linkables.

lutils = require ("linking-utils")
cutils = require ("common-utils")
futils = require ("filter-utils")
log = Log.open_topic ("s-linking")
handles = {}

function checkFilter (si, om, handle_nonstreams)
  -- always handle filters if handle_nonstreams is true, even if it is disabled
  if handle_nonstreams then
    return true
  end

  -- always return true if this is not a filter
  local node = si:get_associated_proxy ("node")
  local link_group = node.properties["node.link-group"]
  if link_group == nil then
    return true
  end

  local direction = cutils.getTargetDirection (si.properties)

  -- always handle filters that are not smart
  if not futils.is_filter_smart (direction, link_group) then
    return true
  end

  -- dont handle smart filters that are disabled
  return not futils.is_filter_disabled (direction, link_group)
end

function checkLinkable (si, om, handle_nonstreams)
  local si_props = si.properties

  -- Always handle si-audio-virtual session items
  if si_props ["item.factory.name"] == "si-audio-virtual" then
    return true, si_props
  end

  -- For the rest of them, only handle stream session items
  if not si_props or (si_props ["item.node.type"] ~= "stream"
      and not handle_nonstreams) then
    return false, si_props
  end

  -- check filters
  if not checkFilter (si, om, handle_nonstreams) then
    return false, si_props
  end

  return true, si_props
end

function unhandleLinkable (si, om)
  local si_id = si.id
  local valid, si_props = checkLinkable (si, om, true)
  if not valid then
    return
  end

  log:info (si, string.format ("unhandling item %d", si_id))

  -- iterate over all the links in the graph and
  -- remove any links associated with this item
  for silink in om:iterate { type = "SiLink" } do
    local out_id = tonumber (silink.properties ["out.item.id"])
    local in_id = tonumber (silink.properties ["in.item.id"])

    if out_id == si_id or in_id == si_id then
      local in_flags = lutils:get_flags (in_id)
      local out_flags = lutils:get_flags (out_id)

      if out_id == si_id and in_flags.peer_id == out_id then
        in_flags.peer_id = nil
      elseif in_id == si_id and out_flags.peer_id == in_id then
        out_flags.peer_id = nil
      end

      silink:remove ()
      log:info (silink, "... link removed")
    end
  end

  lutils:clear_flags (si_id)
end

SimpleEventHook {
  name = "linking/linkable-removed",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "session-item-removed" },
      Constraint { "event.session-item.interface", "=", "linkable" },
    },
  },
  execute = function (event)
    local si = event:get_subject ()
    local source = event:get_source ()
    local om = source:call ("get-object-manager", "session-item")

    unhandleLinkable (si, om)
  end
}:register ()

function handleLinkables (source)
  local om = source:call ("get-object-manager", "session-item")

  for si in om:iterate { type = "SiLinkable" } do
    local valid, si_props = checkLinkable (si, om)
    if not valid then
      goto skip_linkable
    end

    -- check if we need to link this node at all
    local autoconnect = cutils.parseBool (si_props ["node.autoconnect"])
    if not autoconnect then
      log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected")
      goto skip_linkable
    end

    -- push event to find target and link
    source:call ("push-event", "select-target", si, nil)

    ::skip_linkable::
  end
end

SimpleEventHook {
  name = "linking/rescan",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "rescan-for-linking" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    local om = source:call ("get-object-manager", "session-item")

    log:info ("rescanning...")

    -- always unlink all filters that are smart and disabled
    for si in om:iterate {
        type = "SiLinkable",
        Constraint { "node.link-group", "+" },
    } do
      local node = si:get_associated_proxy ("node")
      local link_group = node.properties["node.link-group"]
      local direction = cutils.getTargetDirection (si.properties)
      if futils.is_filter_smart (direction, link_group) and
          futils.is_filter_disabled (direction, link_group) then
        unhandleLinkable (si, om)
      end
    end

    handleLinkables (source)
  end
}:register ()

SimpleEventHook {
  name = "linking/rescan-trigger",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
      Constraint { "event.session-item.interface", "=", "linkable" },
    },
    -- on device Routes changed
    EventInterest {
      Constraint { "event.type", "=", "device-params-changed" },
      Constraint { "event.subject.param-id", "c", "Route", "EnumRoute" },
    },
    -- on any "default" target changed
    EventInterest {
      Constraint { "event.type", "=", "metadata-changed" },
      Constraint { "metadata.name", "=", "default" },
      Constraint { "event.subject.key", "c", "default.audio.source",
          "default.audio.sink", "default.video.source" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    source:call ("schedule-rescan", "linking")
  end
}:register ()

SimpleEventHook {
  name = "linking/rescan-trigger-on-filters-metadata-changed",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "metadata-changed" },
      Constraint { "metadata.name", "=", "filters" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    source:call ("schedule-rescan", "linking")
  end
}:register ()

function handleMoveSetting (enable)
  if (not handles.move_hook) and (enable == true) then
    handles.move_hook = SimpleEventHook {
      name = "linking/rescan-trigger-on-target-metadata-changed",
      interests = {
        EventInterest {
          Constraint { "event.type", "=", "metadata-changed" },
          Constraint { "metadata.name", "=", "default" },
          Constraint { "event.subject.key", "c", "target.object", "target.node" },
        },
      },
      execute = function (event)
        local source = event:get_source ()
        source:call ("schedule-rescan", "linking")
      end
    }
    handles.move_hook:register()
  elseif (handles.move_hook) and (enable == false) then
    handles.move_hook:remove ()
    handles.move_hook = nil
  end
end

Settings.subscribe ("linking.allow-moving-streams", function ()
  handleMoveSetting (Settings.get_boolean ("linking.allow-moving-streams"))
end)
handleMoveSetting (Settings.get_boolean ("linking.allow-moving-streams"))
0707010000017C000081A400000000000000000000000165F8630400000296000000000000000000000000000000000000002B00000000wireplumber-0.5.0/src/scripts/metadata.lua    -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Provides the default metadata object

Script.async_activation = true

-- note that args is a WpSpaJson
local args = ...
args = args:parse(1)

local metadata_name = args["metadata.name"]

log = Log.open_topic ("s-metadata")
log:info ("creating metadata object: " .. metadata_name)

impl_metadata = ImplMetadata (metadata_name)
impl_metadata:activate (Features.ALL, function (m, e)
  if e then
    Script:finish_activation_with_error (
        "failed to activate the ".. metadata_name .." metadata: " .. tostring (e))
  else
    Script:finish_activation ()
  end
end)
  0707010000017D000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002700000000wireplumber-0.5.0/src/scripts/monitors    0707010000017E000081A400000000000000000000000165F86304000007DE000000000000000000000000000000000000003500000000wireplumber-0.5.0/src/scripts/monitors/alsa-midi.lua  -- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
log = Log.open_topic ("s-monitors")

defaults = {}
defaults.node_properties = {  -- Midi bridge node properties
  ["factory.name"] = "api.alsa.seq.bridge",

  -- Name set for the node with ALSA MIDI ports
  ["node.name"] = "Midi-Bridge",

  -- Set priorities so that it can be used as a fallback driver (see pipewire#3562)
  ["priority.session"] = "100",
  ["priority.driver"] = "1",
}

config = {}
config.monitoring = Core.test_feature ("monitor.alsa-midi.monitoring")
config.node_properties = Conf.get_section_as_properties (
    "monitor.alsa-midi.properties", defaults.node_properties)

SND_PATH = "/dev/snd"
SEQ_NAME = "seq"
SND_SEQ_PATH = SND_PATH .. "/" .. SEQ_NAME

midi_node = nil
fm_plugin = nil

function CreateMidiNode ()
  -- create the midi node
  local node = Node("spa-node-factory", config.node_properties)
  node:activate(Feature.Proxy.BOUND, function (n)
    log:info ("activated Midi bridge")
  end)

  return node;
end

if GLib.access (SND_SEQ_PATH, "rw") then
  midi_node = CreateMidiNode ()
elseif config.monitoring then
  fm_plugin = Plugin.find("file-monitor-api")
end

-- Only monitor the MIDI device if file does not exist and plugin API is loaded
if midi_node == nil and fm_plugin ~= nil then
  -- listen for changed events
  fm_plugin:connect ("changed", function (o, file, old, evtype)
    -- files attributes changed
    if evtype == "attribute-changed" then
      if file ~= SND_SEQ_PATH then
        return
      end
      if midi_node == nil and GLib.access (SND_SEQ_PATH, "rw") then
        midi_node = CreateMidiNode ()
        fm_plugin:call ("remove-watch", SND_PATH)
      end
    end

    -- directory is going to be unmounted
    if evtype == "pre-unmount" then
      fm_plugin:call ("remove-watch", SND_PATH)
    end
  end)

  -- add watch
  fm_plugin:call ("add-watch", SND_PATH, "m")
end
  0707010000017F000081A400000000000000000000000165F8630400002F5F000000000000000000000000000000000000003000000000wireplumber-0.5.0/src/scripts/monitors/alsa.lua   -- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
log = Log.open_topic ("s-monitors")

config = {}
config.reserve_device = Core.test_feature ("monitor.alsa.reserve-device")
config.properties = Conf.get_section_as_properties ("monitor.alsa.properties")
config.rules = Conf.get_section_as_json ("monitor.alsa.rules", Json.Array {})

-- unique device/node name tables
device_names_table = nil
node_names_table = nil

function nonempty(str)
  return str ~= "" and str or nil
end

function applyDefaultDeviceProperties (properties)
  properties["api.alsa.use-acp"] = true
  properties["api.acp.auto-port"] = false
  properties["api.dbus.ReserveDevice1.Priority"] = -20
end

function createNode(parent, id, obj_type, factory, properties)
  local dev_props = parent.properties

  -- set the device id and spa factory name; REQUIRED, do not change
  properties["device.id"] = parent["bound-id"]
  properties["factory.name"] = factory

  -- set the default pause-on-idle setting
  properties["node.pause-on-idle"] = false

  -- try to negotiate the max ammount of channels
  if dev_props["api.alsa.use-acp"] ~= "true" then
    properties["audio.channels"] = properties["audio.channels"] or "64"
  end

  local dev = properties["api.alsa.pcm.device"]
              or properties["alsa.device"] or "0"
  local subdev = properties["api.alsa.pcm.subdevice"]
                 or properties["alsa.subdevice"] or "0"
  local stream = properties["api.alsa.pcm.stream"] or "unknown"
  local profile = properties["device.profile.name"]
                  or (stream .. "." .. dev .. "." .. subdev)
  local profile_desc = properties["device.profile.description"]

  -- set priority
  if not properties["priority.driver"] then
    local priority = (dev == "0") and 1000 or 744
    if stream == "capture" then
      priority = priority + 1000
    end

    priority = priority - (tonumber(dev) * 16) - tonumber(subdev)

    if profile:find("^pro%-") then
      priority = priority + 500
    elseif profile:find("^analog%-") then
      priority = priority + 9
    elseif profile:find("^iec958%-") then
      priority = priority + 8
    end

    properties["priority.driver"] = priority
    properties["priority.session"] = priority
  end

  -- ensure the node has a media class
  if not properties["media.class"] then
    if stream == "capture" then
      properties["media.class"] = "Audio/Source"
    else
      properties["media.class"] = "Audio/Sink"
    end
  end

  -- ensure the node has a name
  if not properties["node.name"] then
    local name =
        (stream == "capture" and "alsa_input" or "alsa_output")
        .. "." ..
        (dev_props["device.name"]:gsub("^alsa_card%.(.+)", "%1") or
         dev_props["device.name"] or
         "unnamed-device")
         .. "." ..
         profile

    -- sanitize name
    name = name:gsub("([^%w_%-%.])", "_")

    properties["node.name"] = name

    -- deduplicate nodes with the same name
    for counter = 2, 99, 1 do
      if node_names_table[properties["node.name"]] ~= true then
        node_names_table[properties["node.name"]] = true
        break
      end
      properties["node.name"] = name .. "." .. counter
    end
  end

  -- and a nick
  local nick = nonempty(properties["node.nick"])
      or nonempty(properties["api.alsa.pcm.name"])
      or nonempty(properties["alsa.name"])
      or nonempty(profile_desc)
      or dev_props["device.nick"]
  if nick == "USB Audio" then
    nick = dev_props["device.nick"]
  end
  -- also sanitize nick, replace ':' with ' '
  properties["node.nick"] = nick:gsub("(:)", " ")

  -- ensure the node has a description
  if not properties["node.description"] then
    local desc = nonempty(dev_props["device.description"]) or "unknown"
    local name = nonempty(properties["api.alsa.pcm.name"]) or
                 nonempty(properties["api.alsa.pcm.id"]) or dev

    if profile_desc then
      desc = desc .. " " .. profile_desc
    elseif subdev ~= "0" then
      desc = desc .. " (" .. name .. " " .. subdev .. ")"
    elseif dev ~= "0" then
      desc = desc .. " (" .. name .. ")"
    end

    -- also sanitize description, replace ':' with ' '
    properties["node.description"] = desc:gsub("(:)", " ")
  end

  -- add api.alsa.card.* properties for rule matching purposes
  for k, v in pairs(dev_props) do
    if k:find("^api%.alsa%.card%..*") then
      properties[k] = v
    end
  end

  -- add cpu.vm.name for rule matching purposes
  local vm_type = Core.get_vm_type()
  if nonempty(vm_type) then
    properties["cpu.vm.name"] = vm_type
  end

  -- apply properties from rules defined in JSON .conf file
  properties = JsonUtils.match_rules_update_properties (config.rules, properties)

  if cutils.parseBool (properties ["node.disabled"]) then
    log:notice ("ALSA node " .. properties["node.name"] .. " disabled")
    node_names_table [properties ["node.name"]] = nil
    return
  end

  -- create the node
  local node = Node("adapter", properties)
  node:activate(Feature.Proxy.BOUND)
  parent:store_managed_object(id, node)
end

function createDevice(parent, id, factory, properties)
  local device = SpaDevice(factory, properties)
  if device then
    device:connect("create-object", createNode)
    device:connect("object-removed", function (parent, id)
      local node = parent:get_managed_object(id)
      if not node then
        return
      end

      node_names_table[node.properties["node.name"]] = nil
    end)
    device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
    parent:store_managed_object(id, device)
  else
    log:warning ("Failed to create '" .. factory .. "' device")
  end
end

function prepareDevice(parent, id, obj_type, factory, properties)
  -- ensure the device has an appropriate name
  local name = "alsa_card." ..
    (properties["device.name"] or
     properties["device.bus-id"] or
     properties["device.bus-path"] or
     tostring(id)):gsub("([^%w_%-%.])", "_")

  properties["device.name"] = name

  -- deduplicate devices with the same name
  for counter = 2, 99, 1 do
    if device_names_table[properties["device.name"]] ~= true then
      device_names_table[properties["device.name"]] = true
      break
    end
    properties["device.name"] = name .. "." .. counter
  end

  -- ensure the device has a description
  if not properties["device.description"] then
    local d = nil
    local f = properties["device.form-factor"]
    local c = properties["device.class"]
    local n = properties["api.alsa.card.name"]

    if n == "Loopback" then
      d = I18n.gettext("Loopback")
    elseif f == "internal" then
      d = I18n.gettext("Built-in Audio")
    elseif c == "modem" then
      d = I18n.gettext("Modem")
    end

    d = d or properties["device.product.name"]
          or properties["api.alsa.card.name"]
          or properties["alsa.card_name"]
          or "Unknown device"
    properties["device.description"] = d
  end

  -- ensure the device has a nick
  properties["device.nick"] =
      properties["device.nick"] or
      properties["api.alsa.card.name"] or
      properties["alsa.card_name"]

  -- set the icon name
  if not properties["device.icon-name"] then
    local icon = nil
    local icon_map = {
      -- form factor -> icon
      ["microphone"] = "audio-input-microphone",
      ["webcam"] = "camera-web",
      ["handset"] = "phone",
      ["portable"] = "multimedia-player",
      ["tv"] = "video-display",
      ["headset"] = "audio-headset",
      ["headphone"] = "audio-headphones",
      ["speaker"] = "audio-speakers",
      ["hands-free"] = "audio-handsfree",
    }
    local f = properties["device.form-factor"]
    local c = properties["device.class"]
    local b = properties["device.bus"]

    icon = icon_map[f] or ((c == "modem") and "modem") or "audio-card"
    properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
  end

  -- apply properties from rules defined in JSON .conf file
  applyDefaultDeviceProperties (properties)
  properties = JsonUtils.match_rules_update_properties (config.rules, properties)

  if cutils.parseBool (properties ["device.disabled"]) then
    log:notice ("ALSA card/device " .. properties ["device.name"] .. " disabled")
    device_names_table [properties ["device.name"]] = nil
    return
  end

  -- override the device factory to use ACP
  if cutils.parseBool (properties ["api.alsa.use-acp"]) then
    log:info("Enabling the use of ACP on " .. properties["device.name"])
    factory = "api.alsa.acp.device"
  end

  -- use device reservation, if available
  if rd_plugin and properties["api.alsa.card"] then
    local rd_name = "Audio" .. properties["api.alsa.card"]
    local rd = rd_plugin:call("create-reservation",
        rd_name,
        cutils.get_application_name (),
        properties["device.name"],
        properties["api.dbus.ReserveDevice1.Priority"]);

    properties["api.dbus.ReserveDevice1"] = rd_name

    -- unlike pipewire-media-session, this logic here keeps the device
    -- acquired at all times and destroys it if someone else acquires
    rd:connect("notify::state", function (rd, pspec)
      local state = rd["state"]

      if state == "acquired" then
        -- create the device
        createDevice(parent, id, factory, properties)

      elseif state == "available" then
        -- attempt to acquire again
        rd:call("acquire")

      elseif state == "busy" then
        -- destroy the device
        parent:store_managed_object(id, nil)
      end
    end)

    rd:connect("release-requested", function (rd)
        log:info("release requested")
        parent:store_managed_object(id, nil)
        rd:call("release")
    end)

    rd:call("acquire")
  else
    -- create the device
    createDevice(parent, id, factory, properties)
  end
end

function createMonitor ()
  local m = SpaDevice("api.alsa.enum.udev", config.properties)
  if m == nil then
    log:notice("PipeWire's ALSA SPA plugin is missing or broken. " ..
        "Sound cards will not be supported")
    return nil
  end

  -- handle create-object to prepare device
  m:connect("create-object", prepareDevice)

  -- handle object-removed to destroy device reservations and recycle device name
  m:connect("object-removed", function (parent, id)
    local device = parent:get_managed_object(id)
    if not device then
      return
    end

    if rd_plugin then
      local rd_name = device.properties["api.dbus.ReserveDevice1"]
      if rd_name then
        rd_plugin:call("destroy-reservation", rd_name)
      end
    end
    device_names_table[device.properties["device.name"]] = nil
    for managed_node in device:iterate_managed_objects() do
      node_names_table[managed_node.properties["node.name"]] = nil
    end
  end)

  -- reset the name tables to make sure names are recycled
  device_names_table = {}
  node_names_table = {}

  -- activate monitor
  log:info("Activating ALSA monitor")
  m:activate(Feature.SpaDevice.ENABLED)
  return m
end

-- if the reserve-device plugin is enabled, at the point of script execution
-- it is expected to be connected. if it is not, assume the d-bus connection
-- has failed and continue without it
if config.reserve_device then
  rd_plugin = Plugin.find("reserve-device")
end
if rd_plugin and rd_plugin:call("get-dbus")["state"] ~= "connected" then
  log:notice("reserve-device plugin is not connected to D-Bus, "
              .. "disabling device reservation")
  rd_plugin = nil
end

-- handle rd_plugin state changes to destroy and re-create the ALSA monitor in
-- case D-Bus service is restarted
if rd_plugin then
  local dbus = rd_plugin:call("get-dbus")
  dbus:connect("notify::state", function (b, pspec)
    local state = b["state"]
    log:info ("rd-plugin state changed to " .. state)
    if state == "connected" then
      log:info ("Creating ALSA monitor")
      monitor = createMonitor()
    elseif state == "closed" then
      log:info ("Destroying ALSA monitor")
      monitor = nil
    end
  end)
end

-- create the monitor
monitor = createMonitor()
 07070100000180000081A400000000000000000000000165F86304000012D7000000000000000000000000000000000000003600000000wireplumber-0.5.0/src/scripts/monitors/bluez-midi.lua -- WirePlumber
--
-- Copyright © 2022 Pauli Virtanen
--    @author Pauli Virtanen
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
log = Log.open_topic ("s-monitors")

defaults = {}
defaults.servers = { "bluez_midi.server" }

config = {}
config.seat_monitoring = Core.test_feature ("monitor.bluez.seat-monitoring")
config.properties = Conf.get_section_as_properties ("monitor.bluez-midi.properties")
config.servers = Conf.get_section_as_array ("monitor.bluez-midi.servers", defaults.servers)
config.rules = Conf.get_section_as_json ("monitor.bluez-midi.rules", Json.Array {})

-- unique device/node name tables
node_names_table = nil
id_to_name_table = nil

function setLatencyOffset(node, offset_msec)
  if not offset_msec then
    return
  end

  local props = { "Spa:Pod:Object:Param:Props", "Props" }
  props.latencyOffsetNsec = tonumber(offset_msec) * 1000000

  local param = Pod.Object(props)
  log:debug(param, "setting latency offset on " .. tostring(node))
  node:set_param("Props", param)
end

function createNode(parent, id, type, factory, properties)
  properties["factory.name"] = factory

  -- set the node description
  local desc = properties["node.description"]
  -- sanitize description, replace ':' with ' '
  properties["node.description"] = desc:gsub("(:)", " ")

  -- set the node name
  local name =
      "bluez_midi." .. properties["api.bluez5.address"]
  -- sanitize name
  name = name:gsub("([^%w_%-%.])", "_")
  -- deduplicate nodes with the same name
  properties["node.name"] = name
  for counter = 2, 99, 1 do
    if node_names_table[properties["node.name"]] ~= true then
      node_names_table[properties["node.name"]] = true
      break
    end
    properties["node.name"] = name .. "." .. counter
  end

  properties["api.glib.mainloop"] = "true"

  -- apply properties from the rules in the configuration file
  properties = JsonUtils.match_rules_update_properties (config.rules, properties)

  local latency_offset = properties["node.latency-offset-msec"]
  properties["node.latency-offset-msec"] = nil

  -- create the node
  -- it doesn't necessarily need to be a local node,
  -- the other Bluetooth parts run in the local process,
  -- so it's consistent to have also this here
  local node = LocalNode("spa-node-factory", properties)
  node:activate(Feature.Proxy.BOUND)
  parent:store_managed_object(id, node)
  id_to_name_table[id] = properties["node.name"]
  setLatencyOffset(node, latency_offset)
end

function createMonitor()
  local monitor_props = {}
  for k, v in pairs(config.properties or {}) do
    monitor_props[k] = v
  end

  monitor_props["api.glib.mainloop"] = "true"

  local monitor = SpaDevice("api.bluez5.midi.enum", monitor_props)
  if monitor then
    monitor:connect("create-object", createNode)
    monitor:connect("object-removed", function (parent, id)
        node_names_table[id_to_name_table[id]] = nil
        id_to_name_table[id] = nil
    end)
  else
    log:notice("PipeWire's BlueZ MIDI SPA missing or broken. Bluetooth not supported.")
    return nil
  end

  -- reset the name tables to make sure names are recycled
  node_names_table = {}
  id_to_name_table = {}

  monitor:activate(Feature.SpaDevice.ENABLED)
  return monitor
end

function createServers()
  local servers = {}
  local i = 1

  for k, v in pairs(config.servers) do
    local node_props = {
      ["node.name"] = v,
      ["node.description"] = string.format(I18n.gettext("BLE MIDI %d"), i),
      ["api.bluez5.role"] = "server",
      ["factory.name"] = "api.bluez5.midi.node",
      ["api.glib.mainloop"] = "true",
    }
    node_props = JsonUtils.match_rules_update_properties (config.rules, node_props)

    local latency_offset = node_props["node.latency-offset-msec"]
    node_props["node.latency-offset-msec"] = nil

    local node = LocalNode("spa-node-factory", node_props)
    if node then
      node:activate(Feature.Proxy.BOUND)
      table.insert(servers, node)
      setLatencyOffset(node, latency_offset)
    else
      log:notice("Failed to create BLE MIDI server.")
    end
    i = i + 1
  end

  return servers
end

if config.seat_monitoring then
  logind_plugin = Plugin.find("logind")
end
if logind_plugin then
  -- if logind support is enabled, activate
  -- the monitor only when the seat is active
  function startStopMonitor(seat_state)
    log:info(logind_plugin, "Seat state changed: " .. seat_state)

    if seat_state == "active" then
      monitor = createMonitor()
      servers = createServers()
    elseif monitor then
      monitor:deactivate(Feature.SpaDevice.ENABLED)
      monitor = nil
      servers = nil
    end
  end

  logind_plugin:connect("state-changed", function(p, s) startStopMonitor(s) end)
  startStopMonitor(logind_plugin:call("get-state"))
else
  monitor = createMonitor()
  servers = createServers()
end
 07070100000181000081A400000000000000000000000165F863040000402A000000000000000000000000000000000000003100000000wireplumber-0.5.0/src/scripts/monitors/bluez.lua  -- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT

COMBINE_OFFSET = 64
LOOPBACK_SOURCE_ID = 128
DEVICE_SOURCE_ID = 0

cutils = require ("common-utils")
log = Log.open_topic ("s-monitors")

config = {}
config.seat_monitoring = Core.test_feature ("monitor.bluez.seat-monitoring")
config.properties = Conf.get_section_as_properties ("monitor.bluez.properties")
config.rules = Conf.get_section_as_json ("monitor.bluez.rules", Json.Array {})

-- This is not a setting, it must always be enabled
config.properties["api.bluez5.connection-info"] = true

devices_om = ObjectManager {
  Interest {
    type = "device",
  }
}

nodes_om = ObjectManager {
  Interest {
    type = "node",
    Constraint { "node.name", "#", "*.bluez_*put*"},
    Constraint { "device.id", "+" },
  }
}

function setOffloadActive(device, value)
  local pod = Pod.Object {
    "Spa:Pod:Object:Param:Props", "Props", bluetoothOffloadActive = value
  }
  device:set_params("Props", pod)
end

nodes_om:connect("object-added", function(_, node)
  node:connect("state-changed", function(node, old_state, cur_state)
    local interest = Interest {
      type = "device",
      Constraint { "object.id", "=", node.properties["device.id"]}
    }
    for d in devices_om:iterate (interest) do
      if cur_state == "running" then
        setOffloadActive(d, true)
      else
        setOffloadActive(d, false)
      end
    end
  end)
end)

function createOffloadScoNode(parent, id, type, factory, properties)
  local dev_props = parent.properties

  local args = {
    ["audio.channels"] = 1,
    ["audio.position"] = "[MONO]",
  }

  local desc =
      dev_props["device.description"]
      or dev_props["device.name"]
      or dev_props["device.nick"]
      or dev_props["device.alias"]
      or "bluetooth-device"
  -- sanitize description, replace ':' with ' '
  args["node.description"] = desc:gsub("(:)", " ")

  if factory:find("sink") then
    local capture_args = {
      ["device.id"] = parent["bound-id"],
      ["media.class"] = "Audio/Sink",
      ["node.pause-on-idle"] = false,
    }
    for k, v in pairs(properties) do
      capture_args[k] = v
    end

    local name = "bluez_output" .. "." .. (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." .. tostring(id)
    args["node.name"] = name:gsub("([^%w_%-%.])", "_")
    args["capture.props"] = Json.Object(capture_args)
    args["playback.props"] = Json.Object {
      ["node.passive"] = true,
      ["node.pause-on-idle"] = false,
    }
  elseif factory:find("source") then
    local playback_args = {
      ["device.id"] = parent["bound-id"],
      ["media.class"] = "Audio/Source",
      ["node.pause-on-idle"] = false,
    }
    for k, v in pairs(properties) do
      playback_args[k] = v
    end

    local name = "bluez_input" .. "." .. (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." .. tostring(id)
    args["node.name"] = name:gsub("([^%w_%-%.])", "_")
    args["capture.props"] = Json.Object {
      ["node.passive"] = true,
      ["node.pause-on-idle"] = false,
    }
    args["playback.props"] = Json.Object(playback_args)
  else
    log:warning(parent, "Unsupported factory: " .. factory)
    return
  end

  -- Transform 'args' to a json object here
  local args_json = Json.Object(args)

  -- and get the final JSON as a string from the json object
  local args_string = args_json:get_data()

  local loopback_properties = {}

  local loopback = LocalModule("libpipewire-module-loopback", args_string, loopback_properties)
  parent:store_managed_object(id, loopback)
end

device_set_nodes_om = ObjectManager {
  Interest {
    type = "node",
    Constraint { "api.bluez5.set.leader", "+", type = "pw" },
  }
}

device_set_nodes_om:connect ("object-added", function(_, node)
    -- Connect ObjectConfig events to the right node
    if not monitor then
      return
    end

    local interest = Interest {
      type = "device",
      Constraint { "object.id", "=", node.properties["device.id"] }
    }
    log:info("Device set node found: " .. tostring (node["bound-id"]))
    for device in devices_om:iterate (interest) do
      local device_id = device.properties["api.bluez5.id"]
      if not device_id then
        goto next_device
      end

      local spa_device = monitor:get_managed_object (tonumber (device_id))
      if not spa_device then
        goto next_device
      end

      local id = node.properties["card.profile.device"]
      if id ~= nil then
        log:info(".. assign to device: " .. tostring (device["bound-id"]) .. " node " .. tostring (id))
        spa_device:store_managed_object (id, node)

        -- set routes again to update volumes etc.
        for route in device:iterate_params ("Route") do
          device:set_param ("Route", route)
        end
      end

      ::next_device::
    end
end)

function createSetNode(parent, id, type, factory, properties)
  local args = {}
  local name
  local target_class
  local stream_class
  local rules = {}
  local members_json = Json.Raw (properties["api.bluez5.set.members"])
  local channels_json = Json.Raw (properties["api.bluez5.set.channels"])
  local members = members_json:parse ()
  local channels = channels_json:parse ()

  if properties["media.class"] == "Audio/Sink" then
    name = "bluez_output_internal"
    args["combine.mode"] = "sink"
    target_class = "Audio/Sink/Internal"
    stream_class = "Stream/Output/Audio/Internal"
  else
    name = "bluez_input_internal"
    args["combine.mode"] = "source"
    target_class = "Audio/Source/Internal"
    stream_class = "Stream/Input/Audio/Internal"
  end

  log:info("Device set: " .. properties["node.name"])

  for _, member in pairs(members) do
    log:info("Device set member:" .. member["object.path"])
    table.insert(rules,
      Json.Object {
        ["matches"] = Json.Array {
          Json.Object {
            ["object.path"] = member["object.path"],
            ["media.class"] = target_class,
          },
        },
        ["actions"] = Json.Object {
          ["create-stream"] = Json.Object {
            ["media.class"] = stream_class,
            ["audio.position"] = Json.Array (member["channels"]),
          }
        },
      }
    )
  end

  properties["node.name"] = name
  properties["node.virtual"] = false
  properties["device.api"] = "bluez5"
  properties["api.bluez5.set.members"] = nil
  properties["api.bluez5.set.channels"] = nil
  properties["api.bluez5.set.leader"] = true
  properties["audio.position"] = Json.Array (channels)
  args["combine.props"] = Json.Object (properties)
  args["stream.props"] = Json.Object {}
  args["stream.rules"] = Json.Array (rules)

  local args_json = Json.Object(args)
  local args_string = args_json:get_data()
  local combine_properties = {}
  log:info("Device set node: " .. args_string)
  return LocalModule("libpipewire-module-combine-stream", args_string, combine_properties)
end

function createNode(parent, id, type, factory, properties)
  local dev_props = parent.properties
  local parent_id = parent["bound-id"]

  if cutils.parseBool (config.properties ["bluez5.hw-offload-sco"]) and factory:find("sco") then
    createOffloadScoNode(parent, id, type, factory, properties)
    return
  end

  -- set the device id and spa factory name; REQUIRED, do not change
  properties["device.id"] = parent_id
  properties["factory.name"] = factory

  -- set the default pause-on-idle setting
  properties["node.pause-on-idle"] = false

  -- set the node description
  local desc =
      dev_props["device.description"]
      or dev_props["device.name"]
      or dev_props["device.nick"]
      or dev_props["device.alias"]
      or "bluetooth-device"
  -- sanitize description, replace ':' with ' '
  properties["node.description"] = desc:gsub("(:)", " ")

  local name_prefix = ((factory:find("sink") and "bluez_output") or
       (factory:find("source") and "bluez_input" or factory))

  -- hide the sco-source node because we use the loopback source instead
  if factory == "api.bluez5.sco.source" then
    properties["api.bluez5.internal"] = true
    -- add 'internal' to name prefix to not be confused with loopback node
    name_prefix = name_prefix .. "_internal"
  end

  -- set the node name
  local name = name_prefix .. "." ..
      (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
      tostring(id)
  -- sanitize name
  properties["node.name"] = name:gsub("([^%w_%-%.])", "_")

  -- set priority
  if not properties["priority.driver"] then
    local priority = factory:find("source") and 2010 or 1010
    properties["priority.driver"] = priority
    properties["priority.session"] = priority
  end

  -- autoconnect if it's a stream
  if properties["api.bluez5.profile"] == "headset-audio-gateway" or
     properties["api.bluez5.profile"] == "bap-sink" or
     factory:find("a2dp.source") or factory:find("media.source") then
    properties["node.autoconnect"] = true
  end

  -- apply properties from the rules in the configuration file
  properties = JsonUtils.match_rules_update_properties (config.rules, properties)

  -- create the node; bluez requires "local" nodes, i.e. ones that run in
  -- the same process as the spa device, for several reasons

  if properties["api.bluez5.set.leader"] then
    local combine = createSetNode(parent, id, type, factory, properties)
    parent:store_managed_object(id + COMBINE_OFFSET, combine)
  else
    properties["bluez5.loopback"] = false
    local node = LocalNode("adapter", properties)
    node:activate(Feature.Proxy.BOUND)
    parent:store_managed_object(id, node)
  end
end

function removeNode(parent, id)
  -- Clear also the device set module, if any
  parent:store_managed_object(id + COMBINE_OFFSET, nil)
end

function createDevice(parent, id, type, factory, properties)
  local device = parent:get_managed_object(id)
  if not device then
    -- ensure a proper device name
    local name =
        (properties["device.name"] or
         properties["api.bluez5.address"] or
         properties["device.description"] or
         tostring(id)):gsub("([^%w_%-%.])", "_")

    if not name:find("^bluez_card%.", 1) then
      name = "bluez_card." .. name
    end
    properties["device.name"] = name

    -- set the icon name
    if not properties["device.icon-name"] then
      local icon = nil
      local icon_map = {
        -- form factor -> icon
        ["microphone"] = "audio-input-microphone",
        ["webcam"] = "camera-web",
        ["handset"] = "phone",
        ["portable"] = "multimedia-player",
        ["tv"] = "video-display",
        ["headset"] = "audio-headset",
        ["headphone"] = "audio-headphones",
        ["speaker"] = "audio-speakers",
        ["hands-free"] = "audio-handsfree",
      }
      local f = properties["device.form-factor"]
      local b = properties["device.bus"]

      icon = icon_map[f] or "audio-card"
      properties["device.icon-name"] = icon .. (b and ("-" .. b) or "")
    end

    -- initial profile is to be set by policy-device-profile.lua, not spa-bluez5
    properties["bluez5.profile"] = "off"
    properties["api.bluez5.id"] = id

    -- apply properties from the rules in the configuration file
    properties = JsonUtils.match_rules_update_properties (config.rules, properties)

    -- create the device
    device = SpaDevice(factory, properties)
    if device then
      device:connect("create-object", createNode)
      device:connect("object-removed", removeNode)
      parent:store_managed_object(id, device)
    else
      log:warning ("Failed to create '" .. factory .. "' device")
      return
    end
  end

  log:info(parent, string.format("%d, %s (%s): %s",
        id, properties["device.description"],
        properties["api.bluez5.address"], properties["api.bluez5.connection"]))

  -- activate the device after the bluez profiles are connected
  if properties["api.bluez5.connection"] == "connected" then
    device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
  else
    device:deactivate(Features.ALL)
  end
end

function createMonitor()
  local monitor = SpaDevice("api.bluez5.enum.dbus", config.properties)
  if monitor then
    monitor:connect("create-object", createDevice)
  else
    log:notice("PipeWire's BlueZ SPA plugin is missing or broken. " ..
        "Bluetooth devices will not be supported.")
    return nil
  end
  monitor:activate(Feature.SpaDevice.ENABLED)

  return monitor
end

function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
  local args = Json.Object {
    ["capture.props"] = Json.Object {
      ["node.name"] = string.format ("bluez_capture_internal.%s", dev_name),
      ["media.class"] = "Stream/Input/Audio/Internal",
      ["node.description"] =
          string.format ("Bluetooth internal capture stream for %s", dec_desc),
      ["audio.channels"] = 1,
      ["audio.position"] = "[MONO]",
      ["bluez5.loopback"] = true,
      ["stream.dont-remix"] = true,
      ["node.passive"] = true,
      ["node.dont-fallback"] = true,
      ["node.linger"] = true
    },
    ["playback.props"] = Json.Object {
      ["node.name"] = string.format ("bluez_input.%s", dev_name),
      ["node.description"] = string.format ("%s", dec_desc),
      ["audio.position"] = "[MONO]",
      ["media.class"] = "Audio/Source",
      ["device.id"] = dev_id,
      ["card.profile.device"] = DEVICE_SOURCE_ID,
      ["priority.driver"] = 2010,
      ["priority.session"] = 2010,
      ["bluez5.loopback"] = true,
      ["filter.smart"] = true,
      ["filter.smart.target"] = Json.Object {
        ["factory.name"] = "api.bluez5.sco.source",
        ["device.api"] = "bluez5",
        ["bluez5.loopback"] = false,
        ["device.id"] = dev_id
      }
    }
  }
  return LocalModule("libpipewire-module-loopback", args:get_data(), {})
end

function checkProfiles (dev)
  local device_id = dev["bound-id"]
  local props = dev.properties

  -- Get the associated BT SpaDevice
  local internal_id = tostring (props["api.bluez5.id"])
  local spa_device = monitor:get_managed_object (internal_id)
  if spa_device == nil then
    return
  end

  -- Ignore devices that don't support both A2DP sink and HSP/HFP profiles
  local has_a2dpsink_profile = false
  local has_headset_profile = false
  for p in dev:iterate_params("EnumProfile") do
    local profile = cutils.parseParam (p, "EnumProfile")
    if profile.name:find ("a2dp") and profile.name:find ("sink") then
      has_a2dpsink_profile = true
    elseif profile.name:find ("headset") then
      has_headset_profile = true
    end
  end
  if not has_a2dpsink_profile or not has_headset_profile then
    return
  end

  -- Create the loopback device if never created before
  local loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
  if loopback == nil then
    local dev_name = props["api.bluez5.address"] or props["device.name"]
    local dec_desc = props["device.description"] or props["device.name"]
      or props["device.nick"] or props["device.alias"] or "bluetooth-device"
    -- sanitize description, replace ':' with ' '
    dec_desc = dec_desc:gsub("(:)", " ")
    loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id)
    spa_device:store_managed_object(LOOPBACK_SOURCE_ID, loopback)
  end
end

function onDeviceParamsChanged (dev, param_name)
  if param_name == "EnumProfile" then
    checkProfiles (dev)
  end
end

devices_om:connect("object-added", function(_, dev)
  -- Ignore all devices that are not BT devices
  if dev.properties["device.api"] ~= "bluez5" then
    return
  end

  -- check available profiles
  dev:connect ("params-changed", onDeviceParamsChanged)
  checkProfiles (dev)
end)

if config.seat_monitoring then
  logind_plugin = Plugin.find("logind")
end
if logind_plugin then
  -- if logind support is enabled, activate
  -- the monitor only when the seat is active
  function startStopMonitor(seat_state)
    log:info(logind_plugin, "Seat state changed: " .. seat_state)

    if seat_state == "active" then
      monitor = createMonitor()
    elseif monitor then
      monitor:deactivate(Feature.SpaDevice.ENABLED)
      monitor = nil
    end
  end

  logind_plugin:connect("state-changed", function(p, s) startStopMonitor(s) end)
  startStopMonitor(logind_plugin:call("get-state"))
else
  monitor = createMonitor()
end

nodes_om:activate()
devices_om:activate()
device_set_nodes_om:activate()
  07070100000182000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000003100000000wireplumber-0.5.0/src/scripts/monitors/libcamera  07070100000183000081A400000000000000000000000165F86304000007A6000000000000000000000000000000000000004300000000wireplumber-0.5.0/src/scripts/monitors/libcamera/create-device.lua    -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
mutils = require ("monitor-utils")

log = Log.open_topic ("s-monitors-libcamera")

config = {}
config.rules = Conf.get_section_as_json ("monitor.libcamera.rules", Json.Array {})

function createLibcamNode (parent, id, type, factory, properties)
  local registered = mutils:register_cam_node (parent, id, factory, properties)
  if not registered then
    source = source or Plugin.find ("standard-event-source")
    local e = source:call ("create-event", "create-libcamera-device-node",
      parent, nil)
    e:set_data ("factory", factory)
    e:set_data ("node-properties", properties)
    e:set_data ("node-sub-id", id)

    EventDispatcher.push_event (e)
  end
end

SimpleEventHook {
  name = "monitor/libcamera/create-device",
  after = "monitor/libcamera/name-device",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "create-libcamera-device" },
    },
  },
  execute = function(event)
    local properties = event:get_data ("device-properties")
    local factory = event:get_data ("factory")
    local parent = event:get_subject ()
    local id = event:get_data ("device-sub-id")

    -- apply properties from rules defined in JSON .conf file
    properties = JsonUtils.match_rules_update_properties (config.rules, properties)

    if cutils.parseBool (properties ["device.disabled"]) then
      log:notice ("libcam device " .. properties["device.name"] .. " disabled")
      return
    end
    local device = SpaDevice (factory, properties)

    if device then
      device:connect ("create-object", createLibcamNode)
      device:activate (Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
      parent:store_managed_object (id, device)
    else
      log:warning ("Failed to create '" .. factory .. "' device")
    end
  end
}:register ()
  07070100000184000081A400000000000000000000000165F86304000004D9000000000000000000000000000000000000004100000000wireplumber-0.5.0/src/scripts/monitors/libcamera/create-node.lua  -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
mutils = require ("monitor-utils")

log = Log.open_topic ("s-monitors-libcamera")

config = {}
config.rules = Conf.get_section_as_json ("monitor.libcamera.rules", Json.Array {})

SimpleEventHook {
  name = "monitor/libcamera/create-node",
  after = "monitor/libcamera/name-node",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "create-libcamera-device-node" },
    },
  },
  execute = function(event)
    local properties = event:get_data ("node-properties")
    local parent = event:get_subject ()
    local id = event:get_data ("node-sub-id")

    -- apply properties from rules defined in JSON .conf file
    properties = JsonUtils.match_rules_update_properties (config.rules, properties)

    if cutils.parseBool (properties["node.disabled"]) then
      log:notice ("libcam node" .. properties ["node.name"] .. " disabled")
      return
    end
    -- create the node
    local node = Node ("spa-node-factory", properties)
    node:activate (Feature.Proxy.BOUND)
    parent:store_managed_object (id, node)
  end
}:register ()
   07070100000185000081A400000000000000000000000165F86304000003EF000000000000000000000000000000000000004600000000wireplumber-0.5.0/src/scripts/monitors/libcamera/enumerate-device.lua -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
log = Log.open_topic ("s-monitors-libcamera")

config = {}
config.properties = Conf.get_section_as_properties ("monitor.libcamera.properties")

function createCamDevice (parent, id, type, factory, properties)
  source = source or Plugin.find ("standard-event-source")

  local e = source:call ("create-event", "create-libcamera-device", parent, nil)
  e:set_data ("device-properties", properties)
  e:set_data ("factory", factory)
  e:set_data ("device-sub-id", id)

  EventDispatcher.push_event (e)
end

monitor = SpaDevice ("api.libcamera.enum.manager", config.properties)
if monitor then
  monitor:connect ("create-object", createCamDevice)
  monitor:activate (Feature.SpaDevice.ENABLED)
else
  log:notice ("PipeWire's libcamera SPA plugin is missing or broken. " ..
      "Some camera types may not be supported.")
end
 07070100000186000081A400000000000000000000000165F8630400000560000000000000000000000000000000000000004100000000wireplumber-0.5.0/src/scripts/monitors/libcamera/name-device.lua  -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

mutils = require ("monitor-utils")

log = Log.open_topic ("s-monitors-libcamera")

SimpleEventHook {
  name = "monitor/libcamera/name-device",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "create-libcamera-device" },
    },
  },
  execute = function(event)
    local parent = event:get_subject ()
    local properties = event:get_data ("device-properties")
    local id = event:get_data ("device-sub-id")

    local name = "libcamera_device." ..
        (properties["device.name"] or
          properties["device.bus-id"] or
          properties["device.bus-path"] or
          tostring (id)):gsub ("([^%w_%-%.])", "_")

    properties["device.name"] = name

    -- deduplicate devices with the same name
    for counter = 2, 99, 1 do
      if mutils.find_duplicate (parent, id, "device.name", properties["node.name"]) then
        properties["device.name"] = name .. "." .. counter
      else
        break
      end
    end

    -- ensure the device has a description
    properties["device.description"] =
        properties["device.description"]
        or properties["device.product.name"]
        or "Unknown device"

    event:set_data ("device-properties", properties)
  end
}:register ()
07070100000187000081A400000000000000000000000165F8630400000B24000000000000000000000000000000000000003F00000000wireplumber-0.5.0/src/scripts/monitors/libcamera/name-node.lua    -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

mutils = require ("monitor-utils")

log = Log.open_topic ("s-monitors-libcamera")

SimpleEventHook {
  name = "monitor/libcamera/name-node",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "create-libcamera-device-node" },
    },
  },
  execute = function(event)
    local properties = event:get_data ("node-properties")
    local parent = event:get_subject ()
    local dev_props = parent.properties
    local factory = event:get_data ("factory")
    local id = event:get_data ("node-sub-id")
    local location = properties ["api.libcamera.location"]

    -- set the device id and spa factory name; REQUIRED, do not change
    properties ["device.id"] = parent ["bound-id"]
    properties ["factory.name"] = factory

    -- set the default pause-on-idle setting
    properties ["node.pause-on-idle"] = false

    -- set the node name
    local name =
        (factory:find ("sink") and "libcamera_output") or
        (factory:find ("source") and "libcamera_input" or factory)
        .. "." ..
        (dev_props ["device.name"]:gsub ("^libcamera_device%.(.+)", "%1") or
          dev_props ["device.name"] or
          dev_props ["device.nick"] or
          dev_props ["device.alias"] or
          "libcamera-device")
    -- sanitize name
    name = name:gsub ("([^%w_%-%.])", "_")

    properties ["node.name"] = name

    -- deduplicate nodes with the same name
    for counter = 2, 99, 1 do
      if mutils.find_duplicate (parent, id, "node.name", properties ["node.name"]) then
        properties ["node.name"] = name .. "." .. counter
      else
        break
      end
    end

    -- set the node description
    local desc = dev_props ["device.description"] or "libcamera-device"
    if location == "front" then
      desc = I18n.gettext ("Built-in Front Camera")
    elseif location == "back" then
      desc = I18n.gettext ("Built-in Back Camera")
    end
    -- sanitize description, replace ':' with ' '
    properties ["node.description"] = desc:gsub ("(:)", " ")

    -- set the node nick
    local nick = properties ["node.nick"] or
        dev_props ["device.product.name"] or
        dev_props ["device.description"] or
        dev_props ["device.nick"]
    properties ["node.nick"] = nick:gsub ("(:)", " ")

    -- set priority
    if not properties ["priority.session"] then
      local priority = 700
      if location == "external" then
        priority = priority + 150
      elseif location == "front" then
        priority = priority + 100
      elseif location == "back" then
        priority = priority + 50
      end
      properties ["priority.session"] = priority
    end

    event:set_data ("node-properties", properties)
  end
}:register ()
07070100000188000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002C00000000wireplumber-0.5.0/src/scripts/monitors/v4l2   07070100000189000081A400000000000000000000000165F8630400000788000000000000000000000000000000000000003E00000000wireplumber-0.5.0/src/scripts/monitors/v4l2/create-device.lua -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
mutils = require ("monitor-utils")

log = Log.open_topic ("s-monitors-v4l2")

config = {}
config.rules = Conf.get_section_as_json ("monitor.v4l2.rules", Json.Array {})

function createV4l2camNode (parent, id, type, factory, properties)
  local registered = mutils:register_cam_node (parent, id, factory, properties)
  if not registered then
    source = source or Plugin.find ("standard-event-source")
    local e = source:call ("create-event", "create-v4l2-device-node",
      parent, nil)
    e:set_data ("factory", factory)
    e:set_data ("node-properties", properties)
    e:set_data ("node-sub-id", id)

    EventDispatcher.push_event (e)
  end
end

SimpleEventHook {
  name = "monitor/v4l2/create-device",
  after = "monitor/v4l2/name-device",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "create-v4l2-device" },
    },
  },
  execute = function(event)
    local properties = event:get_data ("device-properties")
    local factory = event:get_data ("factory")
    local parent = event:get_subject ()
    local id = event:get_data ("device-sub-id")

    -- apply properties from rules defined in JSON .conf file
    properties = JsonUtils.match_rules_update_properties (config.rules, properties)

    if cutils.parseBool (properties ["device.disabled"]) then
      log:notice ("V4L2 device " .. properties["device.name"] .. " disabled")
      return
    end
    local device = SpaDevice (factory, properties)

    if device then
      device:connect ("create-object", createV4l2camNode)
      device:activate (Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
      parent:store_managed_object (id, device)
    else
      log:warning ("Failed to create '" .. factory .. "' device")
    end
  end
}:register ()
0707010000018A000081A400000000000000000000000165F86304000004EE000000000000000000000000000000000000003C00000000wireplumber-0.5.0/src/scripts/monitors/v4l2/create-node.lua   -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
mutils = require ("monitor-utils")

log = Log.open_topic ("s-monitors-v4l2")

config = {}
config.rules = Conf.get_section_as_json ("monitor.v4l2.rules", Json.Array {})

SimpleEventHook {
  name = "monitor/v4l2/create-node",
  after = "monitor/v4l2/name-node",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "create-v4l2-device-node" },
    },
  },
  execute = function(event)
    local properties = event:get_data ("node-properties")
    local parent = event:get_subject ()
    local id = event:get_data ("node-sub-id")
    local factory = event:get_data ("factory")

    -- apply properties from rules defined in JSON .conf file
    properties = JsonUtils.match_rules_update_properties (config.rules, properties)

    if cutils.parseBool (properties ["node.disabled"]) then
      log:notice ("V4L2 node" .. properties ["node.name"] .. " disabled")
      return
    end
    -- create the node
    local node = Node ("spa-node-factory", properties)
    node:activate (Feature.Proxy.BOUND)
    parent:store_managed_object (id, node)
  end
}:register ()
  0707010000018B000081A400000000000000000000000165F86304000003D3000000000000000000000000000000000000004100000000wireplumber-0.5.0/src/scripts/monitors/v4l2/enumerate-device.lua  -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
log = Log.open_topic ("s-monitors-v4l2")

config = {}
config.properties = Conf.get_section_as_properties ("monitor.v4l2.properties")

function createCamDevice (parent, id, type, factory, properties)
  source = source or Plugin.find ("standard-event-source")

  local e = source:call ("create-event", "create-v4l2-device", parent, nil)
  e:set_data ("device-properties", properties)
  e:set_data ("factory", factory)
  e:set_data ("device-sub-id", id)

  EventDispatcher.push_event (e)
end

monitor = SpaDevice ("api.v4l2.enum.udev", config.properties)
if monitor then
  monitor:connect ("create-object", createCamDevice)
  monitor:activate (Feature.SpaDevice.ENABLED)
else
  log:notice ("PipeWire's V4L2 SPA plugin is missing or broken. " ..
      "Some camera types may not be supported.")
end
 0707010000018C000081A400000000000000000000000165F863040000054C000000000000000000000000000000000000003C00000000wireplumber-0.5.0/src/scripts/monitors/v4l2/name-device.lua   -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

mutils = require ("monitor-utils")

log = Log.open_topic ("s-monitors-v4l2")

SimpleEventHook {
  name = "monitor/v4l2/name-device",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "create-v4l2-device" },
    },
  },
  execute = function(event)
    local properties = event:get_data ("device-properties")
    local parent = event:get_subject ()
    local id = event:get_data ("device-sub-id")

    local name = "v4l2_device." ..
        (properties["device.name"] or
          properties["device.bus-id"] or
          properties["device.bus-path"] or
          tostring (id)):gsub ("([^%w_%-%.])", "_")

    properties["device.name"] = name

    -- deduplicate devices with the same name
    for counter = 2, 99, 1 do
      if mutils.find_duplicate (parent, id, "device.name", properties["node.name"]) then
        properties["device.name"] = name .. "." .. counter
      else
        break
      end
    end

    -- ensure the device has a description
    properties["device.description"] =
        properties["device.description"]
        or properties["device.product.name"]
        or "Unknown device"

    event:set_data ("device-properties", properties)
  end
}:register ()
0707010000018D000081A400000000000000000000000165F86304000009D4000000000000000000000000000000000000003A00000000wireplumber-0.5.0/src/scripts/monitors/v4l2/name-node.lua -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- SPDX-License-Identifier: MIT

mutils = require ("monitor-utils")

log = Log.open_topic ("s-monitors-v4l2")

SimpleEventHook {
  name = "monitor/v4l2/name-node",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "create-v4l2-device-node" },
    },
  },
  execute = function(event)
    local properties = event:get_data ("node-properties")
    local parent = event:get_subject ()
    local dev_props = parent.properties
    local factory = event:get_data ("factory")
    local id = event:get_data ("node-sub-id")

    -- set the device id and spa factory name; REQUIRED, do not change
    properties["device.id"] = parent["bound-id"]
    properties["factory.name"] = factory

    -- set the default pause-on-idle setting
    properties["node.pause-on-idle"] = false

    -- set the node name
    local name =
        (factory:find ("sink") and "v4l2_output") or
        (factory:find ("source") and "v4l2_input" or factory)
        .. "." ..
        (dev_props["device.name"]:gsub ("^v4l2_device%.(.+)", "%1") or
          dev_props["device.name"] or
          dev_props["device.nick"] or
          dev_props["device.alias"] or
          "v4l2-device")
    -- sanitize name
    name = name:gsub ("([^%w_%-%.])", "_")

    properties["node.name"] = name

    -- deduplicate nodes with the same name
    for counter = 2, 99, 1 do
      if mutils.find_duplicate (parent, id, "node.name", properties["node.name"]) then
        properties["node.name"] = name .. "." .. counter
      else
        break
      end
    end

    -- set the node description
    local desc = dev_props["device.description"] or "v4l2-device"
    desc = desc .. " (V4L2)"
    -- sanitize description, replace ':' with ' '
    properties["node.description"] = desc:gsub ("(:)", " ")

    -- set the node nick
    local nick = properties["node.nick"] or
        dev_props["device.product.name"] or
        dev_props["api.v4l2.cap.card"] or
        dev_props["device.description"] or
        dev_props["device.nick"]
    properties["node.nick"] = nick:gsub ("(:)", " ")

    -- set priority
    if not properties["priority.session"] then
      local path = properties["api.v4l2.path"] or "/dev/video100"
      local dev = path:gsub ("/dev/video(%d+)", "%1")
      properties["priority.session"] = 1000 - (tonumber (dev) * 10)
    end

    event:set_data ("node-properties", properties)
  end
}:register ()
0707010000018E000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002300000000wireplumber-0.5.0/src/scripts/node    0707010000018F000081A400000000000000000000000165F8630400001287000000000000000000000000000000000000003300000000wireplumber-0.5.0/src/scripts/node/create-item.lua    -- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT

-- create-item.lua script takes pipewire nodes and creates session items (a.k.a
-- linkable) objects out of them.

cutils = require ("common-utils")
log = Log.open_topic ("s-node")

items = {}

function configProperties (node)
  local np = node.properties
  local properties = {
    ["item.node"] = node,
    ["item.plugged.usec"] = GLib.get_monotonic_time (),
    ["item.features.no-dsp"] = Settings.get_boolean ("node.features.audio.no-dsp"),
    ["item.features.monitor"] = Settings.get_boolean ("node.features.audio.monitor-ports"),
    ["item.features.control-port"] = Settings.get_boolean ("node.features.audio.control-port"),
    ["node.id"] = node ["bound-id"],
    ["client.id"] = np ["client.id"],
    ["object.path"] = np ["object.path"],
    ["object.serial"] = np ["object.serial"],
    ["target.object"] = np ["target.object"],
    ["priority.session"] = np ["priority.session"],
    ["device.id"] = np ["device.id"],
    ["card.profile.device"] = np ["card.profile.device"],
    ["node.dont-fallback"] = np ["node.dont-fallback"],
    ["node.dont-move"] = np ["node.dont-move"],
    ["node.linger"] = np ["node.linger"],
  }

  for k, v in pairs (np) do
    if k:find ("^node") or k:find ("^stream") or k:find ("^media") then
      properties [k] = v
    end
  end

  local media_class = properties ["media.class"] or ""

  if not properties ["media.type"] then
    for _, i in ipairs ({ "Audio", "Video", "Midi" }) do
      if media_class:find (i) then
        properties ["media.type"] = i
        break
      end
    end
  end

  properties ["item.node.type"] =
      media_class:find ("^Stream/") and "stream" or "device"

  properties ["item.node.direction"] = cutils.mediaClassToDirection (media_class)
  return properties
end

AsyncEventHook {
  name = "node/create-item",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "node-added" },
      Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
    },
    EventInterest {
      Constraint { "event.type", "=", "node-added" },
      Constraint { "media.class", "#", "Video/*", type = "pw-global" },
    },
    EventInterest {
      Constraint { "event.type", "=", "node-added" },
      Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
      Constraint { "wireplumber.is-virtual", "-", type = "pw" },
    },
  },
  steps = {
    start = {
      next = "register",
      execute = function (event, transition)
        local node = event:get_subject ()
        local id = node.id
        local item
        local item_type

        local media_class = node.properties ['media.class']
        if string.find (media_class, "Audio") then
          item_type = "si-audio-adapter"
        else
          item_type = "si-node"
        end

        log:info (node, "creating item for node -> " .. item_type)

        -- create item
        item = SessionItem (item_type)
        items [id] = item

        -- configure item
        if not item:configure (configProperties (node)) then
          transition:return_error ("failed to configure item for node "
              .. tostring (id))
          return
        end

        -- activate item
        item:activate (Features.ALL, function (_, e)
          if e then
            transition:return_error ("failed to activate item: "
                .. tostring (e));
          else
            transition:advance ()
          end
        end)
      end,
    },
    register = {
      next = "none",
      execute = function (event, transition)
        local node = event:get_subject ()
        local bound_id = node ["bound-id"]
        local item = items [node.id]

        log:info (item, "activated item for node " .. tostring (bound_id))
        item:register ()
        transition:advance ()
      end,
    },
  },
}:register ()

SimpleEventHook {
  name = "node/destroy-item",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "node-removed" },
      Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
    },
    EventInterest {
      Constraint { "event.type", "=", "node-removed" },
      Constraint { "media.class", "#", "Video/*", type = "pw-global" },
    },
    EventInterest {
      Constraint { "event.type", "=", "node-removed" },
      Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
      Constraint { "wireplumber.is-virtual", "-", type = "pw" },
    },
  },
  execute = function (event)
    local node = event:get_subject ()
    local id = node.id
    if items [id] then
      items [id]:remove ()
      items [id] = nil
    end

  end
}:register ()
 07070100000190000081A400000000000000000000000165F8630400000450000000000000000000000000000000000000003B00000000wireplumber-0.5.0/src/scripts/node/create-virtual-item.lua    -- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT

-- Receive script arguments from config.lua

-- creates the virtual items defined in the JSON(virtual.conf)

log = Log.open_topic ("s-node")

config = {}
config.virtual_items = Conf.get_section_as_object ("virtual-items")

function createVirtualItem (factory_name, properties)
  -- create virtual item
  local si_v = SessionItem ( factory_name )
  if not si_v then
    log:warning (si_v, "could not create virtual item of type " .. factory_name)
    return
  end

  -- configure virtual item
  if not si_v:configure(properties) then
    log:warning(si_v, "failed to configure virtual item " .. properties.name)
    return
  end

  -- activate and register virtual item
  si_v:activate (Features.ALL, function (item)
    item:register ()
    log:info(item, "registered virtual item " .. properties.name)
  end)
end


for name, properties in pairs(config.virtual_items) do
  properties["name"] = name
  createVirtualItem ("si-audio-virtual", properties)
end
07070100000191000081A400000000000000000000000165F8630400000E64000000000000000000000000000000000000003D00000000wireplumber-0.5.0/src/scripts/node/filter-forward-format.lua  -- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Logic to "forward" the format set on special filter nodes to their
-- virtual device peer node. This is for things like the "loopback" module,
-- which always comes in pairs of 2 nodes, one stream and one virtual device.
--
-- FIXME: this script can be further improved

lutils = require ("linking-utils")
log = Log.open_topic ("s-node")

function findAssociatedLinkGroupNode (si)
  local si_props = si.properties
  local link_group = si_props ["node.link-group"]
  if link_group == nil then
    return nil
  end

  local std_event_source = Plugin.find ("standard-event-source")
  local om = std_event_source:call ("get-object-manager", "session-item")

  -- get the associated media class
  local assoc_direction = cutils.getTargetDirection (si_props)
  local assoc_media_class = si_props ["media.type"] ..
      (assoc_direction == "input" and "/Sink" or "/Source")

  -- find the linkable with same link group and matching assoc media class
  for assoc_si in om:iterate { type = "SiLinkable" } do
    local assoc_props = assoc_si.properties
    local assoc_link_group = assoc_props ["node.link-group"]
    if assoc_link_group == link_group and
        assoc_media_class == assoc_props ["media.class"] then
      return assoc_si
    end
  end

  return nil
end

function onLinkGroupPortsStateChanged (si, old_state, new_state)
  local si_props = si.properties

  -- only handle items with configured ports state
  if new_state ~= "configured" then
    return
  end

  log:info (si, "ports format changed on " .. si_props ["node.name"])

  -- find associated device
  local si_device = findAssociatedLinkGroupNode (si)
  if si_device ~= nil then
    local device_node_name = si_device.properties ["node.name"]

    -- get the stream format
    local f, m = si:get_ports_format ()

    -- unregister the device
    log:info (si_device, "unregistering " .. device_node_name)
    si_device:remove ()

    -- set new format in the device
    log:info (si_device, "setting new format in " .. device_node_name)
    si_device:set_ports_format (f, m, function (item, e)
      if e ~= nil then
        log:warning (item, "failed to configure ports in " ..
          device_node_name .. ": " .. e)
      end

      -- register back the device
      log:info (item, "registering " .. device_node_name)
      item:register ()
    end)
  end
end

SimpleEventHook {
  name = "node/filter-forward-format",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
      Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
    },
  },
  execute = function (event)
    local si = event:get_subject ()

    -- Forward filters ports format to associated virtual devices if enabled
    if Settings.get_boolean ("node.filter.forward-format") then
      local si_props = si.properties
      local link_group = si_props ["node.link-group"]
      local si_flags = lutils:get_flags (si.id)

      -- only listen for ports state changed on audio filter streams
      if si_flags.ports_state_signal ~= true and
          si_props ["item.factory.name"] == "si-audio-adapter" and
          si_props ["item.node.type"] == "stream" and
          link_group ~= nil then
        si:connect ("adapter-ports-state-changed", onLinkGroupPortsStateChanged)
        si_flags.ports_state_signal = true
        log:info (si, "listening ports state changed on " .. si_props ["node.name"])
      end
    end
  end
}:register ()
07070100000192000081A400000000000000000000000165F863040000370F000000000000000000000000000000000000003400000000wireplumber-0.5.0/src/scripts/node/state-stream.lua   -- WirePlumber
--
-- Copyright © 2021-2022 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on restore-stream.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT

cutils = require ("common-utils")
log = Log.open_topic ("s-node")

config = {}
config.rules = Conf.get_section_as_json ("stream.rules", Json.Array {})

-- the state storage
state = nil
state_table = nil

-- Support for the "System Sounds" volume control in pavucontrol
rs_metadata = nil

-- hook to restore stream properties & target
restore_stream_hook = SimpleEventHook {
  name = "node/restore-stream",
  interests = {
    -- match stream nodes
    EventInterest {
      Constraint { "event.type", "=", "node-added" },
      Constraint { "media.class", "matches", "Stream/*" },
    },
    -- and device nodes that are not associated with any routes
    EventInterest {
      Constraint { "event.type", "=", "node-added" },
      Constraint { "media.class", "matches", "Audio/*" },
      Constraint { "device.routes", "is-absent" },
    },
    EventInterest {
      Constraint { "event.type", "=", "node-added" },
      Constraint { "media.class", "matches", "Audio/*" },
      Constraint { "device.routes", "equals", "0" },
    },
  },
  execute = function (event)
    local node = event:get_subject ()
    local stream_props = node.properties
    stream_props = JsonUtils.match_rules_update_properties (config.rules, stream_props)

    local key = formKey (stream_props)
    if not key then
      return
    end

    local stored_values = getStoredStreamProps (key)
    if not stored_values then
      return
    end

    -- restore node Props (volumes, channelMap, etc...)
    if Settings.get_boolean ("node.stream.restore-props") and stream_props ["state.restore-props"] ~= "false"
    then
      local props = {
        "Spa:Pod:Object:Param:Props", "Props",
        volume = stored_values.volume,
        mute = stored_values.mute,
        channelVolumes = stored_values.channelVolumes ~= nil and
            stored_values.channelVolumes or buildDefaultChannelVolumes (node),
        channelMap = stored_values.channelMap,
      }
      -- convert arrays to Spa Pod
      if props.channelVolumes then
        table.insert (props.channelVolumes, 1, "Spa:Float")
        props.channelVolumes = Pod.Array (props.channelVolumes)
      end
      if props.channelMap then
        table.insert (props.channelMap, 1, "Spa:Enum:AudioChannel")
        props.channelMap = Pod.Array (props.channelMap)
      end

      if props.volume or (props.mute ~= nil) or props.channelVolumes or props.channelMap
      then
        log:info (node, "restore values from " .. key)

        local param = Pod.Object (props)
        log:debug (param, "setting props on " .. tostring (stream_props ["node.name"]))
        node:set_param ("Props", param)
      end
    end

    -- restore the node's link target on metadata
    if Settings.get_boolean ("node.stream.restore-target") and stream_props ["state.restore-target"] ~= "false"
    then
      if stored_values.target then
        -- check first if there is a defined target in the node's properties
        -- and skip restoring if this is the case (#335)
        local target_in_props =
            stream_props ["target.object"] or stream_props ["node.target"]

        if not target_in_props then
          local source = event:get_source ()
          local nodes_om = source:call ("get-object-manager", "node")
          local metadata_om = source:call ("get-object-manager", "metadata")

          local target_node = nodes_om:lookup {
            Constraint { "node.name", "=", stored_values.target, type = "pw" }
          }
          local metadata = metadata_om:lookup {
            Constraint { "metadata.name", "=", "default" }
          }

          if target_node and metadata then
            metadata:set (node ["bound-id"], "target.object", "Spa:Id",
                  target_node.properties ["object.serial"])
          end
        else
          log:debug (node,
              "Not restoring the target for " ..
              tostring (stream_props ["node.name"]) ..
              " because it is already set to " .. target_in_props)
        end
      end
    end

  end
}

-- store stream properties on the state file
store_stream_props_hook = SimpleEventHook {
  name = "node/store-stream-props",
  interests = {
    -- match stream nodes
    EventInterest {
      Constraint { "event.type", "=", "node-params-changed" },
      Constraint { "event.subject.param-id", "=", "Props" },
      Constraint { "media.class", "matches", "Stream/*" },
    },
    -- and device nodes that are not associated with any routes
    EventInterest {
      Constraint { "event.type", "=", "node-params-changed" },
      Constraint { "event.subject.param-id", "=", "Props" },
      Constraint { "media.class", "matches", "Audio/*" },
      Constraint { "device.routes", "is-absent" },
    },
    EventInterest {
      Constraint { "event.type", "=", "node-params-changed" },
      Constraint { "event.subject.param-id", "=", "Props" },
      Constraint { "media.class", "matches", "Audio/*" },
      Constraint { "device.routes", "equals", "0" },
    },
  },
  execute = function (event)
    local node = event:get_subject ()
    local stream_props = node.properties
    stream_props = JsonUtils.match_rules_update_properties (config.rules, stream_props)

    if Settings.get_boolean ("node.stream.restore-props") and stream_props ["state.restore-props"] ~= "false"
    then
      local key = formKey (stream_props)
      if not key then
        return
      end

      local stored_values = getStoredStreamProps (key) or {}
      local hasChanges = false

      log:info (node, "saving stream props for " ..
          tostring (stream_props ["node.name"]))

      for p in node:iterate_params ("Props") do
        local props = cutils.parseParam (p, "Props")
        if not props then
          goto skip_prop
        end

        if props.volume ~= stored_values.volume then
          stored_values.volume = props.volume
          hasChanges = true
        end
        if props.mute ~= stored_values.mute then
          stored_values.mute = props.mute
          hasChanges = true
        end
        if props.channelVolumes then
          stored_values.channelVolumes = props.channelVolumes
          hasChanges = true
        end
        if props.channelMap then
          stored_values.channelMap = props.channelMap
          hasChanges = true
        end

        ::skip_prop::
      end

      if hasChanges then
        saveStreamProps (key, stored_values)
      end
    end
  end
}

-- save "target.node"/"target.object" on metadata changes
store_stream_target_hook = SimpleEventHook {
  name = "node/store-stream-target-metadata-changed",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "metadata-changed" },
      Constraint { "metadata.name", "=", "default" },
      Constraint { "event.subject.key", "c", "target.object", "target.node" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    local nodes_om = source:call ("get-object-manager", "node")
    local props = event:get_properties ()
    local subject_id = props ["event.subject.id"]
    local target_key = props ["event.subject.key"]
    local target_value = props ["event.subject.value"]

    local node = nodes_om:lookup {
      Constraint { "bound-id", "=", subject_id, type = "gobject" }
    }
    if not node then
      return
    end

    local stream_props = node.properties
    stream_props = JsonUtils.match_rules_update_properties (config.rules, stream_props)

    if stream_props ["state.restore-target"] == "false" then
      return
    end

    local key = formKey (stream_props)
    if not key then
      return
    end

    local target_name = nil

    if target_value and target_value ~= "-1" then
      local target_node
      if target_key == "target.object" then
        target_node = nodes_om:lookup {
          Constraint { "object.serial", "=", target_value, type = "pw-global" }
        }
      else
        target_node = nodes_om:lookup {
          Constraint { "bound-id", "=", target_value, type = "gobject" }
        }
      end
      if target_node then
        target_name = target_node.properties ["node.name"]
      end
    end

    log:info (node, "saving stream target for " ..
      tostring (stream_props ["node.name"]) .. " -> " .. tostring (target_name))

    local stored_values = getStoredStreamProps (key) or {}
    stored_values.target = target_name
    saveStreamProps (key, stored_values)
  end
}

-- track route-settings metadata changes
route_settings_metadata_added_hook = SimpleEventHook {
  name = "node/route-settings-metadata-added",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "metadata-added" },
      Constraint { "metadata.name", "=", "route-settings" },
    },
  },
  execute = function (event)
    local metadata = event:get_subject ()

    -- copy state into the metadata
    local key = "Output/Audio:media.role:Notification"
    local p = getStoredStreamProps (key)
    if p then
      p.channels = p.channelMap and Json.Array (p.channelMap)
      p.volumes = p.channelVolumes and Json.Array (p.channelVolumes)
      p.channelMap = nil
      p.channelVolumes = nil
      p.target = nil
      metadata:set (0, "restore.stream." .. key, "Spa:String:JSON",
          Json.Object (p):to_string ())
    end
  end
}

-- track route-settings metadata changes
route_settings_metadata_changed_hook = SimpleEventHook {
  name = "node/route-settings-metadata-changed",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "metadata-changed" },
      Constraint { "metadata.name", "=", "route-settings" },
      Constraint { "event.subject.key", "=",
          "restore.stream.Output/Audio:media.role:Notification" },
      Constraint { "event.subject.spa_type", "=", "Spa:String:JSON" },
      Constraint { "event.subject.value", "is-present" },
    },
  },
  execute = function (event)
    local props = event:get_properties ()
    local subject_id = props ["event.subject.id"]
    local key = props ["event.subject.key"]
    local value = props ["event.subject.value"]

    local json = Json.Raw (value)
    if json == nil or not json:is_object () then
      return
    end

    local vparsed = json:parse ()
    local key = string.sub (key, string.len ("restore.stream.") + 1)
    key = string.gsub (key, "%.", ":", 1);

    local stored_values = getStoredStreamProps (key) or {}

    if vparsed.volume ~= nil then
      stored_values.volume = vparsed.volume
    end
    if vparsed.mute ~= nil then
      stored_values.mute = vparsed.mute
    end
    if vparsed.channels ~= nil then
      stored_values.channelMap = vparsed.channels
    end
    if vparsed.volumes ~= nil then
      stored_values.channelVolumes = vparsed.volumes
    end
    saveStreamProps (key, stored_values)
  end
}

function buildDefaultChannelVolumes (node)
  local node_props = node.properties
  local direction = cutils.mediaClassToDirection (node_props ["media.class"] or "")
  local def_vol = 1.0
  local channels = 2
  local res = {}

  local str = node.properties["state.default-volume"]
  if str ~= nil then
    def_vol = tonumber (str)
  elseif direction == "input" then
    def_vol = Settings.get_float ("node.stream.default-capture-volume")
  elseif direction == "output" then
    def_vol = Settings.get_float ("node.stream.default-playback-volume")
  end

  for pod in node:iterate_params("Format") do
    local pod_parsed = pod:parse()
    if pod_parsed ~= nil then
      channels = pod_parsed.properties.channels
      break
    end
  end

  while (#res < channels) do
    table.insert(res, def_vol)
  end

  return res;
end

function getStoredStreamProps (key)
  local value = state_table [key]
  if not value then
    return nil
  end

  local json = Json.Raw (value)
  if not json or not json:is_object () then
    return nil
  end

  return json:parse ()
end

function saveStreamProps (key, p)
  assert (type (p) == "table")

  p.channelMap = p.channelMap and Json.Array (p.channelMap)
  p.channelVolumes = p.channelVolumes and Json.Array (p.channelVolumes)

  state_table [key] = Json.Object (p):to_string ()
  state:save_after_timeout (state_table)
end

function formKey (properties)
  local keys = {
    "media.role",
    "application.id",
    "application.name",
    "media.name",
    "node.name",
  }
  local key_base = nil

  for _, k in ipairs (keys) do
    local p = properties [k]
    if p then
      key_base = string.format ("%s:%s:%s",
          properties ["media.class"]:gsub ("^Stream/", ""), k, p)
      break
    end
  end
  return key_base
end

function toggleState (enable)
  if enable and not state then
    state = State ("stream-properties")
    state_table = state:load ()

    restore_stream_hook:register ()
    store_stream_props_hook:register ()
    store_stream_target_hook:register ()
    route_settings_metadata_changed_hook:register ()

    rs_metadata = ImplMetadata ("route-settings")
    rs_metadata:activate (Features.ALL, function (m, e)
      if e then
        log:warning ("failed to activate route-settings metadata: " .. tostring (e))
      end
    end)

  elseif not enable and state then
    state = nil
    state_table = nil
    restore_stream_hook:remove ()
    store_stream_props_hook:remove ()
    store_stream_target_hook:remove ()
    route_settings_metadata_changed_hook:remove ()
    rs_metadata = nil
  end
end

Settings.subscribe ("node.stream.restore-props", function ()
  toggleState (Settings.get_boolean ("node.stream.restore-props") or
      Settings.get_boolean ("node.stream.restore-target"))
end)

Settings.subscribe ("node.stream.restore-target", function ()
  toggleState (Settings.get_boolean ("node.stream.restore-props") or
      Settings.get_boolean ("node.stream.restore-target"))
end)

toggleState (Settings.get_boolean ("node.stream.restore-props") or
      Settings.get_boolean ("node.stream.restore-target"))
 07070100000193000081A400000000000000000000000165F8630400000773000000000000000000000000000000000000003400000000wireplumber-0.5.0/src/scripts/node/suspend-node.lua   -- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT

log = Log.open_topic ("s-node")

sources = {}

SimpleEventHook {
  name = "node/suspend-node",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "node-state-changed" },
      Constraint { "media.class", "matches", "Audio/*" },
    },
    EventInterest {
      Constraint { "event.type", "=", "node-state-changed" },
      Constraint { "media.class", "matches", "Video/*" },
    },
  },
  execute = function (event)
    local node = event:get_subject ()
    local new_state = event:get_properties ()["event.subject.new-state"]

    log:debug (node, "changed state to " .. new_state)

    -- Always clear the current source if any
    local id = node["bound-id"]
    if sources[id] then
      sources[id]:destroy()
      sources[id] = nil
    end

    -- Add a timeout source if idle for at least 5 seconds
    if new_state == "idle" or new_state == "error" then
      -- honor "session.suspend-timeout-seconds" if specified
      local timeout =
          tonumber(node.properties["session.suspend-timeout-seconds"]) or 5

      if timeout == 0 then
        return
      end

      -- add idle timeout; multiply by 1000, timeout_add() expects ms
      sources[id] = Core.timeout_add(timeout * 1000, function()
        -- Suspend the node
        -- but check first if the node still exists
        if (node:get_active_features() & Feature.Proxy.BOUND) ~= 0 then
          log:info(node, "was idle for a while; suspending ...")
          node:send_command("Suspend")
        end

        -- Unref the source
        sources[id] = nil

        -- false (== G_SOURCE_REMOVE) destroys the source so that this
        -- function does not get fired again after 5 seconds
        return false
      end)
    end
  end
}:register ()
 07070100000194000081A400000000000000000000000165F8630400000966000000000000000000000000000000000000002D00000000wireplumber-0.5.0/src/scripts/policy-dsp.lua  -- WirePlumber
--
-- Copyright © 2022-2023 The WirePlumber project contributors
--    @author Dmitry Sharshakov <d3dx12.xx@gmail.com>
--
-- SPDX-License-Identifier: MIT

local config = ... or {}
config.rules = config.rules or {}

for _, r in ipairs(config.rules) do
  r.interests = {}
  for _, i in ipairs(r.matches) do
    local interest_desc = { type = "properties" }

    for _, c in ipairs(i) do
      c.type = "pw"
      table.insert(interest_desc, Constraint(c))
    end

    local interest = Interest(interest_desc)
    table.insert(r.interests, interest)
  end
end

-- TODO: only check for hotplug of nodes with known DSP rules
nodes_om = ObjectManager {
  Interest { type = "node" },
}

clients_om = ObjectManager {
  Interest { type = "client" }
}

filter_chains = {}
hidden_nodes = {}

nodes_om:connect("object-added", function (om, node)
  for _, r in ipairs(config.rules or {}) do
    for _, interest in ipairs(r.interests) do
      if interest:matches(node["global-properties"]) then
        local id = node["global-properties"]["object.id"]

        if r.filter_chain then
          if filter_chains[id] then
            Log.warning("Sink " .. id .. " has been plugged now, but has a filter chain loaded. Skipping")
          else
            filter_chains[id] = LocalModule("libpipewire-module-filter-chain", r.filter_chain, {}, true)
          end
        end

        if r.hide_parent then
          Log.debug("Hiding node " .. node["bound-id"] .. " from clients")
          for client in clients_om:iterate { type = "client" } do
            if not client["properties"]["wireplumber.daemon"] then
              client:update_permissions { [node["bound-id"]] = "-" }
            end
          end
          hidden_nodes[node["bound-id"]] = id
        end

      end
    end
  end
end)

nodes_om:connect("object-removed", function (om, node)
  local id = node["global-properties"]["object.id"]
  if filter_chains[id] then
    Log.debug("Unloading filter chain associated with sink " .. id)
    filter_chains[id] = nil
  else
    Log.debug("Disconnected sink " .. id .. " does not have any filters to be removed")
  end
end)

clients_om:connect("object-added", function (om, client)
  for id, _ in pairs(hidden_nodes) do
    if not client["properties"]["wireplumber.daemon"] then
      client:update_permissions { [id] = "-" }
    end
  end
end)

nodes_om:activate()
clients_om:activate()
  07070100000195000081A400000000000000000000000165F8630400000D58000000000000000000000000000000000000002D00000000wireplumber-0.5.0/src/scripts/sm-objects.lua  -- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
--
-- The script exposes a metadata object named "sm-objects" that clients can
-- use to load objects into the WirePlumber daemon process. The objects are
-- loaded as soon as the metadata is set and are destroyed when the metadata
-- is cleared.
--
-- To load an object, a client needs to set a metadata entry with:
--
--   * subject:
--  The ID of the owner of the object; you can use 0 here, but the
--  idea is to be able to restrict which clients can change and/or
--  delete these objects by using IDs of other objects appropriately
--
--   * key: "<UNIQUE-OBJECT-NAME>"
--  This is the name that will be used to identify the object.
--  If an object with the same name already exists, it will be destroyed.
--  Note that the keys are unique per subject, so you can have multiple
--  objects with the same name as long as they are owned by different subjects.
--
--   * type: "Spa:String:JSON"
--
--   * value: "{ type = <object-type>,
--               name = <object-name>,
--               args = { ...object arguments... } }"
--  The object type can be one of the following:
--   - "pw-module": loads a pipewire module: `name` and `args` are interpreted
--                  just like a module entry in pipewire.conf
--   - "metadata": loads a metadata object with `metadata.name` = `name`
--                 and any additional properties provided in `args`
--

on_demand_objects = {}

object_constructors = {
  ["pw-module"] = LocalModule,
  ["metadata"] = function (name, args)
    local m = ImplMetadata (name, args)
    m:activate (Features.ALL, function (m, e)
      if e then
        Log.warning ("failed to activate on-demand metadata `" .. name .. "`: " .. tostring (e))
      end
    end)
    return m
  end
}

function handle_metadata_changed (m, subject, key, type, value)
  -- destroy all objects when metadata is cleared
  if not key then
    on_demand_objects = {}
    return
  end

  local object_id = key .. "@" .. tostring(subject)

  -- destroy existing object instance, if needed
  if on_demand_objects[object_id] then
    Log.debug("destroy on-demand object: " .. object_id)
    on_demand_objects[object_id] = nil
  end

  if value then
    local json = Json.Raw(value)
    if not json:is_object() then
      Log.warning("loading '".. object_id .. "' failed: expected JSON object, got: '" .. value .. "'")
      return
    end

    local obj = json:parse(1)
    if not obj.type then
      Log.warning("loading '".. object_id .. "' failed: no object type specified")
      return
    end
    if not obj.name then
      Log.warning("loading '".. object_id .. "' failed: no object name specified")
      return
    end

    local constructor = object_constructors[obj.type]
    if not constructor then
      Log.warning("loading '".. object_id .. "' failed: unknown object type: " .. obj.type)
      return
    end

    Log.info("load on-demand object: " .. object_id .. " -> " .. obj.name)
    on_demand_objects[object_id] = constructor(obj.name, obj.args)
  end
end

objects_metadata = ImplMetadata ("sm-objects")
objects_metadata:activate (Features.ALL, function (m, e)
  if e then
    Log.warning ("failed to activate the sm-objects metadata: " .. tostring (e))
  else
    m:connect("changed", handle_metadata_changed)
  end
end)
07070100000196000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001E00000000wireplumber-0.5.0/src/systemd 07070100000197000081A400000000000000000000000165F863040000036E000000000000000000000000000000000000002A00000000wireplumber-0.5.0/src/systemd/meson.build if systemd.found()
  systemd_config = configuration_data()
  systemd_config.set('WP_BINARY', wireplumber_bin_dir / 'wireplumber')

  # system service
  if get_option('systemd-system-service')
    systemd_system_unit_dir = systemd.get_variable(
        pkgconfig: 'systemdsystemunitdir',
        pkgconfig_define: ['prefix', get_option('prefix')])

    if get_option('systemd-system-unit-dir') != ''
      systemd_system_unit_dir = get_option('systemd-system-unit-dir')
    endif

    subdir('system')
  endif

  # user service
  if get_option('systemd-user-service')
    systemd_user_unit_dir = systemd.get_variable(
        pkgconfig: 'systemduserunitdir',
        pkgconfig_define: ['prefix', get_option('prefix')])

    if get_option('systemd-user-unit-dir') != ''
      systemd_user_unit_dir = get_option('systemd-user-unit-dir')
    endif

    subdir('user')
  endif
endif
  07070100000198000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002500000000wireplumber-0.5.0/src/systemd/system  07070100000199000081A400000000000000000000000165F863040000018C000000000000000000000000000000000000003100000000wireplumber-0.5.0/src/systemd/system/meson.build  configure_file(input : 'wireplumber.service.in',
               output : 'wireplumber.service',
               configuration : systemd_config,
               install_dir : systemd_system_unit_dir)
configure_file(input : 'wireplumber@.service.in',
               output : 'wireplumber@.service',
               configuration : systemd_config,
               install_dir : systemd_system_unit_dir)
0707010000019A000081A400000000000000000000000165F8630400000218000000000000000000000000000000000000003C00000000wireplumber-0.5.0/src/systemd/system/wireplumber.service.in   [Unit]
Description=Multimedia Service Session Manager
After=pipewire.service
BindsTo=pipewire.service
Conflicts=pipewire-media-session.service

[Service]
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
Type=simple
AmbientCapabilities=CAP_SYS_NICE
ExecStart=@WP_BINARY@
Restart=on-failure
User=pipewire
Environment=PIPEWIRE_RUNTIME_DIR=%t/pipewire
Environment=GIO_USE_VFS=local

[Install]
WantedBy=pipewire.service
Alias=pipewire-session-manager.service
0707010000019B000081A400000000000000000000000165F8630400000327000000000000000000000000000000000000003D00000000wireplumber-0.5.0/src/systemd/system/wireplumber@.service.in  # Service file to run WirePlumber in split mode, i.e. run multiple
# WirePlumber instances with different module sets.
# The template argument is used to load a specific profile, e.g.
# wireplumber@main.service loads the "main" profile,
# wireplumber@policy.service loads the "policy" profile, etc.
[Unit]
Description=Multimedia Service Session Manager (%i)
After=pipewire.service
BindsTo=pipewire.service
Conflicts=pipewire-media-session.service

[Service]
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
Type=simple
AmbientCapabilities=CAP_SYS_NICE
ExecStart=@WP_BINARY@ -p %i
Restart=on-failure
User=pipewire
Environment=PIPEWIRE_RUNTIME_DIR=%t/pipewire
Environment=GIO_USE_VFS=local

[Install]
WantedBy=pipewire.service
 0707010000019C000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002300000000wireplumber-0.5.0/src/systemd/user    0707010000019D000081A400000000000000000000000165F8630400000188000000000000000000000000000000000000002F00000000wireplumber-0.5.0/src/systemd/user/meson.build    configure_file(input : 'wireplumber.service.in',
               output : 'wireplumber.service',
               configuration : systemd_config,
               install_dir : systemd_user_unit_dir)
configure_file(input : 'wireplumber@.service.in',
               output : 'wireplumber@.service',
               configuration : systemd_config,
               install_dir : systemd_user_unit_dir)
0707010000019E000081A400000000000000000000000165F86304000001D0000000000000000000000000000000000000003A00000000wireplumber-0.5.0/src/systemd/user/wireplumber.service.in [Unit]
Description=Multimedia Service Session Manager
After=pipewire.service
BindsTo=pipewire.service
Conflicts=pipewire-media-session.service

[Service]
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
Type=simple
ExecStart=@WP_BINARY@
Restart=on-failure
Slice=session.slice
Environment=GIO_USE_VFS=local

[Install]
WantedBy=pipewire.service
Alias=pipewire-session-manager.service
0707010000019F000081A400000000000000000000000165F86304000002DF000000000000000000000000000000000000003B00000000wireplumber-0.5.0/src/systemd/user/wireplumber@.service.in    # Service file to run WirePlumber in split mode, i.e. run multiple
# WirePlumber instances with different module sets.
# The template argument is used to load a specific profile, e.g.
# wireplumber@main.service loads the "main" profile,
# wireplumber@policy.service loads the "policy" profile, etc.
[Unit]
Description=Multimedia Service Session Manager (%i)
After=pipewire.service
BindsTo=pipewire.service
Conflicts=pipewire-media-session.service

[Service]
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
Type=simple
ExecStart=@WP_BINARY@ -p %i
Restart=on-failure
Slice=session.slice
Environment=GIO_USE_VFS=local

[Install]
WantedBy=pipewire.service
 070701000001A0000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001C00000000wireplumber-0.5.0/src/tools   070701000001A1000081A400000000000000000000000165F8630400000167000000000000000000000000000000000000002800000000wireplumber-0.5.0/src/tools/meson.build   executable('wpctl',
  'wpctl.c',
  install: true,
  dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep],
)

install_data('shell-completion/wpctl.zsh',
  install_dir: get_option('datadir') / 'zsh/site-functions',
  rename: '_wpctl'
)

executable('wpexec',
  'wpexec.c',
  install: true,
  dependencies : [gobject_dep, gio_dep, wp_dep, pipewire_dep],
)
 070701000001A2000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002D00000000wireplumber-0.5.0/src/tools/shell-completion  070701000001A3000081A400000000000000000000000165F863040000064A000000000000000000000000000000000000003700000000wireplumber-0.5.0/src/tools/shell-completion/wpctl.zsh    #compdef wpctl

(( $+functions[_wpctl_pw_nodes] )) ||
_wpctl_pw_nodes() {
  local -a pw_objects
  if (( $+commands[pw-dump] )) && (( $+commands[jq] )); then
    local -a pw_objects=(${(@f)"$(2>/dev/null {
    command pw-dump |
      command jq -r '.[] | select(
        .type == "PipeWire:Interface:Node"
      ) |
      {id, type, name: (
        .info.name //
        (.info.props | (
          ."application.name" //
          ."node.name")
        ) //
        .type)
      } |
        "\(.id):\(.name | gsub(":"; "\\:"))"'
      })"})
  fi
  _wpctl_describe_nodes() {_describe "node id" pw_objects "$@"}
  _alternative \
    'pw-defaults:defaults:(@DEFAULT_SINK@ @DEFAULT_SOURCE@)' \
    'pw-node-id:node id:_wpctl_describe_nodes'
}

local -a node_id=(/$'[^\0]#\0'/ ':pw-node-id:node id:_wpctl_pw_nodes')
local -a volume=(/$'[0-9]##(%|)([+-]|)\0'/ ':volume:volume:( )')
local -a toggle=(/$'[^\0]#\0'/ ':(0 1 toggle)')
local -a set_volume=( "$node_id[@]" "$volume[@]" )
local -a set_mute=( "$node_id[@]" "$toggle[@]" )

_regex_words options 'wpctl options' \
  {-h,--help}':show help message and exit'
local -a options=( "$reply[@]" )

_regex_words wpctl-commands 'wpctl commands' \
  'status:show wireplumber status' \
  'get-volume:get object volume:$node_id' \
  'set-default:set a default sink:$node_id' \
  'set-volume:set object volume:$set_volume' \
  'set-mute:set object mute:$set_mute' \
  'set-profile:set object profile:$node_id' \
  'clear-default:unset default sink:$node_id'
local -a wpctlcmd=( /$'[^\0]#\0'/ "$options[@]" "#" "$reply[@]")
_regex_arguments _wpctl "$wpctlcmd[@]"
_wpctl "$@"
  070701000001A4000081A400000000000000000000000165F863040000F0CA000000000000000000000000000000000000002400000000wireplumber-0.5.0/src/tools/wpctl.c   /* WirePlumber
 *
 * Copyright © 2019-2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <stdio.h>
#include <locale.h>
#include <spa/utils/defs.h>
#include <spa/utils/string.h>
#include <pipewire/pipewire.h>
#include <pipewire/keys.h>
#include <pipewire/extensions/session-manager/keys.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wpctl")

static const gchar *DEFAULT_NODE_MEDIA_CLASSES[] = {
  "Audio/Sink",
  "Audio/Source",
  "Video/Source",
};

typedef struct _WpCtl WpCtl;
struct _WpCtl
{
  GOptionContext *context;
  GMainLoop *loop;
  WpCore *core;
  WpObjectManager *om;
  guint pending_plugins;
  gint exit_code;
};

static struct {
  union {
    struct {
      gboolean display_nicknames;
      gboolean display_names;
    } status;
    struct {
      guint64 id;
      gboolean show_referenced;
      gboolean show_associated;
    } inspect;
    struct {
      guint64 id;
    } set_default;
    struct {
      guint64 id;
      gfloat volume;
      gdouble limit;
      gboolean is_pid;
      gchar type;
    } set_volume;

    struct {
      guint64 id;
    } get_volume;

    struct {
      guint64 id;
      guint mute;
      gboolean is_pid;
    } set_mute;

    struct {
      guint64 id;
      gint index;
    } set_profile;

    struct {
      guint64 id;
      gint index;
    } set_route;

    struct {
      guint64 id;
    } clear_default;

    struct {
      const gchar *key;
      const gchar *val;
      gboolean delete;
      gboolean save;
      gboolean reset;
    } settings;

    struct {
      guint64 id;
      const char *level;
    } set_log_level;
  };
} cmdline;

G_DEFINE_QUARK (wpctl-error, wpctl_error_domain)

static void
wp_ctl_clear (WpCtl * self)
{
  g_clear_object (&self->om);
  g_clear_object (&self->core);
  g_clear_pointer (&self->loop, g_main_loop_unref);
  g_clear_pointer (&self->context, g_option_context_free);
}

G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpCtl, wp_ctl_clear)

static void
async_quit (WpCore *core, GAsyncResult *res, WpCtl * self)
{
  g_main_loop_quit (self->loop);
}

#define DEFAULT_AUDIO_SINK_ID ((guint64)1 << 32)
#define DEFAULT_AUDIO_SOURCE_ID ((guint64)1 << 33)
#define DEFAULT_VIDEO_SOURCE_ID ((guint64)1 << 34)

static gboolean
parse_id (gboolean allow_def_audio, gboolean allow_def_video, gchar *arg, guint64 *result_id, GError **error)
{
  if (allow_def_audio && (g_strcmp0(arg, "@DEFAULT_SINK@") == 0 ||
      g_strcmp0(arg, "@DEFAULT_AUDIO_SINK@") == 0)) {
    *result_id = DEFAULT_AUDIO_SINK_ID;
  } else if (allow_def_audio && (g_strcmp0(arg, "@DEFAULT_SOURCE@") == 0 ||
      g_strcmp0(arg, "@DEFAULT_AUDIO_SOURCE@") == 0)) {
    *result_id = DEFAULT_AUDIO_SOURCE_ID;
  } else if (allow_def_video && g_strcmp0(arg, "@DEFAULT_VIDEO_SOURCE@") == 0) {
    *result_id = DEFAULT_VIDEO_SOURCE_ID;
  } else {
    long id = strtol (arg, NULL, 10);
    if (id <= 0 || id >= G_MAXUINT32) {
      g_set_error (error, wpctl_error_domain_quark(), 0,
          "'%s' is not a valid number", arg);
      return FALSE;
    }
    *result_id = id;
  }

  return TRUE;
}

static gboolean
translate_id (WpPlugin *def_nodes_api, guint64 id, guint32 *res, GError **error)
{
  gchar *media_class = NULL;

  if (id == DEFAULT_AUDIO_SINK_ID) {
    media_class = "Audio/Sink";
  } else if (id == DEFAULT_AUDIO_SOURCE_ID) {
    media_class = "Audio/Source";
  } else if (id == DEFAULT_VIDEO_SOURCE_ID) {
    media_class = "Video/Source";
  } else {
    /* SPA_ID_INVALID is a special case used by some parse_positional() to
       indicate that no specific ID was given. It needs to be checked because
       currently SPA_ID_INVALID == G_MAXUINT32. */
    if ((id <= 0 || id >= G_MAXUINT32) && id != SPA_ID_INVALID) {
      g_set_error (error, wpctl_error_domain_quark(), 0,
          "'%ld' is not a valid ID", id);
      return FALSE;
    }

    *res = (guint32)id;
    return TRUE;
  }

  if (!def_nodes_api) {
    g_set_error (error, wpctl_error_domain_quark(), 0,
        "Default nodes API is not loaded\n");
    return FALSE;
  }

  g_signal_emit_by_name (def_nodes_api, "get-default-node", media_class, res);

  if (*res <= 0 || *res >= G_MAXUINT32) {
    g_set_error (error, wpctl_error_domain_quark(), 0,
        "'%d' is not a valid ID (returned by default-nodes-api)", *res);
    return FALSE;
  }

  return TRUE;
}

static gboolean
run_nodes_by_pid (WpObjectManager *om, guint32 pid,
    WpIteratorFoldFunc func, gpointer data)
{
  gboolean res = TRUE;
  g_autoptr (WpIterator) client_it = NULL;
  g_auto (GValue) client_val = G_VALUE_INIT;
  client_it = wp_object_manager_new_filtered_iterator (om,
      WP_TYPE_CLIENT, WP_CONSTRAINT_TYPE_PW_PROPERTY,
      PW_KEY_APP_PROCESS_ID, "=u", pid, NULL);
  for (; wp_iterator_next (client_it, &client_val); g_value_unset (&client_val)) {
    WpPipewireObject *client = g_value_get_object (&client_val);
    guint32 client_id = wp_proxy_get_bound_id (WP_PROXY (client));
    g_autoptr (WpIterator) node_it = NULL;
    node_it = wp_object_manager_new_filtered_iterator (om,
        WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY,
        PW_KEY_CLIENT_ID, "=u", client_id, NULL);
    if (!wp_iterator_fold (node_it, func, NULL, data))
      res = FALSE;
  }

  return res;
}

/* status */

static gboolean
status_prepare (WpCtl * self, GError ** error)
{
  wp_object_manager_add_interest (self->om, WP_TYPE_CLIENT, NULL);
  wp_object_manager_add_interest (self->om, WP_TYPE_DEVICE, NULL);
  wp_object_manager_add_interest (self->om, WP_TYPE_NODE, NULL);
  wp_object_manager_add_interest (self->om, WP_TYPE_PORT, NULL);
  wp_object_manager_add_interest (self->om, WP_TYPE_LINK, NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  wp_object_manager_add_interest (self->om, WP_TYPE_METADATA, NULL);
  return TRUE;
}

#define TREE_INDENT_LINE " │  "
#define TREE_INDENT_NODE " ├─ "
#define TREE_INDENT_END  " └─ "
#define TREE_INDENT_EMPTY "    "

struct print_context
{
  WpCtl *self;
  guint32 default_node;
  WpPlugin *mixer_api;
  GHashTable *printed_filters;
};

static void
print_controls (guint32 id, struct print_context *context)
{
  g_autoptr (GVariant) dict = NULL;

  if (context->mixer_api)
    g_signal_emit_by_name (context->mixer_api, "get-volume", id, &dict);

  if (dict) {
    gboolean mute = FALSE;
    gdouble volume = 1.0;
    if (g_variant_lookup (dict, "mute", "b", &mute) &&
        g_variant_lookup (dict, "volume", "d", &volume))
      printf (" [vol: %.2f%s", volume, mute ? " MUTED]" : "]");
  }
  printf ("\n");
}

static void
print_device (const GValue *item, gpointer data)
{
  WpPipewireObject *obj = g_value_get_object (item);
  guint32 id = wp_proxy_get_bound_id (WP_PROXY (obj));
  const gchar *api = wp_pipewire_object_get_property (obj, PW_KEY_DEVICE_API);
  const gchar *name = NULL;

  if (cmdline.status.display_nicknames)
    name = wp_pipewire_object_get_property (obj, PW_KEY_DEVICE_NICK);
  else if (cmdline.status.display_names)
    name = wp_pipewire_object_get_property (obj, PW_KEY_DEVICE_NAME);

  if (!name)
    name = wp_pipewire_object_get_property (obj, PW_KEY_DEVICE_DESCRIPTION);

  printf (TREE_INDENT_LINE "  %4u. %-35s [%s]\n", id, name, api);
}

static void
print_dev_node (const GValue *item, gpointer data)
{
  WpPipewireObject *obj = g_value_get_object (item);
  struct print_context *context = data;
  guint32 id = wp_proxy_get_bound_id (WP_PROXY (obj));
  gboolean is_default = (context->default_node == id);
  const gchar *name = NULL;

  if (cmdline.status.display_nicknames)
    name = wp_pipewire_object_get_property (obj, PW_KEY_NODE_NICK);
  else if (cmdline.status.display_names)
    name = wp_pipewire_object_get_property (obj, PW_KEY_NODE_NAME);

  if (!name)
    name = wp_pipewire_object_get_property (obj, PW_KEY_NODE_DESCRIPTION);

  printf (TREE_INDENT_LINE "%c %4u. %-35s", is_default ? '*' : ' ', id, name);
  print_controls (id, context);
}

static void
print_filter_node (const GValue *item, gpointer data)
{
  struct print_context *context = data;
  WpPipewireObject *obj = g_value_get_object (item);
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) val = G_VALUE_INIT;
  const gchar *link_group;

  /* Skip already printed filters */
  link_group = wp_pipewire_object_get_property (obj, PW_KEY_NODE_LINK_GROUP);
  if (g_hash_table_contains (context->printed_filters, link_group))
    return;

  /* Print all nodes for this link_group */
  printf (TREE_INDENT_LINE "  - %-60s\n", link_group);
  it = wp_object_manager_new_filtered_iterator (context->self->om,
      WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "=s", link_group,
      NULL);
  for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
    WpPipewireObject *node = g_value_get_object (&val);
    guint32 id = wp_proxy_get_bound_id (WP_PROXY (node));
    const gchar *name, *media_class;

    name = wp_pipewire_object_get_property (node, PW_KEY_NODE_NAME);
    if (cmdline.status.display_nicknames)
      name = wp_pipewire_object_get_property (node, PW_KEY_NODE_NICK);
    else if (cmdline.status.display_names)
      name = wp_pipewire_object_get_property (node, PW_KEY_NODE_NAME);
    if (!name)
      name = wp_pipewire_object_get_property (node, PW_KEY_NODE_DESCRIPTION);
    media_class = wp_pipewire_object_get_property (node, PW_KEY_MEDIA_CLASS);

    printf (TREE_INDENT_LINE "%c %4u. %-60s [%s]\n",
        context->default_node == id ? '*' : ' ', id, name, media_class);
  }
  g_clear_pointer (&it, wp_iterator_unref);

  /* Insert link-group in table to not print them again */
  g_hash_table_insert (context->printed_filters, g_strdup (link_group),
      NULL);
}

static void
print_stream_node (const GValue *item, gpointer data)
{
  WpCtl * self = data;
  WpPipewireObject *obj = g_value_get_object (item);
  guint32 id = wp_proxy_get_bound_id (WP_PROXY (obj));
  const gchar *name = wp_pipewire_object_get_property (obj, PW_KEY_APP_NAME);
  if (!name)
    name = wp_pipewire_object_get_property (obj, PW_KEY_NODE_NAME);

  printf (TREE_INDENT_EMPTY "  %4u. %-60s\n", id, name);

  g_autoptr (WpIterator) it = wp_object_manager_new_filtered_iterator (self->om,
      WP_TYPE_PORT, WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_ID, "=u", id,
      NULL);
  g_auto (GValue) val = G_VALUE_INIT;

  for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
    WpPort *port = g_value_get_object (&val);
    obj = WP_PIPEWIRE_OBJECT (port);
    id = wp_proxy_get_bound_id (WP_PROXY (obj));
    name = wp_pipewire_object_get_property (obj, PW_KEY_PORT_NAME);
    WpDirection dir = wp_port_get_direction (port);

    printf (TREE_INDENT_EMPTY "       %4u. %-15s", id, name);

    g_autoptr (WpLink) link = wp_object_manager_lookup (self->om, WP_TYPE_LINK,
        WP_CONSTRAINT_TYPE_PW_PROPERTY, (dir == WP_DIRECTION_OUTPUT) ?
            PW_KEY_LINK_OUTPUT_PORT : PW_KEY_LINK_INPUT_PORT, "=u", id,
        NULL);
    if (link) {
      guint32 peer_id = -1;
      wp_link_get_linked_object_ids(link,
          NULL, (dir == WP_DIRECTION_INPUT) ? &peer_id : NULL,
          NULL, (dir == WP_DIRECTION_OUTPUT) ? &peer_id : NULL);
      g_autoptr (WpPipewireObject) peer = wp_object_manager_lookup (
          self->om, WP_TYPE_PORT,
          WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", peer_id,
          NULL);
      name = wp_pipewire_object_get_property (peer, PW_KEY_PORT_ALIAS);

      g_autoptr (GEnumClass) klass = g_type_class_ref (WP_TYPE_LINK_STATE);
      GEnumValue *state = g_enum_get_value (klass, wp_link_get_state (link, NULL));

      printf (" %c %s\t[%s]\n", (dir == WP_DIRECTION_OUTPUT) ? '>' : '<', name,
          state->value_nick);
    } else {
      printf ("\n");
    }
  }
}

static void
status_run (WpCtl * self)
{
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) val = G_VALUE_INIT;
  g_autoptr (WpPlugin) def_nodes_api = NULL;
  struct print_context context = { .self = self };

  def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
  context.mixer_api = wp_plugin_find (self->core, "mixer-api");

  /* server + clients */
  printf ("PipeWire '%s' [%s, %s@%s, cookie:%u]\n",
      wp_core_get_remote_name (self->core),
      wp_core_get_remote_version (self->core),
      wp_core_get_remote_user_name (self->core),
      wp_core_get_remote_host_name (self->core),
      wp_core_get_remote_cookie (self->core));

  printf (TREE_INDENT_END "Clients:\n");
  it = wp_object_manager_new_filtered_iterator (self->om, WP_TYPE_CLIENT, NULL);
  for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
    WpProxy *client = g_value_get_object (&val);
    g_autoptr (WpProperties) properties =
        wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (client));

    printf (TREE_INDENT_EMPTY "  %4u. %-35s [%s, %s@%s, pid:%s]\n",
        wp_proxy_get_bound_id (client),
        wp_properties_get (properties, PW_KEY_APP_NAME),
        wp_properties_get (properties, PW_KEY_CORE_VERSION),
        wp_properties_get (properties, PW_KEY_APP_PROCESS_USER),
        wp_properties_get (properties, PW_KEY_APP_PROCESS_HOST),
        wp_properties_get (properties, PW_KEY_APP_PROCESS_ID));
  }
  g_clear_pointer (&it, wp_iterator_unref);
  printf ("\n");

  /* sessions */
  const gchar *MEDIA_TYPES[] = { "Audio", "Video" };

  for (guint i = 0; i < G_N_ELEMENTS (MEDIA_TYPES); i++) {
    const gchar *media_type = MEDIA_TYPES[i];
    g_autoptr (WpIterator) child_it = NULL;

    printf ("%s\n", media_type);

    if (media_type && *media_type != '\0') {
      gchar media_type_glob[16];
      gchar media_class[24];

      g_snprintf (media_type_glob, sizeof(media_type_glob), "*%s*", media_type);

      printf (TREE_INDENT_NODE "Devices:\n");
      child_it = wp_object_manager_new_filtered_iterator (self->om,
          WP_TYPE_DEVICE,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
          NULL);
      wp_iterator_foreach (child_it, print_device, self);
      g_clear_pointer (&child_it, wp_iterator_unref);

      printf (TREE_INDENT_LINE "\n");

      printf (TREE_INDENT_NODE "Sinks:\n");
      g_snprintf (media_class, sizeof(media_class), "%s/Sink", media_type);
      context.default_node = -1;
      if (def_nodes_api)
        g_signal_emit_by_name (def_nodes_api, "get-default-node", media_class,
            &context.default_node);
      child_it = wp_object_manager_new_filtered_iterator (self->om,
          WP_TYPE_NODE,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "*/Sink*",
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
          NULL);
      wp_iterator_foreach (child_it, print_dev_node, (gpointer) &context);
      g_clear_pointer (&child_it, wp_iterator_unref);

      printf (TREE_INDENT_LINE "\n");

      printf (TREE_INDENT_NODE "Sources:\n");
      g_snprintf (media_class, sizeof(media_class), "%s/Source", media_type);
      context.default_node = -1;
      if (def_nodes_api)
        g_signal_emit_by_name (def_nodes_api, "get-default-node", media_class,
            &context.default_node);
      child_it = wp_object_manager_new_filtered_iterator (self->om,
          WP_TYPE_NODE,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "*/Source*",
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
          NULL);
      wp_iterator_foreach (child_it, print_dev_node, (gpointer) &context);
      g_clear_pointer (&child_it, wp_iterator_unref);

      printf (TREE_INDENT_LINE "\n");

      printf (TREE_INDENT_NODE "Filters:\n");
      context.printed_filters = g_hash_table_new_full (g_str_hash,
          g_str_equal, g_free, NULL);
      child_it = wp_object_manager_new_filtered_iterator (self->om,
          WP_TYPE_NODE,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "+",
          NULL);
      wp_iterator_foreach (child_it, print_filter_node, (gpointer) &context);
      g_clear_pointer (&child_it, wp_iterator_unref);
      g_clear_pointer (&context.printed_filters, g_hash_table_unref);

      printf (TREE_INDENT_LINE "\n");

      printf (TREE_INDENT_END "Streams:\n");
      child_it = wp_object_manager_new_filtered_iterator (self->om,
          WP_TYPE_NODE,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "Stream/*",
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", media_type_glob,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
          NULL);
      wp_iterator_foreach (child_it, print_stream_node, self);
      g_clear_pointer (&child_it, wp_iterator_unref);
    }

    printf ("\n");
  }

  /* Settings */
  printf ("Settings\n");

  printf (TREE_INDENT_END "Default Configured Devices:\n");
  if (def_nodes_api) {
    for (guint i = 0; i < G_N_ELEMENTS (DEFAULT_NODE_MEDIA_CLASSES); i++) {
      const gchar *name = NULL;
      g_signal_emit_by_name (def_nodes_api, "get-default-configured-node-name",
          DEFAULT_NODE_MEDIA_CLASSES[i], &name);
      if (name)
        printf (TREE_INDENT_EMPTY "  %4u. %-12s  %s\n", i,
            DEFAULT_NODE_MEDIA_CLASSES[i], name);
    }
  }

  g_clear_object (&context.mixer_api);
  g_main_loop_quit (self->loop);
}

/* get-volume  */

static gboolean
get_volume_parse_positional (gint argc, gchar ** argv, GError **error)
{
  if (argc < 3) {
    g_set_error (error, wpctl_error_domain_quark(), 0,
        "ID is required");
    return FALSE;
  } else {
    return parse_id (true, false, argv[2], &cmdline.get_volume.id, error);
  }
}

static gboolean
get_volume_prepare (WpCtl * self, GError ** error)
{
  wp_object_manager_add_interest (self->om, WP_TYPE_NODE, NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  return TRUE;
}

static void
do_print_volume (WpCtl * self, WpPipewireObject *proxy)
{
  g_autoptr (WpPlugin) mixer_api = wp_plugin_find (self->core, "mixer-api");
  GVariant *variant = NULL;
  gboolean mute = FALSE;
  gdouble volume = 1.0;
  guint32 id = wp_proxy_get_bound_id (WP_PROXY (proxy));

  g_signal_emit_by_name (mixer_api, "get-volume", id, &variant);
  if (!variant) {
    fprintf (stderr, "Node %d does not support volume\n", id);
    return;
  }
  g_variant_lookup (variant, "volume", "d", &volume);
  g_variant_lookup (variant, "mute", "b", &mute);
  g_clear_pointer (&variant, g_variant_unref);

  printf ("Volume: %.2f%s", volume, mute ? " [MUTED]\n" : "\n");
}

static void
get_volume_run (WpCtl * self)
{
  g_autoptr (WpPlugin) def_nodes_api = NULL;
  g_autoptr (GError) error = NULL;
  g_autoptr (WpPipewireObject) proxy = NULL;
  guint32 id;

  def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");

  if (!translate_id (def_nodes_api, cmdline.get_volume.id, &id, &error)) {
    fprintf(stderr, "Translate ID error: %s\n\n", error->message);
    goto out;
  }

  proxy = wp_object_manager_lookup (self->om, WP_TYPE_GLOBAL_PROXY,
    WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, NULL);
  if (!proxy) {
    fprintf (stderr, "Node '%d' not found\n", id);
    goto out;
  }

  do_print_volume (self, proxy);

out:
  g_main_loop_quit (self->loop);
}

/* inspect */

static gboolean
inspect_parse_positional (gint argc, gchar ** argv, GError **error)
{
  if (argc < 3) {
    g_set_error (error, wpctl_error_domain_quark(), 0, "ID is required");
    return FALSE;
  }

  return parse_id (true, true, argv[2], &cmdline.inspect.id, error);
}

static gboolean
inspect_prepare (WpCtl * self, GError ** error)
{
  /* collect all objects */
  wp_object_manager_add_interest (self->om, WP_TYPE_GLOBAL_PROXY, NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  return TRUE;
}

static inline void
inspect_prefix_line (guint nest_level, gboolean node)
{
  for (guint i = 1; i < nest_level; i++)
    printf (TREE_INDENT_EMPTY TREE_INDENT_LINE);
  if (nest_level > 0)
    printf (TREE_INDENT_EMPTY "%s", node ? TREE_INDENT_NODE : TREE_INDENT_LINE);
}

struct {
  const gchar *key;
  const gchar *type;
} assoc_keys[] = {
  { PW_KEY_CLIENT_ID, "Client" },
  { PW_KEY_DEVICE_ID, "Device" },
  { PW_KEY_LINK_INPUT_NODE, NULL },
  { PW_KEY_LINK_INPUT_PORT, NULL },
  { PW_KEY_LINK_OUTPUT_NODE, NULL },
  { PW_KEY_LINK_OUTPUT_PORT, NULL },
  { PW_KEY_LINK_ID, "Link" },
  { PW_KEY_NODE_ID, "Node" },
  { PW_KEY_PORT_ID, "Port" },
  { PW_KEY_SESSION_ID, "Session" },
};

static inline gboolean
key_is_object_reference (const gchar *key)
{
  for (guint i = 0; i < G_N_ELEMENTS (assoc_keys); i++)
    if (!g_strcmp0 (key, assoc_keys[i].key))
      return TRUE;
  return FALSE;
}

static inline const gchar *
get_association_key (WpProxy * proxy)
{
  for (guint i = 0; i < G_N_ELEMENTS (assoc_keys); i++) {
    if (assoc_keys[i].type &&
        strstr (WP_PROXY_GET_CLASS (proxy)->pw_iface_type, assoc_keys[i].type))
      return assoc_keys[i].key;
  }
  return NULL;
}

struct property_item {
  const gchar *key;
  const gchar *value;
};

static gint
property_item_compare (gconstpointer a, gconstpointer b)
{
  return g_strcmp0 (
      ((struct property_item *) a)->key,
      ((struct property_item *) b)->key);
}

static void
inspect_print_object (WpCtl * self, WpProxy * proxy, guint nest_level)
{
  g_autoptr (WpProperties) properties =
      WP_IS_PIPEWIRE_OBJECT (proxy) ?
      wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (proxy)) :
      wp_properties_new_empty ();
  g_autoptr (WpProperties) global_p =
      wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (proxy));
  g_autoptr (GArray) array =
      g_array_new (FALSE, FALSE, sizeof (struct property_item));

  /* print basic object info */
  inspect_prefix_line (nest_level, TRUE);
  printf ("id %u, type %s\n",
      wp_proxy_get_bound_id (proxy),
      WP_PROXY_GET_CLASS (proxy)->pw_iface_type);

  /* merge the two property sets */
  properties = wp_properties_ensure_unique_owner (properties);
  wp_properties_add (properties, global_p);
  wp_properties_set (properties, "object.id", NULL);

  /* copy key/value pointers to an array for sorting */
  {
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) item = G_VALUE_INIT;

    for (it = wp_properties_new_iterator (properties);
          wp_iterator_next (it, &item);
          g_value_unset (&item)) {
      WpPropertiesItem *pi = g_value_get_boxed (&item);
      struct property_item prop_item = {
        .key = wp_properties_item_get_key (pi),
        .value = wp_properties_item_get_value (pi),
      };
      g_array_append_val (array, prop_item);
    }
  }

  /* sort */
  g_array_sort (array, property_item_compare);

  /* print */
  for (guint i = 0; i < array->len; i++) {
    struct property_item *prop_item =
        &g_array_index (array, struct property_item, i);
    gboolean is_global =
        (wp_properties_get (global_p, prop_item->key) != NULL);

    inspect_prefix_line (nest_level, FALSE);
    printf ("  %c %s = \"%s\"\n", is_global ? '*' : ' ',
        prop_item->key, prop_item->value);

    /* if the property is referencing an object, print the object */
    if (cmdline.inspect.show_referenced && nest_level == 0 &&
        key_is_object_reference (prop_item->key))
    {
      guint id = (guint) strtol (prop_item->value, NULL, 10);
      g_autoptr (WpProxy) refer_proxy =
          wp_object_manager_lookup (self->om, WP_TYPE_GLOBAL_PROXY,
              WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, NULL);

      if (refer_proxy)
        inspect_print_object (self, refer_proxy, nest_level + 1);
    }
  }

  /* print associated objects */
  if (cmdline.inspect.show_associated && nest_level == 0) {
    const gchar *lookup_key = get_association_key (proxy);
    if (lookup_key) {
      g_autoptr (WpIterator) it =
          wp_object_manager_new_filtered_iterator (self->om,
              WP_TYPE_PIPEWIRE_OBJECT, WP_CONSTRAINT_TYPE_PW_PROPERTY,
              lookup_key, "=u", wp_proxy_get_bound_id (proxy), NULL);
      g_auto (GValue) item = G_VALUE_INIT;

      inspect_prefix_line (nest_level, TRUE);
      printf ("associated objects:\n");

      for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
        WpProxy *assoc_proxy = g_value_get_object (&item);
        inspect_print_object (self, assoc_proxy, nest_level + 1);
      }
    }
  }
}

static void
inspect_run (WpCtl * self)
{
  g_autoptr (WpProxy) proxy = NULL;
  g_autoptr (WpPlugin) def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
  g_autoptr (GError) error = NULL;
  guint32 id;


  if (!translate_id (def_nodes_api, cmdline.inspect.id, &id, &error)) {
    fprintf(stderr, "Translate ID error: %s\n\n", error->message);
    goto out_err;
  }

  proxy = wp_object_manager_lookup (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, NULL);
  if (!proxy) {
    fprintf (stderr, "Object '%d' not found\n", id);
    goto out_err;
  }

  inspect_print_object (self, proxy, 0);

out:
  g_main_loop_quit (self->loop);
  return;

out_err:
  self->exit_code = 3;
  goto out;
}

/* set-default */
static gboolean
set_default_parse_positional (gint argc, gchar ** argv, GError **error)
{
  if (argc < 3) {
    g_set_error (error, wpctl_error_domain_quark(), 0, "ID is required");
    return FALSE;
  }

  return parse_id (false, false, argv[2], &cmdline.set_default.id, error);
}

static gboolean
set_default_prepare (WpCtl * self, GError ** error)
{
  wp_object_manager_add_interest (self->om, WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY,
      "object.id", "=u", cmdline.set_default.id,
      NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_METADATA,
      WP_OBJECT_FEATURES_ALL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_NODE,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  return TRUE;
}

static void
set_default_run (WpCtl * self)
{
  g_autoptr (WpPlugin) def_nodes_api = NULL;
  g_autoptr (WpProxy) proxy = NULL;
  guint32 id;
  g_autoptr (GError) error = NULL;
  const gchar *media_class;

  def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
  if (!def_nodes_api) {
    fprintf (stderr, "Default nodes API not loaded\n");
    goto out;
  }

  if (!translate_id (def_nodes_api, cmdline.set_default.id, &id, &error)) {
    fprintf(stderr, "Translate ID error: %s\n\n", error->message);
    goto out;
  }

  proxy = wp_object_manager_lookup (self->om, WP_TYPE_NODE, NULL);
  if (!proxy) {
    fprintf (stderr, "Node '%d' not found\n", id);
    goto out;
  }

  media_class = wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (proxy),
      PW_KEY_MEDIA_CLASS);
  for (guint i = 0; i < G_N_ELEMENTS (DEFAULT_NODE_MEDIA_CLASSES); i++) {
    if (!g_strcmp0 (media_class, DEFAULT_NODE_MEDIA_CLASSES[i])) {
      gboolean res = FALSE;
      const gchar *name = wp_pipewire_object_get_property (
          WP_PIPEWIRE_OBJECT (proxy), PW_KEY_NODE_NAME);
      if (!name) {
        fprintf (stderr, "node %u does not have a valid node.name\n", id);
        goto out;
      }

      g_signal_emit_by_name (def_nodes_api, "set-default-configured-node-name",
          DEFAULT_NODE_MEDIA_CLASSES[i], name, &res);
      if (!res) {
        fprintf (stderr, "failed to set default node %u (media.class = %s)\n", id,
            media_class);
        goto out;
      }

      wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
      return;
    }
  }

  fprintf (stderr, "%u is not a device node (media.class = %s)\n", id, media_class);

out:
  self->exit_code = 3;
  g_main_loop_quit (self->loop);
}

/* set-volume */

static gboolean
set_volume_parse_positional (gint argc, gchar ** argv, GError **error)
{
  g_autoptr (GRegex) regex = NULL;
  g_autoptr (GMatchInfo) info = NULL;

  if (argc < 4) {
    g_set_error_literal (error, wpctl_error_domain_quark(), 0,
        "ID and VOL[%][-/+] are required");
    return FALSE;
  }

  regex = g_regex_new ("^(\\d*\\.?\\d*)(%?)([-+]?)$", 0, 0, NULL);

  if (g_regex_match(regex, argv[3], 0, &info)) {
    gchar *str = g_match_info_fetch (info, 1);
    cmdline.set_volume.volume = strtof (str, NULL);
    cmdline.set_volume.type = 'a';
    g_free (str);

    str = g_match_info_fetch (info, 2);
    if (g_strcmp0 (str, "%") == 0)
      cmdline.set_volume.volume /= 100;
    g_free (str);

    str = g_match_info_fetch (info, 3);
    if (g_strcmp0 (str, "-") == 0) {
      cmdline.set_volume.volume = -(cmdline.set_volume.volume);
      cmdline.set_volume.type = 's';
    } else if (g_strcmp0 (str, "+") == 0) {
      cmdline.set_volume.type = 's';
    }
    g_free (str);
  } else {
    g_set_error (error, wpctl_error_domain_quark(), 0,
        "Invalid volume argument. See wpctl set-volume --help");
    return FALSE;
  }

  return parse_id (!cmdline.set_volume.is_pid, false, argv[2],
      &cmdline.set_volume.id, error);
}

static gboolean
set_volume_prepare (WpCtl * self, GError ** error)
{
  wp_object_manager_add_interest (self->om, WP_TYPE_NODE, NULL);
  wp_object_manager_add_interest (self->om, WP_TYPE_CLIENT, NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  return TRUE;
}

static gboolean
do_set_volume (WpCtl * self, WpPipewireObject *proxy)
{
  g_autoptr (WpPlugin) mixer_api = wp_plugin_find (self->core, "mixer-api");
  g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  GVariant *variant = NULL;
  gboolean res = FALSE;
  gdouble curr_volume = 1.0;
  guint32 id = wp_proxy_get_bound_id (WP_PROXY (proxy));

  if (cmdline.set_volume.type == 's') {
    g_signal_emit_by_name (mixer_api, "get-volume", id, &variant);
      if (!variant) {
      fprintf (stderr, "Node %d does not support volume\n", id);
      g_clear_pointer (&variant, g_variant_unref);
      return FALSE;
    }
    g_variant_lookup (variant, "volume", "d", &curr_volume);
    g_clear_pointer (&variant, g_variant_unref);

    cmdline.set_volume.volume = (cmdline.set_volume.volume + curr_volume);
  }
  if (cmdline.set_volume.volume < 0) {
    cmdline.set_volume.volume = 0.0;
  }
  if (cmdline.set_volume.limit > 0) {
    if (cmdline.set_volume.volume > cmdline.set_volume.limit) {
      cmdline.set_volume.volume = cmdline.set_volume.limit;
    }
  }

  g_variant_builder_add (&b, "{sv}", "volume",
      g_variant_new_double (cmdline.set_volume.volume));
  variant = g_variant_builder_end (&b);

  g_signal_emit_by_name (mixer_api, "set-volume", id, variant, &res);
  if (!res) {
    fprintf (stderr, "Node %d does not support volume\n", id);
    return FALSE;
  }

  return TRUE;
}

static gboolean
do_set_volume_cb (const GValue *node_val, GValue *ret, gpointer data)
{
  WpCtl * self = data;
  WpPipewireObject *node = g_value_get_object (node_val);
  return do_set_volume (self, node);
}

static void
set_volume_run (WpCtl * self)
{
  g_autoptr (WpPipewireObject) proxy = NULL;
  g_autoptr (WpPlugin) def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
  g_autoptr (GError) error = NULL;
  guint32 id;

  if (!translate_id (def_nodes_api, cmdline.set_volume.id, &id, &error)) {
    fprintf(stderr, "Translate ID error: %s\n\n", error->message);
    goto out;
  }

  if (cmdline.set_volume.is_pid) {
    if (!run_nodes_by_pid (self->om, id, do_set_volume_cb, self)) {
      fprintf (stderr, "Could not set volume in all nodes with PID '%d'\n", id);
      goto out;
    }
  } else {
    proxy = wp_object_manager_lookup (self->om, WP_TYPE_GLOBAL_PROXY,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "=u", id, NULL);
    if (!proxy) {
      fprintf (stderr, "Object '%d' not found\n", id);
      goto out;
    }
    if (!do_set_volume (self, proxy))
      goto out;
  }

  wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
  return;

out:
  self->exit_code = 3;
  g_main_loop_quit (self->loop);
}

/* set-mute */

static gboolean
set_mute_parse_positional (gint argc, gchar ** argv, GError **error)
{
  if (argc < 4) {
    g_set_error (error, wpctl_error_domain_quark(), 0,
        "ID and one of '1', '0' or 'toggle' are required");
    return FALSE;
  }

  if (!g_strcmp0 (argv[3], "1"))
    cmdline.set_mute.mute = 1;
  else if (!g_strcmp0 (argv[3], "0"))
    cmdline.set_mute.mute = 0;
  else if (!g_strcmp0 (argv[3], "toggle"))
    cmdline.set_mute.mute = 2;
  else {
    g_set_error (error, wpctl_error_domain_quark(), 0,
        "'%s' is not a valid mute option", argv[3]);
    return FALSE;
  }

  return parse_id (!cmdline.set_mute.is_pid, false, argv[2],
      &cmdline.set_mute.id, error);
}

static gboolean
set_mute_prepare (WpCtl * self, GError ** error)
{
  wp_object_manager_add_interest (self->om, WP_TYPE_NODE, NULL);
  wp_object_manager_add_interest (self->om, WP_TYPE_CLIENT, NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  return TRUE;
}

static gboolean
do_set_mute (WpCtl * self, WpPipewireObject *proxy)
{
  g_autoptr (WpPlugin) mixer_api = wp_plugin_find (self->core, "mixer-api");
  g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  GVariant *variant = NULL;
  gboolean res = FALSE;
  gboolean mute = FALSE;
  guint32 id = wp_proxy_get_bound_id (WP_PROXY (proxy));

  g_signal_emit_by_name (mixer_api, "get-volume", id, &variant);
  if (!variant) {
    fprintf (stderr, "Node %d does not support mute\n", id);
    return FALSE;
  }

  g_variant_lookup (variant, "mute", "b", &mute);
  g_clear_pointer (&variant, g_variant_unref);

  if (cmdline.set_mute.mute == 2)
    mute = !mute;
  else
    mute = !!cmdline.set_mute.mute;

  g_variant_builder_add (&b, "{sv}", "mute", g_variant_new_boolean (mute));
  variant = g_variant_builder_end (&b);

  g_signal_emit_by_name (mixer_api, "set-volume", id, variant, &res);

  return TRUE;
}

static gboolean
do_set_mute_cb (const GValue *node_val, GValue *ret, gpointer data)
{
  WpCtl * self = data;
  WpPipewireObject *node = g_value_get_object (node_val);
  return do_set_mute (self, node);
}

static void
set_mute_run (WpCtl * self)
{
  g_autoptr (WpPipewireObject) proxy = NULL;
  g_autoptr (WpPlugin) def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
  g_autoptr (GError) error = NULL;
  guint32 id;

  if (!translate_id (def_nodes_api, cmdline.set_mute.id, &id, &error)) {
    fprintf(stderr, "Translate ID error: %s\n\n", error->message);
    goto out;
  }

  if (cmdline.set_mute.is_pid) {
    if (!run_nodes_by_pid (self->om, id, do_set_mute_cb, self)) {
      fprintf (stderr, "Could not set mute in all nodes with PID '%d'\n", id);
      goto out;
    }
  } else {
    proxy = wp_object_manager_lookup (self->om, WP_TYPE_GLOBAL_PROXY,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "=u", id, NULL);
    if (!proxy) {
      fprintf (stderr, "Object '%d' not found\n", id);
      goto out;
    }
    if (!do_set_mute (self, proxy))
        goto out;
  }

  wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
  return;

out:
  self->exit_code = 3;
  g_main_loop_quit (self->loop);
}

/* set-profile */

static gboolean
set_profile_parse_positional (gint argc, gchar ** argv, GError **error)
{
  if (argc < 4) {
    g_set_error (error, wpctl_error_domain_quark(), 0,
        "ID and INDEX are required");
    return FALSE;
  }

  cmdline.set_profile.index = atoi (argv[3]);
  return parse_id (true, true, argv[2], &cmdline.set_profile.id, error);
}

static gboolean
set_profile_prepare (WpCtl * self, GError ** error)
{
  wp_object_manager_add_interest (self->om, WP_TYPE_GLOBAL_PROXY, NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  return TRUE;
}

static void
set_profile_run (WpCtl * self)
{
  g_autoptr (WpPlugin) def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
  g_autoptr (WpPipewireObject) proxy = NULL;
  g_autoptr (GError) error = NULL;
  guint32 node_id;

  if (!translate_id (def_nodes_api, cmdline.set_profile.id, &node_id, &error)) {
    fprintf(stderr, "Translate ID error: %s\n\n", error->message);
    goto out;
  }

  proxy = wp_object_manager_lookup (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "=u", node_id, NULL);
  if (!proxy) {
    fprintf (stderr, "Object '%d' not found\n", node_id);
    goto out;
  }
  wp_pipewire_object_set_param (proxy, "Profile", 0,
      wp_spa_pod_new_object (
        "Spa:Pod:Object:Param:Profile", "Profile",
        "index", "i", cmdline.set_profile.index,
        NULL));
  wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
  return;

out:
  self->exit_code = 3;
  g_main_loop_quit (self->loop);
}

/* set-route */

static gboolean
set_route_parse_positional (gint argc, gchar ** argv, GError **error)
{
  if (argc < 4) {
    g_set_error (error, wpctl_error_domain_quark(), 0,
        "ID and INDEX required");
    return FALSE;
  }

  cmdline.set_route.index = atoi (argv[3]);
  return parse_id (true, true, argv[2], &cmdline.set_route.id, error);
}

static gboolean
set_route_prepare (WpCtl * self, GError ** error)
{
  wp_object_manager_add_interest (self->om, WP_TYPE_GLOBAL_PROXY, NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  return TRUE;
}

static void
set_route_run (WpCtl * self)
{
  g_autoptr (WpPlugin) def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
  g_autoptr (WpPipewireObject) proxy = NULL;
  g_autoptr (WpPipewireObject) device_proxy = NULL;
  g_autoptr (GError) error = NULL;
  guint32 node_id;

  if (!translate_id (def_nodes_api, cmdline.set_route.id, &node_id, &error)) {
    fprintf(stderr, "Translate ID error: %s\n\n", error->message);
    goto out;
  }

  proxy = wp_object_manager_lookup (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "=u", node_id, NULL);
  if (!proxy) {
    fprintf (stderr, "Object '%d' not found\n", node_id);
    goto out;
  }

  const char * route_device_str = wp_pipewire_object_get_property(proxy, "card.profile.device");
  if (!route_device_str) {
    fprintf (stderr, "Property 'card.profile.device' not found\n");
    goto out;
  }
  guint32 route_device = atoi (route_device_str);

  const char * device_id_str = wp_pipewire_object_get_property (proxy, "device.id");
  if (!device_id_str) {
    fprintf (stderr, "Property 'device.id' not found\n");
    goto out;
  }
  guint32 device_id = atoi (device_id_str);

  device_proxy = wp_object_manager_lookup (self->om, WP_TYPE_GLOBAL_PROXY,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "=u", device_id, NULL);
  if (!device_proxy) {
    fprintf (stderr, "Object '%d' not found\n", device_id);
    goto out;
  }

  wp_pipewire_object_set_param (device_proxy, "Route", 0,
      wp_spa_pod_new_object (
        "Spa:Pod:Object:Param:Route", "Route",
        "index", "i", cmdline.set_route.index,
        "device", "i", route_device,
        NULL));
  wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
  return;

out:
  self->exit_code = 3;
  g_main_loop_quit (self->loop);
}

/* clear-default */

static gboolean
clear_default_parse_positional (gint argc, gchar ** argv, GError **error)
{
  if (argc >= 3) {
    long id = strtol (argv[2], NULL, 10);
    if (id < 0 || id >= (long)G_N_ELEMENTS (DEFAULT_NODE_MEDIA_CLASSES)) {
      g_set_error (error, wpctl_error_domain_quark(), 0,
          "The setting ID value must be between 0 and %ld inclusive",
              G_N_ELEMENTS (DEFAULT_NODE_MEDIA_CLASSES) - 1);
      return FALSE;
    }
    cmdline.clear_default.id = id;
  } else {
    cmdline.clear_default.id = SPA_ID_INVALID;
  }

  return TRUE;
}

static gboolean
clear_default_prepare (WpCtl * self, GError ** error)
{
  return TRUE;
}

static void
clear_default_run (WpCtl * self)
{
  g_autoptr (WpPlugin) def_nodes_api = NULL;
  gboolean res = FALSE;

  def_nodes_api = wp_plugin_find (self->core, "default-nodes-api");
  if (!def_nodes_api) {
    fprintf (stderr, "Default nodes API not loaded\n");
    goto out;
  }

  /* clear all defaults if id was not given */
  if (cmdline.clear_default.id == SPA_ID_INVALID) {
    for (guint i = 0; i < G_N_ELEMENTS (DEFAULT_NODE_MEDIA_CLASSES); i++) {
      g_signal_emit_by_name (def_nodes_api, "set-default-configured-node-name",
          DEFAULT_NODE_MEDIA_CLASSES[i], NULL, &res);
      if (!res) {
        fprintf (stderr, "failed to clear default configured node (%s)\n",
            DEFAULT_NODE_MEDIA_CLASSES[i]);
        goto out;
      }
    }
  } else {
    g_signal_emit_by_name (def_nodes_api, "set-default-configured-node-name",
        DEFAULT_NODE_MEDIA_CLASSES[cmdline.clear_default.id], NULL, &res);
    if (!res) {
      fprintf (stderr, "failed to clear default configured node (%s)\n",
          DEFAULT_NODE_MEDIA_CLASSES[cmdline.clear_default.id]);
      goto out;
    }
  }

  wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
  return;

out:
  self->exit_code = 3;
  g_main_loop_quit (self->loop);
}

/* settings */

static gboolean
settings_parse_positional (gint argc, gchar ** argv, GError **error)
{
  cmdline.settings.key = NULL;
  cmdline.settings.val = NULL;
  if (argc >= 3) {
    cmdline.settings.key = argv[2];
    if (argc >= 4)
      cmdline.settings.val = argv[3];
  }

  if (cmdline.settings.delete && cmdline.settings.save) {
    g_set_error (error, wpctl_error_domain_quark(), 0,
                   "Cannot use --delete and --save flags at the same time");
    return FALSE;
  }
  if (cmdline.settings.delete && cmdline.settings.reset) {
    g_set_error (error, wpctl_error_domain_quark(), 0,
                   "Cannot use --delete and --reset flags at the same time");
    return FALSE;
  }
  if (cmdline.settings.save && cmdline.settings.reset) {
    g_set_error (error, wpctl_error_domain_quark(), 0,
                   "Cannot use --save and --reset flags at the same time");
    return FALSE;
  }

  return TRUE;
}

static gboolean
settings_prepare (WpCtl * self, GError ** error)
{
  wp_object_manager_add_interest (self->om, WP_TYPE_METADATA, NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_METADATA,
      WP_OBJECT_FEATURES_ALL);
  return TRUE;
}

static const char *
settings_spec_type_to_string (WpSettingsSpecType type)
{
  switch (type) {
    case WP_SETTINGS_SPEC_TYPE_BOOL:
      return "Boolean";
    case WP_SETTINGS_SPEC_TYPE_INT:
      return "Integer";
    case WP_SETTINGS_SPEC_TYPE_FLOAT:
      return "Float";
    case WP_SETTINGS_SPEC_TYPE_STRING:
      return "String";
    case WP_SETTINGS_SPEC_TYPE_ARRAY:
      return "Array";
    case WP_SETTINGS_SPEC_TYPE_OBJECT:
      return "Object";
    case WP_SETTINGS_SPEC_TYPE_UNKNOWN:
    default:
      break;
  }
  return "Uknown";
}

static void
print_setting (WpSettings *s, const gchar *key)
{
  g_autoptr (WpSpaJson) value = NULL;
  g_autoptr (WpSpaJson) saved = NULL;
  g_autoptr (WpSettingsSpec) spec = NULL;
  const gchar *desc;
  WpSettingsSpecType val_type;
  g_autoptr (WpSpaJson) def = NULL;
  g_autoptr (WpSpaJson) min = NULL;
  g_autoptr (WpSpaJson) max = NULL;

  value = wp_settings_get (s, key);
  saved = wp_settings_get_saved (s, key);
  spec = wp_settings_get_spec (s, key);
  desc = wp_settings_spec_get_description (spec);
  val_type = wp_settings_spec_get_value_type (spec);
  def = wp_settings_spec_get_default_value (spec);
  min = wp_settings_spec_get_min_value (spec);
  max = wp_settings_spec_get_max_value (spec);

  /* print key */
  printf ("- Name: %s\n", key);

  /* print spec */
  printf ("  Desc: %s\n", desc);
  printf ("  Type: %s\n", settings_spec_type_to_string (val_type));
  printf ("  Default: %s", wp_spa_json_get_data (def));
  if (min && max)
    printf ("\t[Min: %s, Max: %s]", wp_spa_json_get_data (min),
        wp_spa_json_get_data (max));
  printf ("\n");
  printf ("  Value: %s", wp_spa_json_get_data (value));
  if (saved)
    printf ("\t[Saved: %s]", wp_spa_json_get_data (saved));
  printf ("\n\n");
}

static void
print_settings (WpSettings *s)
{
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;

  printf ("Settings:\n\n");

  it = wp_settings_new_iterator (s);
  while (wp_iterator_next (it, &item)) {
    WpSettingsItem *si = g_value_get_boxed (&item);
    const gchar *key = wp_settings_item_get_key (si);
    print_setting (s, key);
    g_value_unset (&item);
  }
}

static void
settings_run (WpCtl * self)
{
  g_autoptr (WpSettings) s = NULL;
  const gchar *key = cmdline.settings.key, *val = cmdline.settings.val;
  gboolean delete_flag = cmdline.settings.delete;
  gboolean save_flag = cmdline.settings.save;
  gboolean reset_flag = cmdline.settings.reset;

  s = wp_settings_find (self->core, NULL);
  if (!s) {
    printf ("Could not find registered settings\n");
    goto out;
  }

  /* If no key or value are provided */
  if (!key && !val) {
    if (!delete_flag && !save_flag && !reset_flag) {
      print_settings (s);
    } else if (!delete_flag && save_flag && !reset_flag) {
      wp_settings_save_all (s);
      fprintf (stderr, "Saved all settings\n");
    } else if (delete_flag && !save_flag && !reset_flag) {
      wp_settings_delete_all (s);
      fprintf (stderr, "Deleted all saved settings\n");
    } else if (!delete_flag && !save_flag && reset_flag) {
      wp_settings_reset_all (s);
      fprintf (stderr, "Reset all settings\n");
    } else {
      g_assert_not_reached ();
    }
  }

  /* If key is only provided */
  else if (key && !val) {
    if (!delete_flag && !save_flag && !reset_flag) {
      g_autoptr (WpSpaJson) value = NULL;
      value = wp_settings_get (s, key);
      if (value) {
        g_autoptr (WpSpaJson) saved = wp_settings_get_saved (s, key);
        printf ("Value: %s", wp_spa_json_get_data (value));
        if (saved)
          printf (" (Saved: %s)", wp_spa_json_get_data (saved));
        printf ("\n");
      } else {
        printf ("Setting '%s' not found\n", key);
      }
    } else if (!delete_flag && save_flag && !reset_flag) {
      if (wp_settings_save (s, key))
        printf ("Saved setting '%s' successfully\n", key);
      else
        printf ("Setting '%s' not found\n", key);
    } else if (delete_flag && !save_flag && !reset_flag) {
      if (wp_settings_delete (s, key))
        printf ("Deleted setting '%s' successfully\n", key);
      else
        printf ("Setting '%s' not found\n", key);
    } else if (!delete_flag && !save_flag && reset_flag) {
      if (wp_settings_reset (s, key))
        printf ("Reset setting '%s' successfully\n", key);
      else
        printf ("Setting '%s' not found\n", key);
    } else {
      g_assert_not_reached ();
    }
  }

  /* If both key and value are provided */
  else if (key && val) {
    if (!delete_flag && !save_flag && !reset_flag) {
      g_autoptr (WpSpaJson) value = wp_spa_json_new_from_string (val);
      if (wp_settings_set (s, key, value))
        printf ("Updated setting '%s' to: %s\n", key, val);
      else
        printf ("Failed to set setting '%s' to: %s\n", key, val);
    } else if (!delete_flag && save_flag && !reset_flag) {
      g_autoptr (WpSpaJson) value = wp_spa_json_new_from_string (val);
      if (wp_settings_set (s, key, value) && wp_settings_save (s, key))
        printf ("Updated and saved setting '%s' to: %s\n", key, val);
      else
        printf ("Failed to update and save setting '%s' to: %s\n", key, val);
    } else if (delete_flag && !save_flag && !reset_flag) {
      if (wp_settings_delete (s, key))
        printf ("Deleted setting '%s' successfully\n", key);
      else
        printf ("Setting %s not found\n", key);
    } else if (!delete_flag && !save_flag && reset_flag) {
      if (wp_settings_reset (s, key))
        printf ("Reset setting '%s' successfully\n", key);
      else
        printf ("Setting '%s' not found\n", key);
    } else {
      g_assert_not_reached ();
    }
  } else {
    g_assert_not_reached ();
  }

  wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
  return;

out:
  self->exit_code = 3;
  g_main_loop_quit (self->loop);
}

/* set-log-level */

static gboolean
set_log_level_parse_positional (gint argc, gchar ** argv, GError **error)
{
  if (argc == 4) {
    if (!spa_atou64 (argv[2], &cmdline.set_log_level.id, 10)) {
      g_set_error (error, wpctl_error_domain_quark(), 0,
                   "failed to parse client id");
      return FALSE;
    }
    cmdline.set_log_level.level = argv[3];
  } else if (argc == 3) {
    cmdline.set_log_level.id = SPA_ID_INVALID;
    cmdline.set_log_level.level = argv[2];
  } else {
    g_set_error (error, wpctl_error_domain_quark(), 0,
                 "wrong number of arguments for set-log-level");
    return FALSE;
  }

  if (spa_streq(cmdline.set_log_level.level, "-"))
    cmdline.set_log_level.level = NULL;

  return TRUE;
}

static gboolean
set_log_level_prepare (WpCtl * self, GError ** error)
{
  wp_object_manager_add_interest (self->om, WP_TYPE_METADATA,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY,
      "metadata.name", "=s", "settings",
      NULL);
  wp_object_manager_add_interest (self->om, WP_TYPE_CLIENT,
      WP_CONSTRAINT_TYPE_PW_PROPERTY,
      "wireplumber.daemon", "+",
      NULL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_METADATA,
      WP_OBJECT_FEATURES_ALL);
  wp_object_manager_request_object_features (self->om, WP_TYPE_CLIENT,
      WP_OBJECT_FEATURES_ALL);
  return TRUE;
}

static void
set_log_level_run (WpCtl * self)
{
  g_autoptr (WpPlugin) def_nodes_api = NULL;
  g_autoptr (GError) error = NULL;
  g_autoptr (WpIterator) client_it = NULL;
  g_auto (GValue) client_val = G_VALUE_INIT;

  g_autoptr (WpMetadata) settings = wp_object_manager_lookup (self->om, WP_TYPE_METADATA, NULL);
  if (!settings) {
    fprintf (stderr, "No settings metadata found\n");
    goto out;
  }

  if (cmdline.set_log_level.id == SPA_ID_INVALID)
    client_it = wp_object_manager_new_filtered_iterator (self->om, WP_TYPE_CLIENT, NULL);

  if (client_it) {
      for (; wp_iterator_next (client_it, &client_val); g_value_unset (&client_val)) {
        WpPipewireObject *client = g_value_get_object (&client_val);
        guint32 client_id = wp_proxy_get_bound_id (WP_PROXY (client));

        if (client_id == SPA_ID_INVALID)
          continue;

        wp_metadata_set (settings, client_id, "log.level", "", cmdline.set_log_level.level);
      }
  } else {
    wp_metadata_set (settings, cmdline.set_log_level.id, "log.level", "", cmdline.set_log_level.level);
  }

  wp_core_sync (self->core, NULL, (GAsyncReadyCallback) async_quit, self);
  return;

out:
  self->exit_code = 3;
  g_main_loop_quit (self->loop);
}

#define N_ENTRIES 4

static const struct subcommand {
  /* the name to match on the command line */
  const gchar *name;
  /* description of positional arguments, shown in the help message */
  const gchar *positional_args;
  /* short description, shown at the top of the help message */
  const gchar *summary;
  /* long description, shown at the bottom of the help message */
  const gchar *description;
  /* additional cmdline arguments for this subcommand */
  const GOptionEntry entries[N_ENTRIES];
  /* function to parse positional arguments */
  gboolean (*parse_positional) (gint, gchar **, GError **);
  /* function to prepare the object manager */
  gboolean (*prepare) (WpCtl *, GError **);
  /* function to run after the object manager is installed */
  void (*run) (WpCtl *);
} subcommands[] = {
  {
    .name = "status",
    .positional_args = "",
    .summary = "Displays the current state of objects in PipeWire",
    .description = NULL,
    .entries = {
      { "nick", 'k', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &cmdline.status.display_nicknames,
        "Display device and node nicknames instead of descriptions", NULL },
      { "name", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &cmdline.status.display_names,
        "Display device and node names instead of descriptions", NULL },
      { NULL }
    },
    .parse_positional = NULL,
    .prepare = status_prepare,
    .run = status_run,
  },
  {
    .name = "get-volume",
    .positional_args = "ID",
    .summary = "Displays volume information about the specified node in PipeWire",
    .description = NULL,
    .entries = { { NULL } },
    .parse_positional = get_volume_parse_positional,
    .prepare = get_volume_prepare,
    .run = get_volume_run,
  },
  {
    .name = "inspect",
    .positional_args = "ID",
    .summary = "Displays information about the specified object",
    .description = NULL,
    .entries = {
      { "referenced", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &cmdline.inspect.show_referenced,
        "Show objects that are referenced in properties", NULL },
      { "associated", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &cmdline.inspect.show_associated, "Show associated objects", NULL },
      { NULL }
    },
    .parse_positional = inspect_parse_positional,
    .prepare = inspect_prepare,
    .run = inspect_run,
  },
  {
    .name = "set-default",
    .positional_args = "ID",
    .summary = "Sets ID to be the default object of its kind "
               "(capture/playback) in its session",
    .description = NULL,
    .entries = { { NULL } },
    .parse_positional = set_default_parse_positional,
    .prepare = set_default_prepare,
    .run = set_default_run,
  },
  {
    .name = "set-volume",
    .positional_args = "ID VOL[%][-/+]",
    .summary =
        "Sets the volume of ID from specified argument. (floating point, 1.0 is 100%)\n"
        "  VOL%[-/+] - Step up/down volume by specified percent (Example: 0.5%+)\n"
        "  VOL[-/+] - Step up/down volume by specified value (Example: 0.5+)\n"
        "  VOL - Set volume as the specified value (Example: 0.5)",
    .description = NULL,
    .entries = {
      { "pid", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &cmdline.set_volume.is_pid,
        "Selects all nodes associated to the given PID number", NULL },
      { "limit", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE,
        &cmdline.set_volume.limit,
        "Limits the final volume percentage to below this value. (floating point, 1.0 is 100%)", NULL },
      { NULL }
    },
    .parse_positional = set_volume_parse_positional,
    .prepare = set_volume_prepare,
    .run = set_volume_run,
  },
  {
    .name = "set-mute",
    .positional_args = "ID 1|0|toggle",
    .summary = "Changes the mute state of ID",
    .description = NULL,
    .entries = {
      { "pid", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &cmdline.set_mute.is_pid,
        "Selects all nodes associated to the given PID number", NULL },
      { NULL }
    },
    .parse_positional = set_mute_parse_positional,
    .prepare = set_mute_prepare,
    .run = set_mute_run,
  },
  {
    .name = "set-profile",
    .positional_args = "ID INDEX",
    .summary = "Sets the profile of ID to INDEX (integer, 0 is 'off')",
    .description = NULL,
    .entries = { { NULL } },
    .parse_positional = set_profile_parse_positional,
    .prepare = set_profile_prepare,
    .run = set_profile_run,
  },
  {
    .name = "set-route",
    .positional_args = "ID INDEX",
    .summary = "Sets the route of ID to INDEX (integer, 0 is 'off')",
    .description = NULL,
    .entries = { { NULL } },
    .parse_positional = set_route_parse_positional,
    .prepare = set_route_prepare,
    .run = set_route_run,
  },
  {
    .name = "clear-default",
    .positional_args = "[ID]",
    .summary = "Clears the default configured node (no ID means 'all')",
    .description = NULL,
    .entries = { { NULL } },
    .parse_positional = clear_default_parse_positional,
    .prepare = clear_default_prepare,
    .run = clear_default_run,
  },
  {
    .name = "settings",
    .positional_args = "[KEY] [VAL]",
    .summary = "Shows, changes or removes settings",
    .description = NULL,
    .entries = {
      { "delete", 'd', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &cmdline.settings.delete,
        "Deletes the saved setting value (no KEY means 'all')", NULL },
      { "save", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &cmdline.settings.save,
        "Saves the setting value (no KEY means 'all', no VAL means current value)", NULL },
      { "reset", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &cmdline.settings.reset,
        "Resets the saved setting to its default value", NULL },
      { NULL }
    },
    .parse_positional = settings_parse_positional,
    .prepare = settings_prepare,
    .run = settings_run,
  },
  {
    .name = "set-log-level",
    .positional_args = "[ID] LEVEL",
    .summary = "Sets the log level of a client (no ID means Wireplumber, 0 means Pipewire server)",
    .description = NULL,
    .entries = { { NULL } },
    .parse_positional = set_log_level_parse_positional,
    .prepare = set_log_level_prepare,
    .run = set_log_level_run,
  }
};

static void
on_settings_activated (WpSettings *s, GAsyncResult *res, WpCtl *ctl)
{
  GError *error = NULL;

  if (!wp_object_activate_finish (WP_OBJECT (s), res, &error)) {
    fprintf (stderr, "%s\n", error->message);
    ctl->exit_code = 1;
    g_main_loop_quit (ctl->loop);
    return;
  }

  wp_core_register_object (ctl->core, g_object_ref (s));
}

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, WpCtl *ctl)
{
  GError *error = NULL;

  if (!wp_core_load_component_finish (core, res, &error)) {
    fprintf (stderr, "%s\n", error->message);
    ctl->exit_code = 1;
    g_main_loop_quit (ctl->loop);
    return;
  }

  if (--ctl->pending_plugins == 0) {
    g_autoptr (WpPlugin) mixer_api = wp_plugin_find (core, "mixer-api");
    g_object_set (mixer_api, "scale", 1 /* cubic */, NULL);
    wp_core_install_object_manager (ctl->core, ctl->om);
  }
}

gint
main (gint argc, gchar **argv)
{
  g_auto (WpCtl) ctl = {0};
  const struct subcommand *cmd = NULL;
  g_autoptr (GError) error = NULL;
  g_autofree gchar *summary = NULL;
  g_autoptr (WpSettings) settings = NULL;

  setlocale (LC_ALL, "");
  setlocale (LC_NUMERIC, "C");
  wp_init (WP_INIT_ALL);

  ctl.context = g_option_context_new (
      "COMMAND [COMMAND_OPTIONS] - WirePlumber Control CLI");
  ctl.loop = g_main_loop_new (NULL, FALSE);
  ctl.core = wp_core_new (NULL, NULL, NULL);
  ctl.om = wp_object_manager_new ();

  /* find the subcommand */
  if (argc > 1) {
    for (guint i = 0; i < G_N_ELEMENTS (subcommands); i++) {
      if (!g_strcmp0 (argv[1], subcommands[i].name)) {
        cmd = &subcommands[i];
        break;
      }
    }
  }

  /* prepare the subcommand options */
  if (cmd) {
    GOptionGroup *group;

    /* options */
    group = g_option_group_new (cmd->name, NULL, NULL, &ctl, NULL);
    g_option_group_add_entries (group, cmd->entries);
    g_option_context_set_main_group (ctl.context, group);

    /* summary */
    summary = g_strdup_printf ("Command: %s %s\n  %s",
        cmd->name, cmd->positional_args, cmd->summary);
    g_option_context_set_summary (ctl.context, summary);

    /* description */
    if (cmd->description)
      g_option_context_set_description (ctl.context, cmd->description);
  }
  else {
    /* build the generic summary */
    GString *summary_str = g_string_new ("Commands:");
    for (guint i = 0; i < G_N_ELEMENTS (subcommands); i++) {
      g_string_append_printf (summary_str, "\n  %s %s", subcommands[i].name,
          subcommands[i].positional_args);
    }
    summary = g_string_free (summary_str, FALSE);
    g_option_context_set_summary (ctl.context, summary);
    g_option_context_set_description (ctl.context, "Pass -h after a command "
        "to see command-specific options\n");
  }

  /* parse options */
  if (!g_option_context_parse (ctl.context, &argc, &argv, &error) ||
      (cmd && cmd->parse_positional &&
          !cmd->parse_positional (argc, argv, &error))) {
    fprintf (stderr, "Error: %s\n\n", error->message);
    cmd = NULL;
  }

  /* no active subcommand, show usage and exit */
  if (!cmd) {
    g_autofree gchar *help =
        g_option_context_get_help (ctl.context, FALSE, NULL);
    printf ("%s", help);
    return 1;
  }

  /* prepare the subcommand */
  if (!cmd->prepare (&ctl, &error)) {
    fprintf (stderr, "%s\n", error->message);
    return 1;
  }

  /* load and register settings */
  settings = wp_settings_new (ctl.core, NULL);
  wp_object_activate (WP_OBJECT (settings),
      WP_OBJECT_FEATURES_ALL,
      NULL,
      (GAsyncReadyCallback)on_settings_activated,
      &ctl);

  /* load required API modules */
  ctl.pending_plugins++;
  wp_core_load_component (ctl.core, "libwireplumber-module-default-nodes-api",
      "module", NULL, NULL, NULL, (GAsyncReadyCallback) on_plugin_loaded, &ctl);
  ctl.pending_plugins++;
  wp_core_load_component (ctl.core, "libwireplumber-module-mixer-api",
      "module", NULL, NULL, NULL, (GAsyncReadyCallback) on_plugin_loaded, &ctl);

  /* connect */
  if (!wp_core_connect (ctl.core)) {
    fprintf (stderr, "Could not connect to PipeWire\n");
    return 2;
  }

  /* run */
  g_signal_connect_swapped (ctl.core, "disconnected",
      (GCallback) g_main_loop_quit, ctl.loop);
  g_signal_connect_swapped (ctl.om, "installed",
      (GCallback) cmd->run, &ctl);

  g_main_loop_run (ctl.loop);

  return ctl.exit_code;
}
  070701000001A5000081A400000000000000000000000165F86304000019D2000000000000000000000000000000000000002500000000wireplumber-0.5.0/src/tools/wpexec.c  /* WirePlumber
 *
 * Copyright © 2019-2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <glib-unix.h>
#include <pipewire/keys.h>
#include <stdio.h>
#include <locale.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wpexec")

#define WP_DOMAIN_DAEMON (wp_domain_daemon_quark ())
static G_DEFINE_QUARK (wireplumber-daemon, wp_domain_daemon);

enum WpExitCode
{
  /* based on sysexits.h */
  WP_EXIT_OK = 0,
  WP_EXIT_USAGE = 64,       /* command line usage error */
  WP_EXIT_UNAVAILABLE = 69, /* service unavailable */
  WP_EXIT_SOFTWARE = 70,    /* internal software error */
};

static gchar * exec_script = NULL;
static gchar *exec_args_s = NULL;
static WpSpaJson *exec_args = NULL;

static gboolean
parse_exec_script_arg (const gchar *option_name, const gchar *value,
    gpointer data, GError **error)
{
  /* the first argument is the script */
  if (!exec_script) {
    exec_script = g_strdup (value);
    return TRUE;
  }

  /* the second argument is a json object with script arguments */
  if (!exec_args) {
    exec_args_s = g_strdup (value);
    exec_args = wp_spa_json_new_wrap_string (exec_args_s);
    if (!exec_args || !wp_spa_json_is_object (exec_args)) {
      g_set_error (error, WP_DOMAIN_DAEMON, WP_EXIT_USAGE,
          "script argument must be a JSON object");
      return FALSE;
    }
  }

  return TRUE;
}

static GOptionEntry entries[] =
{
  { G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
    parse_exec_script_arg, NULL, NULL },
  { NULL }
};

/*** WpInitTransition ***/

struct _WpInitTransition
{
  WpTransition parent;
  guint plugins_loaded;
};

enum {
  STEP_CONNECT = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_ACTIVATE_PLUGINS,
  STEP_ACTIVATE_SCRIPT,
};

G_DECLARE_FINAL_TYPE (WpInitTransition, wp_init_transition,
                      WP, INIT_TRANSITION, WpTransition)
G_DEFINE_TYPE (WpInitTransition, wp_init_transition, WP_TYPE_TRANSITION)

static void
wp_init_transition_init (WpInitTransition * self)
{
}

static guint
wp_init_transition_get_next_step (WpTransition * transition, guint step)
{
  switch (step) {
  case WP_TRANSITION_STEP_NONE: return STEP_CONNECT;
  case STEP_CONNECT:            return STEP_ACTIVATE_PLUGINS;
  case STEP_ACTIVATE_SCRIPT:    return WP_TRANSITION_STEP_NONE;

  case STEP_ACTIVATE_PLUGINS: {
    WpInitTransition *self = WP_INIT_TRANSITION (transition);
    if (self->plugins_loaded == 2)
      return STEP_ACTIVATE_SCRIPT;
    else
      return STEP_ACTIVATE_PLUGINS;
  }

  default:
    g_return_val_if_reached (WP_TRANSITION_STEP_ERROR);
  }
}

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, WpInitTransition *self)
{
  GError *error = NULL;

  if (!wp_core_load_component_finish (core, res, &error)) {
    wp_transition_return_error (WP_TRANSITION (self), error);
    return;
  }
  ++self->plugins_loaded;
  wp_transition_advance (WP_TRANSITION (self));
}

static void
wp_init_transition_execute_step (WpTransition * transition, guint step)
{
  WpInitTransition *self = WP_INIT_TRANSITION (transition);
  WpCore *core = wp_transition_get_source_object (transition);

  switch (step) {
  case STEP_CONNECT:
    g_signal_connect_object (core, "connected",
        G_CALLBACK (wp_transition_advance), transition, G_CONNECT_SWAPPED);

    if (!wp_core_connect (core)) {
      wp_transition_return_error (transition, g_error_new (WP_DOMAIN_DAEMON,
          WP_EXIT_UNAVAILABLE, "Failed to connect to PipeWire"));
      return;
    }
    break;

  case STEP_ACTIVATE_PLUGINS: {
    wp_core_load_component (core, "libwireplumber-module-lua-scripting",
        "module", NULL, NULL, NULL, (GAsyncReadyCallback) on_plugin_loaded, self);
    wp_core_load_component (core, "libwireplumber-module-standard-event-source",
        "module", NULL, NULL, NULL, (GAsyncReadyCallback) on_plugin_loaded, self);
    break;
  }

  case STEP_ACTIVATE_SCRIPT: {
    wp_core_load_component (core, exec_script, "script/lua", exec_args, NULL, NULL,
          (GAsyncReadyCallback) on_plugin_loaded, self);
    break;
  }

  case WP_TRANSITION_STEP_ERROR:
    break;

  default:
    g_assert_not_reached ();
  }
}

static void
wp_init_transition_class_init (WpInitTransitionClass * klass)
{
  WpTransitionClass * transition_class = (WpTransitionClass *) klass;

  transition_class->get_next_step = wp_init_transition_get_next_step;
  transition_class->execute_step = wp_init_transition_execute_step;
}

/*** WpExec ***/

typedef struct
{
  WpCore *core;
  GMainLoop *loop;
  gint exit_code;
} WpExec;

static void
wpexec_clear (WpExec * self)
{
  g_clear_pointer (&self->loop, g_main_loop_unref);
  g_clear_object (&self->core);
}

G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpExec, wpexec_clear)

static gboolean
signal_handler (gpointer data)
{
  WpExec *d = data;
  g_main_loop_quit (d->loop);
  return G_SOURCE_REMOVE;
}

static gboolean
init_start (WpTransition * transition)
{
  wp_transition_advance (transition);
  return G_SOURCE_REMOVE;
}

static void
init_done (WpCore * core, GAsyncResult * res, WpExec * d)
{
  g_autoptr (GError) error = NULL;
  if (!wp_transition_finish (res, &error)) {
    fprintf (stderr, "%s\n", error->message);
    d->exit_code = (error->domain == WP_DOMAIN_DAEMON) ?
        error->code : WP_EXIT_SOFTWARE;
    g_main_loop_quit (d->loop);
  }
}

gint
main (gint argc, gchar **argv)
{
  g_auto (WpExec) d = {0};
  g_autoptr (GOptionContext) context = NULL;
  g_autoptr (GError) error = NULL;

  setlocale (LC_ALL, "");
  setlocale (LC_NUMERIC, "C");
  wp_init (WP_INIT_ALL);

  context = g_option_context_new ("- WirePlumber script interpreter");
  g_option_context_add_main_entries (context, entries, NULL);
  if (!g_option_context_parse (context, &argc, &argv, &error)) {
    fprintf (stderr, "%s\n", error->message);
    return WP_EXIT_USAGE;
  }

  /* init wireplumber core */
  d.loop = g_main_loop_new (NULL, FALSE);
  d.core = wp_core_new (NULL, NULL, wp_properties_new (
          PW_KEY_APP_NAME, "wpexec",
          NULL));
  g_signal_connect_swapped (d.core, "disconnected",
      G_CALLBACK (g_main_loop_quit), d.loop);

  /* watch for exit signals */
  g_unix_signal_add (SIGINT, signal_handler, &d);
  g_unix_signal_add (SIGTERM, signal_handler, &d);
  g_unix_signal_add (SIGHUP, signal_handler, &d);

  /* initialization transition */
  g_idle_add ((GSourceFunc) init_start,
      wp_transition_new (wp_init_transition_get_type (), d.core,
          NULL, (GAsyncReadyCallback) init_done, &d));

  /* run */
  g_main_loop_run (d.loop);
  wp_core_disconnect (d.core);
  return d.exit_code;
}
  070701000001A6000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001E00000000wireplumber-0.5.0/subprojects 070701000001A7000081A400000000000000000000000165F86304000001A0000000000000000000000000000000000000002700000000wireplumber-0.5.0/subprojects/lua.wrap    [wrap-file]
directory = lua-5.4.4
source_url = https://www.lua.org/ftp/lua-5.4.4.tar.gz
source_filename = lua-5.4.4.tar.gz
source_hash = 164c7849653b80ae67bec4b7473b884bf5cc8d2dca05653475ec2ed27b9ebf61
patch_filename = lua_5.4.4-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/lua_5.4.4-1/get_patch
patch_hash = e61cd965c629d6543176f41a9f1cb9050edfd1566cf00ce768ff211086e40bdc

[provide]
lua-5.4 = lua_dep

070701000001A8000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001800000000wireplumber-0.5.0/tests   070701000001A9000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001F00000000wireplumber-0.5.0/tests/common    070701000001AA000081A400000000000000000000000165F8630400001802000000000000000000000000000000000000003300000000wireplumber-0.5.0/tests/common/base-test-fixture.h    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "test-server.h"
#include "test-log.h"
#include <wp/wp.h>

typedef enum {
  WP_BASE_TEST_FLAG_CLIENT_CORE = (1<<0),
  WP_BASE_TEST_FLAG_DONT_CONNECT = (1<<1),
} WpBaseTestFlags;

typedef struct {
  /* the local pipewire server */
  WpTestServer server;

  /* the main loop */
  GMainContext *context;
  GMainLoop *loop;

  /* watchdog */
  GSource *timeout_source;

  /* our session manager core */
  WpCore *core;

  /* the "client" core, which receives proxies
    (second client to our internal server) */
  WpCore *client_core;

 /* Custom JSON config file if any,
  * this overrides the default conf file that is picked
  */
  gchar *conf_file;

} WpBaseTestFixture;

static gboolean
timeout_callback (WpBaseTestFixture * self)
{
  wp_critical ("test timed out");
  g_test_fail ();
  g_main_loop_quit (self->loop);

  return G_SOURCE_REMOVE;
}

static void
disconnected_callback (WpCore *core, WpBaseTestFixture * self)
{
  wp_critical_object (core, "%s core disconnected",
      (core == self->client_core) ? "client" : "sm");
  g_test_fail ();
  g_main_loop_quit (self->loop);
}

static void
on_core_connected (WpObject * core, GAsyncResult * res, WpBaseTestFixture * self)
{
  g_autoptr (GError) error = NULL;
  wp_object_activate_finish (core, res, &error);
  g_assert_no_error (error);
  g_main_loop_quit (self->loop);
}

static void
wp_base_test_fixture_setup (WpBaseTestFixture * self, WpBaseTestFlags flags)
{
  g_autoptr (WpProperties) props = NULL;
  g_autoptr (WpConf) conf = NULL;

  /* init test server */
  wp_test_server_setup (&self->server);

  /* init our main loop */
  self->context = g_main_context_new ();
  self->loop = g_main_loop_new (self->context, FALSE);
  g_main_context_push_thread_default (self->context);

  /* watchdog */
  self->timeout_source = g_timeout_source_new_seconds (8);
  g_source_set_callback (self->timeout_source, (GSourceFunc) timeout_callback,
      self, NULL);
  g_source_attach (self->timeout_source, self->context);

  /* init our core */
  props = wp_properties_new (PW_KEY_REMOTE_NAME, self->server.name, NULL);

  if (self->conf_file) {
    g_autoptr (GError) error = NULL;
    conf = wp_conf_new_open (self->conf_file, NULL, &error);
    g_assert_no_error (error);
    g_assert_nonnull (conf);
  }

  self->core = wp_core_new (self->context, g_steal_pointer (&conf),
      wp_properties_ref (props));
  g_assert_true (self->core);

  g_signal_connect (self->core, "disconnected",
      (GCallback) disconnected_callback, self);

  if (!(flags & WP_BASE_TEST_FLAG_DONT_CONNECT)) {
    wp_object_activate (WP_OBJECT (self->core), WP_CORE_FEATURE_CONNECTED, NULL,
        (GAsyncReadyCallback) on_core_connected, self);
    g_main_loop_run (self->loop);
    g_assert_true (wp_core_is_connected (self->core));
  }

  /* init the second client's core */
  if (flags & WP_BASE_TEST_FLAG_CLIENT_CORE) {
    self->client_core = wp_core_new (self->context, NULL,
        wp_properties_ref (props));
    g_signal_connect (self->client_core, "disconnected",
        (GCallback) disconnected_callback, self);

    if (!(flags & WP_BASE_TEST_FLAG_DONT_CONNECT)) {
      wp_object_activate (WP_OBJECT (self->client_core), WP_CORE_FEATURE_CONNECTED,
          NULL, (GAsyncReadyCallback) on_core_connected, self);
      g_main_loop_run (self->loop);
      g_assert_true (wp_core_is_connected (self->client_core));
    }
  }
}

static void
test_core_done_cb (WpCore *core, GAsyncResult *res, WpBaseTestFixture *self)
{
  g_autoptr (GError) error = NULL;
  g_assert_true (wp_core_sync_finish (core, res, &error));
  g_assert_null (error);
  g_main_loop_quit (self->loop);
}

static void
wp_base_test_fixture_teardown (WpBaseTestFixture * self)
{
  /* wait for all client core pending tasks to be done */
  if (self->client_core && wp_core_is_connected (self->client_core)) {
    wp_core_sync (self->client_core, NULL,
        (GAsyncReadyCallback) test_core_done_cb, self);
    g_main_loop_run (self->loop);
    g_signal_handlers_disconnect_by_data (self->client_core, self);
    wp_core_disconnect (self->client_core);
  }

  /* wait for all core pending tasks to be done */
  if (self->core && wp_core_is_connected (self->core)) {
    wp_core_sync (self->core, NULL, (GAsyncReadyCallback) test_core_done_cb,
        self);
    g_main_loop_run (self->loop);
    g_signal_handlers_disconnect_by_data (self->core, self);
    wp_core_disconnect (self->core);
  }

  /* double check and ensure that there is no event pending */
  while (g_main_context_pending (self->context))
    g_main_context_iteration (self->context, TRUE);

  g_main_context_pop_thread_default (self->context);
  g_clear_pointer (&self->conf_file, g_free);
  g_clear_object (&self->client_core);
  g_clear_object (&self->core);
  g_clear_pointer (&self->timeout_source, g_source_unref);
  g_clear_pointer (&self->loop, g_main_loop_unref);
  g_clear_pointer (&self->context, g_main_context_unref);
  wp_test_server_teardown (&self->server);
}

static G_GNUC_UNUSED void
test_object_activate_finish_cb (WpObject * object, GAsyncResult * res,
    WpBaseTestFixture * f)
{
  g_autoptr (GError) error = NULL;
  gboolean augment_ret = wp_object_activate_finish (object, res, &error);
  g_assert_no_error (error);
  g_assert_true (augment_ret);

  g_main_loop_quit (f->loop);
}

static G_GNUC_UNUSED void
test_ensure_object_manager_is_installed (WpObjectManager * om, WpCore * core,
    GMainLoop * loop)
{
  gulong id = g_signal_connect_swapped (om, "installed",
      G_CALLBACK (g_main_loop_quit), loop);
  wp_core_install_object_manager (core, om);
  if (!wp_object_manager_is_installed (om))
    g_main_loop_run (loop);
  g_signal_handler_disconnect (om, id);
}

static G_GNUC_UNUSED gboolean
test_is_spa_lib_installed (WpBaseTestFixture *f, const gchar *factory_name) {
  struct spa_handle *handle;

  handle = pw_context_load_spa_handle (f->server.context, factory_name, NULL);
  if (!handle)
    return FALSE;

  pw_unload_spa_handle (handle);
  return TRUE;
}
  070701000001AB000081A400000000000000000000000165F86304000000DA000000000000000000000000000000000000002A00000000wireplumber-0.5.0/tests/common/test-log.h /* WirePlumber
 *
 * Copyright © 2023 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("tests")
  070701000001AC000081A400000000000000000000000165F8630400000692000000000000000000000000000000000000002D00000000wireplumber-0.5.0/tests/common/test-server.h  /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <pipewire/pipewire.h>
#include <pipewire/impl.h>
#include <glib.h>
#include <unistd.h>

typedef struct {
  gchar *name;
  struct pw_thread_loop *thread_loop;
  struct pw_context *context;
} WpTestServer;

static inline void
wp_test_server_setup (WpTestServer *self)
{
  struct pw_properties *properties;

  self->name = g_strdup_printf ("wp-test-server-%d-%d", getpid(),
      g_random_int ());
  properties = pw_properties_new(
      PW_KEY_CORE_DAEMON, "1",
      PW_KEY_CORE_NAME, self->name,
      NULL);

  self->thread_loop = pw_thread_loop_new ("wp-test-server", NULL);
  self->context = pw_context_new (pw_thread_loop_get_loop (self->thread_loop),
      properties, 0);

  pw_context_load_module (self->context, "libpipewire-module-access", NULL, NULL);

  pw_thread_loop_start (self->thread_loop);
}

static inline void
wp_test_server_teardown (WpTestServer *self)
{
  if (self->thread_loop)
    pw_thread_loop_stop (self->thread_loop);
  g_clear_pointer (&self->context, pw_context_destroy);
  g_clear_pointer (&self->thread_loop, pw_thread_loop_destroy);
  g_clear_pointer (&self->name, g_free);
}

typedef void WpTestServerLocker;

static inline WpTestServerLocker *
wp_test_server_locker_new (WpTestServer * self)
{
  pw_thread_loop_lock (self->thread_loop);
  return self;
}

static inline void
wp_test_server_locker_free (WpTestServerLocker * self)
{
  pw_thread_loop_unlock (((WpTestServer *)self)->thread_loop);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (WpTestServerLocker, wp_test_server_locker_free)
  070701000001AD000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002100000000wireplumber-0.5.0/tests/examples  070701000001AE000081ED00000000000000000000000165F8630400000BEE000000000000000000000000000000000000003200000000wireplumber-0.5.0/tests/examples/bt-pinephone.lua #!/usr/bin/wpexec
--
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--    @author Frédéric Danis <frederic.danis@collabora.com>
--
-- SPDX-License-Identifier: MIT
--
-- This is an example of the offload SCO nodes platform specific management,
-- in this case for the PinePhone.
--
-- The PinePhone provides specific ALSA ports to route audio to the Bluetooth
-- chipset. This script selects these ports when the offload SCO nodes state
-- change to 'running'.
--
-- This scriptcan be executed as a standalone executable, or it can be placed
-- in WirePlumber's scripts directory and loaded together with other scripts.
-----------------------------------------------------------------------------

devices_om = ObjectManager {
  Interest {
    type = "device",
  }
}

nodes_om = ObjectManager {
  Interest {
    type = "node",
    Constraint { "node.name", "#", "*.bluez_*put*"},
    Constraint { "device.id", "+" },
  }
}

function parseParam(param, id)
  local route = param:parse()
  if route.pod_type == "Object" and route.object_id == id then
    return route.properties
  else
    return nil
  end
end

function setPlatformRoute (route_name, direction)
  local platform_sound = nil
  local device_id = nil
  local route_save = nil

  local interest = Interest {
    type = "device",
    Constraint { "device.name", "=", "alsa_card.platform-sound"}
  }
  for d in devices_om:iterate (interest) do
    platform_sound = d

    for p in platform_sound:iterate_params("Route") do
      route = parseParam(p, "Route")
      if route.direction == direction then
        device_id = route.device
        route_save = route.save
      end
    end

    for p in platform_sound:iterate_params("EnumRoute") do
      enum_route = parseParam(p, "EnumRoute")

      if enum_route.name == route_name then
        route_index = enum_route.index
      end
    end
  end

  if route.index == route_index then
    return
  end

  -- default props
  local props = {
    "Spa:Pod:Object:Param:Props", "Route",
    mute = false,
  }

  -- construct Route param
  local param = Pod.Object {
    "Spa:Pod:Object:Param:Route", "Route",
    index = route_index,
    device = device_id,
    props = Pod.Object(props),
    save = route_save,
  }

  Log.info(param, "setting route on " .. tostring(platform_sound))
  platform_sound:set_param("Route", param)
end

nodes_om:connect("object-added", function(_, node)
  node:connect("state-changed", function(node, old_state, cur_state)
    local interest = Interest {
      type = "device",
      Constraint { "object.id", "=", node.properties["device.id"]}
    }
    for d in devices_om:iterate (interest) do
      if cur_state == "running" then
        -- Both direction should be set to allow audio streaming
        setPlatformRoute("[Out] BluetoothHeadset", "Output")
        setPlatformRoute("[In] BluetoothHeadset", "Input")
      else
        setPlatformRoute("[Out] Earpiece", "Output")
        setPlatformRoute("[In] DigitalMic", "Input")
      end
    end
  end)
end)

nodes_om:activate()
devices_om:activate()
  070701000001AF000081ED00000000000000000000000165F8630400000939000000000000000000000000000000000000003700000000wireplumber-0.5.0/tests/examples/bt-profile-switch.lua    #!/usr/bin/wpexec
--
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
--
-- This is an example of a standalone policy making script. It can be executed
-- either on top of another instance of wireplumber or pipewire-media-session,
-- as a standalone executable, or it can be placed in WirePlumber's scripts
-- directory and loaded together with other scripts.
--
-- The script basically watches for a client application called
-- "ZOOM VoiceEngine", and when it appears (i.e. Zoom starts), it switches
-- the profile of all connected bluetooth devices to the "headset-head-unit"
-- (a.k.a HSP Headset Audio) profile. When Zoom exits, it switches again the
-- profile of all bluetooth devices to A2DP Sink.
--
-- The script can be customized further to look for other clients and/or
-- change the profile of a specific device, by customizing the constraints.
-----------------------------------------------------------------------------

devices_om = ObjectManager {
  Interest { type = "device",
    Constraint { "device.api", "=", "bluez5" },
  }
}

clients_om = ObjectManager {
  Interest { type = "client",
    Constraint { "application.name", "=", "ZOOM VoiceEngine" },
  }
}

function set_profile(profile_name)
  for device in devices_om:iterate() do
    local index = nil
    local desc = nil

    for profile in device:iterate_params("EnumProfile") do
      local p = profile:parse()
      if p.properties.name == profile_name then
        index = p.properties.index
        desc = p.properties.description
        break
      end
    end

    if index then
      local pod = Pod.Object {
        "Spa:Pod:Object:Param:Profile", "Profile",
        index = index
      }

      print("Setting profile of '"
            .. device.properties["device.description"]
            .. "' to: " .. desc)
      device:set_params("Profile", pod)
    end
  end
end

clients_om:connect("object-added", function (om, client)
  print("Client '" .. client.properties["application.name"] .. "' connected")
  set_profile("headset-head-unit")
end)

clients_om:connect("object-removed", function (om, client)
  print("Client '" .. client.properties["application.name"] .. "' disconnected")
  set_profile("a2dp-sink")
end)

devices_om:activate()
clients_om:activate()
   070701000001B0000081A400000000000000000000000165F8630400000BC3000000000000000000000000000000000000003200000000wireplumber-0.5.0/tests/examples/filter-chain.lua #!/usr/bin/wpexec
--
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
--
-- This is an example of how to load a pipewire module that takes a JSON
-- string as arguments.
--
-- For illustration purposes, this loads module-filter-chain with a 6-band
-- equalizer. This is the same configuration found in the sink-eq6.conf
-- file that can be found in pipewire's source tree.
-----------------------------------------------------------------------------

-- For illustration purposes, we declare 'args' here as a native lua table,
-- we populate it in multiple steps and we transform it later to a json object
local args = {
  ["node.description"] = "Equalizer Sink",
  ["media.name"] = "Equalizer Sink",
  ["audio.channels"] = 2,
  ["audio.position"] = Json.Array { "FL", "FR" },
}

args["filter.graph"] = Json.Object {
  nodes = Json.Array {
    Json.Object {
        type  = "builtin",
        name  = "eq_band_1",
        label = "bq_lowshelf",
        control = Json.Object { Freq = 100.0, Q = 1.0, Gain = 0.0 },
    },
    Json.Object {
        type  = "builtin",
        name  = "eq_band_2",
        label = "bq_peaking",
        control = Json.Object { Freq = 100.0, Q = 1.0, Gain = 0.0 },
    },
    Json.Object {
        type  = "builtin",
        name  = "eq_band_3",
        label = "bq_peaking",
        control = Json.Object { Freq = 500.0, Q = 1.0, Gain = 0.0 },
    },
    Json.Object {
        type  = "builtin",
        name  = "eq_band_4",
        label = "bq_peaking",
        control = Json.Object { Freq = 2000.0, Q = 1.0, Gain = 0.0 },
    },
    Json.Object {
        type  = "builtin",
        name  = "eq_band_5",
        label = "bq_peaking",
        control = Json.Object { Freq = 5000.0, Q = 1.0, Gain = 0.0 },
    },
    Json.Object {
        type  = "builtin",
        name  = "eq_band_6",
        label = "bq_highshelf",
        control = Json.Object { Freq = 5000.0, Q = 1.0, Gain = 0.0 },
    },
  },
  links = Json.Array {
    Json.Object { output = "eq_band_1:Out", input = "eq_band_2:In" },
    Json.Object { output = "eq_band_2:Out", input = "eq_band_3:In" },
    Json.Object { output = "eq_band_3:Out", input = "eq_band_4:In" },
    Json.Object { output = "eq_band_4:Out", input = "eq_band_5:In" },
    Json.Object { output = "eq_band_5:Out", input = "eq_band_6:In" },
  },
}

args["capture.props"] = Json.Object {
  ["node.name"]   = "effect_input.eq6",
  ["media.class"] = "Audio/Sink",
}

args["playback.props"] = Json.Object {
  ["node.name"]   = "effect_output.eq6",
  ["node.passive"] = true,
}

-- Transform 'args' to a json object here
local args_json = Json.Object(args)

-- and get the final JSON as a string from the json object
local args_string = args_json:get_data()

local properties = {}

print("Loading module-filter-chain with arguments = ")
print(args_string)

filter_chain = LocalModule("libpipewire-module-filter-chain", args_string, properties)
 070701000001B1000081A400000000000000000000000165F86304000002B3000000000000000000000000000000000000003D00000000wireplumber-0.5.0/tests/examples/get-default-sink-volume.lua  #!/usr/bin/wpexec
--
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
--

-- Load the necessary wireplumber api modules
Core.require_api("default-nodes", "mixer", function(...)
  local default_nodes, mixer = ...

  -- configure volumes to be printed in the cubic scale
  -- this is also what the pulseaudio API shows
  mixer.scale = "cubic"

  local id = default_nodes:call("get-default-node", "Audio/Sink")
  local volume = mixer:call("get-volume", id)

  -- dump everything
  Debug.dump_table(volume)

  -- or maybe just the volume...
  -- print(volume.volume)

  Core.quit()
end)
 070701000001B2000081ED00000000000000000000000165F8630400000FDB000000000000000000000000000000000000003100000000wireplumber-0.5.0/tests/examples/interactive.lua  #!/usr/bin/wpexec
--
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
--
-- This is an example of an interactive script
--
-- Execute with:
--   wpexec ./interactive.lua '{ option1 = "value1" , option2 = "value2" }' ...
-- or:
--   ./interactive.lua '{ option1 = "value1" , option2 = "value2" }'
-----------------------------------------------------------------------------

--
-- Collects arguments passed in from the command line
-- Assuming '{ option1 = "value1" , option2 = "value2" }' were passed,
-- this will be a table like this:
-- { ["option1"] = "value1", ["option2"] = "value2" }
--
local argv = ...
if argv then
  argv = argv:parse()

  print ("Command-line arguments:")
  Debug.dump_table (argv)
end

--
-- Retrieve remote core info
--
local info = Core.get_info()

print ("\nPipeWire daemon info:")
for k, v in pairs(info) do
  if k == "properties" then
    print ("\tproperties:")
    for kk, vv in pairs(v) do
      print ("\t\t" .. kk .. ": " .. vv)
    end
  else
    print ("\t" .. k .. ": " .. v)
  end
end

--
-- Retrieve objects using an ObjectManager
--
-- Note: obj_mgr here cannot be a local variable; we need it to stay alive
-- after the execution has returned to the main loop
--
obj_mgr = ObjectManager {
  Interest { type = "client" },
  Interest { type = "device" },
  Interest { type = "node" },
}

-- Listen for the 'installed' signal from the ObjectManager
-- and execute a function when it is fired
-- This function will be called from the main loop at some point later
obj_mgr:connect("installed", function (om)

  --
  -- Print connected clients
  --
  print ("\nClients:")
  for obj in om:iterate { type = "client" } do

    --
    -- 'bound-id' and 'global-properties' are GObject
    -- properties of WpProxy / WpGlobalProxy
    --
    local id = obj["bound-id"]
    local global_props = obj["global-properties"]

    print ("\t" .. id .. ": " .. global_props["application.name"]
                .. " (PID: " .. global_props["pipewire.sec.pid"] .. ")")
  end

  --
  -- Print devices
  --
  print ("\nDevices:")
  for obj in om:iterate { type = "device" } do
    local id = obj["bound-id"]
    local global_props = obj["global-properties"]

    print ("\t" .. id .. ": " .. global_props["device.name"]
                .. " (" .. global_props["device.description"] .. ")")
  end

  -- Common function to print nodes
  local function printNode(node)
    local id = node["bound-id"]
    local global_props = node["global-properties"]

    -- standard lua string.match() function used here
    if global_props["media.class"]:match("Stream/.*") then
      print ("\t" .. id .. ": " .. global_props["application.name"])
    else
      print ("\t" .. id .. ": " .. global_props["object.path"]
                .. " (" .. global_props["node.description"] .. ")")
    end
  end

  --
  -- Print device sinks
  --
  print ("\nSinks:")
  --
  -- Interest can have additional constraints that can be used to filter
  -- the results. In this case we are only interested in nodes with their
  -- "media.class" global property matching the glob expression "*/Sink"
  --
  local interest = Interest { type = "node",
    Constraint { "media.class", "matches", "*/Sink" }
  }
  for obj in om:iterate(interest) do
    printNode(obj)
  end

  --
  -- Print device sources
  --
  print ("\nSources:")
  local interest = Interest { type = "node",
    Constraint { "media.class", "matches", "*/Source" }
  }
  for obj in om:iterate(interest) do
    printNode(obj)
  end

  --
  -- Print client streams
  --
  print ("\nStreams:")
  local interest = Interest { type = "node",
    Constraint { "media.class", "matches", "Stream/*" }
  }
  for obj in om:iterate(interest) do
    printNode(obj)
  end

  -- Disconnect from pipewire and quit wireplumber
  -- This only works in script interactive mode
  Core.quit()
end)

-- Activate the object manager. This is required to start it,
-- otherwise it doesn't do anything
obj_mgr:activate()
 070701000001B3000081A400000000000000000000000165F863040000080E000000000000000000000000000000000000002400000000wireplumber-0.5.0/tests/meson.build   valgrind = find_program('valgrind', required: false)
if valgrind.found()

  glib_supp = get_option('glib-supp')
  if glib_supp == ''
    glib_supp = glib_dep.get_variable(pkgconfig: 'prefix')
    glib_supp = glib_supp / 'share' / 'glib-2.0' / 'valgrind' / 'glib.supp'
  endif
  if fs.is_file(glib_supp)
    message('Using glib.supp:', glib_supp)
  else
    message('glib.supp not found, valgrind tests will not work correctly')
  endif

  valgrind_env = environment({
    'G_SLICE': 'always-malloc',
  })

  add_test_setup('valgrind',
    exe_wrapper: [ valgrind,
      '--suppressions=' + glib_supp,
      '--leak-check=full',
      '--gen-suppressions=all',
      '--error-exitcode=3',
      '--keep-debuginfo=yes',
    ],
    env: valgrind_env,
    timeout_multiplier: 2)
endif

# The common test environment
common_test_env = environment({
  'HOME': '/invalid',
  'XDG_RUNTIME_DIR': '/invalid',
  'PIPEWIRE_RUNTIME_DIR': '/tmp',
  'XDG_CONFIG_HOME': meson.current_build_dir() / '.config',
  'XDG_STATE_HOME': meson.current_build_dir() / '.local' / 'state',
  'FILE_MONITOR_DIR': meson.current_build_dir() / '.local' / 'file_monitor',
  'WIREPLUMBER_DATA_DIR': meson.current_source_dir() / '..' / 'src',
  'WIREPLUMBER_MODULE_DIR': meson.current_build_dir() / '..' / 'modules',
  'WIREPLUMBER_DEBUG': 'T,pw.*:I,spa.*:I,mod.*:I',
})

spa_plugindir = spa_dep.get_variable(
  pkgconfig: 'plugindir', internal: 'plugindir', default_value: '')
pipewire_moduledir = pipewire_dep.get_variable(
  pkgconfig: 'moduledir', internal: 'moduledir', default_value: '')
pipewire_confdatadir = pipewire_dep.get_variable(
  pkgconfig: 'confdatadir', internal: 'confdatadir', default_value: '')

if spa_plugindir != ''
  common_test_env.set('SPA_PLUGIN_DIR', spa_plugindir)
endif
if pipewire_moduledir != ''
  common_test_env.set('PIPEWIRE_MODULE_DIR', pipewire_moduledir)
endif
if pipewire_confdatadir != ''
  common_test_env.set('PIPEWIRE_CONFIG_DIR', pipewire_confdatadir)
endif

subdir('wp')
if build_modules
  subdir('wplua')
  subdir('scripts')
  subdir('modules')
endif
  070701000001B4000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002000000000wireplumber-0.5.0/tests/modules   070701000001B5000081A400000000000000000000000165F8630400000A6F000000000000000000000000000000000000003200000000wireplumber-0.5.0/tests/modules/dbus-connection.c /* WirePlumber
 *
 * Copyright © 2022-2023 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include "../../modules/dbus-connection-state.h"
#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;
  GTestDBus *test_dbus;
} TestFixture;

static void
test_setup (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&self->base, 0);
  self->test_dbus = g_test_dbus_new (G_TEST_DBUS_NONE);
  g_test_dbus_up (self->test_dbus);
}

static void
test_teardown (TestFixture *self, gconstpointer user_data)
{
  g_test_dbus_down (self->test_dbus);
  g_clear_object (&self->test_dbus);
  wp_base_test_fixture_teardown (&self->base);
}

static void
on_dbus_state_changed (GObject * dbus, GParamSpec * spec, TestFixture *f)
{
  WpDBusConnectionState state = -1;
  g_object_get (dbus, "state", &state, NULL);
  g_assert_cmpint (state, ==, WP_DBUS_CONNECTION_STATE_CLOSED);

  g_object_set_data (G_OBJECT (dbus), "state-closed", GUINT_TO_POINTER (0x1));
}

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, TestFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_no_error (error);
  g_assert_true (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
test_dbus_connection (TestFixture *f, gconstpointer user_data)
{
  g_autoptr (WpPlugin) dbus = NULL;
  g_autoptr (GDBusConnection) conn = NULL;
  WpDBusConnectionState state = -1;

  wp_core_load_component (f->base.core,
      "libwireplumber-module-dbus-connection", "module", NULL, NULL, NULL,
      (GAsyncReadyCallback) on_plugin_loaded, f);
  g_main_loop_run (f->base.loop);

  dbus = wp_plugin_find (f->base.core, "dbus-connection");
  g_assert_nonnull (dbus);

  g_object_get (dbus, "state", &state, NULL);
  g_assert_cmpint (state, ==, WP_DBUS_CONNECTION_STATE_CONNECTED);

  g_object_get (dbus, "connection", &conn, NULL);
  g_assert_nonnull (conn);
  g_clear_object (&conn);

  g_signal_connect (dbus, "notify::state", G_CALLBACK (on_dbus_state_changed),
     f);

  wp_object_deactivate (WP_OBJECT (dbus), WP_PLUGIN_FEATURE_ENABLED);

  // set by on_dbus_state_changed
  g_assert_cmphex (GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (dbus), "state-closed")), ==, 0x1);

  g_object_get (dbus, "connection", &conn, NULL);
  g_assert_null (conn);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/dbus/connection", TestFixture, NULL,
      test_setup, test_dbus_connection, test_teardown);

  return g_test_run ();
}
 070701000001B6000081A400000000000000000000000165F8630400000B97000000000000000000000000000000000000002F00000000wireplumber-0.5.0/tests/modules/file-monitor.c    /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <fcntl.h>
#include <sys/inotify.h>

#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;
  WpPlugin *plugin;
  gchar *path;
  gchar *file;
  gchar *evtype;
} TestFixture;

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, TestFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_no_error (error);
  g_assert_true (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
test_file_monitor_setup (TestFixture * f, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&f->base, WP_BASE_TEST_FLAG_DONT_CONNECT);

  wp_core_load_component (f->base.core,
      "libwireplumber-module-file-monitor-api", "module", NULL, NULL, NULL,
      (GAsyncReadyCallback) on_plugin_loaded, f);
  g_main_loop_run (f->base.loop);

  f->plugin = wp_plugin_find (f->base.core, "file-monitor-api");
  g_assert_nonnull (f->plugin);

  f->path = g_strdup (g_getenv ("FILE_MONITOR_DIR"));
  g_mkdir_with_parents (f->path, 0700);
}

static void
test_file_monitor_teardown (TestFixture * f, gconstpointer user_data)
{
  g_clear_pointer (&f->evtype, g_free);
  g_clear_pointer (&f->file, g_free);
  g_clear_pointer (&f->path, g_free);
  g_clear_object (&f->plugin);
  wp_base_test_fixture_teardown (&f->base);
}

static void
on_changed (WpPlugin *plugin, const gchar *file, const gchar *old,
    const char *evtype, TestFixture * f)
{
  g_assert_nonnull (file);
  g_assert_nonnull (evtype);
  f->file = g_strdup (file);
  f->evtype = g_strdup (evtype);
  g_main_loop_quit (f->base.loop);
}

static void
test_file_monitor_basic (TestFixture * f, gconstpointer user_data)
{
  gboolean res = FALSE;

  /* delete the 'foo' file if it exists in path */
  g_autofree gchar *filename = g_build_filename (f->path, "foo", NULL);
  (void) remove (filename);

  /* handle changed signal */
  f->file = NULL;
  f->evtype = NULL;
  g_signal_connect (f->plugin, "changed", G_CALLBACK (on_changed), f);

  /* add watch */
  g_signal_emit_by_name (f->plugin, "add-watch", f->path, "m", &res);
  g_assert_true (res);

  /* create the foo file in path */
  int fd = open (filename, O_CREAT | O_EXCL, 0700);
  g_assert_cmpint (fd, >=, 0);

  /* run */
  g_main_loop_run (f->base.loop);
  g_assert_cmpstr (f->file, ==, filename);
  g_assert_cmpstr (f->evtype, ==, "created");

  /* removed watch */
  g_signal_emit_by_name (f->plugin, "remove-watch", f->path);

  /* remove 'foo' */
  close (fd);
  (void) remove (filename);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/modules/file-monitor/basic",
      TestFixture, NULL,
      test_file_monitor_setup,
      test_file_monitor_basic,
      test_file_monitor_teardown);

  return g_test_run ();
}
 070701000001B7000081A400000000000000000000000165F86304000004EA000000000000000000000000000000000000002C00000000wireplumber-0.5.0/tests/modules/meson.build   common_deps = [gobject_dep, gio_dep, wp_dep, pipewire_dep]
common_env = common_test_env
common_env.set('G_TEST_SRCDIR', meson.current_source_dir())
common_env.set('G_TEST_BUILDDIR', meson.current_build_dir())

if get_option('dbus-tests')
  test(
    'test-dbus-connection',
    executable('test-dbus-connection', 'dbus-connection.c',
        dependencies: common_deps),
    env: common_env,
  )

  test(
    'test-reserve-device',
    executable('test-reserve-device', 'reserve-device.c',
      dependencies: common_deps),
    env: common_env,
  )
endif

test(
  'test-file-monitor',
  executable('test-file-monitor', 'file-monitor.c',
    dependencies: common_deps),
  env: common_env,
)

test(
  'test-si-node',
  executable('test-si-node', 'si-node.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-si-audio-adapter',
  executable('test-si-audio-adapter', 'si-audio-adapter.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-si-audio-virtual',
  executable('test-si-audio-virtual', 'si-audio-virtual.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-si-standard-link',
  executable('test-si-standard-link', 'si-standard-link.c',
      dependencies: common_deps),
  env: common_env,
)
  070701000001B8000081A400000000000000000000000165F86304000028DA000000000000000000000000000000000000003100000000wireplumber-0.5.0/tests/modules/reserve-device.c  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include "../../modules/dbus-connection-state.h"
#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;
  GTestDBus *test_dbus;
  WpPlugin *dbus_1;
  WpPlugin *dbus_2;
  WpPlugin *rd_plugin_1;
  WpPlugin *rd_plugin_2;
  gint expected_rd1_state;
  gint expected_rd2_state;
} RdTestFixture;

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, RdTestFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_no_error (error);
  g_assert_true (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
test_rd_setup (RdTestFixture *f, gconstpointer data)
{
  WpDBusConnectionState state = -1;

  wp_base_test_fixture_setup (&f->base,
      WP_BASE_TEST_FLAG_CLIENT_CORE | WP_BASE_TEST_FLAG_DONT_CONNECT);

  f->test_dbus = g_test_dbus_new (G_TEST_DBUS_NONE);
  g_test_dbus_up (f->test_dbus);

  {
    wp_core_load_component (f->base.core,
        "libwireplumber-module-dbus-connection", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);
  }
  {
    wp_core_load_component (f->base.client_core,
        "libwireplumber-module-dbus-connection", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);
  }
  {
    wp_core_load_component (f->base.core,
        "libwireplumber-module-reserve-device", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);
  }
  {
    wp_core_load_component (f->base.client_core,
        "libwireplumber-module-reserve-device", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);
  }

  f->dbus_1 = wp_plugin_find (f->base.core, "dbus-connection");
  g_assert_nonnull (f->dbus_1);

  f->dbus_2 = wp_plugin_find (f->base.client_core, "dbus-connection");
  g_assert_nonnull (f->dbus_2);

  f->rd_plugin_1 = wp_plugin_find (f->base.core, "reserve-device");
  g_assert_nonnull (f->rd_plugin_1);

  f->rd_plugin_2 = wp_plugin_find (f->base.client_core, "reserve-device");
  g_assert_nonnull (f->rd_plugin_2);

  g_object_get (f->dbus_1, "state", &state, NULL);
  g_assert_cmpint (state, ==, WP_DBUS_CONNECTION_STATE_CONNECTED);

  g_object_get (f->dbus_2, "state", &state, NULL);
  g_assert_cmpint (state, ==, WP_DBUS_CONNECTION_STATE_CONNECTED);
}

static void
test_rd_teardown (RdTestFixture *f, gconstpointer data)
{
  g_clear_object (&f->dbus_1);
  g_clear_object (&f->dbus_2);
  g_clear_object (&f->rd_plugin_1);
  g_clear_object (&f->rd_plugin_2);
  g_test_dbus_down (f->test_dbus);
  g_clear_object (&f->test_dbus);
  wp_base_test_fixture_teardown (&f->base);
}

static void
ensure_plugins_stable_state (GObject * obj, GParamSpec * spec, RdTestFixture *f)
{
  WpDBusConnectionState state1 = -1, state2 = -1;
  g_object_get (f->dbus_1, "state", &state1, NULL);
  g_object_get (f->dbus_2, "state", &state2, NULL);

  if (state1 != WP_DBUS_CONNECTION_STATE_CONNECTING &&
      state2 != WP_DBUS_CONNECTION_STATE_CONNECTING &&
      state1 == state2)
    g_main_loop_quit (f->base.loop);
}

static void
test_rd_plugin (RdTestFixture *f, gconstpointer data)
{
  GObject *rd1 = NULL, *rd2 = NULL, *rd_video = NULL, *tmp = NULL;
  gint priority = 0;
  gchar *str;

  g_signal_emit_by_name (f->rd_plugin_1, "create-reservation",
      "Audio0", "WirePlumber", "hw:0,0", 10, &rd1);
  g_assert_nonnull (rd1);
  g_signal_emit_by_name (f->rd_plugin_2, "create-reservation",
      "Audio0", "Other Server", "hw:0,0", 15, &rd2);
  g_assert_nonnull (rd2);
  g_signal_emit_by_name (f->rd_plugin_1, "create-reservation",
      "Video0", "WirePlumber", "/dev/video0", 10, &rd_video);
  g_assert_nonnull (rd_video);

  g_signal_emit_by_name (f->rd_plugin_1, "get-reservation", "Video1", &tmp);
  g_assert_null (tmp);
  g_signal_emit_by_name (f->rd_plugin_2, "get-reservation", "Video0", &tmp);
  g_assert_null (tmp);

  g_signal_emit_by_name (f->rd_plugin_1, "get-reservation", "Audio0", &tmp);
  g_assert_nonnull (tmp);
  g_assert_true (tmp == rd1);
  g_clear_object (&tmp);

  g_object_get (rd1, "name", &str, NULL);
  g_assert_cmpstr (str, ==, "Audio0");
  g_free (str);
  g_object_get (rd2, "name", &str, NULL);
  g_assert_cmpstr (str, ==, "Audio0");
  g_free (str);
  g_object_get (rd_video, "name", &str, NULL);
  g_assert_cmpstr (str, ==, "Video0");
  g_free (str);
  g_object_get (rd1, "application-name", &str, NULL);
  g_assert_cmpstr (str, ==, "WirePlumber");
  g_free (str);
  g_object_get (rd1, "application-device-name", &str, NULL);
  g_assert_cmpstr (str, ==, "hw:0,0");
  g_free (str);
  g_object_get (rd1, "priority", &priority, NULL);
  g_assert_cmpint (priority, ==, 10);
  g_object_get (rd2, "priority", &priority, NULL);
  g_assert_cmpint (priority, ==, 15);

  g_signal_emit_by_name (f->rd_plugin_1, "destroy-reservation", "Audio0");
  g_signal_emit_by_name (f->rd_plugin_1, "get-reservation", "Audio0", &tmp);
  g_assert_null (tmp);
  g_signal_emit_by_name (f->rd_plugin_2, "get-reservation", "Audio0", &tmp);
  g_assert_nonnull (tmp);
  g_assert_true (tmp == rd2);
  g_clear_object (&tmp);
  g_clear_object (&rd2);
  g_clear_object (&rd1);
  g_clear_object (&rd_video);
}

static void
test_rd_conn_closed (RdTestFixture *f, gconstpointer data)
{
  GObject *rd1 = NULL;
  WpDBusConnectionState state = -1;

  g_signal_connect (f->dbus_1, "notify::state",
      G_CALLBACK (ensure_plugins_stable_state), f);
  g_signal_connect (f->dbus_2, "notify::state",
      G_CALLBACK (ensure_plugins_stable_state), f);

  g_signal_emit_by_name (f->rd_plugin_1, "create-reservation",
      "Audio0", "WirePlumber", "hw:0,0", 10, &rd1);
  g_assert_nonnull (rd1);
  g_clear_object (&rd1);

  /* stop the bus, expect the connections to close
     and state to go back to CLOSED */
  g_test_dbus_stop (f->test_dbus);
  g_main_loop_run (f->base.loop);

  g_object_get (f->dbus_1, "state", &state, NULL);
  g_assert_cmpint (state, ==, WP_DBUS_CONNECTION_STATE_CLOSED);
  g_object_get (f->dbus_2, "state", &state, NULL);
  g_assert_cmpint (state, ==, WP_DBUS_CONNECTION_STATE_CLOSED);

  g_signal_emit_by_name (f->rd_plugin_1, "get-reservation", "Audio0", &rd1);
  g_assert_null (rd1);
}

static void
expect_rd1_state (GObject * rd, GParamSpec * spec, RdTestFixture *f)
{
  gint state;
  g_object_get (rd, "state", &state, NULL);
  if (state == f->expected_rd1_state)
    g_main_loop_quit (f->base.loop);
}

static void
expect_rd2_state (GObject * rd, GParamSpec * spec, RdTestFixture *f)
{
  gint state;
  g_object_get (rd, "state", &state, NULL);
  if (state == f->expected_rd2_state)
    g_main_loop_quit (f->base.loop);
}

static void
handle_release_requested (GObject * rd, gboolean forced, RdTestFixture *f)
{
  gint state = 0xffff;
  g_signal_emit_by_name (rd, "release");
  g_object_get (rd, "state", &state, NULL);
  g_assert_cmpint (state, ==, 2);
  g_main_loop_quit (f->base.loop);
}

static void
test_rd_acquire_release (RdTestFixture *f, gconstpointer data)
{
  GObject *rd1 = NULL, *rd2 = NULL;
  gint state = 0;
  gchar *str = NULL;

  g_signal_emit_by_name (f->rd_plugin_1, "create-reservation",
      "Audio0", "WirePlumber", "hw:0,0", 10, &rd1);
  g_assert_nonnull (rd1);
  g_signal_emit_by_name (f->rd_plugin_2, "create-reservation",
      "Audio0", "Other Server", "hw:0,0", 15, &rd2);
  g_assert_nonnull (rd2);

  g_signal_connect (rd1, "notify::state", G_CALLBACK (expect_rd1_state), f);
  g_signal_connect (rd2, "notify::state", G_CALLBACK (expect_rd2_state), f);

  /* acquire */
  wp_info ("rd1 acquire");

  f->expected_rd1_state = 3;
  g_signal_emit_by_name (rd1, "acquire");
  g_main_loop_run (f->base.loop);
  g_object_get (rd1, "state", &state, NULL);
  g_assert_cmpint (state, ==, 3);
  g_object_get (rd1, "owner-application-name", &str, NULL);
  g_assert_cmpstr (str, ==, "WirePlumber");
  g_free (str);

  g_signal_connect (rd1, "release-requested",
      G_CALLBACK (handle_release_requested), f);

  /* acquire with higher priority */
  wp_info ("rd2 acquire, higher prio");
  g_signal_emit_by_name (rd2, "acquire");

  /* rd1 is now released */
  g_main_loop_run (f->base.loop);
  g_object_get (rd1, "state", &state, NULL);
  g_assert_cmpint (state, ==, 2);

  /* rd2 acquired */
  f->expected_rd2_state = 3;
  g_main_loop_run (f->base.loop);
  g_object_get (rd2, "state", &state, NULL);
  g_assert_cmpint (state, ==, 3);

  /* rd1 busy */
  g_signal_connect_swapped (rd1, "notify::owner-application-name",
      G_CALLBACK (g_main_loop_quit), f->base.loop);
  g_main_loop_run (f->base.loop);
  g_object_get (rd1, "state", &state, NULL);
  g_assert_cmpint (state, ==, 1);
  g_object_get (rd1, "owner-application-name", &str, NULL);
  g_assert_cmpstr (str, ==, "Other Server");
  g_free (str);
  g_signal_handlers_disconnect_by_func (rd1, G_CALLBACK (g_main_loop_quit),
      f->base.loop);

  /* try to acquire back with lower priority */
  wp_info ("rd1 acquire, lower prio");
  g_signal_emit_by_name (rd1, "acquire");

  /* ... expect this to fail */
  f->expected_rd1_state = 1;
  g_main_loop_run (f->base.loop);
  g_object_get (rd1, "state", &state, NULL);
  g_assert_cmpint (state, ==, 1);

  g_object_get (rd1, "owner-application-name", &str, NULL);
  g_assert_cmpstr (str, ==, "Other Server");
  g_free (str);

  /* release */
  wp_info ("rd2 release");
  g_signal_emit_by_name (rd2, "release");
  g_object_get (rd2, "state", &state, NULL);
  g_assert_cmpint (state, ==, 2);

  f->expected_rd1_state = 2;
  g_main_loop_run (f->base.loop);
  g_object_get (rd1, "state", &state, NULL);
  g_assert_cmpint (state, ==, 2);

  g_object_get (rd1, "owner-application-name", &str, NULL);
  g_assert_null (str);

  g_clear_object (&rd1);
  g_clear_object (&rd2);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/modules/rd/plugin", RdTestFixture, NULL,
      test_rd_setup, test_rd_plugin, test_rd_teardown);
  g_test_add ("/modules/rd/conn_closed", RdTestFixture, NULL,
      test_rd_setup, test_rd_conn_closed, test_rd_teardown);
  g_test_add ("/modules/rd/acquire_release", RdTestFixture, NULL,
      test_rd_setup, test_rd_acquire_release, test_rd_teardown);

  return g_test_run ();
}
  070701000001B9000081A400000000000000000000000165F863040000109A000000000000000000000000000000000000003300000000wireplumber-0.5.0/tests/modules/si-audio-adapter.c    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;
} TestFixture;

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, TestFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_no_error (error);
  g_assert_true (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
test_si_audio_adapter_setup (TestFixture * f, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&f->base, 0);

  /* load modules */
  {
    g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&f->base.server);

    g_assert_cmpint (pw_context_add_spa_lib (f->base.server.context,
            "audiotestsrc", "audiotestsrc/libspa-audiotestsrc"), ==, 0);
    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-spa-node-factory", NULL, NULL));
    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-adapter", NULL, NULL));
  }
  {
    wp_core_load_component (f->base.core,
        "libwireplumber-module-si-audio-adapter", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);
  }
}

static void
test_si_audio_adapter_teardown (TestFixture * f, gconstpointer user_data)
{
  wp_base_test_fixture_teardown (&f->base);
}

static void
test_si_audio_adapter_configure_activate (TestFixture * f,
    gconstpointer user_data)
{
  g_autoptr (WpNode) node = NULL;
  g_autoptr (WpSessionItem) adapter = NULL;

  /* skip test if audiotestsrc is not installed */
  if (!test_is_spa_lib_installed (&f->base, "audiotestsrc")) {
    g_test_skip ("The pipewire audiotestsrc factory was not found");
    return;
  }

  /* create audiotestsrc adapter node */
  node = wp_node_new_from_factory (f->base.core,
      "adapter",
      wp_properties_new (
          "factory.name", "audiotestsrc",
          "node.name", "audiotestsrc.adapter",
          NULL));
  g_assert_nonnull (node);
  wp_object_activate (WP_OBJECT (node), WP_OBJECT_FEATURES_ALL,
      NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);

  /* create adapter */
  adapter = wp_session_item_make (f->base.core, "si-audio-adapter");
  g_assert_nonnull (adapter);
  g_assert_true (WP_IS_SI_LINKABLE (adapter));

  /* configure */
  {
    WpProperties *props = wp_properties_new_empty ();
    wp_properties_setf (props, "item.node", "%p", node);
    wp_properties_set (props, "media.class", "Audio/Source");
    g_assert_true (wp_session_item_configure (adapter, props));
    g_assert_true (wp_session_item_is_configured (adapter));
  }

  /* validate configuration */
  {
    const gchar *str = NULL;
    g_autoptr (WpProperties) props = wp_session_item_get_properties (adapter);
    g_assert_nonnull (props);
    str = wp_properties_get (props, "item.factory.name");
    g_assert_nonnull (str);
    g_assert_cmpstr ("si-audio-adapter", ==, str);
  }

  /* activate */
  wp_object_activate (WP_OBJECT (adapter), WP_SESSION_ITEM_FEATURE_ACTIVE,
      NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);
  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (adapter)), ==,
      WP_SESSION_ITEM_FEATURE_ACTIVE);

  /* deactivate - configuration should not be altered  */
  wp_object_deactivate (WP_OBJECT (adapter), WP_SESSION_ITEM_FEATURE_ACTIVE);
  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (adapter)), ==, 0);
  g_assert_true (wp_session_item_is_configured (adapter));

  /* reset */
  wp_session_item_reset (adapter);
  g_assert_false (wp_session_item_is_configured (adapter));
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  /* configure-activate */

  g_test_add ("/modules/si-audio-adapter/configure-activate",
      TestFixture, NULL,
      test_si_audio_adapter_setup,
      test_si_audio_adapter_configure_activate,
      test_si_audio_adapter_teardown);

  return g_test_run ();
}
  070701000001BA000081A400000000000000000000000165F863040000185C000000000000000000000000000000000000003300000000wireplumber-0.5.0/tests/modules/si-audio-virtual.c    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;
} TestFixture;

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, TestFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_no_error (error);
  g_assert_true (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
test_si_audio_virtual_setup (TestFixture * f, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&f->base, 0);

  /* load modules */
  {
    g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&f->base.server);

    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-spa-node-factory", NULL, NULL));
    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-adapter", NULL, NULL));
  }
  {
    wp_core_load_component (f->base.core,
        "libwireplumber-module-si-audio-adapter", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);
  }
  {
    wp_core_load_component (f->base.core,
        "libwireplumber-module-si-audio-virtual", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);
  }
}

static void
test_si_audio_virtual_teardown (TestFixture * f, gconstpointer user_data)
{
  wp_base_test_fixture_teardown (&f->base);
}

static void
test_si_audio_virtual_configure_activate (TestFixture * f,
    gconstpointer user_data)
{
  g_autoptr (WpSessionItem) item = NULL;

  /* skip the test if null-audio-sink factory is not installed */
  if (!test_is_spa_lib_installed (&f->base, "support.null-audio-sink")) {
    g_test_skip ("The pipewire null-audio-sink factory was not found");
    return;
  }

  /* create item */

  item = wp_session_item_make (f->base.core, "si-audio-virtual");
  g_assert_nonnull (item);

  /* configure item */

  {
    WpProperties *props = wp_properties_new_empty ();
    wp_properties_set (props, "name", "virtual");
    wp_properties_set (props, "media.class", "Audio/Source");
    g_assert_true (wp_session_item_configure (item, props));
    g_assert_true (wp_session_item_is_configured (item));
  }

  {
    const gchar *str = NULL;
    g_autoptr (WpProperties) props = wp_session_item_get_properties (item);
    g_assert_nonnull (props);
    str = wp_properties_get (props, "name");
    g_assert_nonnull (str);
    g_assert_cmpstr ("virtual", ==, str);
    str = wp_properties_get (props, "item.node.direction");
    g_assert_nonnull (str);
    g_assert_cmpstr ("output", ==, str);
    str = wp_properties_get (props, "item.factory.name");
    g_assert_nonnull (str);
    g_assert_cmpstr ("si-audio-virtual", ==, str);
  }

  /* activate item */

  wp_object_activate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_ACTIVE,
      NULL,  (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);
  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (item)), ==,
      WP_SESSION_ITEM_FEATURE_ACTIVE);

  /* reset */
  wp_session_item_reset (item);
  g_assert_false (wp_session_item_is_configured (item));
}

static void
test_si_audio_virtual_export (TestFixture * f, gconstpointer user_data)
{
  g_autoptr (WpSessionItem) item = NULL;
  g_autoptr (WpObjectManager) clients_om = NULL;
  g_autoptr (WpClient) self_client = NULL;

  /* skip the test if null-audio-sink factory is not installed */
  if (!test_is_spa_lib_installed (&f->base, "support.null-audio-sink")) {
    g_test_skip ("The pipewire null-audio-sink factory was not found");
    return;
  }

  clients_om = wp_object_manager_new ();
  wp_object_manager_add_interest (clients_om, WP_TYPE_CLIENT, NULL);
  wp_object_manager_request_object_features (clients_om,
      WP_TYPE_CLIENT, WP_PROXY_FEATURE_BOUND);
  g_signal_connect_swapped (clients_om, "objects-changed",
      G_CALLBACK (g_main_loop_quit), f->base.loop);
  wp_core_install_object_manager (f->base.core, clients_om);
  g_main_loop_run (f->base.loop);
  self_client = wp_object_manager_lookup (clients_om, WP_TYPE_CLIENT, NULL);
  g_assert_nonnull (self_client);

  /* create item */

  item = wp_session_item_make (f->base.core, "si-audio-virtual");
  g_assert_nonnull (item);

  /* configure item */
  {
    WpProperties *props = wp_properties_new_empty ();
    wp_properties_set (props, "name", "virtual");
    wp_properties_set (props, "media.class", "Audio/Source");
    g_assert_true (wp_session_item_configure (item, props));
    g_assert_true (wp_session_item_is_configured (item));
  }

  /* activate item */

  {
    wp_object_activate (WP_OBJECT (item),
        WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED,
        NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
    g_main_loop_run (f->base.loop);
    g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (item)), ==,
        WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED);
  }

  {
    g_autoptr (WpNode) n = NULL;
    g_autoptr (WpProperties) props = NULL;

    n = wp_session_item_get_associated_proxy (item, WP_TYPE_NODE);
    g_assert_nonnull (n);
    props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (n));
    g_assert_nonnull (props);

    g_assert_cmpstr (wp_properties_get (props, "media.class"), ==,
        "Audio/Source");
  }

  /* reset */
  wp_session_item_reset (item);
  g_assert_false (wp_session_item_is_configured (item));
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  /* configure-activate */
  g_test_add ("/modules/si-audio-virtual/configure-activate",
      TestFixture, NULL,
      test_si_audio_virtual_setup,
      test_si_audio_virtual_configure_activate,
      test_si_audio_virtual_teardown);

 /* export */
 g_test_add ("/modules/si-audio-virtual/export",
      TestFixture, NULL,
      test_si_audio_virtual_setup,
      test_si_audio_virtual_export,
      test_si_audio_virtual_teardown);

  return g_test_run ();
}
070701000001BB000081A400000000000000000000000165F8630400001AA0000000000000000000000000000000000000002A00000000wireplumber-0.5.0/tests/modules/si-node.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;
} TestFixture;

typedef struct {
  const gchar *factory;
  const gchar *name;
  const gchar *media_class;
  const gchar *expected_media_class;
  WpDirection expected_direction;
} TestData;

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, TestFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_no_error (error);
  g_assert_true (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
test_si_node_setup (TestFixture * f, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&f->base, 0);

  /* load modules */
  {
    g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&f->base.server);

    g_assert_cmpint (pw_context_add_spa_lib (f->base.server.context,
            "audiotestsrc", "audiotestsrc/libspa-audiotestsrc"), ==, 0);
    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-spa-node-factory", NULL, NULL));
  }
  {
    wp_core_load_component (f->base.core,
        "libwireplumber-module-si-node", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);
  }
}

static void
test_si_node_teardown (TestFixture * f, gconstpointer user_data)
{
  wp_base_test_fixture_teardown (&f->base);
}

static void
test_si_node_configure_activate (TestFixture * f, gconstpointer user_data)
{
  const TestData *data = user_data;
  g_autoptr (WpNode) node = NULL;
  g_autoptr (WpSessionItem) item = NULL;

  /* skip test if spa factory is not insalled */
  if (!test_is_spa_lib_installed (&f->base, data->factory)) {
    g_autofree gchar *msg = NULL;
    msg = g_strdup_printf ("The pipewire %s factory was not found",
        data->factory);
    g_test_skip (msg);
    return;
  }

  /* create item */

  item = wp_session_item_make (f->base.core, "si-node");
  g_assert_nonnull (item);
  g_assert_true (WP_IS_SI_LINKABLE (item));

  node = wp_node_new_from_factory (f->base.core,
      "spa-node-factory",
      wp_properties_new (
          "factory.name", data->factory,
          "node.name", data->name,
          NULL));
  g_assert_nonnull (node);

  wp_object_activate (WP_OBJECT (node), WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL,
      NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);

  /* configure */

  {
    WpProperties *props = wp_properties_new_empty ();
    wp_properties_setf (props, "item.node", "%p", node);
    wp_properties_set (props, "media.class", data->media_class);
    g_assert_true (wp_session_item_configure (item, props));
    g_assert_true (wp_session_item_is_configured (item));
  }

  {
    const gchar *str = NULL;
    g_autoptr (WpProperties) props = wp_session_item_get_properties (item);
    g_assert_nonnull (props);
    str = wp_properties_get (props, "media.class");
    g_assert_nonnull (str);
    g_assert_cmpstr (data->expected_media_class, ==, str);
    str = wp_properties_get (props, "item.factory.name");
    g_assert_nonnull (str);
    g_assert_cmpstr ("si-node", ==, str);
  }

  /* activate */

  wp_object_activate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_ACTIVE,
      NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);
  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (item)), ==,
      WP_SESSION_ITEM_FEATURE_ACTIVE);
  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (node)), ==,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL | WP_NODE_FEATURE_PORTS);

  if (data->expected_direction == WP_DIRECTION_INPUT)
    g_assert_cmpuint (wp_node_get_n_input_ports (node, NULL), ==, 1);
  else
    g_assert_cmpuint (wp_node_get_n_output_ports (node, NULL), ==, 1);
  g_assert_cmpuint (wp_node_get_n_ports (node), ==, 1);

  {
    guint32 node_id, port_id, channel;
    g_autoptr (GVariant) v =
        wp_si_linkable_get_ports (WP_SI_LINKABLE (item),
        (data->expected_direction == WP_DIRECTION_INPUT) ? "input" : "output");

    g_assert_true (g_variant_is_of_type (v, G_VARIANT_TYPE ("a(uuu)")));
    g_assert_cmpint (g_variant_n_children (v), ==, 1);
    g_variant_get_child (v, 0, "(uuu)", &node_id, &port_id, &channel);
    g_assert_cmpuint (node_id, ==, wp_proxy_get_bound_id (WP_PROXY (node)));
    g_assert_cmpuint (channel, ==, 0);

    {
      g_autoptr (WpIterator) it = wp_node_new_ports_iterator (node);
      g_auto (GValue) val = G_VALUE_INIT;
      WpProxy *port;

      g_assert_true (wp_iterator_next (it, &val));
      port = g_value_get_object (&val);
      g_assert_nonnull (port);
      g_assert_cmpuint (port_id, ==, wp_proxy_get_bound_id (port));
    }
  }

  /* deactivate - configuration should not be altered  */

  wp_object_deactivate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_ACTIVE);

  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (item)), ==, 0);
  g_assert_true (wp_session_item_is_configured (item));
  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (node)), ==,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL | WP_NODE_FEATURE_PORTS);

  {
    const gchar *str = NULL;
    g_autoptr (WpProperties) props = wp_session_item_get_properties (item);
    g_assert_nonnull (props);
    str = wp_properties_get (props, "media.class");
    g_assert_nonnull (str);
    g_assert_cmpstr (data->expected_media_class, ==, str);
    str = wp_properties_get (props, "item.factory.name");
    g_assert_nonnull (str);
    g_assert_cmpstr ("si-node", ==, str);
  }

  /* reset - configuration resets */

  wp_session_item_reset (item);
  g_assert_false (wp_session_item_is_configured (item));

  {
    g_autoptr (WpProperties) props =
        wp_session_item_get_properties (item);
    g_assert_null (props);
  }
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  /* data */

  const TestData nullsink_data = {
    "support.null-audio-sink", "nullsink0", "Fake/Sink", "Fake/Sink", WP_DIRECTION_INPUT
  };
  const TestData audiotestsrc_data = {
    "audiotestsrc", "audiotestsrc0", "Audio/Source", "Audio/Source", WP_DIRECTION_OUTPUT
  };

  /* configure-activate */

  g_test_add ("/modules/si-node/configure-activate/nullsink",
      TestFixture, &nullsink_data,
      test_si_node_setup,
      test_si_node_configure_activate,
      test_si_node_teardown);

  g_test_add ("/modules/si-node/configure-activate/audiotestsrc",
      TestFixture, &audiotestsrc_data,
      test_si_node_setup,
      test_si_node_configure_activate,
      test_si_node_teardown);

  return g_test_run ();
}
070701000001BC000081A400000000000000000000000165F863040000244E000000000000000000000000000000000000003300000000wireplumber-0.5.0/tests/modules/si-standard-link.c    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;

  WpSessionItem *src_item;
  WpSessionItem *sink_item;

} TestFixture;

static WpSessionItem *
load_node (TestFixture * f, const gchar * factory, const gchar * media_class,
    const gchar * type)
{
  g_autoptr (WpNode) node = NULL;
  g_autoptr (WpSessionItem) adapter = NULL;

  /* create audiotestsrc adapter node */
  node = wp_node_new_from_factory (f->base.core,
      "adapter",
      wp_properties_new (
          "factory.name", factory,
          "node.name", factory,
          "media.class", media_class,
          "audio.channels", "2",
          "audio.position", "[ FL, FR ]",
          NULL));
  g_assert_nonnull (node);
  wp_object_activate (WP_OBJECT (node), WP_OBJECT_FEATURES_ALL,
      NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);

  /* create adapter */
  adapter = wp_session_item_make (f->base.core, "si-audio-adapter");
  g_assert_nonnull (adapter);
  g_assert_true (WP_IS_SI_LINKABLE (adapter));

  /* configure */
  {
    WpProperties *props = wp_properties_new_empty ();
    wp_properties_setf (props, "item.node", "%p", node);
    wp_properties_set (props, "media.class", media_class);
    wp_properties_set (props, "item.node.type", type);
    g_assert_true (wp_session_item_configure (adapter, props));
    g_assert_true (wp_session_item_is_configured (adapter));
  }

  /* activate adapter */

  wp_object_activate (WP_OBJECT (adapter),
      WP_SESSION_ITEM_FEATURE_ACTIVE,
      NULL,  (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);
  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (adapter)), ==,
      WP_SESSION_ITEM_FEATURE_ACTIVE);

  return g_steal_pointer (&adapter);
}

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, TestFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_no_error (error);
  g_assert_true (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
test_si_standard_link_setup (TestFixture * f, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&f->base, 0);

  /* load modules */
  {
    g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&f->base.server);

    g_assert_cmpint (pw_context_add_spa_lib (f->base.server.context,
            "audiotestsrc", "audiotestsrc/libspa-audiotestsrc"), ==, 0);
    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-adapter", NULL, NULL));
    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-link-factory", NULL, NULL));
  }
  {
    wp_core_load_component (f->base.core,
        "libwireplumber-module-si-audio-adapter", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);

    wp_core_load_component (f->base.core,
        "libwireplumber-module-si-standard-link", "module", NULL, NULL, NULL,
        (GAsyncReadyCallback) on_plugin_loaded, f);
    g_main_loop_run (f->base.loop);
  }

  if (test_is_spa_lib_installed (&f->base, "audiotestsrc"))
    f->src_item = load_node (f, "audiotestsrc", "Stream/Output/Audio", "stream");
  if (test_is_spa_lib_installed (&f->base, "support.null-audio-sink"))
    f->sink_item = load_node (f, "support.null-audio-sink", "Audio/Sink", "device");
}

static void
test_si_standard_link_teardown (TestFixture * f, gconstpointer user_data)
{
  g_clear_object (&f->sink_item);
  g_clear_object (&f->src_item);
  wp_base_test_fixture_teardown (&f->base);
}

static void
test_si_standard_link_main (TestFixture * f, gconstpointer user_data)
{
  g_autoptr (WpSessionItem) link = NULL;

  /* skip the test if audiotestsrc could not be loaded */
  if (!f->src_item) {
    g_test_skip ("The pipewire audiotestsrc factory was not found");
    return;
  }

  /* skip the test if null-audio-sink could not be loaded */
  if (!f->sink_item) {
    g_test_skip ("The pipewire null-audio-sink factory was not found");
    return;
  }

  /* create the link */
  link = wp_session_item_make (f->base.core, "si-standard-link");
  g_assert_nonnull (link);

  /* configure the link */
  {
    g_autoptr (WpProperties) props = wp_properties_new_empty ();
    wp_properties_setf (props, "out.item", "%p", f->src_item);
    wp_properties_setf (props, "in.item", "%p", f->sink_item);
    wp_properties_set (props, "out.item.port.context", "output");
    wp_properties_set (props, "in.item.port.context", "input");
    g_assert_true (wp_session_item_configure (link, g_steal_pointer (&props)));
  }

  /* activate */
  wp_object_activate (WP_OBJECT (link), WP_SESSION_ITEM_FEATURE_ACTIVE,
      NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);

  /* verify the graph state */
  {
    g_autoptr (WpNode) out_node = NULL;
    g_autoptr (WpNode) in_node = NULL;
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) val = G_VALUE_INIT;
    g_autoptr (WpObjectManager) om = wp_object_manager_new ();
    guint total_links = 0;

    wp_object_manager_add_interest (om, WP_TYPE_NODE, NULL);
    wp_object_manager_add_interest (om, WP_TYPE_PORT, NULL);
    wp_object_manager_add_interest (om, WP_TYPE_LINK, NULL);
    wp_object_manager_request_object_features (om, WP_TYPE_PROXY,
        WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
    test_ensure_object_manager_is_installed (om, f->base.core,
        f->base.loop);

    out_node = wp_object_manager_lookup (om, WP_TYPE_NODE,
        WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.name", "=s", "audiotestsrc",
        NULL);
    g_assert_nonnull (out_node);
    in_node = wp_object_manager_lookup (om, WP_TYPE_NODE,
        WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.name", "=s", "support.null-audio-sink",
        NULL);
    g_assert_nonnull (in_node);
    g_assert_cmpuint (wp_object_manager_get_n_objects (om), ==, 8);

    it = wp_object_manager_new_filtered_iterator (om, WP_TYPE_LINK, NULL);
    for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
      guint32 out_nd_id, out_pt_id, in_nd_id, in_pt_id;
      g_autoptr (WpPort) out_port = NULL;
      g_autoptr (WpPort) in_port = NULL;
      WpLink *link = g_value_get_object (&val);
      wp_link_get_linked_object_ids (link, &out_nd_id, &out_pt_id, &in_nd_id,
          &in_pt_id);
      g_assert_cmpuint (out_nd_id, ==, wp_proxy_get_bound_id (WP_PROXY (out_node)));
      g_assert_cmpuint (in_nd_id, ==, wp_proxy_get_bound_id (WP_PROXY (in_node)));
      out_port = wp_object_manager_lookup (om, WP_TYPE_PORT,
          WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", out_pt_id,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.id", "=u", out_nd_id,
          NULL);
      g_assert_nonnull (out_port);
      in_port = wp_object_manager_lookup (om, WP_TYPE_PORT,
          WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", in_pt_id,
          WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.id", "=u", in_nd_id,
          NULL);
      g_assert_nonnull (in_port);
      total_links++;
    }
    g_assert_cmpuint (total_links, ==, 2);
  }

  /* deactivate */
  wp_object_deactivate (WP_OBJECT (link), WP_SESSION_ITEM_FEATURE_ACTIVE);

  /* verify the graph state */
  {
    g_autoptr (WpNode) out_node = NULL;
    g_autoptr (WpNode) in_node = NULL;
    g_autoptr (WpPort) out_port = NULL;
    g_autoptr (WpPort) in_port = NULL;
    g_autoptr (WpLink) link = NULL;
    g_autoptr (WpObjectManager) om = wp_object_manager_new ();

    wp_object_manager_add_interest (om, WP_TYPE_NODE, NULL);
    wp_object_manager_add_interest (om, WP_TYPE_PORT, NULL);
    wp_object_manager_add_interest (om, WP_TYPE_LINK, NULL);
    wp_object_manager_request_object_features (om, WP_TYPE_PROXY,
        WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
    test_ensure_object_manager_is_installed (om, f->base.core,
        f->base.loop);

    out_node = wp_object_manager_lookup (om, WP_TYPE_NODE,
        WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.name", "=s", "audiotestsrc",
        NULL);
    g_assert_nonnull (out_node);
    in_node = wp_object_manager_lookup (om, WP_TYPE_NODE,
        WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.name", "=s", "support.null-audio-sink",
        NULL);
    g_assert_nonnull (in_node);
    out_port = wp_object_manager_lookup (om, WP_TYPE_PORT,
        WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.direction", "=s", "out",
        NULL);
    g_assert_nonnull (out_port);
    in_port = wp_object_manager_lookup (om, WP_TYPE_PORT,
        WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.direction", "=s", "in",
        NULL);
    g_assert_nonnull (in_port);
    link = wp_object_manager_lookup (om, WP_TYPE_LINK, NULL);
    g_assert_null (link);
    g_assert_cmpuint (wp_object_manager_get_n_objects (om), ==, 6);
  }
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/modules/si-standard-link/main",
      TestFixture, NULL,
      test_si_standard_link_setup,
      test_si_standard_link_main,
      test_si_standard_link_teardown);

  return g_test_run ();
}
  070701000001BD000081A400000000000000000000000165F863040000266F000000000000000000000000000000000000002800000000wireplumber-0.5.0/tests/script-tester.c   /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"
#include <spa/param/audio/format-utils.h>

#define DEFAULT_RATE		44100
#define DEFAULT_CHANNELS	2

typedef struct _ScriptRunnerFixture ScriptRunnerFixture;
static void load_component (ScriptRunnerFixture *f, const gchar *name,
    const gchar *type);

struct _WpScriptTester
{
  WpPlugin parent;
  struct pw_stream *stream;
  ScriptRunnerFixture *test_fixture;
};

enum {
  ACTION_CREATE_STREAM_NODE,
  N_SIGNALS
};

enum {
  PROP_0,
  PROP_TEST_FIXTURE,
  PROP_SUPPORTED_FEATURES,
};

static guint signals [N_SIGNALS] = { 0 };

/* plugin for lua test scripts to trigger stream node creation, after all the
 * device nodes are created and ready.
 */
G_DECLARE_FINAL_TYPE (WpScriptTester, wp_script_tester,
    WP, SCRIPT_TESTER, WpPlugin)
G_DEFINE_TYPE (WpScriptTester, wp_script_tester, WP_TYPE_PLUGIN)

struct _ScriptRunnerFixture {
  WpBaseTestFixture base;
  WpScriptTester *plugin;
};

static void
wp_script_tester_init (WpScriptTester *self)
{
}

static G_GNUC_UNUSED void
dummy_cb (WpObject *object, GAsyncResult *res, WpBaseTestFixture *f)
{
}

static void
wp_script_tester_restart_plugin (WpScriptTester *self, const gchar *name)
{
  ScriptRunnerFixture *f = self->test_fixture;
  g_autoptr (WpPlugin) plugin = wp_plugin_find (f->base.core, name);

  wp_object_deactivate (WP_OBJECT (plugin), WP_PLUGIN_FEATURE_ENABLED);
  wp_object_activate (WP_OBJECT (plugin), WP_PLUGIN_FEATURE_ENABLED,
    NULL, (GAsyncReadyCallback) dummy_cb, f);
}
  static void
wp_script_tester_create_stream (WpScriptTester *self, const gchar *stream_type,
    WpProperties *stream_props)
{
  ScriptRunnerFixture *f = self->test_fixture;
  WpProperties *props = NULL;
  const struct spa_pod *params [1];
  uint8_t buffer [1024];
  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buffer, sizeof (buffer));
  int direction;

  wp_info ("create stream_type(%s) with props(%p)", stream_type, stream_props);

  if (g_str_equal (stream_type, "playback"))
    direction = PW_DIRECTION_OUTPUT;
  else
    direction = PW_DIRECTION_INPUT;

  props = wp_properties_new (
      PW_KEY_MEDIA_TYPE, "Audio",
      PW_KEY_NODE_NAME, "stream-node",
      NULL);

  if (stream_props)
    wp_properties_add (props, stream_props);

  self->stream = pw_stream_new (
      wp_core_get_pw_core (f->base.client_core),
      "stream-node", wp_properties_to_pw_properties (props));

  params [0] = spa_format_audio_raw_build (&b, SPA_PARAM_EnumFormat,
      &SPA_AUDIO_INFO_RAW_INIT (
          .format = SPA_AUDIO_FORMAT_F32,
          .channels = DEFAULT_CHANNELS,
          .rate = DEFAULT_RATE));

  pw_stream_connect (self->stream,
      direction,
      PW_ID_ANY,
      PW_STREAM_FLAG_AUTOCONNECT |
      PW_STREAM_FLAG_MAP_BUFFERS,
      params, 1);
}

static void
wp_script_tester_set_property (GObject *object, guint property_id,
  const GValue *value, GParamSpec *pspec)
{
  WpScriptTester *self = WP_SCRIPT_TESTER (object);

  switch (property_id) {
  case PROP_TEST_FIXTURE:
    self->test_fixture = g_value_get_pointer (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_script_tester_get_property (GObject *object, guint property_id, GValue *value,
  GParamSpec *pspec)
{
  WpScriptTester *self = WP_SCRIPT_TESTER (object);

  switch (property_id) {
  case PROP_TEST_FIXTURE:
    g_value_set_pointer (value, self->test_fixture);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_script_tester_class_init (WpScriptTesterClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  object_class->get_property = wp_script_tester_get_property;
  object_class->set_property = wp_script_tester_set_property;

  g_object_class_install_property (object_class, PROP_TEST_FIXTURE,
      g_param_spec_pointer ("test-fixture", "test-fixture", "The Test Fixture",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  signals [ACTION_CREATE_STREAM_NODE] = g_signal_new_class_handler (
      "create-stream", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_script_tester_create_stream,
      NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_STRING, WP_TYPE_PROPERTIES);

  signals [ACTION_CREATE_STREAM_NODE] = g_signal_new_class_handler (
      "restart-plugin", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      (GCallback) wp_script_tester_restart_plugin,
      NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);

}

static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, ScriptRunnerFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_no_error (error);
  g_assert_true (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
load_component (ScriptRunnerFixture *f, const gchar *name, const gchar *type)
{
  g_autoptr (WpSpaJson) arguments = NULL;

  if (g_str_equal (name, "metadata.lua")) {
    arguments = wp_spa_json_new_object ("metadata.name", "s", "default",
        NULL);
  }

  wp_core_load_component (f->base.core, name, type, arguments, NULL, NULL,
      (GAsyncReadyCallback) on_plugin_loaded, f);
  g_main_loop_run (f->base.loop);
}

static void
script_run (ScriptRunnerFixture *f, gconstpointer argv)
{
  gchar **args = (gchar **) argv;
  const gchar *test_script = args [2];

  /* load the test script */
  load_component (f, (const gchar *) test_script, "script/lua");
}

static void
load_components (ScriptRunnerFixture *f, gconstpointer argv)
{
  g_autoptr (WpPlugin) plugin = NULL;
  g_autoptr (GError) error = NULL;
  g_autofree gchar *pluginname = NULL;
  gchar **args = (gchar **) argv;
  gchar *test_suite = args [1];
  /* TODO: we could do some more stuff here to provide the test script with an
     API to deal with the main loop and test asynchronous stuff, if necessary */

  load_component (f, "libwireplumber-module-lua-scripting", "module");

  load_component (f, "libwireplumber-module-settings", "module");
  load_component (f, "settings-instance", "built-in");

  if (g_str_equal (test_suite, "script-tests")) {

    load_component (f, "libwireplumber-module-standard-event-source", "module");

    load_component (f, "libwireplumber-module-si-audio-adapter", "module");
    load_component (f, "libwireplumber-module-si-standard-link", "module");
    load_component (f, "libwireplumber-module-si-audio-virtual", "module");

    load_component (f, "default-nodes/apply-default-node.lua", "script/lua");
    load_component (f, "default-nodes/state-default-nodes.lua", "script/lua");
    load_component (f, "default-nodes/find-best-default-node.lua", "script/lua");
    load_component (f, "default-nodes/rescan.lua", "script/lua");

    load_component (f, "metadata.lua", "script/lua");
    load_component (f, "libwireplumber-module-default-nodes-api", "module");

    load_component (f, "node/create-item.lua", "script/lua");

    load_component (f, "linking/find-best-target.lua", "script/lua");
    load_component (f, "linking/find-default-target.lua", "script/lua");
    load_component (f, "linking/find-defined-target.lua", "script/lua");
    load_component (f, "linking/find-filter-target.lua", "script/lua");
    load_component (f, "linking/get-filter-from-target.lua", "script/lua");
    load_component (f, "linking/link-target.lua", "script/lua");
    load_component (f, "linking/prepare-link.lua", "script/lua");
    load_component (f, "linking/rescan.lua", "script/lua");

    {
      g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&f->base.server);

      g_assert_nonnull (pw_context_load_module (f->base.server.context,
          "libpipewire-module-adapter", NULL, NULL));

      g_assert_nonnull (pw_context_load_module (f->base.server.context,
          "libpipewire-module-link-factory", NULL, NULL));

      g_assert_cmpint (pw_context_add_spa_lib (f->base.server.context,
          "audiotestsrc", "audiotestsrc/libspa-audiotestsrc"), == , 0);
    }
  }
}

static void
base_tests_setup (ScriptRunnerFixture *f, gconstpointer data)
{
  f->base.conf_file =
      g_strdup_printf ("%s/settings/wireplumber.conf", g_getenv ("G_TEST_SRCDIR"));

  wp_base_test_fixture_setup (&f->base, WP_BASE_TEST_FLAG_CLIENT_CORE);

  load_components (f, data);
}

static void
script_tests_setup (ScriptRunnerFixture *f, gconstpointer data)
{
  f->base.conf_file =
      g_strdup_printf ("%s/config/wireplumber.conf", g_getenv ("G_TEST_SRCDIR"));

  wp_base_test_fixture_setup (&f->base, WP_BASE_TEST_FLAG_CLIENT_CORE);

  load_components (f, data);

  f->plugin = g_object_new (wp_script_tester_get_type (),
      "name", "script-tester",
      "core", f->base.core,
      "test-fixture", f,
      NULL);

  wp_core_register_object (f->base.core, (WpPlugin *)f->plugin);
}

static void
base_tests_teardown (ScriptRunnerFixture *f, gconstpointer data)
{
  wp_base_test_fixture_teardown (&f->base);
}

static void
script_tests_teardown (ScriptRunnerFixture *f, gconstpointer data)
{
  wp_base_test_fixture_teardown (&f->base);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_assert_cmpint (argc, >= , 1);
  gchar **args = (gchar **) argv;
  gchar *test_suite = args [1];

  if (g_str_equal (test_suite, "script-tests"))
    g_test_add ("/lua/linking-tests", ScriptRunnerFixture, argv,
        script_tests_setup, script_run, script_tests_teardown);
  else
    g_test_add ("/lua/wprun/tests", ScriptRunnerFixture, argv,
        base_tests_setup, script_run, base_tests_teardown);

  return g_test_run ();
}
 070701000001BE000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002000000000wireplumber-0.5.0/tests/scripts   070701000001BF000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002700000000wireplumber-0.5.0/tests/scripts/config    070701000001C0000081A400000000000000000000000165F8630400000C81000000000000000000000000000000000000003800000000wireplumber-0.5.0/tests/scripts/config/wireplumber.conf   context.modules = [
    { name = libpipewire-module-protocol-native }
    { name = libpipewire-module-metadata }
    { name = libpipewire-module-spa-device-factory }
    { name = libpipewire-module-spa-node-factory }
    { name = libpipewire-module-client-node }
    { name = libpipewire-module-client-device }
    { name = libpipewire-module-adapter }
]

wireplumber.settings.schema = {
  ## Bluetooth
  bluetooth.use-persistent-storage = {
    description = "Whether to use persistent BT storage or not"
    type = "bool"
    default = true
  }
  bluetooth.autoswitch-to-headset-profile = {
    description = "Whether to autoswitch to BT headset profile or not"
    type = "bool"
    default = true
  }

  ## Device
  device.restore-profile = {
    description = "Whether to restore device profile or not"
    type = "bool"
    default = true
  }
  device.restore-routes = {
    description = "Whether to restore device routes or not"
    type = "bool"
    default = true
  }
  device.routes.default-sink-volume = {
    description = "The default volume for sink devices"
    type = "float"
    default = 0.064
    min = 0.0
    max = 1.0
  }
  device.routes.default-source-volume = {
    description = "The default volume for source devices"
    type = "float"
    default = 1.0
    min = 0.0
    max = 1.0
  }

  ## Linking
  linking.allow-moving-streams = {
    description = "Whether to allow metadata to move streams at runtime or not"
    type = "bool"
    default = true
  }
  linking.follow-default-target = {
    description = "Whether to allow streams follow the default device or not"
    type = "bool"
    default = true
  }

  ## Monitor
  monitor.camera-discovery-timeout = {
    description = "The camera discovery timeout in milliseconds"
    type = "int"
    default = 100
    min = 0
    max = 60000
  }

  ## Node
  node.features.audio.no-dsp = {
    description = "Whether to never convert audio to F32 format or not"
    type = "bool"
    default = false
  }
  node.features.audio.monitor-ports = {
    description = "Whether to enable monitor ports on audio nodes or not"
    type = "bool"
    default = true
  }
  node.features.audio.control-port = {
    description = "Whether to enable control ports on audio nodes or not"
    type = "bool"
    default = false
  }
  node.stream.restore-props = {
    description = "Whether to restore properties on stream nodes or not"
    type = "bool"
    default = true
  }
  node.stream.restore-target = {
    description = "Whether to restore target on stream nodes or not"
    type = "bool"
    default = true
  }
  node.stream.default-playback-volume = {
    description = "The default volume for playback nodes"
    type = "float"
    default = 1.0
    min = 0.0
    max = 1.0
  }
  node.stream.default-capture-volume = {
    description = "The default volume for capture nodes"
    type = "float"
    default = 1.0
    min = 0.0
    max = 1.0
  }
  node.filter.forward-format = {
    description = "Whether to forward format on filter nodes or not"
    type = "bool"
    default = false
  }
  node.restore-default-targets = {
    description = "Whether to restore default targets or not"
    type = "bool"
    default = true
  }
}
   070701000001C1000081A400000000000000000000000165F8630400000FAA000000000000000000000000000000000000002C00000000wireplumber-0.5.0/tests/scripts/meson.build   common_deps = [wplua_dep, pipewire_dep, wp_dep]
common_env = common_test_env
common_env.prepend('WIREPLUMBER_DATA_DIR', meson.current_source_dir())
common_env.set('G_TEST_SRCDIR', meson.current_source_dir())
common_env.set('G_TEST_BUILDDIR', meson.current_build_dir())

script_tester = executable('script-tester',
    '..'/'script-tester.c',
    dependencies: common_deps
)

test(
  'test-linking-non-default-device-node',
  script_tester,
  args: ['script-tests', '00-test-linking-non-default-device-node.lua'],
  env: common_env,
)

test(
  'test-linking-non-default-device-node-capture',
  script_tester,
  args: ['script-tests', '01-test-linking-non-default-device-node-capture.lua'],
  env: common_env,
)

test(
  'test-linking-default-device-node',
  script_tester,
  args: ['script-tests', '02-test-linking-default-device-node.lua'],
  env: common_env,
)

test(
  'test-linking-default-device-node-capture',
  script_tester,
  args: ['script-tests', '03-test-linking-default-device-node-capture.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-string',
  script_tester,
  args: ['script-tests', '04-test-linking-defined-device-node-target-object-string.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-int',
  script_tester,
  args: ['script-tests', '05-test-linking-defined-device-node-target-object-int.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-name',
  script_tester,
  args: ['script-tests', '06-test-linking-defined-device-node-target-object-name.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-string-capture',
  script_tester,
  args: ['script-tests', '07-test-linking-defined-device-node-target-object-string-capture.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-int-capture',
  script_tester,
  args: ['script-tests', '08-test-linking-defined-device-node-target-object-int-capture.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-name-capture',
  script_tester,
  args: ['script-tests', '09-test-linking-defined-device-node-target-object-name-capture.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-node-target',
  script_tester,
  args: ['script-tests', '10-test-linking-defined-device-node-node-target.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-node-target-capture',
  script_tester,
  args: ['script-tests', '11-test-linking-defined-device-node-node-target-capture.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-node-metadata',
  script_tester,
  args: ['script-tests', '12-test-linking-defined-device-node-target-node-metadata.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-node-metadata-capture',
  script_tester,
  args: ['script-tests', '13-test-linking-defined-device-node-target-node-metadata-capture.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-metadata',
  script_tester,
  args: ['script-tests', '14-test-linking-defined-device-node-target-object-metadata.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-metadata-capture',
  script_tester,
  args: ['script-tests', '15-test-linking-defined-device-node-target-object-metadata-capture.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-int-metadata',
  script_tester,
  args: ['script-tests', '16-test-linking-defined-device-node-target-object-int-metadata.lua'],
  env: common_env,
)

test(
  'test-linking-defined-device-node-target-object-int-metadata-capture',
  script_tester,
  args: ['script-tests', '17-test-linking-defined-device-node-target-object-int-metadata-capture.lua'],
  env: common_env,
)


test(
  '00-test-default-nodes-initial-metadata-update',
  script_tester,
  args: ['script-tests', '00-test-default-nodes-initial-metadata-update.lua'],
  env: common_env,
)
  070701000001C2000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002800000000wireplumber-0.5.0/tests/scripts/scripts   070701000001C3000081A400000000000000000000000165F8630400000BA9000000000000000000000000000000000000005A00000000wireplumber-0.5.0/tests/scripts/scripts/00-test-default-nodes-initial-metadata-update.lua -- check if default keys are restored correctly by the default-nodes hooks on
-- bootup.

-- First we create a bunch of devices and update the default.configured.* keys
-- in other words select the default device.
-- then reload the metadata plugin which recreates the default metadata.
-- default-node/* scripts are supposed to restore the previously selected
-- default device.

local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("audio-sink-device-node", "Audio/Sink")
tu.createDeviceNode ("audio-source-device-node", "Audio/Source")
tu.createDeviceNode ("video-source-device-node", "Video/Source")

-- create second pair of devices just to make the test case better
tu.createDeviceNode ("audio-sink-device-node-1", "Audio/Sink")
tu.createDeviceNode ("audio-source-device-node-1", "Audio/Source")
tu.createDeviceNode ("video-source-device-node-1", "Video/Source")

local device_match_count = 0

local expected_values_table = {
  ["default.configured.audio.sink"] = "audio-sink-device-node",
  ["default.configured.audio.source"] = "audio-source-device-node",
  ["default.configured.video.source"] = "video-source-device-node",
  ["audio-sink-device-node"] = "default.configured.audio.sink",
  ["audio-source-device-node"] = "default.configured.audio.source",
  ["video-source-device-node"] = "default.configured.video.source",
}

SimpleEventHook {
  name = "node/test-default-nodes",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "node-added" },
      Constraint { "node.name", "c", "audio-sink-device-node",
          "audio-source-device-node", "video-source-device-node" },
    },
  },
  execute = function (event)
    local node = event:get_subject ()
    local name = node.properties ["node.name"]
    local key = expected_values_table [name]

    device_match_count = device_match_count + 1

    tu.default_metadata:set (0, key, "Spa:String:JSON",
        Json.Object { ["name"] = name }:to_string ())

    if device_match_count == 3 then
      tu.restartPlugin ("script:metadata.lua")
      device_match_count = 0
    end
  end
}:register ()

metadata_added_hook = SimpleEventHook {
  name = "test-default-nodes/metadata-added",
  after = "default-nodes/metadata-added",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "metadata-added" },
      Constraint { "metadata.name", "=", "default" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    local om = source:call ("get-object-manager", "metadata")
    local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }

    for k, v in pairs (expected_values_table) do
      local obj = metadata:find (0, k)
      if obj then
        local json = Json.Raw (obj)
        if json:parse ().name == v then
          device_match_count = device_match_count + 1
        end
      end
    end

    if device_match_count == 3 then
      Script:finish_activation ()
    end

  end
}:register ()
   070701000001C4000081A400000000000000000000000165F8630400000775000000000000000000000000000000000000005400000000wireplumber-0.5.0/tests/scripts/scripts/00-test-linking-non-default-device-node.lua   -- Tests linking of streams and non default devices. These devices nodes are
-- neither defined nor default.

local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Sink")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]

    if tu.linkablesReady () and name ~= "stream-node" then
      tu.createStreamNode ("playback")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)
    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "nondefault-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
   070701000001C5000081A400000000000000000000000165F8630400000776000000000000000000000000000000000000005C00000000wireplumber-0.5.0/tests/scripts/scripts/01-test-linking-non-default-device-node-capture.lua   -- Tests linking of streams and non default devices. These devices nodes are
-- neither defined nor default.

local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Source")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    if tu.linkablesReady () and name ~= "stream-node" then
      tu.createStreamNode ("capture")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "nondefault-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
  070701000001C6000081A400000000000000000000000165F86304000007FD000000000000000000000000000000000000005000000000wireplumber-0.5.0/tests/scripts/scripts/02-test-linking-default-device-node.lua   -- Tests linking of streams and default devices. Two device nodes are setup,
-- among which only one is selected as default device node.

local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Sink")
tu.createDeviceNode ("default-device-node", "Audio/Sink")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]

    if tu.linkablesReady () and name ~= "stream-node" then
      tu.createStreamNode ("playback")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "default-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    assert (target == pu.findDefaultLinkable (si))

    Script:finish_activation ()
  end
}:register ()
   070701000001C7000081A400000000000000000000000165F8630400000800000000000000000000000000000000000000005800000000wireplumber-0.5.0/tests/scripts/scripts/03-test-linking-default-device-node-capture.lua   -- Tests linking of streams and default devices. Two device nodes are setup,
-- among which only one is selected as default device node.

local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Source")
tu.createDeviceNode ("default-device-node", "Audio/Source")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]

    if tu.linkablesReady () and name ~= "stream-node" then
      tu.createStreamNode ("capture")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "default-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    assert (target == pu.findDefaultLinkable (si))

    Script:finish_activation ()
  end
}:register ()
070701000001C8000081A400000000000000000000000165F86304000008B4000000000000000000000000000000000000006500000000wireplumber-0.5.0/tests/scripts/scripts/04-test-linking-defined-device-node-target-object-string.lua  -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device(target.object).

-- The target.object here is a string
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Sink")
tu.createDeviceNode ("default-device-node", "Audio/Sink")
tu.createDeviceNode ("defined-device-node", "Audio/Sink")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    if tu.linkablesReady () and name ~= "stream-node" then
      local props = {
        ["target.object"] = tu.lnkbls ["defined-device-node"].properties ["node.id"]
      }
      tu.createStreamNode ("playback", props)
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "defined-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
070701000001C9000081A400000000000000000000000165F86304000008BD000000000000000000000000000000000000006200000000wireplumber-0.5.0/tests/scripts/scripts/05-test-linking-defined-device-node-target-object-int.lua -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device(target.object).

-- The target.object here is a int
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Sink")
tu.createDeviceNode ("default-device-node", "Audio/Sink")
tu.createDeviceNode ("defined-device-node", "Audio/Sink")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    if tu.linkablesReady () and name ~= "stream-node" then
      local props = {
        ["target.object"] = tonumber (tu.lnkbls ["defined-device-node"].properties ["node.id"]),
      }
      tu.createStreamNode ("playback", props)
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "defined-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
   070701000001CA000081A400000000000000000000000165F86304000008B9000000000000000000000000000000000000006300000000wireplumber-0.5.0/tests/scripts/scripts/06-test-linking-defined-device-node-target-object-name.lua    -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device(target.object).

-- The target.object here is a node name
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Sink")
tu.createDeviceNode ("default-device-node", "Audio/Sink")
tu.createDeviceNode ("defined-device-node", "Audio/Sink")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    if tu.linkablesReady () and name ~= "stream-node" then
      local props = {
        ["target.object"] = tu.lnkbls ["defined-device-node"].properties ["node.name"]
      }
      tu.createStreamNode ("playback", props)
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "defined-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
   070701000001CB000081A400000000000000000000000165F86304000008B4000000000000000000000000000000000000006D00000000wireplumber-0.5.0/tests/scripts/scripts/07-test-linking-defined-device-node-target-object-string-capture.lua  -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device(target.object).

-- The target.object here is a node name
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Source")
tu.createDeviceNode ("default-device-node", "Audio/Source")
tu.createDeviceNode ("defined-device-node", "Audio/Source")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    if tu.linkablesReady () and name ~= "stream-node" then
      local props = {
        ["target.object"] = tu.lnkbls ["defined-device-node"].properties ["node.id"]
      }
      tu.createStreamNode ("capture", props)
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
    pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
      tostring (si_props ["node.name"]),
      tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "defined-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
070701000001CC000081A400000000000000000000000165F86304000008C5000000000000000000000000000000000000006A00000000wireplumber-0.5.0/tests/scripts/scripts/08-test-linking-defined-device-node-target-object-int-capture.lua -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device(target.object).

-- The target.object here is a string
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Source")
tu.createDeviceNode ("default-device-node", "Audio/Source")
tu.createDeviceNode ("defined-device-node", "Audio/Source")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    if tu.linkablesReady () and name ~= "stream-node" then
      local props = {
        ["target.object"] = tonumber (tu.lnkbls ["defined-device-node"].properties ["node.id"]),
      }
      tu.createStreamNode ("capture", props)
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "defined-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
   070701000001CD000081A400000000000000000000000165F86304000008BE000000000000000000000000000000000000006B00000000wireplumber-0.5.0/tests/scripts/scripts/09-test-linking-defined-device-node-target-object-name-capture.lua    -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device(target.object).

-- The target.object here is a node name
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Source")
tu.createDeviceNode ("default-device-node", "Audio/Source")
tu.createDeviceNode ("defined-device-node", "Audio/Source")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    if tu.linkablesReady () and name ~= "stream-node" then
      local props = {
        ["target.object"] = tu.lnkbls ["defined-device-node"].properties ["node.name"]
      }
      tu.createStreamNode ("capture", props)
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "defined-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
  070701000001CE000081A400000000000000000000000165F86304000008C1000000000000000000000000000000000000005C00000000wireplumber-0.5.0/tests/scripts/scripts/10-test-linking-defined-device-node-node-target.lua   -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device(using "node.target" property).

-- The "node.target" here is a string
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Sink")
tu.createDeviceNode ("default-device-node", "Audio/Sink")
tu.createDeviceNode ("defined-device-node", "Audio/Sink")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    if tu.linkablesReady () and name ~= "stream-node" then
      local props = {
        ["node.target"] = tu.lnkbls ["defined-device-node"].properties ["node.id"]
      }
      tu.createStreamNode ("playback", props)
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "defined-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
   070701000001CF000081A400000000000000000000000165F86304000008CC000000000000000000000000000000000000006400000000wireplumber-0.5.0/tests/scripts/scripts/11-test-linking-defined-device-node-node-target-capture.lua   -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device(using "node.target" property).

-- The "node.target" here is a string
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Source")
tu.createDeviceNode ("default-device-node", "Audio/Source")
tu.createDeviceNode ("defined-device-node", "Audio/Source")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    if tu.linkablesReady () and name ~= "stream-node" then
      local props = {
        ["node.target"] = tu.lnkbls ["defined-device-node"].properties ["node.id"]
      }
      tu.createStreamNode ("capture", props)
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
          pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
          tostring (si_props ["node.name"]),
          tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "defined-device-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
070701000001D0000081A400000000000000000000000165F8630400000BB0000000000000000000000000000000000000006500000000wireplumber-0.5.0/tests/scripts/scripts/12-test-linking-defined-device-node-target-node-metadata.lua  -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device("target.node").

-- The "target.node" here is defined in default metadata.
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Sink")
tu.createDeviceNode ("default-device-node", "Audio/Sink")
tu.createDeviceNode ("defined-device-node", "Audio/Sink")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]

    if tu.linkablesReady () and name ~= "stream-node" then
      -- all linkables created execept stream-node
      tu.createStreamNode ("playback")
    elseif tu.linkablesReady () and tu.lnkbls ["stream-node"] then
      -- when "stream-node" linkable is ready
      tu.setTargetInMetadata ("target.node", "defined-device-node")
    end
  end
}:register ()

-- hook to selet defined target
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    if tu.linkablesReady () then
      tu.setTargetInMetadata ("target.node", "defined-device-node")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
    pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
      tostring (si_props ["node.name"]),
      tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)
    if (target.properties ["node.name"] == "defined-device-node") then
      Script:finish_activation ()
    end
  end
}:register ()
070701000001D1000081A400000000000000000000000165F8630400000946000000000000000000000000000000000000006D00000000wireplumber-0.5.0/tests/scripts/scripts/13-test-linking-defined-device-node-target-node-metadata-capture.lua  -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device("target.node").

-- The "target.node" here is defined in default metadata.
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Source")
tu.createDeviceNode ("default-device-node", "Audio/Source")
tu.createDeviceNode ("defined-device-node", "Audio/Source")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]

    if tu.linkablesReady () and name ~= "stream-node" then
      -- all linkables created execept stream-node
      tu.createStreamNode ("capture")
    elseif tu.linkablesReady () and tu.lnkbls ["stream-node"] then
      -- when "stream-node" linkable is ready
      tu.setTargetInMetadata ("target.node", "defined-device-node")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)
    if (target.properties ["node.name"] == "defined-device-node") then
      Script:finish_activation ()
    end
  end
}:register ()
  070701000001D2000081A400000000000000000000000165F8630400000940000000000000000000000000000000000000006700000000wireplumber-0.5.0/tests/scripts/scripts/14-test-linking-defined-device-node-target-object-metadata.lua    -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device("target.object").

-- The "target.object" here is defined in default metadata.
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Sink")
tu.createDeviceNode ("default-device-node", "Audio/Sink")
tu.createDeviceNode ("defined-device-node", "Audio/Sink")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]

    if tu.linkablesReady () and name ~= "stream-node" then
      -- all linkables created execept stream-node
      tu.createStreamNode ("playback")
    elseif tu.linkablesReady () and tu.lnkbls ["stream-node"] then
      -- when "stream-node" linkable is ready
      tu.setTargetInMetadata ("target.object", "defined-device-node")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
    pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
      tostring (si_props ["node.name"]),
      tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    if (target.properties ["node.name"] == "defined-device-node") then
      Script:finish_activation ()
    end
  end
}:register ()
070701000001D3000081A400000000000000000000000165F863040000094D000000000000000000000000000000000000006F00000000wireplumber-0.5.0/tests/scripts/scripts/15-test-linking-defined-device-node-target-object-metadata-capture.lua    -- Tests linking of streams and defined devices. Three device nodes are created,
-- among which only one is selected as the defined device("target.object").

-- The "target.object" here is defined in default metadata.
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Source")
tu.createDeviceNode ("default-device-node", "Audio/Source")
tu.createDeviceNode ("defined-device-node", "Audio/Source")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]

    if tu.linkablesReady () and name ~= "stream-node" then
      -- all linkables created execept stream-node
      tu.createStreamNode ("capture")
    elseif tu.linkablesReady () and tu.lnkbls ["stream-node"] then
      -- when "stream-node" linkable is ready
      tu.setTargetInMetadata ("target.object", "defined-device-node")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    if (target.properties ["node.name"] == "defined-device-node") then
      Script:finish_activation ()
    end
  end
}:register ()
   070701000001D4000081A400000000000000000000000165F8630400000A33000000000000000000000000000000000000006B00000000wireplumber-0.5.0/tests/scripts/scripts/16-test-linking-defined-device-node-target-object-int-metadata.lua    -- Tests linking of streams and defined devices. Here in this test case we
-- update defined device in both the node properties and in metadata.

-- The device in metadata takes priority over the one in node properties.
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Sink")
tu.createDeviceNode ("default-device-node", "Audio/Sink")
tu.createDeviceNode ("defined-device-node-in-props", "Audio/Sink")
tu.createDeviceNode ("defined-device-node-in-metadata", "Audio/Sink")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]

    if tu.linkablesReady () and name ~= "stream-node" then
      -- all linkables created execept stream-node
      local props = {
        ["target.object"] = tu.lnkbls ["defined-device-node-in-props"].properties ["node.id"]
      }
      tu.createStreamNode ("playback", props)
    elseif tu.linkablesReady () and tu.lnkbls ["stream-node"] then
      -- when "stream-node" linkable is ready
      tu.setTargetInMetadata ("target.object", "defined-device-node-in-metadata")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)
    if (target.properties ["node.name"] == "defined-device-node-in-metadata") then
      Script:finish_activation ()
    end
  end
}:register ()
 070701000001D5000081A400000000000000000000000165F8630400000A30000000000000000000000000000000000000007300000000wireplumber-0.5.0/tests/scripts/scripts/17-test-linking-defined-device-node-target-object-int-metadata-capture.lua    -- Tests linking of streams and defined devices. Here in this test case we
-- update defined device in both the node properties and in metadata.

-- The device in metadata takes priority over the one in node properties.
local pu = require ("linking-utils")
local tu = require ("test-utils")

Script.async_activation = true

tu.createDeviceNode ("nondefault-device-node", "Audio/Source")
tu.createDeviceNode ("default-device-node", "Audio/Source")
tu.createDeviceNode ("defined-device-node-in-props", "Audio/Source")
tu.createDeviceNode ("defined-device-node-in-metadata", "Audio/Source")

-- hook to create stream node, stream is created after the device nodes are
-- ready
SimpleEventHook {
  name = "linkable-added@test-linking",
  after = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]

    if tu.linkablesReady () and name ~= "stream-node" then
      -- all linkables created execept stream-node
      local props = {
        ["target.object"] = tu.lnkbls ["defined-device-node-in-props"].properties ["node.id"]
      }
      tu.createStreamNode ("capture", props)
    elseif tu.linkablesReady () and tu.lnkbls ["stream-node"] then
      -- when "stream-node" linkable is ready
      tu.setTargetInMetadata ("target.object", "defined-device-node-in-metadata")
    end
  end
}:register ()

SimpleEventHook {
  name = "linking/test-linking",
  after = "linking/link-target",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "select-target" },
    },
  },
  execute = function (event)
    local source, om, si, si_props, si_flags, target =
        pu:unwrap_select_target_event (event)

    if not target then
      return
    end

    Log.info (si, string.format ("handling item: %s (%s) si id(%s)",
        tostring (si_props ["node.name"]),
        tostring (si_props ["node.id"]), si.id))

    local link = pu.lookupLink (si.id, si_flags.peer_id)
    assert (link ~= nil)
    assert (si_props ["node.name"] == "stream-node")
    assert (target.properties ["node.name"] == "defined-device-node-in-metadata")
    assert ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0)

    Script:finish_activation ()
  end
}:register ()
070701000001D6000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002C00000000wireplumber-0.5.0/tests/scripts/scripts/lib   070701000001D7000081A400000000000000000000000165F8630400000CD1000000000000000000000000000000000000003B00000000wireplumber-0.5.0/tests/scripts/scripts/lib/test-utils.lua    -- WirePlumber

-- Copyright © 2022 Collabora Ltd.
--    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>

-- SPDX-License-Identifier: MIT

-- Script is a Lua Module of common Lua test utility functions
local cu = require ("common-utils")

local u = {}

u.nodes = {}
u.lnkbls = {}
u.lnkbl_count = 0

function u.createDeviceNode (name, media_class)
  local properties = {}
  properties ["node.name"] = name
  properties ["media.class"] = media_class
  if media_class == "Audio/Sink" then
    properties ["factory.name"] = "support.null-audio-sink"
  else
    properties ["factory.name"] = "audiotestsrc"
  end

  node = Node ("adapter", properties)
  node:activate (Features.ALL, function (n)
    local name = n.properties ["node.name"]
    Log.info (n, "created and activated device node: " .. name)
    u.nodes [name] = n

    -- wait for linkables to be created.
    u.lnkbls [name] = nil
    u.lnkbl_count = u.lnkbl_count + 1
  end)
  return node
end

-- hook to keep track of the linkables created.
SimpleEventHook {
  name = "linkable-added@test-utils-linking",
  interests = {
    -- on linkable added or removed, where linkable is adapter or plain node
    EventInterest {
      Constraint { "event.type", "=", "session-item-added" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
    },
  },
  execute = function (event)
    local lnkbl = event:get_subject ()
    local name = lnkbl.properties ["node.name"]
    local mc = lnkbl.properties ["media.class"]

    Log.info (lnkbl, "activated linkable: " .. name .. " with " .. mc)

    u.lnkbls [name] = lnkbl

    -- select "default-device-node" as default device.
    if name == "default-device-node" then
      local key = nil

      if mc == "Audio/Sink" then
        key = "default.configured.audio.sink"
      elseif mc == "Audio/Source" then
        key = "default.configured.audio.source"
      end

      -- configure default device.
      u.default_metadata:set(0, key, "Spa:String:JSON", Json.Object { ["name"] = name }:get_data())
    end
  end
}:register ()

u.script_tester_plugin = Plugin.find ("script-tester")

function u.createStreamNode (stream_type, props)
  u.script_tester_plugin:call ("create-stream", stream_type, props)

  u.lnkbls ["stream-node"] = nil
  u.lnkbl_count = u.lnkbl_count + 1
end

function u.restartPlugin (name)
  u.script_tester_plugin:call ("restart-plugin", name)
end

u.default_metadata = cu.get_object_manager ("metadata"):lookup {
  Constraint { "metadata.name", "=", "default" },
}
assert (u.default_metadata ~= nil)

u.settings_metadata = cu.get_object_manager ("metadata"):lookup {
  Constraint { "metadata.name", "=", "sm-settings" },
}
assert (u.settings_metadata ~= nil)

-- update the defined target for stream session item in metadata.
function u.setTargetInMetadata (prop, target_node_name)
  u.default_metadata:set (u.lnkbls ["stream-node"].properties ["node.id"], prop,
      "Spa:Id", u.lnkbls [target_node_name].properties ["node.id"])
end

function u.linkablesReady ()
  local count = 0
  for k, v in pairs (u.lnkbls) do
    if v then
      count = count + 1
    end
  end
  if count == u.lnkbl_count then
    return true
  end

  return false
end

return u
   070701000001D8000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001B00000000wireplumber-0.5.0/tests/wp    070701000001D9000081A400000000000000000000000165F8630400001C6A000000000000000000000000000000000000002E00000000wireplumber-0.5.0/tests/wp/component-loader.c /* WirePlumber
 *
 * Copyright © 2023 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */
#include "../common/base-test-fixture.h"

struct _WpTestPlugin
{
  WpPlugin parent;
  gboolean enabled;
};

#define WP_TYPE_TEST_PLUGIN (wp_test_plugin_get_type ())
G_DECLARE_FINAL_TYPE (WpTestPlugin, wp_test_plugin, WP, TEST_PLUGIN, WpPlugin)
G_DEFINE_TYPE (WpTestPlugin, wp_test_plugin, WP_TYPE_PLUGIN)

static void
wp_test_plugin_init (WpTestPlugin * self)
{
}

static void
wp_test_plugin_enable (WpPlugin * self, WpTransition * transition)
{
  WP_TEST_PLUGIN (self)->enabled = TRUE;

  if (g_str_equal (wp_plugin_get_name (self), "fail")) {
    wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
        WP_LIBRARY_ERROR_INVALID_ARGUMENT, "fail"));
  } else {
    wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
  }
}

static void
wp_test_plugin_class_init (WpTestPluginClass * klass)
{
  WpPluginClass *pclass = (WpPluginClass *) klass;
  pclass->enable = wp_test_plugin_enable;
}


struct _WpTestCompLoader
{
  GObject parent;
  GPtrArray *history;
};

static void wp_test_comp_loader_iface_init (WpComponentLoaderInterface * iface);

#define WP_TYPE_TEST_COMP_LOADER (wp_test_comp_loader_get_type ())
G_DECLARE_FINAL_TYPE (WpTestCompLoader, wp_test_comp_loader,
                      WP, TEST_COMP_LOADER, GObject)
G_DEFINE_TYPE_WITH_CODE (WpTestCompLoader, wp_test_comp_loader,
                         G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (
                         WP_TYPE_COMPONENT_LOADER,
                         wp_test_comp_loader_iface_init))

static void
wp_test_comp_loader_init (WpTestCompLoader * self)
{
  self->history = g_ptr_array_new_with_free_func (g_free);
}

static void
wp_test_comp_loader_finalize (GObject * self)
{
  g_clear_pointer (&WP_TEST_COMP_LOADER (self)->history, g_ptr_array_unref);
  G_OBJECT_CLASS (wp_test_comp_loader_parent_class)->finalize (self);
}

static void
wp_test_comp_loader_class_init (WpTestCompLoaderClass * klass)
{
  GObjectClass *oclass = (GObjectClass *) klass;
  oclass->finalize = wp_test_comp_loader_finalize;
}

static gboolean
wp_test_comp_loader_supports_type (WpComponentLoader * cl, const gchar * type)
{
  return g_str_equal (type, "test");
}

static void
wp_test_comp_loader_load (WpComponentLoader * self, WpCore * core,
    const gchar * component, const gchar * type, WpSpaJson * args,
    GCancellable * cancellable, GAsyncReadyCallback callback, gpointer data)
{
  g_autoptr (GTask) task = g_task_new (self, cancellable, callback, data);
  GObject *plugin = g_object_new (WP_TYPE_TEST_PLUGIN,
      "name", component,
      "core", core,
      NULL);
  g_ptr_array_add (WP_TEST_COMP_LOADER (self)->history, g_strdup (component));
  g_task_return_pointer (task, plugin, g_object_unref);
}

static GObject *
wp_test_comp_loader_load_finish (WpComponentLoader * self,
    GAsyncResult * res, GError ** error)
{
  return g_task_propagate_pointer (G_TASK (res), error);
}

static void
wp_test_comp_loader_iface_init (WpComponentLoaderInterface * iface)
{
  iface->supports_type = wp_test_comp_loader_supports_type;
  iface->load = wp_test_comp_loader_load;
  iface->load_finish = wp_test_comp_loader_load_finish;
}


typedef struct {
  WpBaseTestFixture base;
  WpTestCompLoader *loader;
} TestFixture;

static void
test_setup (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&self->base, 0);
  self->loader = g_object_new (WP_TYPE_TEST_COMP_LOADER, NULL);
  wp_core_register_object (self->base.core, self->loader);
}

static void
test_teardown (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_teardown (&self->base);
}

static void
on_component_loaded (WpCore * core, GAsyncResult * res, TestFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_no_error (error);
  g_assert_true (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
test_load (TestFixture *f, gconstpointer data)
{
  wp_core_load_component (f->base.core, "name123", "test", NULL,
      "feature.name123", NULL, (GAsyncReadyCallback) on_component_loaded, f);
  g_main_loop_run (f->base.loop);

  g_autoptr (WpPlugin) plugin = wp_plugin_find (f->base.core, "name123");
  g_assert_nonnull (plugin);
  g_assert_true (WP_IS_TEST_PLUGIN (plugin));
  g_assert_true (WP_TEST_PLUGIN (plugin)->enabled);
  g_assert_true (wp_core_test_feature (f->base.core, "feature.name123"));
}

static void
on_component_failed (WpCore * core, GAsyncResult * res, TestFixture *f)
{
  gboolean loaded;
  GError *error = NULL;

  loaded = wp_core_load_component_finish (core, res, &error);
  g_assert_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT);
  g_assert_false (loaded);

  g_main_loop_quit (f->base.loop);
}

static void
test_load_failure (TestFixture *f, gconstpointer data)
{
  wp_core_load_component (f->base.core, "fail", "test", NULL,
      "feature.fail", NULL, (GAsyncReadyCallback) on_component_failed, f);
  g_main_loop_run (f->base.loop);

  g_assert_cmpuint (f->loader->history->len, ==, 1);
  g_assert_cmpstr (f->loader->history->pdata[0], ==, "fail");

  g_autoptr (WpPlugin) plugin = wp_plugin_find (f->base.core, "fail");
  g_assert_null (plugin);
  g_assert_false (wp_core_test_feature (f->base.core, "feature.fail"));
}

static void
test_dependencies_setup (TestFixture *f, gconstpointer data)
{
  f->base.conf_file =
    g_strdup_printf ("%s/component-loader.conf", g_getenv ("G_TEST_SRCDIR"));
  test_setup (f, data);
}

static void
test_dependencies (TestFixture *f, gconstpointer data)
{
  wp_core_load_component (f->base.core, "test", "profile", NULL,
      NULL, NULL, (GAsyncReadyCallback) on_component_loaded, f);
  g_main_loop_run (f->base.loop);

  // NULL-terminate the array
  g_ptr_array_add (f->loader->history, NULL);

  /* verify the order of loading the plugins was as expected */
  const gchar *expected[] = {
    "five", "one", "six", "two", "three", "four", "seven", NULL };
  g_assert_cmpstrv (f->loader->history->pdata, expected);

  g_assert_true (wp_core_test_feature (f->base.core, "support.one"));
  g_assert_true (wp_core_test_feature (f->base.core, "support.two"));
  g_assert_true (wp_core_test_feature (f->base.core, "support.three"));
  g_assert_true (wp_core_test_feature (f->base.core, "support.four"));
  g_assert_true (wp_core_test_feature (f->base.core, "virtual.four"));
  g_assert_true (wp_core_test_feature (f->base.core, "support.five"));
  g_assert_true (wp_core_test_feature (f->base.core, "support.six"));
  g_assert_false (wp_core_test_feature (f->base.core, "support.seven"));
  g_assert_false (wp_core_test_feature (f->base.core, "support.eight"));
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/comploader/load", TestFixture, NULL,
      test_setup, test_load, test_teardown);
  g_test_add ("/wp/comploader/load_failure", TestFixture, NULL,
      test_setup, test_load_failure, test_teardown);
  g_test_add ("/wp/comploader/dependencies", TestFixture, NULL,
      test_dependencies_setup, test_dependencies, test_teardown);

  return g_test_run ();
}
  070701000001DA000081A400000000000000000000000165F86304000006A4000000000000000000000000000000000000003100000000wireplumber-0.5.0/tests/wp/component-loader.conf  context.modules = [
  { name = libpipewire-module-protocol-native }
]

wireplumber.profiles = {
  test = {
    virtual.four = required
  }
}

wireplumber.components = [
  # expected load order:
  # five, one, six, two, three, four, seven
  # eight is not loaded - optional feature
  {
    name = one
    type = test
    provides = support.one
  }
  {
    name = two
    type = test
    provides = support.two
    requires = [ support.one ]
  }
  {
    type = virtual
    provides = virtual.four
    requires = [ INVALID ]
  }
  {
    name = three
    type = test
    provides = INVALID
    wants = [ support.two ]
  }
  {
    name = four
    type = test
    provides = support.four
    requires = [ support.five ]
    wants = [ support.three ]
  }
  {
    name = five
    type = test
    provides = support.five
  }
  {
    name = six
    type = test
    provides = support.six
    requires = [ support.one ]
  }
  {
    name = seven
    type = test
    requires = [ support.five ]
  }
  {
    name = eight
    type = test
    provides = support.eight
    requires = [ support.four ]
  }
]

wireplumber.components.rules = [
  {
    matches = [
      {
        name = two
      }
    ]
    actions = {
      merge = {
        # final array should be [ support.one, support.six ]
        # if this fails, support.six will not be loaded
        requires = [ support.six ]
      }
    }
  }

  {
    matches = [
      {
        name = three
      }
    ]
    actions = {
      merge = {
        provides = support.three
      }
    }
  }

  {
    matches = [
      {
        provides = virtual.four
      }
    ]
    actions = {
      override = {
        requires = [ support.four ]
      }
    }
  }
]
070701000001DB000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002000000000wireplumber-0.5.0/tests/wp/conf   070701000001DC000081A400000000000000000000000165F863040000333D000000000000000000000000000000000000002200000000wireplumber-0.5.0/tests/wp/conf.c /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"

typedef struct {
  WpConf *conf;
} TestConfFixture;

static void
test_conf_setup (TestConfFixture *self, gconstpointer user_data)
{
  g_autoptr (GError) error = NULL;
  g_autofree gchar *file =
      g_strdup_printf ("%s/conf/wireplumber.conf", g_getenv ("G_TEST_SRCDIR"));
  self->conf = wp_conf_new_open (file, NULL, &error);
  g_assert_no_error (error);
  g_assert_nonnull (self->conf);
}

static void
test_conf_teardown (TestConfFixture *self, gconstpointer user_data)
{
  g_clear_object (&self->conf);
}

static void
test_conf_basic (TestConfFixture *f, gconstpointer data)
{
  g_assert_nonnull (f->conf);

  /* Boolean Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section.array.boolean");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    gboolean v1 = FALSE, v2 = TRUE;
    g_assert_true (wp_spa_json_parse_array (s, "b", &v1, "b", &v2, NULL));
    g_assert_true (v1);
    g_assert_false (v2);
  }

  /* Int Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section.array.int");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    gint v1 = 0, v2 = 0, v3 = 0;
    g_assert_true (wp_spa_json_parse_array (s, "i", &v1, "i", &v2, "i", &v3,
        NULL));
    g_assert_cmpint (v1, ==, 1);
    g_assert_cmpint (v2, ==, 2);
    g_assert_cmpint (v3, ==, 3);
  }

  /* Float Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section.array.float");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    float v1 = 0.0, v2 = 0.0, v3 = 0.0;
    g_assert_true (wp_spa_json_parse_array (s, "f", &v1, "f", &v2, "f", &v3,
        NULL));
    g_assert_cmpfloat_with_epsilon (v1, 1.11, 0.001);
    g_assert_cmpfloat_with_epsilon (v2, 2.22, 0.001);
    g_assert_cmpfloat_with_epsilon (v3, 3.33, 0.001);
  }

  /* String Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section.array.string");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    g_autofree gchar *v1 = NULL, *v2 = NULL;
    g_assert_true (wp_spa_json_parse_array (s, "s", &v1, "s", &v2, NULL));
    g_assert_nonnull (v1);
    g_assert_nonnull (v2);
    g_assert_cmpstr (v1, ==, "foo");
    g_assert_cmpstr (v2, ==, "bar");
  }

  /* Array Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section.array.array");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    g_autoptr (WpSpaJson) v1 = NULL;
    g_autoptr (WpSpaJson) v2 = NULL;
    g_assert_true (wp_spa_json_parse_array (s, "J", &v1, "J", &v2, NULL));
    g_assert_nonnull (v1);
    g_assert_nonnull (v2);
    g_assert_true (wp_spa_json_is_array (v1));
    g_assert_true (wp_spa_json_is_array (v2));
    gboolean v3 = FALSE, v4 = TRUE;
    g_assert_true (wp_spa_json_parse_array (v1, "b", &v3, NULL));
    g_assert_true (v3);
    g_assert_true (wp_spa_json_parse_array (v2, "b", &v4, NULL));
    g_assert_false (v4);
  }

  /* Object Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section.array.object");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    g_autoptr (WpSpaJson) v1 = NULL;
    g_autoptr (WpSpaJson) v2 = NULL;
    g_assert_true (wp_spa_json_parse_array (s, "J", &v1, "J", &v2, NULL));
    g_assert_nonnull (v1);
    g_assert_nonnull (v2);
    g_assert_true (wp_spa_json_is_object (v1));
    g_assert_true (wp_spa_json_is_object (v2));
    g_autofree gchar *v3 = NULL;
    gint v4 = 0;
    g_assert_true (wp_spa_json_object_get (v1, "key1", "s", &v3, NULL));
    g_assert_cmpstr (v3, ==, "foo");
    g_assert_true (wp_spa_json_object_get (v2, "key2", "i", &v4, NULL));
    g_assert_cmpint (v4, ==, 4);
  }

  /* Object */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section.object");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_object (s));
    gboolean v1 = FALSE;
    gint v2 = 0;
    float v3 = 0.0;
    g_autofree gchar *v4 = NULL;
    g_autoptr (WpSpaJson) v5 = NULL;
    g_autoptr (WpSpaJson) v6 = NULL;
    g_assert_true (wp_spa_json_object_get (s,
        "key.boolean", "b", &v1,
        "key.int", "i", &v2,
        "key.float", "f", &v3,
        "key.string", "s", &v4,
        "key.array", "J", &v5,
        "key.object", "J", &v6,
        NULL));
    g_assert_true (v1);
    g_assert_cmpint (v2, ==, -1);
    g_assert_cmpfloat_with_epsilon (v3, 3.14, 0.001);
    g_assert_cmpstr (v4, ==, "wireplumber");
    g_assert_true (wp_spa_json_is_array (v5));
    g_autofree gchar *v7 = NULL, *v8 = NULL;
    g_assert_true (wp_spa_json_parse_array (v5, "s", &v7, "s", &v8, NULL));
    g_assert_cmpstr (v7, ==, "an");
    g_assert_cmpstr (v8, ==, "array");
    g_assert_true (wp_spa_json_is_object (v6));
    gboolean v9 = TRUE;
    g_assert_true (wp_spa_json_object_get (v6,
        "key.nested.boolean", "b", &v9,
        NULL));
    g_assert_false (v9);
  }
}

static void
test_conf_merge (TestConfFixture *f, gconstpointer data)
{
  g_assert_nonnull (f->conf);

  /* Boolean Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section-merged.array.boolean");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    gboolean v1 = TRUE, v2 = FALSE;
    g_assert_true (wp_spa_json_parse_array (s, "b", &v1, "b", &v2, NULL));
    g_assert_false (v1);
    g_assert_true (v2);
  }

  /* Int Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section-merged.array.int");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    gint v1 = 0, v2 = 0;
    g_assert_true (wp_spa_json_parse_array (s, "i", &v1, "i", &v2, NULL));
    g_assert_cmpint (v1, ==, 4);
    g_assert_cmpint (v2, ==, 5);
  }

  /* Float Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section-merged.array.float");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    float v1 = 0.0, v2 = 0.0;
    g_assert_true (wp_spa_json_parse_array (s, "f", &v1, "f", &v2, NULL));
    g_assert_cmpfloat_with_epsilon (v1, 4.44, 0.001);
    g_assert_cmpfloat_with_epsilon (v2, 5.55, 0.001);
  }

  /* String Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section-merged.array.string");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    g_autofree gchar *v1 = NULL, *v2 = NULL;
    g_assert_true (wp_spa_json_parse_array (s, "s", &v1, "s", &v2, NULL));
    g_assert_nonnull (v1);
    g_assert_nonnull (v2);
    g_assert_cmpstr (v1, ==, "first");
    g_assert_cmpstr (v2, ==, "second");
  }

  /* Array Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section-merged.array.array");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    g_autoptr (WpSpaJson) v1 = NULL;
    g_autoptr (WpSpaJson) v2 = NULL;
    g_assert_true (wp_spa_json_parse_array (s, "J", &v1, "J", &v2, NULL));
    g_assert_nonnull (v1);
    g_assert_nonnull (v2);
    g_assert_true (wp_spa_json_is_array (v1));
    g_assert_true (wp_spa_json_is_array (v2));
    gboolean v3 = FALSE, v4 = TRUE;
    g_assert_true (wp_spa_json_parse_array (v1, "b", &v3, NULL));
    g_assert_true (v3);
    g_assert_true (wp_spa_json_parse_array (v2, "b", &v4, NULL));
    g_assert_false (v4);
  }

  /* Object Array */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section-merged.array.object");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_array (s));
    g_autoptr (WpSpaJson) v1 = NULL;
    g_autoptr (WpSpaJson) v2 = NULL;
    g_assert_true (wp_spa_json_parse_array (s, "J", &v1, "J", &v2, NULL));
    g_assert_nonnull (v1);
    g_assert_nonnull (v2);
    g_assert_true (wp_spa_json_is_object (v1));
    g_assert_true (wp_spa_json_is_object (v2));
    g_autofree gchar *v3 = NULL;
    gint v4 = 0;
    g_assert_true (wp_spa_json_object_get (v1, "key1", "s", &v3, NULL));
    g_assert_cmpstr (v3, ==, "foo");
    g_assert_true (wp_spa_json_object_get (v2, "key2", "i", &v4, NULL));
    g_assert_cmpint (v4, ==, 4);
  }

  /* Object */
  {
    g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
        "wireplumber.section-merged.object");
    g_assert_nonnull (s);
    g_assert_true (wp_spa_json_is_object (s));
    gboolean v1 = FALSE;
    gint v2 = 0;
    float v3 = 0.0;
    g_autofree gchar *v4 = NULL;
    g_autoptr (WpSpaJson) v5 = NULL;
    g_autoptr (WpSpaJson) v6 = NULL;
    g_assert_true (wp_spa_json_object_get (s,
        "key.boolean", "b", &v1,
        "key.int", "i", &v2,
        "key.float", "f", &v3,
        "key.string", "s", &v4,
        "key.array", "J", &v5,
        "key.object", "J", &v6,
        NULL));
    g_assert_false (v1);
    g_assert_cmpint (v2, ==, 6);
    g_assert_cmpfloat_with_epsilon (v3, 6.66, 0.001);
    g_assert_cmpstr (v4, ==, "merged");
    g_assert_true (wp_spa_json_is_array (v5));
    g_autofree gchar *v7 = NULL, *v8 = NULL;
    g_assert_true (wp_spa_json_parse_array (v5, "s", &v7, "s", &v8, NULL));
    g_assert_cmpstr (v7, ==, "an");
    g_assert_cmpstr (v8, ==, "array");
    g_assert_true (wp_spa_json_is_object (v6));
    gboolean v9 = TRUE;
    g_assert_true (wp_spa_json_object_get (v6,
        "key.nested.boolean", "b", &v9,
        NULL));
    g_assert_false (v9);
  }
}

static void
test_conf_merge_nested (TestConfFixture *f, gconstpointer data)
{
  g_assert_nonnull (f->conf);

  g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
      "wireplumber.section-nested-merged");
  g_assert_nonnull (s);
  g_assert_true (wp_spa_json_is_object (s));

  /* Make sure both keys exist in the nested object  */
  {
    g_autoptr (WpSpaJson) v1 = NULL;
    g_assert_true (wp_spa_json_object_get (s, "nested-object", "J", &v1, NULL));
    g_assert_nonnull (v1);
    g_assert_true (wp_spa_json_is_object (v1));
    gboolean v2 = FALSE;
    g_assert_true (wp_spa_json_object_get (v1, "key1", "b", &v2, NULL));
    gint v3 = 0;
    g_assert_true (wp_spa_json_object_get (v1, "key2", "i", &v3, NULL));
    g_assert_cmpint (v3, ==, 3);
  }

  /* Make sure array has all its elements */
  {
    g_autoptr (WpSpaJson) v1 = NULL;
    g_assert_true (wp_spa_json_object_get (s, "nested-array", "J", &v1, NULL));
    g_assert_nonnull (v1);
    g_assert_true (wp_spa_json_is_array (v1));
    gint v2 = 0, v3 = 0, v4 = 0, v5 = 0;
    g_assert_true (wp_spa_json_parse_array (v1,
        "i", &v2, "i", &v3, "i", &v4, "i", &v5, NULL));
    g_assert_cmpint (v2, ==, 1);
    g_assert_cmpint (v3, ==, 2);
    g_assert_cmpint (v4, ==, 3);
    g_assert_cmpint (v5, ==, 4);
  }
}

static void
test_conf_override (TestConfFixture *f, gconstpointer data)
{
  g_assert_nonnull (f->conf);

  g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
      "wireplumber.section-override");
  g_assert_nonnull (s);
  g_assert_true (wp_spa_json_is_object (s));

  /* Make sure key1 does not exist because it was overridden */
  gboolean v1 = FALSE;
  g_assert_false (wp_spa_json_object_get (s, "key1", "b", &v1, NULL));

  /* Make sure key2 exists */
  gint v2 = 0;
  g_assert_true (wp_spa_json_object_get (s, "key2", "i", &v2, NULL));
  g_assert_cmpint (v2, ==, 5);
}

static void
test_conf_override_nested (TestConfFixture *f, gconstpointer data)
{
  g_assert_nonnull (f->conf);

  g_autoptr (WpSpaJson) s = wp_conf_get_section (f->conf,
      "wireplumber.section-nested-override");
  g_assert_nonnull (s);
  g_assert_true (wp_spa_json_is_object (s));

  g_autoptr (WpSpaJson) v1 = NULL;
  g_assert_true (wp_spa_json_object_get (s, "nested-object", "J", &v1, NULL));
  g_assert_nonnull (v1);
  g_assert_true (wp_spa_json_is_object (v1));

  /* Make sure key1 does not exist because it was overridden */
  gboolean v2 = FALSE;
  g_assert_false (wp_spa_json_object_get (v1, "key1", "b", &v2, NULL));

  /* Make sure key2 exists */
  gint v3 = 0;
  g_assert_true (wp_spa_json_object_get (v1, "key2", "i", &v3, NULL));
  g_assert_cmpint (v3, ==, 3);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/conf/basic", TestConfFixture, NULL,
      test_conf_setup, test_conf_basic, test_conf_teardown);
  g_test_add ("/wp/conf/merge", TestConfFixture, NULL,
      test_conf_setup, test_conf_merge, test_conf_teardown);
  g_test_add ("/wp/conf/merge_nested", TestConfFixture, NULL,
      test_conf_setup, test_conf_merge_nested, test_conf_teardown);
  g_test_add ("/wp/conf/override", TestConfFixture, NULL,
      test_conf_setup, test_conf_override, test_conf_teardown);
  g_test_add ("/wp/conf/override_nested", TestConfFixture, NULL,
      test_conf_setup, test_conf_override_nested, test_conf_teardown);

  return g_test_run ();
}
   070701000001DD000081A400000000000000000000000165F86304000004D8000000000000000000000000000000000000003100000000wireplumber-0.5.0/tests/wp/conf/wireplumber.conf  context.modules = [
  { name = libpipewire-module-protocol-native }
]

wireplumber.section.array.boolean = [ true, false ]

wireplumber.section.array.int = [ 1, 2, 3 ]

wireplumber.section.array.float = [ 1.11, 2.22, 3.33 ]

wireplumber.section.array.string = [ "foo", "bar" ]

wireplumber.section.array.array = [ [true], [false] ]

wireplumber.section.array.object = [ { key1 = foo }, { key2 = 4 } ]

wireplumber.section.object = {
  key.boolean = true
  key.int = -1
  key.float = 3.14
  key.string = "wireplumber"
  key.array = ["an", "array"]
  key.object = {
    key.nested.boolean = false
  }
}

wireplumber.section-merged.array.boolean = [ false ]

wireplumber.section-merged.array.int = [ 4 ]

wireplumber.section-merged.array.float = [ 4.44 ]

wireplumber.section-merged.array.string = [ "first" ]

wireplumber.section-merged.object = {
  key.boolean = false
  key.int = 6
}

wireplumber.section-merged.array.array = [ [true] ]

wireplumber.section-merged.array.object = [ { key1 = foo } ]

wireplumber.section-nested-merged = {
  nested-object = {
    key1 = true
  }
  nested-array = [1, 2]
}

wireplumber.section-override = {
  "key1" = true;
}

wireplumber.section-nested-override = {
  nested-object = {
    key1 = true
  }
}
070701000001DE000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000003300000000wireplumber-0.5.0/tests/wp/conf/wireplumber.conf.d    070701000001DF000081A400000000000000000000000165F86304000002DF000000000000000000000000000000000000003E00000000wireplumber-0.5.0/tests/wp/conf/wireplumber.conf.d/merge.conf wireplumber.section-merged.array.boolean = [ true ]

wireplumber.section-merged.array.int = [ 5 ]

wireplumber.section-merged.array.float = [ 5.55 ]

wireplumber.section-merged.array.string = [ "second" ]

wireplumber.section-merged.object = {
  key.float = 6.66
  key.string = "merged"
  key.array = ["an", "array"]
  key.object = {
    key.nested.boolean = false
  }
}

wireplumber.section-merged.array.array = [ [false] ]

wireplumber.section-merged.array.object = [ { key2 = 4 } ]

wireplumber.section-nested-merged = {
  nested-object = {
    key2 = 3
  }
  nested-array = [3, 4]
}

override.wireplumber.section-override = {
  "key2" = 5;
}

wireplumber.section-nested-override = {
  override.nested-object = {
    key2 = 3
  }
}
 070701000001E0000081A400000000000000000000000165F86304000013F0000000000000000000000000000000000000002200000000wireplumber-0.5.0/tests/wp/core.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"
#include <wp/wp.h>

typedef struct {
  WpBaseTestFixture base;
  WpObjectManager *om;
  gboolean disconnected;
} TestFixture;

static void
test_core_setup (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_DONT_CONNECT);
  /* remove the "disconnected" handler that fails the test */
  g_signal_handlers_disconnect_by_data (self->base.core, &self->base);
  self->om = wp_object_manager_new ();
  self->disconnected = FALSE;
}

static void
test_core_teardown (TestFixture *self, gconstpointer user_data)
{
  g_clear_object (&self->om);
  wp_base_test_fixture_teardown (&self->base);
}

static void
expect_connected (WpCore * core, TestFixture * f)
{
  g_main_loop_quit (f->base.loop);
}

static void
expect_disconnected (WpCore * core, TestFixture * f)
{
  f->disconnected = TRUE;
  g_main_loop_quit (f->base.loop);
}

static void
expect_object_added (WpObjectManager *om, WpProxy *proxy, TestFixture *f)
{
  g_assert_true (WP_IS_CLIENT (proxy));
  g_main_loop_quit (f->base.loop);
}

static void
test_core_server_disconnected (TestFixture *f, gconstpointer data)
{
  g_signal_connect (f->base.core, "connected",
      G_CALLBACK (expect_connected), f);
  g_signal_connect (f->base.core, "disconnected",
      G_CALLBACK (expect_disconnected), f);
  g_signal_connect (f->om, "object-added",
      G_CALLBACK (expect_object_added), f);

  wp_object_manager_add_interest (f->om, WP_TYPE_CLIENT, NULL);
  wp_core_install_object_manager (f->base.core, f->om);

  /* connect */
  g_assert_true (wp_core_connect (f->base.core));
  g_main_loop_run (f->base.loop);
  g_assert_true (wp_core_is_connected (f->base.core));

  /* wait for the object manager to collect the client proxy */
  g_main_loop_run (f->base.loop);
  g_assert_cmpuint (wp_object_manager_get_n_objects (f->om), ==, 1);

  /* destroy the server and wait for the disconnected signal */
  wp_test_server_teardown (&f->base.server);
  g_main_loop_run (f->base.loop);
  g_assert_true (f->disconnected);

  g_assert_false (wp_core_is_connected (f->base.core));
  g_assert_cmpuint (wp_object_manager_get_n_objects (f->om), ==, 0);
}

static void
test_core_client_disconnected (TestFixture *f, gconstpointer data)
{
  g_signal_connect (f->base.core, "connected",
      G_CALLBACK (expect_connected), f);
  g_signal_connect (f->base.core, "disconnected",
      G_CALLBACK (expect_disconnected), f);
  g_signal_connect (f->om, "object-added",
      G_CALLBACK (expect_object_added), f);

  wp_object_manager_add_interest (f->om, WP_TYPE_CLIENT, NULL);
  wp_core_install_object_manager (f->base.core, f->om);

  /* connect */
  g_assert_true (wp_core_connect (f->base.core));
  g_main_loop_run (f->base.loop);
  g_assert_true (wp_core_is_connected (f->base.core));

  /* wait for the object manager to collect the client proxy */
  g_main_loop_run (f->base.loop);
  g_assert_cmpuint (wp_object_manager_get_n_objects (f->om), ==, 1);

  /* disconnect and expect the disconnected signal */
  wp_core_disconnect (f->base.core);
  g_assert_true (f->disconnected);

  g_assert_false (wp_core_is_connected (f->base.core));
  g_assert_cmpuint (wp_object_manager_get_n_objects (f->om), ==, 0);
}

static void
test_core_clone (TestFixture *f, gconstpointer data)
{
  g_assert_false (wp_core_is_connected (f->base.core));

  /* clone */
  g_autoptr (WpCore) clone = wp_core_clone (f->base.core);
  g_assert_nonnull (clone);
  g_assert_false (wp_core_is_connected (clone));

  g_signal_connect (f->base.core, "connected",
      G_CALLBACK (expect_connected), f);
  g_signal_connect (clone, "connected",
      G_CALLBACK (expect_connected), f);

  /* connect clone */
  g_assert_true (wp_core_connect (clone));
  g_main_loop_run (f->base.loop);
  g_assert_true (wp_core_is_connected (clone));
  g_assert_false (wp_core_is_connected (f->base.core));

  /* connect core */
  g_assert_true (wp_core_connect (f->base.core));
  g_main_loop_run (f->base.loop);
  g_assert_true (wp_core_is_connected (clone));
  g_assert_true (wp_core_is_connected (f->base.core));

  /* disconnect clone */
  wp_core_disconnect (clone);
  g_assert_false (wp_core_is_connected (clone));
  g_assert_true (wp_core_is_connected (f->base.core));

  /* disconnect core */
  wp_core_disconnect (f->base.core);
  g_assert_false (wp_core_is_connected (f->base.core));
  g_assert_false (wp_core_is_connected (clone));
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/core/server-disconnected", TestFixture, NULL,
      test_core_setup, test_core_server_disconnected, test_core_teardown);
  g_test_add ("/wp/core/client-disconnected", TestFixture, NULL,
      test_core_setup, test_core_client_disconnected, test_core_teardown);
  g_test_add ("/wp/core/clone", TestFixture, NULL,
      test_core_setup, test_core_clone, test_core_teardown);

  return g_test_run ();
}
070701000001E1000081A400000000000000000000000165F863040000445F000000000000000000000000000000000000002400000000wireplumber-0.5.0/tests/wp/events.c   /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;
  GPtrArray *hooks_executed;
  GPtrArray *events;
  WpTransition *transition;
} TestFixture;

static void
test_events_setup (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&self->base, 0);
  self->hooks_executed = g_ptr_array_new ();
  self->events = g_ptr_array_new ();
}

static void
test_events_teardown (TestFixture *self, gconstpointer user_data)
{
  g_clear_pointer (&self->hooks_executed, g_ptr_array_unref);
  g_clear_pointer (&self->events, g_ptr_array_unref);
  wp_base_test_fixture_teardown (&self->base);
}

#define HOOK_FUNC(x) \
  static void \
  hook_##x (WpEvent * event, TestFixture * self) \
  { \
    g_debug ("in hook_" #x); \
    g_ptr_array_add (self->hooks_executed, hook_##x); \
    g_ptr_array_add (self->events, event); \
  }

HOOK_FUNC (a)
HOOK_FUNC (b)
HOOK_FUNC (c)
HOOK_FUNC (d)

static void
hook_quit (WpEvent *event, TestFixture *self)
{
  g_debug ("in hook_quit");
  g_ptr_array_add (self->hooks_executed, hook_quit);
  g_ptr_array_add (self->events, event);
  g_main_loop_quit (self->base.loop);
}

static void
test_events_order (TestFixture *self, gconstpointer user_data)
{
  g_autoptr (WpEventDispatcher) dispatcher = NULL;
  g_autoptr (WpEventHook) hook = NULL;
  WpEvent *event1 = NULL, *event2 = NULL, *event3 = NULL, *event4;

  dispatcher = wp_event_dispatcher_get_instance (self->base.core);
  g_assert_nonnull (dispatcher);

  hook = wp_simple_event_hook_new ("hook-a", NULL, NULL,
    g_cclosure_new ((GCallback) hook_a, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  hook = wp_simple_event_hook_new ("hook-quit", NULL, NULL,
    g_cclosure_new ((GCallback) hook_quit, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "quit", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  event1 = wp_event_new ("type1", 20, NULL, NULL, NULL);
  event2 = wp_event_new ("type1", 20, NULL, NULL, NULL);
  event3 = wp_event_new ("type1", 30, NULL, NULL, NULL);
  event4 = wp_event_new ("quit",  10, NULL, NULL, NULL);
  wp_event_dispatcher_push_event (dispatcher, event1);
  wp_event_dispatcher_push_event (dispatcher, event2);
  wp_event_dispatcher_push_event (dispatcher, event3);
  wp_event_dispatcher_push_event (dispatcher, event4);

  g_main_loop_run (self->base.loop);
  g_assert_cmpint (self->hooks_executed->len, == , 4);
  g_assert_cmpint (self->events->len, == , 4);

  g_assert_true (hook_a == self->hooks_executed->pdata [0]);
  g_assert_true (event3 == self->events->pdata [0]);
  g_assert_true (hook_a == self->hooks_executed->pdata [1]);
  g_assert_true (event1 == self->events->pdata [1]);
  g_assert_true (hook_a == self->hooks_executed->pdata [2]);
  g_assert_true (event2 == self->events->pdata [2]);
  g_assert_true (hook_quit == self->hooks_executed->pdata [3]);
  g_assert_true (event4 == self->events->pdata [3]);
}

static void
test_events_basic (TestFixture *self, gconstpointer user_data)
{
  g_autoptr (WpEventDispatcher) dispatcher = NULL;
  g_autoptr (WpEventHook) hook = NULL;
  WpEvent *event1 = NULL, *event2 = NULL, *event3 = NULL;
  const gchar **before, **after;

  dispatcher = wp_event_dispatcher_get_instance (self->base.core);
  g_assert_nonnull (dispatcher);

  before = (const gchar *[]) { "hook-d", "hook-b", NULL };
  after = (const gchar *[]) { "hook-c", NULL };
  hook = wp_simple_event_hook_new ("hook-a", before, after,
    g_cclosure_new ((GCallback) hook_a, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = NULL;
  after = NULL;
  hook = wp_simple_event_hook_new ("hook-b", before, after,
    g_cclosure_new ((GCallback) hook_b, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = NULL;
  after = NULL;
  hook = wp_simple_event_hook_new ("hook-c", before, after,
      g_cclosure_new ((GCallback) hook_c, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  /* wrong before/after dependencies shouldn't matter as this hook is not used */
  before = (const gchar *[]) { "hook-c", "hook-a", NULL };
  after = (const gchar *[]) { "hook-b", NULL };
  hook = wp_simple_event_hook_new ("hook-d", before, after,
      g_cclosure_new ((GCallback) hook_d, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type2", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = NULL;
  after = (const gchar *[]) { "hook-a", "hook-b", "hook-c", "hook-d", NULL };
  hook = wp_simple_event_hook_new ("hook-quit", before, after,
    g_cclosure_new ((GCallback) hook_quit, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type3", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  /* first event run */
  event1 = wp_event_new ("type1", 10, NULL, NULL, NULL);
  wp_event_dispatcher_push_event (dispatcher, event1);

  g_assert_cmpint (self->hooks_executed->len, == , 0);
  g_assert_cmpint (self->events->len, == , 0);
  g_main_loop_run (self->base.loop);
  g_assert_cmpint (self->hooks_executed->len, == , 4);
  g_assert_cmpint (self->events->len, == , 4);
  g_assert_true (hook_c == self->hooks_executed->pdata [0]);
  g_assert_true (event1 == self->events->pdata [0]);
  g_assert_true (hook_a == self->hooks_executed->pdata [1]);
  g_assert_true (event1 == self->events->pdata [1]);
  g_assert_true (hook_b == self->hooks_executed->pdata [2]);
  g_assert_true (event1 == self->events->pdata [2]);
  g_assert_true (hook_quit == self->hooks_executed->pdata [3]);
  g_assert_true (event1 == self->events->pdata [3]);

  g_ptr_array_remove_range (self->hooks_executed, 0, self->hooks_executed->len);
  g_assert_cmpint (self->hooks_executed->len, == , 0);
  g_ptr_array_remove_range (self->events, 0, self->events->len);
  g_assert_cmpint (self->events->len, == , 0);

  /* second event run */
  event1 = wp_event_new ("type1", 10,
    wp_properties_new ("test.prop", "some-val", NULL), NULL, NULL);
  event2 = wp_event_new ("type2", 100,
    wp_properties_new ("test.prop", "some-val", NULL), NULL, NULL);

  wp_event_dispatcher_push_event (dispatcher, event1);
  wp_event_dispatcher_push_event (dispatcher, event2);

  g_assert_cmpint (self->hooks_executed->len, == , 0);
  g_assert_cmpint (self->events->len, == , 0);
  g_main_loop_run (self->base.loop);
  g_assert_cmpint (self->hooks_executed->len, == , 5);
  g_assert_cmpint (self->events->len, == , 5);
  g_assert_true (hook_d == self->hooks_executed->pdata [0]);
  g_assert_true (event2 == self->events->pdata [0]);
  g_assert_true (hook_c == self->hooks_executed->pdata [1]);
  g_assert_true (event1 == self->events->pdata [1]);
  g_assert_true (hook_a == self->hooks_executed->pdata [2]);
  g_assert_true (event1 == self->events->pdata [2]);
  g_assert_true (hook_b == self->hooks_executed->pdata [3]);
  g_assert_true (event1 == self->events->pdata [3]);
  g_assert_true (hook_quit == self->hooks_executed->pdata [4]);
  g_assert_true (event1 == self->events->pdata [4]);

  g_ptr_array_remove_range (self->hooks_executed, 0, self->hooks_executed->len);
  g_assert_cmpint (self->hooks_executed->len, == , 0);
  g_ptr_array_remove_range (self->events, 0, self->events->len);
  g_assert_cmpint (self->events->len, == , 0);

  /* third event run */
  event1 = wp_event_new ("type1", 10,
    wp_properties_new ("test.prop", "some-val", NULL), NULL, NULL);
  event2 = wp_event_new ("type2", 100,
    wp_properties_new ("test.prop", "some-val", NULL), NULL, NULL);
  event3 = wp_event_new ("type3", 50,
    wp_properties_new ("test.prop", "some-val", NULL), NULL, NULL);

  wp_event_dispatcher_push_event (dispatcher, event3);
  wp_event_dispatcher_push_event (dispatcher, event2);
  wp_event_dispatcher_push_event (dispatcher, event1);
  wp_event_stop_processing (event1);

  g_assert_cmpint (self->hooks_executed->len, == , 0);
  g_assert_cmpint (self->events->len, == , 0);
  g_main_loop_run (self->base.loop);
  g_assert_cmpint (self->hooks_executed->len, == , 2);
  g_assert_cmpint (self->events->len, == , 2);
  g_assert_true (hook_d == self->hooks_executed->pdata [0]);
  g_assert_true (event2 == self->events->pdata [0]);
  g_assert_true (hook_quit == self->hooks_executed->pdata [1]);
  g_assert_true (event3 == self->events->pdata [1]);
}

enum {
  STEP_ONE = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_TWO,
};

static guint
async_hook_get_next_step (WpTransition *transition, guint step,
  TestFixture *self)
{
  switch (step) {
  case WP_TRANSITION_STEP_NONE: return STEP_ONE;
  case STEP_ONE: return STEP_TWO;
  case STEP_TWO: return WP_TRANSITION_STEP_NONE;
  default: return WP_TRANSITION_STEP_ERROR;
  }
}

static void
async_hook_execute_step (WpTransition *transition, guint step,
  TestFixture *self)
{
  switch (step) {
  case STEP_ONE:
    g_ptr_array_add (self->hooks_executed, async_hook_execute_step);
    self->transition = transition;
    g_main_loop_quit (self->base.loop);
    break;
  case STEP_TWO:
    self->transition = NULL;
    wp_transition_advance (transition);
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
test_events_async_hook (TestFixture *self, gconstpointer user_data)
{
  g_autoptr (WpEventDispatcher) dispatcher = NULL;
  g_autoptr (WpEventHook) hook = NULL;
  const gchar **before, **after;

  dispatcher = wp_event_dispatcher_get_instance (self->base.core);
  g_assert_nonnull (dispatcher);

  before = NULL;
  after = NULL;
  hook = wp_simple_event_hook_new ("hook-a", before, after,
      g_cclosure_new ((GCallback) hook_a, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = (const gchar *[]) { "hook-quit", NULL };
  after = (const gchar *[]) { "hook-a", NULL };
  hook = wp_simple_event_hook_new ("hook-b", before, after,
      g_cclosure_new ((GCallback) hook_b, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = (const gchar *[]) { "hook-a", NULL };
  after = NULL;
  hook = wp_simple_event_hook_new ("hook-c", before, after,
      g_cclosure_new ((GCallback) hook_c, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = NULL;
  after = NULL;
  hook = wp_simple_event_hook_new ("hook-quit", before, after,
      g_cclosure_new ((GCallback) hook_quit, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type3", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = (const gchar *[]) { "hook-a", NULL };
  after = (const gchar *[]) { "hook-c", NULL };
  hook = wp_async_event_hook_new ("async-test-hook", before, after,
      g_cclosure_new ((GCallback) async_hook_get_next_step, self, NULL),
      g_cclosure_new ((GCallback) async_hook_execute_step, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  wp_event_dispatcher_push_event (dispatcher,
    wp_event_new ("type1", 10, NULL, NULL, NULL));

  g_assert_cmpint (self->hooks_executed->len, == , 0);
  g_main_loop_run (self->base.loop);
  g_assert_cmpint (self->hooks_executed->len, == , 2);
  g_assert_true (hook_c == self->hooks_executed->pdata [0]);
  g_assert_true (async_hook_execute_step == self->hooks_executed->pdata [1]);

  g_assert_nonnull (self->transition);
  wp_transition_advance (self->transition);
  g_assert_null (self->transition);

  g_assert_cmpint (self->hooks_executed->len, == , 2);
  g_main_loop_run (self->base.loop);
  g_assert_cmpint (self->hooks_executed->len, == , 5);
  g_assert_true (hook_c == self->hooks_executed->pdata [0]);
  g_assert_true (async_hook_execute_step == self->hooks_executed->pdata [1]);
  g_assert_true (hook_a == self->hooks_executed->pdata [2]);
  g_assert_true (hook_b == self->hooks_executed->pdata [3]);
  g_assert_true (hook_quit == self->hooks_executed->pdata [4]);
}

static void
test_events_glob_deps (TestFixture *self, gconstpointer user_data)
{
  g_autoptr (WpEventDispatcher) dispatcher = NULL;
  g_autoptr (WpEventHook) hook = NULL;
  WpEvent *event1 = NULL;
  const gchar **before, **after;

  dispatcher = wp_event_dispatcher_get_instance (self->base.core);
  g_assert_nonnull (dispatcher);

  before = (const gchar *[]) { "*-d", "*-b", NULL };
  after = (const gchar *[]) { "hook-c", NULL };
  hook = wp_simple_event_hook_new ("hook-a", before, after,
    g_cclosure_new ((GCallback) hook_a, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = NULL;
  after = NULL;
  hook = wp_simple_event_hook_new ("hook-b", before, after,
    g_cclosure_new ((GCallback) hook_b, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = NULL;
  after = NULL;
  hook = wp_simple_event_hook_new ("hook-c", before, after,
      g_cclosure_new ((GCallback) hook_c, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = NULL;
  after = (const gchar *[]) { "h*-b", NULL };
  hook = wp_simple_event_hook_new ("hook-d", before, after,
      g_cclosure_new ((GCallback) hook_d, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  before = NULL;
  after = (const gchar *[]) { "hook-a", "hook-b", "hook-c", "hook-d", NULL };
  hook = wp_simple_event_hook_new ("hook-quit", before, after,
    g_cclosure_new ((GCallback) hook_quit, self, NULL));
  wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
    WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "type1", NULL);
  wp_event_dispatcher_register_hook (dispatcher, hook);
  g_clear_object (&hook);

  /* first event run */
  event1 = wp_event_new ("type1", 10, NULL, NULL, NULL);
  wp_event_dispatcher_push_event (dispatcher, event1);

  g_assert_cmpint (self->hooks_executed->len, == , 0);
  g_main_loop_run (self->base.loop);
  g_assert_cmpint (self->hooks_executed->len, == , 5);
  g_assert_true (hook_c == self->hooks_executed->pdata [0]);
  g_assert_true (hook_a == self->hooks_executed->pdata [1]);
  g_assert_true (hook_b == self->hooks_executed->pdata [2]);
  g_assert_true (hook_d == self->hooks_executed->pdata [3]);
  g_assert_true (hook_quit == self->hooks_executed->pdata [4]);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/events/order", TestFixture, NULL,
    test_events_setup, test_events_order, test_events_teardown);
  g_test_add ("/wp/events/basic", TestFixture, NULL,
    test_events_setup, test_events_basic, test_events_teardown);
  g_test_add ("/wp/events/async_hook", TestFixture, NULL,
    test_events_setup, test_events_async_hook, test_events_teardown);
  g_test_add ("/wp/events/glob_deps", TestFixture, NULL,
    test_events_setup, test_events_glob_deps, test_events_teardown);

  return g_test_run ();
}
 070701000001E2000081A400000000000000000000000165F86304000006DF000000000000000000000000000000000000002500000000wireplumber-0.5.0/tests/wp/factory.c  /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */
#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;
  WpObjectManager *om;
} TestFixture;

static void
test_factory_setup (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&self->base, 0);
  self->om = wp_object_manager_new ();
}

static void
test_factory_teardown (TestFixture *self, gconstpointer user_data)
{
  g_clear_object (&self->om);
  wp_base_test_fixture_teardown (&self->base);
}

static void
test_factory_enumeration_object_added (WpObjectManager *om,
    WpFactory *factory, TestFixture *fixture)
{
  g_autoptr (WpProperties) properties =
    wp_global_proxy_get_global_properties(WP_GLOBAL_PROXY(factory));
  const gchar* name = wp_properties_get (properties, PW_KEY_FACTORY_NAME);
  g_assert_nonnull(name);
  g_debug("factory name=%s", name);

  /* among all the pw factory objects look for client-node-factory object */
  if (!g_strcmp0(name, "client-node"))
    g_main_loop_quit(fixture->base.loop);
}

static void
test_factory_enumeration (TestFixture *self, gconstpointer user_data)
{
  g_signal_connect (self->om, "object_added",
      (GCallback) test_factory_enumeration_object_added, self);

  wp_object_manager_add_interest(self->om, WP_TYPE_FACTORY, NULL);
  wp_core_install_object_manager(self->base.core, self->om);
  g_main_loop_run(self->base.loop);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/factory/enumeration", TestFixture, NULL,
      test_factory_setup, test_factory_enumeration, test_factory_teardown);

  return g_test_run ();
}
 070701000001E3000081A400000000000000000000000165F8630400002BE9000000000000000000000000000000000000002800000000wireplumber-0.5.0/tests/wp/json-utils.c   /* WirePlumber
 *
 * Copyright © 2023 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"

static void
test_match_rules_update_properties (void)
{
  static const gchar * const rules_json_string =
      "["
      "  {"
      "    matches = ["
      "      {"
      "        device.name = \"~alsa_card.*\""
      "      }"
      "    ]"
      "    actions = {"
      "      update-props = {"
      "        api.alsa.use-acp = true"
      "        api.acp.auto-port = false"
      "      }"
      "    }"
      "  }"
      "  {"
      "    matches = ["
      "      {"
      "        node.name = \"alsa_output.0.my-alsa-device\""
      "      }"
      "    ]"
      "    actions = {"
      "      update-props = {"
      "        audio.rate = 96000"
      "        node.description = \"My ALSA Node\""
      "        media.class = null"
      "      }"
      "    }"
      "  }"
      "]";

  g_autoptr (WpSpaJson) rules = wp_spa_json_new_wrap_stringn (rules_json_string,
      strlen (rules_json_string));
  g_assert_nonnull (rules);

  /* Unmatched */
  {
    g_autoptr (WpProperties) match_props = NULL;

    match_props = wp_properties_new (
        "device.name", "unmatched-device-name",
        NULL);

    g_assert_cmpint (wp_json_utils_match_rules_update_properties (rules, match_props), ==, 0);
  }

  /* Match regex with props filled */
  {
    g_autoptr (WpProperties) match_props = NULL;
    const gchar *str = NULL;

    match_props = wp_properties_new (
        "device.name", "alsa_card_0.my-alsa-device",
        NULL);
    g_assert_nonnull (match_props);

    str = wp_properties_get (match_props, "device.name");
    g_assert_cmpstr (str, ==, "alsa_card_0.my-alsa-device");
    str = wp_properties_get (match_props, "api.alsa.use-acp");
    g_assert_null (str);
    str = wp_properties_get (match_props, "api.acp.auto-port");
    g_assert_null (str);

    g_assert_cmpint (wp_json_utils_match_rules_update_properties (rules, match_props), ==, 2);

    str = wp_properties_get (match_props, "device.name");
    g_assert_cmpstr (str, ==, "alsa_card_0.my-alsa-device");
    str = wp_properties_get (match_props, "api.alsa.use-acp");
    g_assert_cmpstr (str, ==, "true");
    str = wp_properties_get (match_props, "api.acp.auto-port");
    g_assert_cmpstr (str, ==, "false");
  }

  /* Match equal with props filled */
  {
    g_autoptr (WpProperties) match_props = NULL;
    const gchar *str = NULL;

    match_props = wp_properties_new (
        "node.name", "alsa_output.0.my-alsa-device",
        NULL);
    g_assert_nonnull (match_props);

    str = wp_properties_get (match_props, "node.name");
    g_assert_cmpstr (str, ==, "alsa_output.0.my-alsa-device");
    str = wp_properties_get (match_props, "audio.rate");
    g_assert_null (str);
    str = wp_properties_get (match_props, "node.description");
    g_assert_null (str);
    str = wp_properties_get (match_props, "media.class");
    g_assert_null (str);

    g_assert_cmpint (wp_json_utils_match_rules_update_properties (rules, match_props), ==, 2);

    str = wp_properties_get (match_props, "node.name");
    g_assert_cmpstr (str, ==, "alsa_output.0.my-alsa-device");
    str = wp_properties_get (match_props, "audio.rate");
    g_assert_cmpstr (str, ==, "96000");
    str = wp_properties_get (match_props, "node.description");
    g_assert_cmpstr (str, ==, "My ALSA Node");
    str = wp_properties_get (match_props, "media.class");
    g_assert_null (str);
  }

  /* Match equal with 1 prop updated */
  {
    g_autoptr (WpProperties) match_props = NULL;
    const gchar *str = NULL;

    match_props = wp_properties_new (
        "node.name", "alsa_output.0.my-alsa-device",
        "audio.rate", "96000",
        "node.description", "Test",
        NULL);
    g_assert_nonnull (match_props);

    str = wp_properties_get (match_props, "node.name");
    g_assert_cmpstr (str, ==, "alsa_output.0.my-alsa-device");
    str = wp_properties_get (match_props, "audio.rate");
    g_assert_cmpstr (str, ==, "96000");
    str = wp_properties_get (match_props, "node.description");
    g_assert_cmpstr (str, ==, "Test");
    str = wp_properties_get (match_props, "media.class");
    g_assert_null (str);

    g_assert_cmpint (wp_json_utils_match_rules_update_properties (rules, match_props), ==, 1);

    str = wp_properties_get (match_props, "node.name");
    g_assert_cmpstr (str, ==, "alsa_output.0.my-alsa-device");
    str = wp_properties_get (match_props, "audio.rate");
    g_assert_cmpstr (str, ==, "96000");
    str = wp_properties_get (match_props, "node.description");
    g_assert_cmpstr (str, ==, "My ALSA Node");
    str = wp_properties_get (match_props, "media.class");
    g_assert_null (str);
  }

  /* Match equal with prop deleted */
  {
    g_autoptr (WpProperties) match_props = NULL;
    const gchar *str = NULL;

    match_props = wp_properties_new (
        "node.name", "alsa_output.0.my-alsa-device",
        "media.class", "Audio/Sink",
        "audio.rate", "48000",
        "node.description", "Test",
        NULL);
    g_assert_nonnull (match_props);

    str = wp_properties_get (match_props, "node.name");
    g_assert_cmpstr (str, ==, "alsa_output.0.my-alsa-device");
    str = wp_properties_get (match_props, "audio.rate");
    g_assert_cmpstr (str, ==, "48000");
    str = wp_properties_get (match_props, "node.description");
    g_assert_cmpstr (str, ==, "Test");
    str = wp_properties_get (match_props, "media.class");
    g_assert_cmpstr (str, ==, "Audio/Sink");

    g_assert_cmpint (wp_json_utils_match_rules_update_properties (rules, match_props), ==, 3);

    str = wp_properties_get (match_props, "node.name");
    g_assert_cmpstr (str, ==, "alsa_output.0.my-alsa-device");
    str = wp_properties_get (match_props, "audio.rate");
    g_assert_cmpstr (str, ==, "96000");
    str = wp_properties_get (match_props, "node.description");
    g_assert_cmpstr (str, ==, "My ALSA Node");
    str = wp_properties_get (match_props, "media.class");
    g_assert_null (str);
  }
}

static gboolean
match_rules_cb (gpointer data, const gchar * action, WpSpaJson * value,
    GError ** error)
{
  WpProperties *match_props = data;

  if (g_str_equal (action, "update-props")) {
    wp_properties_update_from_json (match_props, value);
  }
  else if (g_str_equal (action, "set-answer")) {
    g_autofree gchar *str = wp_spa_json_to_string (value);
    wp_properties_set (match_props, "answer.universe", str);
  }
  else if (g_str_equal (action, "generate-error")) {
    g_autofree gchar *str = wp_spa_json_parse_string (value);
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "error: %s", str);
    return FALSE;
  }
  else if (g_str_equal (action, "set-description")) {
    g_autofree gchar *str = wp_spa_json_parse_string (value);
    wp_properties_set (match_props, "device.description", str);
  }

  return TRUE;
}

static void
test_match_rules (void)
{
  static const gchar * const rules_json_string =
      "["
      "  {"
      "    matches = ["
      "      {"
      "        device.name = \"~alsa_card.*\""
      "      }"
      "    ]"
      "    actions = {"
      "      update-props = {"
      "        device.name = alsa_card.1"
      "        api.acp.auto-port = false"
      "      }"
      "      set-answer = 42"
      "    }"
      "  }"
      "  {"
      "    matches = ["
      "      {"
      "        test.error = true"
      "      }"
      "    ]"
      "    actions = {"
      "      generate-error = \"test.error is true\""
      "    }"
      "  }"
      "  {"
      "    matches = ["
      "      {"
      "        device.name = \"alsa_card.1\""
      "      }"
      "    ]"
      "    actions = {"
      "      set-description = \"My ALSA Device\""
      "    }"
      "  }"
      "]";

  g_autoptr (WpSpaJson) rules = wp_spa_json_new_wrap_stringn (rules_json_string,
      strlen (rules_json_string));
  g_assert_nonnull (rules);

  /* no error */
  {
    g_autoptr (GError) error = NULL;
    g_autoptr (WpProperties) match_props = NULL;
    const gchar *str = NULL;

    match_props = wp_properties_new (
        "device.name", "alsa_card.0",
        "test.error", "false",
        NULL);

    str = wp_properties_get (match_props, "device.name");
    g_assert_cmpstr (str, ==, "alsa_card.0");
    str = wp_properties_get (match_props, "api.acp.auto-port");
    g_assert_null (str);
    str = wp_properties_get (match_props, "answer.universe");
    g_assert_null (str);
    str = wp_properties_get (match_props, "test.error");
    g_assert_cmpstr (str, ==, "false");
    str = wp_properties_get (match_props, "device.description");
    g_assert_null (str);

    g_assert_true (wp_json_utils_match_rules (rules, match_props, match_rules_cb,
        match_props, &error));
    g_assert_no_error (error);

    str = wp_properties_get (match_props, "device.name");
    g_assert_cmpstr (str, ==, "alsa_card.1");
    str = wp_properties_get (match_props, "api.acp.auto-port");
    g_assert_cmpstr (str, ==, "false");
    str = wp_properties_get (match_props, "answer.universe");
    g_assert_cmpstr (str, ==, "42");
    str = wp_properties_get (match_props, "test.error");
    g_assert_cmpstr (str, ==, "false");
    str = wp_properties_get (match_props, "device.description");
    g_assert_cmpstr (str, ==, "My ALSA Device");
  }

  /* with error */
  {
    g_autoptr (GError) error = NULL;
    g_autoptr (WpProperties) match_props = NULL;
    const gchar *str = NULL;

    match_props = wp_properties_new (
        "device.name", "alsa_card.256",
        "test.error", "true",
        NULL);

    str = wp_properties_get (match_props, "device.name");
    g_assert_cmpstr (str, ==, "alsa_card.256");
    str = wp_properties_get (match_props, "api.acp.auto-port");
    g_assert_null (str);
    str = wp_properties_get (match_props, "answer.universe");
    g_assert_null (str);
    str = wp_properties_get (match_props, "test.error");
    g_assert_cmpstr (str, ==, "true");
    str = wp_properties_get (match_props, "device.description");
    g_assert_null (str);

    g_assert_false (wp_json_utils_match_rules (rules, match_props, match_rules_cb,
        match_props, &error));
    g_assert_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED);
    g_assert_cmpstr (error->message, ==, "error: test.error is true");

    str = wp_properties_get (match_props, "device.name");
    g_assert_cmpstr (str, ==, "alsa_card.1");
    str = wp_properties_get (match_props, "api.acp.auto-port");
    g_assert_cmpstr (str, ==, "false");
    str = wp_properties_get (match_props, "answer.universe");
    g_assert_cmpstr (str, ==, "42");
    str = wp_properties_get (match_props, "test.error");
    g_assert_cmpstr (str, ==, "true");
    str = wp_properties_get (match_props, "device.description");
    g_assert_null (str);
  }
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  g_log_set_writer_func (wp_log_writer_default, NULL, NULL);

  g_test_add_func ("/wp/json-utils/match_rules_update_props",
      test_match_rules_update_properties);
  g_test_add_func ("/wp/json-utils/match_rules", test_match_rules);

  return g_test_run ();
}
   070701000001E4000081A400000000000000000000000165F86304000009C2000000000000000000000000000000000000002700000000wireplumber-0.5.0/tests/wp/meson.build    common_deps = [gobject_dep, gio_dep, wp_dep, pipewire_dep]
common_env = common_test_env
common_env.set('G_TEST_SRCDIR', meson.current_source_dir())
common_env.set('G_TEST_BUILDDIR', meson.current_build_dir())

test(
  'test-component-loader',
  executable('test-component-loader', 'component-loader.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-core',
  executable('test-core', 'core.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-events',
  executable('test-events', 'events.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-metadata',
  executable('test-metadata', 'metadata.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-object-interest',
  executable('test-object-interest', 'object-interest.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-object-manager',
  executable('test-object-manager', 'object-manager.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-properties',
  executable('test-properties', 'properties.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-proxy',
  executable('test-proxy', 'proxy.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-session-item',
  executable('test-session-item', 'session-item.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-spa-json',
  executable('test-spa-json', 'spa-json.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-spa-pod',
  executable('test-spa-pod', 'spa-pod.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-spa-type',
  executable('test-spa-type', 'spa-type.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-state',
  executable('test-state', 'state.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-settings',
  executable('test-settings', 'settings.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-transition',
  executable('test-transition', 'transition.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-factory',
  executable('test-factory', 'factory.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-conf',
  executable('test-conf', 'conf.c',
      dependencies: common_deps),
  env: common_env,
)

test(
  'test-json-utils',
  executable('test-json-utils', 'json-utils.c',
      dependencies: common_deps),
  env: common_env,
)
  070701000001E5000081A400000000000000000000000165F8630400004A5C000000000000000000000000000000000000002600000000wireplumber-0.5.0/tests/wp/metadata.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;

  WpObjectManager *export_om;
  WpObjectManager *proxy_om;

  WpMetadata *impl_metadata;
  WpMetadata *proxy_metadata;

  gint n_events;

} TestFixture;

static void
test_metadata_setup (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_CLIENT_CORE);
  self->export_om = wp_object_manager_new ();
  self->proxy_om = wp_object_manager_new ();
}

static void
test_metadata_teardown (TestFixture *self, gconstpointer user_data)
{
  g_clear_object (&self->proxy_om);
  g_clear_object (&self->export_om);
  wp_base_test_fixture_teardown (&self->base);
}

static void
test_metadata_basic_exported_object_added (WpObjectManager *om,
    WpMetadata *metadata, TestFixture *fixture)
{
  g_debug ("exported object added");

  g_assert_true (WP_IS_IMPL_METADATA (metadata));

  g_assert_null (fixture->impl_metadata);
  fixture->impl_metadata = WP_METADATA (metadata);

  if (++fixture->n_events == 3)
    g_main_loop_quit (fixture->base.loop);
}

static void
test_metadata_basic_exported_object_removed (WpObjectManager *om,
    WpMetadata *metadata, TestFixture *fixture)
{
  g_debug ("exported object removed");

  g_assert_true (WP_IS_IMPL_METADATA (metadata));

  g_assert_nonnull (fixture->impl_metadata);
  fixture->impl_metadata = NULL;

  if (++fixture->n_events == 2)
    g_main_loop_quit (fixture->base.loop);
}

static void
test_metadata_basic_proxy_object_added (WpObjectManager *om,
    WpMetadata *metadata, TestFixture *fixture)
{
  g_debug ("proxy object added");

  g_assert_true (WP_IS_METADATA (metadata));

  g_assert_null (fixture->proxy_metadata);
  fixture->proxy_metadata = WP_METADATA (metadata);

  if (++fixture->n_events == 3)
    g_main_loop_quit (fixture->base.loop);
}

static void
test_metadata_basic_proxy_object_removed (WpObjectManager *om,
    WpMetadata *metadata, TestFixture *fixture)
{
  g_debug ("proxy object removed");

  g_assert_true (WP_IS_METADATA (metadata));

  g_assert_nonnull (fixture->proxy_metadata);
  fixture->proxy_metadata = NULL;

  if (++fixture->n_events == 2)
    g_main_loop_quit (fixture->base.loop);
}

static void
test_metadata_basic_export_done (WpObject * metadata, GAsyncResult * res,
    TestFixture *fixture)
{
  g_autoptr (GError) error = NULL;

  g_debug ("export done");

  g_assert_true (wp_object_activate_finish (metadata, res, &error));
  g_assert_no_error (error);

  g_assert_true (WP_IS_IMPL_METADATA (metadata));

  if (++fixture->n_events == 3)
    g_main_loop_quit (fixture->base.loop);
}

static void
test_metadata_basic_changed (WpMetadata *metadata, guint32 subject,
    const gchar *key, const gchar *type, const gchar *value,
    TestFixture *fixture)
{
  g_debug ("changed: %u %s", subject, key);

  g_assert_true (WP_IS_METADATA (metadata));
  g_assert_cmpuint (subject, !=, PW_ID_ANY);

  if (++fixture->n_events == 4)
    g_main_loop_quit (fixture->base.loop);
}

static void
test_metadata_basic (TestFixture *fixture, gconstpointer data)
{
  g_autoptr (WpMetadata) metadata = NULL;

  /* set up the export side */
  g_signal_connect (fixture->export_om, "object_added",
      (GCallback) test_metadata_basic_exported_object_added, fixture);
  g_signal_connect (fixture->export_om, "object-removed",
      (GCallback) test_metadata_basic_exported_object_removed, fixture);
  wp_object_manager_add_interest (fixture->export_om, WP_TYPE_IMPL_METADATA,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
      NULL);
  wp_object_manager_request_object_features (fixture->export_om,
      WP_TYPE_IMPL_METADATA, WP_OBJECT_FEATURES_ALL);
  wp_core_install_object_manager (fixture->base.core, fixture->export_om);

  /* set up the proxy side */
  g_signal_connect (fixture->proxy_om, "object_added",
      (GCallback) test_metadata_basic_proxy_object_added, fixture);
  g_signal_connect (fixture->proxy_om, "object-removed",
      (GCallback) test_metadata_basic_proxy_object_removed, fixture);
  wp_object_manager_add_interest (fixture->proxy_om, WP_TYPE_METADATA,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
      NULL);
  wp_object_manager_request_object_features (fixture->proxy_om,
      WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
  wp_core_install_object_manager (fixture->base.client_core, fixture->proxy_om);

  /* create metadata */
  metadata = WP_METADATA (wp_impl_metadata_new (fixture->base.core));
  wp_metadata_set (metadata, 0, "test-key", NULL, "test-value");
  wp_metadata_set (metadata, 15, "toast", "Spa:Int", "15");

  /* verify properties are set before export */
  {
    g_autoptr (WpIterator) iter = wp_metadata_new_iterator (metadata, PW_ID_ANY);
    g_auto (GValue) val = G_VALUE_INIT;
    guint subject = -1;
    const gchar *key = NULL, *type = NULL, *value = NULL;
    WpMetadataItem *mi;

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "test-key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "test-value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));

    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 15);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "toast");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "Spa:Int");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "15");
    g_value_unset (&val);

    g_assert_false (wp_iterator_next (iter, &val));
  }

  /* do export */
  wp_object_activate (WP_OBJECT (metadata), WP_OBJECT_FEATURES_ALL, NULL,
      (GAsyncReadyCallback) test_metadata_basic_export_done, fixture);

  /* run until objects are created and features are cached */
  fixture->n_events = 0;
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (fixture->n_events, ==, 3);
  g_assert_nonnull (fixture->impl_metadata);
  g_assert_nonnull (fixture->proxy_metadata);
  g_assert_true (fixture->impl_metadata == metadata);

  /* test round 1: verify the values on the proxy */
  {
    g_autoptr (WpIterator) iter =
        wp_metadata_new_iterator (fixture->proxy_metadata, PW_ID_ANY);
    g_auto (GValue) val = G_VALUE_INIT;
    guint subject = -1;
    const gchar *key = NULL, *type = NULL, *value = NULL;
    WpMetadataItem *mi;

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "test-key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "test-value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 15);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "toast");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "Spa:Int");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "15");
    g_value_unset (&val);

    g_assert_false (wp_iterator_next (iter, &val));
  }

  /* setup change signals */
  g_signal_connect (fixture->proxy_metadata, "changed",
      (GCallback) test_metadata_basic_changed, fixture);
  g_signal_connect (metadata, "changed",
      (GCallback) test_metadata_basic_changed, fixture);

  /* change properties on the proxy */
  wp_metadata_set (fixture->proxy_metadata, 15, "toast", "Spa:Int", "20");
  wp_metadata_set (fixture->proxy_metadata, 0, "3rd.key", NULL, "3rd.value");

  /* run until the change is on both sides */
  fixture->n_events = 0;
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (fixture->n_events, ==, 4);

  /* test round 2: verify the value change on both sides */
  {
    g_autoptr (WpIterator) iter =
        wp_metadata_new_iterator (fixture->proxy_metadata, PW_ID_ANY);
    g_auto (GValue) val = G_VALUE_INIT;
    guint subject = -1;
    const gchar *key = NULL, *type = NULL, *value = NULL;
    WpMetadataItem *mi;

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "test-key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "test-value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 15);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "toast");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "Spa:Int");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "20");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "3rd.key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "3rd.value");
    g_value_unset (&val);

    g_assert_false (wp_iterator_next (iter, &val));
  }
  {
    g_autoptr (WpIterator) iter =
        wp_metadata_new_iterator (metadata, PW_ID_ANY);
    g_auto (GValue) val = G_VALUE_INIT;
    guint subject = -1;
    const gchar *key = NULL, *type = NULL, *value = NULL;
    WpMetadataItem *mi;

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "test-key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "test-value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 15);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "toast");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "Spa:Int");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "20");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "3rd.key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "3rd.value");
    g_value_unset (&val);

    g_assert_false (wp_iterator_next (iter, &val));
  }

  /* change properties on the exported */
  fixture->n_events = 0;
  wp_metadata_set (metadata, 0, "4th.key", NULL, "4th.value");
  wp_metadata_set (metadata, 0, "test-key", NULL, "new.value");

  /* run until the change is on both sides */
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (fixture->n_events, ==, 4);

  /* test round 3: verify the value change on both sides */
  {
    g_autoptr (WpIterator) iter =
        wp_metadata_new_iterator (fixture->proxy_metadata, PW_ID_ANY);
    g_auto (GValue) val = G_VALUE_INIT;
    guint subject = -1;
    const gchar *key = NULL, *type = NULL, *value = NULL;
    WpMetadataItem *mi;

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "test-key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "new.value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 15);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "toast");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "Spa:Int");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "20");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "3rd.key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "3rd.value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "4th.key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "4th.value");
    g_value_unset (&val);

    g_assert_false (wp_iterator_next (iter, &val));
  }
  {
    g_autoptr (WpIterator) iter =
        wp_metadata_new_iterator (metadata, PW_ID_ANY);
    g_auto (GValue) val = G_VALUE_INIT;
    guint subject = -1;
    const gchar *key = NULL, *type = NULL, *value = NULL;
    WpMetadataItem *mi;

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "test-key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "new.value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 15);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "toast");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "Spa:Int");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "20");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "3rd.key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "3rd.value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "4th.key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "4th.value");
    g_value_unset (&val);

    g_assert_false (wp_iterator_next (iter, &val));
  }

  /* find with constraints */
  {
    g_autoptr (WpIterator) iter = wp_metadata_new_iterator (metadata, 0);
    g_auto (GValue) val = G_VALUE_INIT;
    guint subject = -1;
    const gchar *key = NULL, *type = NULL, *value = NULL;
    WpMetadataItem *mi;

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "test-key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "new.value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "3rd.key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "3rd.value");
    g_value_unset (&val);

    g_assert_true (wp_iterator_next (iter, &val));
    mi = g_value_get_boxed (&val);
    subject = wp_metadata_item_get_subject (mi);
    g_assert_cmpint (subject, ==, 0);
    key = wp_metadata_item_get_key (mi);
    g_assert_cmpstr (key, ==, "4th.key");
    type = wp_metadata_item_get_value_type (mi);
    g_assert_cmpstr (type, ==, "string");
    value = wp_metadata_item_get_value (mi);
    g_assert_cmpstr (value, ==, "4th.value");
    g_value_unset (&val);

    g_assert_false (wp_iterator_next (iter, &val));
  }
  {
    const gchar *value = NULL, *type = NULL;
    value = wp_metadata_find (metadata, 0, "3rd.key", &type);
    g_assert_cmpstr (type, ==, "string");
    g_assert_cmpstr (value, ==, "3rd.value");
  }

  /* destroy impl metadata */
  fixture->n_events = 0;
  g_clear_object (&metadata);

  /* run until objects are destroyed */
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (fixture->n_events, ==, 2);
  g_assert_null (fixture->impl_metadata);
  g_assert_null (fixture->proxy_metadata);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/metadata/basic", TestFixture, NULL,
      test_metadata_setup, test_metadata_basic, test_metadata_teardown);

  return g_test_run ();
}
070701000001E6000081A400000000000000000000000165F86304000073A1000000000000000000000000000000000000002D00000000wireplumber-0.5.0/tests/wp/object-interest.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"

enum {
  PROP_0,
  PROP_TEST_STRING,
  PROP_TEST_INT,
  PROP_TEST_UINT,
  PROP_TEST_INT64,
  PROP_TEST_UINT64,
  PROP_TEST_FLOAT,
  PROP_TEST_DOUBLE,
  PROP_TEST_BOOLEAN,
};

struct _TestObjA
{
  GObject parent;
  gchar *test_string;
  gint test_int;
  guint test_uint;
  gint64 test_int64;
  guint64 test_uint64;
  gfloat test_float;
  gdouble test_double;
  gboolean test_boolean;
};

#define TEST_TYPE_A (test_obj_a_get_type ())
G_DECLARE_FINAL_TYPE (TestObjA, test_obj_a, TEST, OBJ_A, GObject)
G_DEFINE_TYPE (TestObjA, test_obj_a, G_TYPE_OBJECT)

static void
test_obj_a_init (TestObjA * self)
{
}

static void
test_obj_a_finalize (GObject * object)
{
  TestObjA *self = TEST_OBJ_A (object);
  g_free (self->test_string);
  G_OBJECT_CLASS (test_obj_a_parent_class)->finalize (object);
}

static void
test_obj_a_get_property (GObject * object, guint id, GValue * value,
    GParamSpec * pspec)
{
  TestObjA *self = TEST_OBJ_A (object);

  switch (id) {
    case PROP_TEST_STRING:
      g_value_set_string (value, self->test_string);
      break;
    case PROP_TEST_INT:
      g_value_set_int (value, self->test_int);
      break;
    case PROP_TEST_UINT:
      g_value_set_uint (value, self->test_uint);
      break;
    case PROP_TEST_INT64:
      g_value_set_int64 (value, self->test_int64);
      break;
    case PROP_TEST_UINT64:
      g_value_set_uint64 (value, self->test_uint64);
      break;
    case PROP_TEST_FLOAT:
      g_value_set_float (value, self->test_float);
      break;
    case PROP_TEST_DOUBLE:
      g_value_set_double (value, self->test_double);
      break;
    case PROP_TEST_BOOLEAN:
      g_value_set_boolean (value, self->test_boolean);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, id, pspec);
      break;
  }
}

static void
test_obj_a_set_property (GObject * object, guint id, const GValue * value,
    GParamSpec * pspec)
{
  TestObjA *self = TEST_OBJ_A (object);

  switch (id) {
    case PROP_TEST_STRING:
      self->test_string = g_value_dup_string (value);
      break;
    case PROP_TEST_INT:
      self->test_int = g_value_get_int (value);
      break;
    case PROP_TEST_UINT:
      self->test_uint = g_value_get_uint (value);
      break;
    case PROP_TEST_INT64:
      self->test_int64 = g_value_get_int64 (value);
      break;
    case PROP_TEST_UINT64:
      self->test_uint64 = g_value_get_uint64 (value);
      break;
    case PROP_TEST_FLOAT:
      self->test_float = g_value_get_float (value);
      break;
    case PROP_TEST_DOUBLE:
      self->test_double = g_value_get_double (value);
      break;
    case PROP_TEST_BOOLEAN:
      self->test_boolean = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, id, pspec);
      break;
  }
}

static void
test_obj_a_class_init (TestObjAClass * klass)
{
  GObjectClass *obj_class = (GObjectClass *) klass;

  obj_class->finalize = test_obj_a_finalize;
  obj_class->get_property = test_obj_a_get_property;
  obj_class->set_property = test_obj_a_set_property;

  g_object_class_install_property (obj_class, PROP_TEST_STRING,
      g_param_spec_string ("test-string", "test-string", "blurb", NULL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_INT,
      g_param_spec_int ("test-int", "test-int", "blurb",
          G_MININT, G_MAXINT, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_UINT,
      g_param_spec_uint ("test-uint", "test-uint", "blurb",
          0, G_MAXUINT, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_INT64,
      g_param_spec_int64 ("test-int64", "test-int64", "blurb",
          G_MININT64, G_MAXINT64, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_UINT64,
      g_param_spec_uint64 ("test-uint64", "test-uint64", "blurb",
          0, G_MAXUINT64, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_FLOAT,
      g_param_spec_float ("test-float", "test-float", "blurb",
          -20.0f, 20.0f, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_DOUBLE,
      g_param_spec_double ("test-double", "test-double", "blurb",
          -20.0, 20.0, 0.0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_BOOLEAN,
      g_param_spec_boolean ("test-boolean", "test-boolean", "blurb", FALSE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}

struct _TestObjB
{
  TestObjA parent;
};

#define TEST_TYPE_B (test_obj_b_get_type ())
G_DECLARE_FINAL_TYPE (TestObjB, test_obj_b, TEST, OBJ_B, TestObjA)
G_DEFINE_TYPE (TestObjB, test_obj_b, TEST_TYPE_A)

static void
test_obj_b_init (TestObjB * self)
{
}

static void
test_obj_b_class_init (TestObjBClass * klass)
{
}

typedef struct {
  GObject *object;
} TestFixture;

static void
test_object_interest_setup (TestFixture * f, gconstpointer data)
{
  f->object = g_object_new (TEST_TYPE_B,
      "test-string", "toast",
      "test-int", -30,
      "test-uint", 50,
      "test-int64", G_GINT64_CONSTANT (-0x1d636b02300a7aa7),
      "test-uint64", G_GUINT64_CONSTANT (0x1d636b02300a7aa7),
      "test-float", 3.14f,
      "test-double", 3.1415926545897932384626433,
      "test-boolean", TRUE,
      NULL);
  g_assert_nonnull (f->object);
}

static void
test_object_interest_teardown (TestFixture * f, gconstpointer data)
{
  g_clear_object (&f->object);
}

#define TEST_EXPECT_MATCH(interest) \
  G_STMT_START { \
    g_autoptr (GError) error = NULL; \
    gboolean ret; \
    \
    g_assert_nonnull (interest); \
    \
    ret = wp_object_interest_validate (interest, &error); \
    g_assert_no_error (error); \
    g_assert_true (ret); \
    \
    g_assert_true (wp_object_interest_matches (interest, f->object)); \
    \
    g_clear_pointer (&interest, wp_object_interest_unref); \
  } G_STMT_END

#define TEST_EXPECT_NO_MATCH(interest) \
  G_STMT_START { \
    g_autoptr (GError) error = NULL; \
    gboolean ret; \
    \
    g_assert_nonnull (interest); \
    \
    ret = wp_object_interest_validate (interest, &error); \
    g_assert_no_error (error); \
    g_assert_true (ret); \
    \
    g_assert_false (wp_object_interest_matches (interest, f->object)); \
    \
    g_clear_pointer (&interest, wp_object_interest_unref); \
  } G_STMT_END

#define TEST_EXPECT_MATCH_WP_PROPS(interest, props, global_props) \
  G_STMT_START { \
    g_autoptr (GError) error = NULL; \
    gboolean ret; \
    \
    g_assert_nonnull (interest); \
    \
    ret = wp_object_interest_validate (interest, &error); \
    g_assert_no_error (error); \
    g_assert_true (ret); \
    \
    g_assert_cmphex (wp_object_interest_matches_full (interest, 0, \
        WP_TYPE_NODE, NULL, props, global_props), ==, WP_INTEREST_MATCH_ALL); \
    \
    g_clear_pointer (&interest, wp_object_interest_unref); \
  } G_STMT_END

#define TEST_EXPECT_NO_MATCH_WP_PROPS(interest, props, global_props) \
  G_STMT_START { \
    g_autoptr (GError) error = NULL; \
    gboolean ret; \
    \
    g_assert_nonnull (interest); \
    \
    ret = wp_object_interest_validate (interest, &error); \
    g_assert_no_error (error); \
    g_assert_true (ret); \
    \
    g_assert_cmphex (wp_object_interest_matches_full (interest, 0, \
        WP_TYPE_NODE, NULL, props, global_props), !=, WP_INTEREST_MATCH_ALL); \
    \
    g_clear_pointer (&interest, wp_object_interest_unref); \
  } G_STMT_END

#define TEST_EXPECT_VALIDATION_ERROR(interest) \
  G_STMT_START { \
    g_autoptr (GError) error = NULL; \
    gboolean ret; \
    \
    g_assert_nonnull (interest); \
    \
    ret = wp_object_interest_validate (interest, &error); \
    g_assert_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT); \
    g_assert_false (ret); \
    \
    g_clear_pointer (&interest, wp_object_interest_unref); \
  } G_STMT_END

static void
test_object_interest_unconstrained (TestFixture * f, gconstpointer data)
{
  g_autoptr (WpObjectInterest) i = NULL;

  i = wp_object_interest_new_type (TEST_TYPE_A);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new_type (WP_TYPE_PROXY);
  TEST_EXPECT_NO_MATCH (i);
}

static void
test_object_interest_constraint_equals (TestFixture * f, gconstpointer data)
{
  g_autoptr (WpObjectInterest) i = NULL;

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "=s", "toast", NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "=s", "fail", NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "=i", -30, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "=i", 100, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "=u", 50, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "=u", 100, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64",
      "=x", G_GINT64_CONSTANT (-0x1d636b02300a7aa7), NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64",
      "=x", G_GINT64_CONSTANT (100), NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
      "=t", G_GUINT64_CONSTANT (0x1d636b02300a7aa7), NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
      "=t", G_GUINT64_CONSTANT (100), NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
      "=d", 3.1415926545897932384626433, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "=d", 3.14, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "=d", 3.14, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "=d", 1.0, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-boolean", "=b", TRUE, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-boolean", "=b", FALSE, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "=d", 3.1415926545897932384626433,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "=u", 50,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "=s", "toast",
      NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
      "=d", 3.1415926545897932384626433,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "=u", 50,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "=s", "FAIL",
      NULL);
  TEST_EXPECT_NO_MATCH (i);
}

static void
test_object_interest_constraint_not_equals (TestFixture * f, gconstpointer data)
{
  g_autoptr (WpObjectInterest) i = NULL;

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "!s", "toast", NULL);
  TEST_EXPECT_NO_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "!s", "fail", NULL);
  TEST_EXPECT_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "!i", -30, NULL);
  TEST_EXPECT_NO_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "!i", 100, NULL);
  TEST_EXPECT_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "!u", 50, NULL);
  TEST_EXPECT_NO_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "!u", 100, NULL);
  TEST_EXPECT_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64",
      "!x", G_GINT64_CONSTANT (-0x1d636b02300a7aa7), NULL);
  TEST_EXPECT_NO_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64",
      "!x", G_GINT64_CONSTANT (100), NULL);
  TEST_EXPECT_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
      "!t", G_GUINT64_CONSTANT (0x1d636b02300a7aa7), NULL);
  TEST_EXPECT_NO_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
      "!t", G_GUINT64_CONSTANT (100), NULL);
  TEST_EXPECT_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
      "!d", 3.1415926545897932384626433, NULL);
  TEST_EXPECT_NO_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "!d", 3.14, NULL);
  TEST_EXPECT_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "!d", 3.14, NULL);
  TEST_EXPECT_NO_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "!d", 1.0, NULL);
  TEST_EXPECT_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-boolean", "!b", TRUE, NULL);
  TEST_EXPECT_NO_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-boolean", "!b", FALSE, NULL);
  TEST_EXPECT_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "!d", 2.0,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "!u", 51,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "!s", "FAIL",
      WP_CONSTRAINT_TYPE_G_PROPERTY, "unknown-property", "!s", "FAIL",
      NULL);
  TEST_EXPECT_MATCH (i);
}

static void
test_object_interest_constraint_list (TestFixture * f, gconstpointer data)
{
  g_autoptr (WpObjectInterest) i = NULL;

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string",
      "c(sss)", "success", "toast", "test", NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string",
      "c(ss)", "not-a-toast", "fail", NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "c(iii)", -30, 20, -10, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "c(i)", 100, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "c(uu)", 100, 50, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "c(u)", 100, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64", "c(xx)",
      G_GINT64_CONSTANT (100), G_GINT64_CONSTANT (-0x1d636b02300a7aa7), NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64",
      "c(x)", G_GINT64_CONSTANT (100), NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
      "c(t)", G_GUINT64_CONSTANT (0x1d636b02300a7aa7), NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64",
      "c(t)", G_GUINT64_CONSTANT (100), NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
      "c(dd)", 2.0, 3.1415926545897932384626433, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "c(d)", 3.14, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "c(dd)", 2.0, 3.14, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "c(dd)", 1.0, 2.0, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
      "c(d)", 3.1415926545897932384626433,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "c(u)", 50,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "c(ss)", "random", "toast",
      NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double",
      "c(d)", 3.1415926545897932384626433,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "c(u)", 50,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "c(s)", "FAIL",
      NULL);
  TEST_EXPECT_NO_MATCH (i);
}

static void
test_object_interest_constraint_range (TestFixture * f, gconstpointer data)
{
  g_autoptr (WpObjectInterest) i = NULL;

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "~(ii)", -40, 20, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "~(ii)", 10, 100, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "~(uu)", 40, 100, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "~(uu)", 100, 150, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64", "~(xx)",
      G_GINT64_CONSTANT (-0x1d636b02300a7aaa),
      G_GINT64_CONSTANT (-0x1d636b02300a7aa0),
      NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int64", "~(xx)",
      G_GINT64_CONSTANT (0),
      G_GINT64_CONSTANT (100),
      NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64", "~(tt)",
      G_GUINT64_CONSTANT (0x1d636b02300a7aa0),
      G_GUINT64_CONSTANT (0x1d636b02300a7aaa),
      NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint64", "~(tt)",
      G_GUINT64_CONSTANT (0),
      G_GUINT64_CONSTANT (100),
      NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "~(dd)", 2.0, 4.0, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "~(dd)", -1.0, 3.14, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "~(dd)", 2.0, 4.0, NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-float", "~(dd)", -1.0, 3.13, NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "~(dd)", 0.0, 10.0,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "~(uu)", 0, 100,
      NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-double", "~(dd)", 10.0, 20.0,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-uint", "~(uu)", 0, 100,
      NULL);
  TEST_EXPECT_NO_MATCH (i);
}

static void
test_object_interest_constraint_matches (TestFixture * f, gconstpointer data)
{
  g_autoptr (WpObjectInterest) i = NULL;

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "to*", NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "t*st", NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "*a?t", NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "egg*", NULL);
  TEST_EXPECT_NO_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "#s", "t?est", NULL);
  TEST_EXPECT_NO_MATCH (i);
}

static void
test_object_interest_constraint_present_absent (TestFixture * f,
    gconstpointer data)
{
  g_autoptr (WpObjectInterest) i = NULL;

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-int", "+", NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "toast", "+", NULL);
  TEST_EXPECT_NO_MATCH (i);


  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "toast", "-", NULL);
  TEST_EXPECT_MATCH (i);

  i = wp_object_interest_new (TEST_TYPE_A,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "test-string", "-", NULL);
  TEST_EXPECT_NO_MATCH (i);
}

static void
test_object_interest_pw_props (TestFixture * f, gconstpointer data)
{
  g_autoptr (WpProperties) props = NULL;
  g_autoptr (WpProperties) global_props = NULL;
  g_autoptr (WpObjectInterest) i = NULL;

  props = wp_properties_new (
      "object.id", "10",
      "port.name", "test",
      "port.physical", "true",
      "audio.channel", "FR",
      "audio.volume", "0.8",
      "format.dsp", "32 bit float mono audio",
      NULL);

  global_props = wp_properties_new (
      "object.id", "10",
      "format.dsp", "32 bit float mono audio",
      NULL);

  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~(ii)", 0, 100, NULL);
  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);

  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=i", 11, NULL);
  TEST_EXPECT_NO_MATCH_WP_PROPS (i, props, global_props);

  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "format.dsp", "#s", "*audio*", NULL);
  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);

  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.physical", "=b", TRUE, NULL);
  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);

  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "audio.channel", "c(sss)",
      "MONO", "FL", "FR", NULL);
  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);

  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "audio.volume", "=d", 0.8, NULL);
  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);

  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "audio.volume", "~(dd)", 0.0, 0.5,
      NULL);
  TEST_EXPECT_NO_MATCH_WP_PROPS (i, props, global_props);

  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "=i", 10,
      NULL);
  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);

  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "+",
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "format.dsp", "+",
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "port.name", "-",
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "port.physical", "-",
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.name", "+",
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.physical", "+",
      NULL);
  TEST_EXPECT_MATCH_WP_PROPS (i, props, global_props);
}

static void
test_object_interest_validate (TestFixture * f, gconstpointer data)
{
  g_autoptr (WpObjectInterest) i = NULL;

  /* invalid type */
  i = wp_object_interest_new (WP_TYPE_NODE, 32, "object.id", "+", NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);

  /* bad verb; the varargs constructor would assert here */
  i = wp_object_interest_new_type (WP_TYPE_NODE);
  wp_object_interest_add_constraint (i, WP_CONSTRAINT_TYPE_PW_PROPERTY,
      "object.id", 0, g_variant_new_string ("10"));
  TEST_EXPECT_VALIDATION_ERROR (i);

  /* no subject; the varargs version would assert here */
  i = wp_object_interest_new_type (WP_TYPE_NODE);
  wp_object_interest_add_constraint (i, WP_CONSTRAINT_TYPE_PW_PROPERTY,
      NULL, WP_CONSTRAINT_VERB_EQUALS, g_variant_new_int32 (10));
  TEST_EXPECT_VALIDATION_ERROR (i);

  /* no value for verb that requires it */
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=", NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~", NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "c", NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "#", NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);

  /* value given for verb that doesn't require it */
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "+s", "10", NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "-s", "10", NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);

  /* tuple required */
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "ci", 10, NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~i", 10, NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);

  /* invalid value type */
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=y", (guchar) 10, NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=n", (gint16) 10, NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "=q", (guint16) 10, NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "c(bb)", TRUE, FALSE, NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~(ss)", "0", "20", NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "#i", 10, NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);

  /* tuple with different types */
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "c(si)", "9", 10, NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
  i = wp_object_interest_new (WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id", "~(iu)", -10, 20, NULL);
  TEST_EXPECT_VALIDATION_ERROR (i);
}

int
main (int argc, char *argv[])
{
  g_test_init (&argc, &argv, NULL);
  g_log_set_writer_func (wp_log_writer_default, NULL, NULL);

  g_test_add ("/wp/object-interest/unconstrained",
      TestFixture, NULL,
      test_object_interest_setup,
      test_object_interest_unconstrained,
      test_object_interest_teardown);

  g_test_add ("/wp/object-interest/equals",
      TestFixture, NULL,
      test_object_interest_setup,
      test_object_interest_constraint_equals,
      test_object_interest_teardown);

  g_test_add ("/wp/object-interest/not-equals",
      TestFixture, NULL,
      test_object_interest_setup,
      test_object_interest_constraint_not_equals,
      test_object_interest_teardown);

  g_test_add ("/wp/object-interest/list",
      TestFixture, NULL,
      test_object_interest_setup,
      test_object_interest_constraint_list,
      test_object_interest_teardown);

  g_test_add ("/wp/object-interest/range",
      TestFixture, NULL,
      test_object_interest_setup,
      test_object_interest_constraint_range,
      test_object_interest_teardown);

  g_test_add ("/wp/object-interest/matches",
      TestFixture, NULL,
      test_object_interest_setup,
      test_object_interest_constraint_matches,
      test_object_interest_teardown);

  g_test_add ("/wp/object-interest/present-absent",
      TestFixture, NULL,
      test_object_interest_setup,
      test_object_interest_constraint_present_absent,
      test_object_interest_teardown);

  g_test_add ("/wp/object-interest/pw-props",
      TestFixture, NULL,
      test_object_interest_setup,
      test_object_interest_pw_props,
      test_object_interest_teardown);

  g_test_add ("/wp/object-interest/validate",
      TestFixture, NULL,
      test_object_interest_setup,
      test_object_interest_validate,
      test_object_interest_teardown);

  return g_test_run ();
}
   070701000001E7000081A400000000000000000000000165F8630400001548000000000000000000000000000000000000002C00000000wireplumber-0.5.0/tests/wp/object-manager.c   /* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"

struct _TestSiDummy
{
  WpSessionItem parent;
};

G_DECLARE_FINAL_TYPE (TestSiDummy, si_dummy, TEST, SI_DUMMY, WpSessionItem)
G_DEFINE_TYPE (TestSiDummy, si_dummy, WP_TYPE_SESSION_ITEM)

static void
si_dummy_init (TestSiDummy * self)
{
}

static gboolean
si_dummy_configure (WpSessionItem * item, WpProperties * props)
{
  TestSiDummy *self = TEST_SI_DUMMY (item);
  wp_session_item_set_properties (WP_SESSION_ITEM (self), props);
  return TRUE;
}

static void
si_dummy_class_init (TestSiDummyClass * klass)
{
  WpSessionItemClass *si_class = (WpSessionItemClass *) klass;
  si_class->configure = si_dummy_configure;
}

typedef struct {
  WpBaseTestFixture base;
  WpObjectManager *om;
} TestFixture;

static void
test_om_setup (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_CLIENT_CORE);
}

static void
test_om_teardown (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_teardown (&self->base);
}

static void
test_om_interest_on_pw_props (TestFixture *f, gconstpointer user_data)
{
  g_autoptr (WpNode) node = NULL;
  g_autoptr (WpObjectManager) om = NULL;

  /* load modules on the server side */
  {
    g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&f->base.server);

    g_assert_cmpint (pw_context_add_spa_lib (f->base.server.context,
            "audiotestsrc", "audiotestsrc/libspa-audiotestsrc"), ==, 0);
    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-adapter", NULL, NULL));
  }

  /* export node on the client core */
  node = wp_node_new_from_factory (f->base.client_core,
      "adapter",
      wp_properties_new (
          "factory.name", "audiotestsrc",
          "node.name", "Test Source",
          "test.answer", "42",
          NULL));
  g_assert_nonnull (node);

  wp_object_activate (WP_OBJECT (node), WP_OBJECT_FEATURES_ALL,
      NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);

  /* ensure the base core is in sync */
  wp_core_sync (f->base.core, NULL, (GAsyncReadyCallback) test_core_done_cb, f);
  g_main_loop_run (f->base.loop);

  /* request that node from the base core */
  om = wp_object_manager_new ();
  wp_object_manager_add_interest (om, WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "node.name", "=s", "Test Source",
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "test.answer", "=s", "42",
      NULL);
  test_ensure_object_manager_is_installed (om, f->base.core, f->base.loop);

  g_assert_cmpuint (wp_object_manager_get_n_objects (om), ==, 1);
  g_clear_object (&om);

  /* request "test.answer" to be absent... this will not match */
  om = wp_object_manager_new ();
  wp_object_manager_add_interest (om, WP_TYPE_NODE,
      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "node.name", "=s", "Test Source",
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "test.answer", "-",
      NULL);
  test_ensure_object_manager_is_installed (om, f->base.core, f->base.loop);

  g_assert_cmpuint (wp_object_manager_get_n_objects (om), ==, 0);
}

static void
test_om_iterate_remove (TestFixture *f, gconstpointer user_data)
{
  g_autoptr (WpObjectManager) om = NULL;
  WpSessionItem *si = NULL;

  si = g_object_new (si_dummy_get_type (), "core", f->base.core, NULL);
  g_assert_true (wp_session_item_configure (si,
      wp_properties_new ("property1", "4321", NULL)));
  wp_session_item_register (si);

  si = g_object_new (si_dummy_get_type (), "core", f->base.core, NULL);
  g_assert_true (wp_session_item_configure (si,
      wp_properties_new ("property1", "2345", NULL)));
  wp_session_item_register (si);

  si = g_object_new (si_dummy_get_type (), "core", f->base.core, NULL);
  g_assert_true (wp_session_item_configure (si,
      wp_properties_new ("property1", "1234", NULL)));
  wp_session_item_register (si);

  si = g_object_new (si_dummy_get_type (), "core", f->base.core, NULL);
  g_assert_true (wp_session_item_configure (si,
      wp_properties_new ("property1", "1234", NULL)));
  wp_session_item_register (si);

  om = wp_object_manager_new ();
  wp_object_manager_add_interest (om, si_dummy_get_type (), NULL);
  test_ensure_object_manager_is_installed (om, f->base.core, f->base.loop);

  {
    g_autoptr (WpIterator) it = wp_object_manager_new_iterator (om);
    g_auto (GValue) value = G_VALUE_INIT;
    while (wp_iterator_next (it, &value)) {
      si = g_value_get_object (&value);
      g_assert_true (WP_IS_SESSION_ITEM (si));
      if (!g_strcmp0 (wp_session_item_get_property (si, "property1"), "1234")) {
        wp_session_item_remove (si);
      }
      g_value_unset (&value);
    }
  }

  g_assert_cmpint (wp_object_manager_get_n_objects (om), ==, 2);
  g_assert_null (wp_object_manager_lookup (om, si_dummy_get_type (),
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "property1", "=s", "1234", NULL));
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/om/interest-on-pw-props", TestFixture, NULL,
      test_om_setup, test_om_interest_on_pw_props, test_om_teardown);
  g_test_add ("/wp/om/iterate_remove", TestFixture, NULL,
      test_om_setup, test_om_iterate_remove, test_om_teardown);

  return g_test_run ();
}
070701000001E8000081A400000000000000000000000165F8630400001704000000000000000000000000000000000000002800000000wireplumber-0.5.0/tests/wp/properties.c   /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"
#include <pipewire/pipewire.h>

static void
test_properties_basic (void)
{
  g_autoptr (WpProperties) p = NULL;

  p = wp_properties_new_empty ();
  g_assert_nonnull (p);

  g_assert_cmpint (wp_properties_set (p, "foo.bar", "test-value"), ==, 1);
  g_assert_cmpstr (wp_properties_get (p, "foo.bar"), ==, "test-value");
  g_assert_cmpstr (wp_properties_get (p, "nonexistent"), ==, NULL);

  /* remove the key */
  g_assert_cmpint (wp_properties_set (p, "foo.bar", NULL), ==, 1);
  g_assert_cmpstr (wp_properties_get (p, "foo.bar"), ==, NULL);
  /* now returns 0 because it does not exist */
  g_assert_cmpint (wp_properties_set (p, "foo.bar", NULL), ==, 0);

  g_assert_true (wp_properties_ref (p) == p);
  wp_properties_unref (p);
}

static void
test_properties_wrap_dict (void)
{
  g_autoptr (WpProperties) p = NULL;
  const struct spa_dict_item dict_items[] = {
    { "key1", "value1" },
    { "key2", "value2" },
  };
  const struct spa_dict dict = SPA_DICT_INIT_ARRAY(dict_items);

  p = wp_properties_new_wrap_dict (&dict);
  g_assert_nonnull (p);

  g_assert_cmpint (wp_properties_get_count (p), ==,
      SPA_N_ELEMENTS (dict_items));
  g_assert_cmpstr (wp_properties_get (p, "key1"), ==, "value1");
  g_assert_cmpstr (wp_properties_get (p, "key2"), ==, "value2");
  g_assert_cmpstr (wp_properties_get (p, "key3"), ==, NULL);

  g_assert_true (wp_properties_peek_dict (p) == &dict);
}

static void
test_properties_copy_dict (void)
{
  g_autoptr (WpProperties) p = NULL;
  const struct spa_dict_item dict_items[] = {
    { "key1", "value1" },
    { "key2", "value2" },
  };
  const struct spa_dict dict = SPA_DICT_INIT_ARRAY(dict_items);

  p = wp_properties_new_copy_dict (&dict);
  g_assert_nonnull (p);

  g_assert_cmpstr (wp_properties_get (p, "key1"), ==, "value1");
  g_assert_cmpstr (wp_properties_get (p, "key2"), ==, "value2");
  g_assert_cmpstr (wp_properties_get (p, "key3"), ==, NULL);

  g_assert_true (wp_properties_peek_dict (p) != &dict);
}

static void
test_properties_wrap (void)
{
  g_autoptr (WpProperties) p = NULL;
  struct pw_properties *props;

  props = pw_properties_new ("key1", "value1", NULL);
  g_assert_nonnull (props);
  p = wp_properties_new_wrap (props);
  g_assert_nonnull (p);

  g_assert_true (wp_properties_peek_dict (p) == &props->dict);
  g_assert_cmpstr (wp_properties_get (p, "key1"), ==, "value1");

  wp_properties_unref (g_steal_pointer (&p));
  /* because wrap does not free the original object, this should not crash */
  pw_properties_free (props);
}

static void
test_properties_take (void)
{
  g_autoptr (WpProperties) p = NULL;
  struct pw_properties *props;

  props = pw_properties_new ("key1", "value1", NULL);
  g_assert_nonnull (props);
  p = wp_properties_new_take (props);
  g_assert_nonnull (p);

  g_assert_true (wp_properties_peek_dict (p) == &props->dict);
  g_assert_cmpstr (wp_properties_get (p, "key1"), ==, "value1");

  /* value changes should be reflected on both objects */
  g_assert_cmpint (wp_properties_setf (p, "foobar", "%d", 2), ==, 1);
  g_assert_cmpstr (pw_properties_get (props, "foobar"), ==, "2");

  g_assert_cmpint (pw_properties_setf (props, "test", "some-%s", "value"), ==, 1);
  g_assert_cmpstr (wp_properties_get (p, "test"), ==, "some-value");

  /* no leaks because p frees props */
}

static void
test_properties_to_pw_props (void)
{
  g_autoptr (WpProperties) p = NULL;
  struct pw_properties *props;

  p = wp_properties_new ("key1", "value1", NULL);
  g_assert_nonnull (p);
  g_assert_cmpstr (wp_properties_get (p, "key1"), ==, "value1");

  props = wp_properties_to_pw_properties (p);
  g_assert_nonnull (props);
  g_assert_cmpstr (pw_properties_get (props, "key1"), ==, "value1");

  /* we have different underlying objects */
  g_assert_true (wp_properties_peek_dict (p) != &props->dict);
  g_assert_cmpint (pw_properties_set (props, "test", "some-value"), ==, 1);
  g_assert_cmpstr (wp_properties_get (p, "test"), ==, NULL);

  pw_properties_free (props);
}

static void
test_properties_iterate (void)
{
  g_autoptr (WpProperties) p = NULL;

  p = wp_properties_new_empty ();
  g_assert_nonnull (p);
  g_assert_cmpint (wp_properties_set (p, "key0", "value0"), ==, 1);
  g_assert_cmpint (wp_properties_set (p, "key1", "value1"), ==, 1);
  g_assert_cmpint (wp_properties_set (p, "key2", "value2"), ==, 1);
  g_assert_cmpint (wp_properties_set (p, "key3", "value3"), ==, 1);
  g_assert_cmpint (wp_properties_set (p, "key4", "value4"), ==, 1);

  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;
  gint i = 0;
  for (it = wp_properties_new_iterator (p);
          wp_iterator_next (it, &item);
          g_value_unset (&item)) {
    WpPropertiesItem *pi = g_value_get_boxed (&item);
    g_autofree gchar *expected_key = g_strdup_printf ("key%d", i);
    g_autofree gchar *expected_value = g_strdup_printf ("value%d", i);
    g_assert_cmpstr (expected_value, ==, wp_properties_item_get_value (pi));
    g_assert_cmpstr (expected_key, ==, wp_properties_item_get_key (pi));
    i++;
  }
  g_assert_cmpint (i, ==, 5);
}

int
main (int argc, char *argv[])
{
  g_test_init (&argc, &argv, NULL);
  g_log_set_writer_func (wp_log_writer_default, NULL, NULL);

  g_test_add_func ("/wp/properties/basic", test_properties_basic);
  g_test_add_func ("/wp/properties/wrap_dict", test_properties_wrap_dict);
  g_test_add_func ("/wp/properties/copy_dict", test_properties_copy_dict);
  g_test_add_func ("/wp/properties/wrap", test_properties_wrap);
  g_test_add_func ("/wp/properties/take", test_properties_take);
  g_test_add_func ("/wp/properties/to_pw_props", test_properties_to_pw_props);
  g_test_add_func ("/wp/properties/iterate", test_properties_iterate);

  return g_test_run ();
}
070701000001E9000081A400000000000000000000000165F8630400002598000000000000000000000000000000000000002300000000wireplumber-0.5.0/tests/wp/proxy.c    /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;
  /* the object manager that listens for proxies */
  WpObjectManager *om;
} TestFixture;

static void
test_proxy_setup (TestFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&self->base, 0);
  self->om = wp_object_manager_new ();
}

static void
test_proxy_teardown (TestFixture *self, gconstpointer user_data)
{
  g_clear_object (&self->om);
  wp_base_test_fixture_teardown (&self->base);
}

static void
test_proxy_basic_activated (WpObject *proxy, GAsyncResult *res,
    TestFixture *fixture)
{
  g_autoptr (GError) error = NULL;
  g_assert_true (wp_object_activate_finish (proxy, res, &error));
  g_assert_no_error (error);

  g_assert_true (wp_object_get_active_features (proxy) & WP_PROXY_FEATURE_BOUND);
  g_assert_nonnull (wp_proxy_get_pw_proxy (WP_PROXY (proxy)));

  g_main_loop_quit (fixture->base.loop);
}

static void
test_proxy_basic_object_added (WpObjectManager *om, WpGlobalProxy *proxy,
    TestFixture *fixture)
{
  g_assert_nonnull (proxy);
  {
    g_autoptr (WpCore) pcore = NULL;
    g_autoptr (WpCore) omcore = NULL;
    g_object_get (proxy, "core", &pcore, NULL);
    g_object_get (om, "core", &omcore, NULL);
    g_assert_nonnull (pcore);
    g_assert_nonnull (omcore);
    g_assert_true (pcore == omcore);
  }
  g_assert_cmphex (wp_global_proxy_get_permissions (proxy), ==, PW_PERM_ALL);
  g_assert_true (WP_IS_CLIENT (proxy));

  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (proxy)), ==, 0);
  g_assert_null (wp_proxy_get_pw_proxy (WP_PROXY (proxy)));

  {
    g_autoptr (WpProperties) props =
        wp_global_proxy_get_global_properties (proxy);
    g_assert_nonnull (props);
    g_assert_cmpstr (wp_properties_get (props, PW_KEY_PROTOCOL), ==,
        "protocol-native");
  }

  wp_object_activate (WP_OBJECT (proxy), WP_PROXY_FEATURE_BOUND, NULL,
      (GAsyncReadyCallback) test_proxy_basic_activated, fixture);
}

static void
test_proxy_basic (TestFixture *fixture, gconstpointer data)
{
  /* our test server should advertise exactly one
   * client: our core; use this to test WpProxy */
  g_signal_connect (fixture->om, "object-added",
      (GCallback) test_proxy_basic_object_added, fixture);

  wp_object_manager_add_interest (fixture->om, WP_TYPE_CLIENT, NULL);
  wp_core_install_object_manager (fixture->base.core, fixture->om);

  g_main_loop_run (fixture->base.loop);
}

static void
test_node_enum_params_done (WpPipewireObject *node, GAsyncResult *res,
    TestFixture *f)
{
  g_autoptr (WpIterator) params = NULL;
  g_autoptr (GError) error = NULL;
  g_auto (GValue) item = G_VALUE_INIT;
  guint n_params = 0;

  params = wp_pipewire_object_enum_params_finish (node, res, &error);
  g_assert_no_error (error);
  g_assert_nonnull (params);

  for (; wp_iterator_next (params, &item); g_value_unset (&item)) {
    WpSpaPod *pod = NULL;
    g_assert_cmpuint (G_VALUE_TYPE (&item), ==, WP_TYPE_SPA_POD);
    pod = g_value_get_boxed (&item);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_object (pod));
    g_assert_cmpuint (wp_spa_type_from_name ("Spa:Pod:Object:Param:PropInfo"),
        ==, wp_spa_pod_get_spa_type (pod));
    n_params++;
  }
  g_assert_cmpint (n_params, >, 0);

  g_main_loop_quit (f->base.loop);
}

static void
test_node (TestFixture *f, gconstpointer data)
{
  g_autoptr (WpPipewireObject) proxy = NULL;
  const struct pw_node_info *info;

  /* load audiotestsrc on the server side */
  {
    g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&f->base.server);

    g_assert_cmpint (pw_context_add_spa_lib (f->base.server.context,
            "audiotestsrc", "audiotestsrc/libspa-audiotestsrc"), ==, 0);
    if (!test_is_spa_lib_installed (&f->base, "audiotestsrc")) {
      g_test_skip ("The pipewire audiotestsrc factory was not found");
      return;
    }

    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-adapter", NULL, NULL));
  }

  proxy = WP_PIPEWIRE_OBJECT (wp_node_new_from_factory (f->base.core,
      "adapter",
      wp_properties_new (
          "factory.name", "audiotestsrc",
          "node.name", "audiotestsrc.adapter",
          NULL)));
  g_assert_nonnull (proxy);
  wp_object_activate (WP_OBJECT (proxy), WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL,
      NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);

  /* basic tests */
  g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (proxy)), ==,
      WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
  g_assert_nonnull (wp_proxy_get_pw_proxy (WP_PROXY (proxy)));
  g_assert_true (WP_IS_NODE (proxy));

  /* info */
  {
    info = wp_pipewire_object_get_native_info (proxy);
    g_assert_nonnull (info);
    g_assert_cmpint (wp_proxy_get_bound_id (WP_PROXY (proxy)), ==, info->id);
  }

  /* properties */
  {
    const gchar *id = wp_pipewire_object_get_property (proxy, PW_KEY_OBJECT_ID);
    g_assert_nonnull (id);
    g_assert_cmpint (info->id, ==, atoi(id));
  }
  {
    const char *id;
    g_autoptr (WpProperties) props = NULL;

    props = wp_pipewire_object_get_properties (proxy);
    g_assert_nonnull (props);
    g_assert_true (wp_properties_peek_dict (props) == info->props);
    id = wp_properties_get (props, PW_KEY_OBJECT_ID);
    g_assert_nonnull (id);
    g_assert_cmpint (info->id, ==, atoi(id));
  }

  /* param info */
  {
    const gchar *flags_str;
    g_autoptr (GVariant) param_info = wp_pipewire_object_get_param_info (proxy);

    g_assert_nonnull (param_info);
    g_assert_true (g_variant_is_of_type (param_info, G_VARIANT_TYPE ("a{ss}")));
    g_assert_cmpuint (g_variant_n_children (param_info), ==, info->n_params);
    g_assert_true (g_variant_lookup (param_info, "PropInfo", "&s", &flags_str));
    g_assert_cmpstr (flags_str, ==, "r");
    g_assert_true (g_variant_lookup (param_info, "Props", "&s", &flags_str));
    g_assert_cmpstr (flags_str, ==, "rw");
  }

  /* enum params */
  wp_pipewire_object_enum_params (proxy, "PropInfo", NULL, NULL,
      (GAsyncReadyCallback) test_node_enum_params_done, f);
  g_main_loop_run (f->base.loop);
}

static void
activate_error_cb (WpObject * object, GAsyncResult * res,
    WpBaseTestFixture * f)
{
  g_autoptr (GError) error = NULL;
  gboolean augment_ret = wp_object_activate_finish (object, res, &error);
  g_assert_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED);
  g_assert_false (augment_ret);

  g_main_loop_quit (f->loop);
}

static void
test_link_error (TestFixture *f, gconstpointer data)
{
  g_autoptr (WpPipewireObject) proxy = NULL;

  /* load audiotestsrc on the server side */
  {
    g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&f->base.server);

    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-link-factory", NULL, NULL));
  }

  proxy = WP_PIPEWIRE_OBJECT (wp_link_new_from_factory (f->base.core,
      "link-factory",
      wp_properties_new (
          "link.output.node", "invalid",
          "link.input.node", "invalid",
          NULL)));
  g_assert_nonnull (proxy);
  wp_object_activate (WP_OBJECT (proxy), WP_OBJECT_FEATURES_ALL,
      NULL, (GAsyncReadyCallback) activate_error_cb, f);
  g_main_loop_run (f->base.loop);
}

static void
enum_params_error_cb (WpPipewireObject * object, GAsyncResult * res,
    WpBaseTestFixture * f)
{
  g_autoptr (GError) error = NULL;
  g_autoptr (WpIterator) it =
      wp_pipewire_object_enum_params_finish (object, res, &error);
  g_assert_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED);
  g_assert_null (it);

  g_main_loop_quit (f->loop);
}

static void
test_enum_params_error (TestFixture *f, gconstpointer data)
{
  g_autoptr (WpNode) node = NULL;

  /* load audiotestsrc on the server side */
  {
    g_autoptr (WpTestServerLocker) lock =
        wp_test_server_locker_new (&f->base.server);

    g_assert_cmpint (pw_context_add_spa_lib (f->base.server.context,
            "audiotestsrc", "audiotestsrc/libspa-audiotestsrc"), ==, 0);
    g_assert_nonnull (pw_context_load_module (f->base.server.context,
            "libpipewire-module-adapter", NULL, NULL));
  }

  node = wp_node_new_from_factory (f->base.core,
      "adapter",
      wp_properties_new (
          "factory.name", "audiotestsrc",
          "node.name", "audiotestsrc",
          NULL));
  g_assert_nonnull (node);

  wp_object_activate (WP_OBJECT (node), WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL,
      NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
  g_main_loop_run (f->base.loop);

  /* EnumRoute doesn't exist on audiotestsrc, obviously */
  wp_pipewire_object_enum_params (WP_PIPEWIRE_OBJECT (node), "EnumRoute", NULL,
      NULL, (GAsyncReadyCallback) enum_params_error_cb, f);
  g_main_loop_run (f->base.loop);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/proxy/basic", TestFixture, NULL,
      test_proxy_setup, test_proxy_basic, test_proxy_teardown);
  g_test_add ("/wp/proxy/node", TestFixture, NULL,
      test_proxy_setup, test_node, test_proxy_teardown);
  g_test_add ("/wp/proxy/link_error", TestFixture, NULL,
      test_proxy_setup, test_link_error, test_proxy_teardown);
  g_test_add ("/wp/proxy/enum_params_error", TestFixture, NULL,
      test_proxy_setup, test_enum_params_error, test_proxy_teardown);

  return g_test_run ();
}
070701000001EA000081A400000000000000000000000165F8630400003FE3000000000000000000000000000000000000002A00000000wireplumber-0.5.0/tests/wp/session-item.c ﻿/* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <stdio.h>

#include <wp/wp.h>
#include "../common/base-test-fixture.h"

G_DEFINE_QUARK (test-domain, test_domain)

typedef struct {
  WpBaseTestFixture base;
  WpObjectManager *si_om;
  gint n_items;
} TestSessionItemFixture;

static void
test_session_item_setup (TestSessionItemFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_CLIENT_CORE);
  self->si_om = wp_object_manager_new ();
  self->n_items = 0;
}

static void
test_session_item_teardown (TestSessionItemFixture *self, gconstpointer user_data)
{
  g_clear_object (&self->si_om);
  wp_base_test_fixture_teardown (&self->base);
}

struct _TestSiDummy
{
  WpSessionItem parent;
  const gchar *name;
  gboolean fail;
  gboolean activate_done;
  gboolean export_done;
};

G_DECLARE_FINAL_TYPE (TestSiDummy, si_dummy, TEST, SI_DUMMY, WpSessionItem)
G_DEFINE_TYPE (TestSiDummy, si_dummy, WP_TYPE_SESSION_ITEM)

static void
si_dummy_init (TestSiDummy * self)
{
}

static void
si_dummy_reset (WpSessionItem * item)
{
  TestSiDummy *self = TEST_SI_DUMMY (item);

  /* deactivate first */
  wp_object_deactivate (WP_OBJECT (self),
      WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED);

  /* reset */
  self->fail = FALSE;

  WP_SESSION_ITEM_CLASS (si_dummy_parent_class)->reset (item);
}

static gboolean
si_dummy_configure (WpSessionItem * item, WpProperties * props)
{
  TestSiDummy *self = TEST_SI_DUMMY (item);
  const gchar *str = NULL;

  /* reset previous config */
  si_dummy_reset (item);

  str = wp_properties_get (props, "fail");
  if (!str || sscanf(str, "%u", &self->fail) != 1)
    return FALSE;

  wp_session_item_set_properties (WP_SESSION_ITEM (self), props);
  return TRUE;
}

static gpointer
si_dummy_get_associated_proxy (WpSessionItem * item, GType proxy_type)
{
  return NULL;
}

static void
si_dummy_disable_active (WpSessionItem *si)
{
  TestSiDummy *self = TEST_SI_DUMMY (si);

  self->activate_done = FALSE;
  wp_object_update_features (WP_OBJECT (self), 0,
      WP_SESSION_ITEM_FEATURE_ACTIVE);
}

static void
si_dummy_disable_exported (WpSessionItem *si)
{
  TestSiDummy *self = TEST_SI_DUMMY (si);

  self->export_done = FALSE;
  wp_object_update_features (WP_OBJECT (self), 0,
      WP_SESSION_ITEM_FEATURE_EXPORTED);
}

static gboolean
si_dummy_step_activate (gpointer data)
{
  WpTransition *transition = data;
  g_assert_true (WP_IS_TRANSITION (transition));

  TestSiDummy *self = wp_transition_get_source_object (transition);
  g_assert_true (TEST_IS_SI_DUMMY (self));

  if (self->fail) {
    wp_transition_return_error (transition,
        g_error_new (test_domain_quark (), 0, "error"));
  } else {
    self->activate_done = TRUE;
    wp_object_update_features (WP_OBJECT (self),
        WP_SESSION_ITEM_FEATURE_ACTIVE, 0);
  }

  return G_SOURCE_REMOVE;
}

static void
si_dummy_enable_active (WpSessionItem *si, WpTransition *transition)
{
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (si));
  wp_core_idle_add (core, NULL, si_dummy_step_activate, transition, NULL);
}

static gboolean
si_dummy_step_export (gpointer data)
{
  WpTransition *transition = data;
  g_assert_true (WP_IS_TRANSITION (transition));

  TestSiDummy *self = wp_transition_get_source_object (transition);
  g_assert_true (TEST_IS_SI_DUMMY (self));

  if (self->fail) {
    wp_transition_return_error (transition,
        g_error_new (test_domain_quark (), 0, "error"));
  } else {
    self->export_done = TRUE;
    wp_object_update_features (WP_OBJECT (self),
        WP_SESSION_ITEM_FEATURE_EXPORTED, 0);
  }

  return G_SOURCE_REMOVE;
}

static void
si_dummy_enable_exported (WpSessionItem *si, WpTransition *transition)
{
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (si));
  wp_core_idle_add (core, NULL, si_dummy_step_export, transition, NULL);
}

static void
si_dummy_class_init (TestSiDummyClass * klass)
{
  WpSessionItemClass *si_class = (WpSessionItemClass *) klass;

  si_class->reset = si_dummy_reset;
  si_class->configure = si_dummy_configure;
  si_class->get_associated_proxy = si_dummy_get_associated_proxy;
  si_class->disable_active = si_dummy_disable_active;
  si_class->disable_exported = si_dummy_disable_exported;
  si_class->enable_active = si_dummy_enable_active;
  si_class->enable_exported = si_dummy_enable_exported;
}

static void
expect_activate_success (WpObject * object, GAsyncResult * res, gpointer data)
{
  GMainLoop *loop = data;
  g_autoptr (GError) error = NULL;
  g_assert_true (TEST_IS_SI_DUMMY (object));
  g_assert_true (wp_object_activate_finish (object, res, &error));
  g_assert_no_error (error);
  g_main_loop_quit (loop);
}

static void
expect_activate_failure (WpObject * object, GAsyncResult * res, gpointer data)
{
  GMainLoop *loop = data;
  g_autoptr (GError) error = NULL;
  g_assert_true (TEST_IS_SI_DUMMY (object));
  g_assert_false (wp_object_activate_finish (object, res, &error));
  g_assert_error (error, test_domain_quark (), 0);
  g_main_loop_quit (loop);
}

static void
test_configuration (TestSessionItemFixture *fixture, gconstpointer data)
{
  g_autoptr (WpSessionItem) item = NULL;
  TestSiDummy *dummy;

  item = g_object_new (si_dummy_get_type (), "core", fixture->base.core, NULL);
  dummy = TEST_SI_DUMMY (item);

  {
    g_autoptr (WpProperties) p = wp_properties_new_empty ();
    wp_properties_setf (p, "fail", "%u", TRUE);
    g_assert_true (wp_session_item_configure (item, g_steal_pointer (&p)));
    g_assert_true (wp_session_item_is_configured (item));
    g_assert_true (dummy->fail);
  }

  {
    g_autoptr (WpProperties) p = wp_session_item_get_properties (item);
    g_assert_nonnull (p);
    const gchar * str = wp_properties_get (p, "fail");
    gboolean fail = FALSE;
    g_assert_nonnull (str);
    g_assert_true (sscanf(str, "%u", &fail) == 1);
    g_assert_true (fail);
  }
}

static void
test_activation (TestSessionItemFixture *fixture, gconstpointer data)
{
  g_autoptr (WpSessionItem) item = NULL;
  TestSiDummy *dummy;

  item = g_object_new (si_dummy_get_type (), "core", fixture->base.core, NULL);
  dummy = TEST_SI_DUMMY (item);

  {
    g_autoptr (WpProperties) p = wp_properties_new_empty ();
    wp_properties_setf (p, "fail", "%u", FALSE);
    g_assert_true (wp_session_item_configure (item, g_steal_pointer (&p)));
    g_assert_true (wp_session_item_is_configured (item));
  }

  wp_object_activate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_ACTIVE,
      NULL, (GAsyncReadyCallback) expect_activate_success, fixture->base.loop);
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (wp_object_get_active_features (WP_OBJECT (item)), ==,
      WP_SESSION_ITEM_FEATURE_ACTIVE);
  g_assert_true (dummy->activate_done);

  wp_object_deactivate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_ACTIVE);
  g_assert_cmpint (wp_object_get_active_features (WP_OBJECT (item)), ==, 0);
  g_assert_false (dummy->activate_done);
}

static void
test_activation_error (TestSessionItemFixture *fixture, gconstpointer data)
{
  g_autoptr (WpSessionItem) item = NULL;
  TestSiDummy *dummy;

  item = g_object_new (si_dummy_get_type (), "core", fixture->base.core, NULL);
  dummy = TEST_SI_DUMMY (item);

  {
    g_autoptr (WpProperties) p = wp_properties_new_empty ();
    wp_properties_setf (p, "fail", "%u", TRUE);
    g_assert_true (wp_session_item_configure (item, g_steal_pointer (&p)));
    g_assert_true (wp_session_item_is_configured (item));
  }

  wp_object_activate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_ACTIVE,
      NULL, (GAsyncReadyCallback) expect_activate_failure, fixture->base.loop);
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (wp_object_get_active_features (WP_OBJECT (item)), ==, 0);
  g_assert_false (dummy->activate_done);
  g_assert_true (dummy->fail);

  wp_object_deactivate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_ACTIVE);
  g_assert_cmpint (wp_object_get_active_features (WP_OBJECT (item)), ==, 0);
  g_assert_true (dummy->fail);
  g_assert_false (dummy->activate_done);

  wp_session_item_reset (item);
  g_assert_false (dummy->fail);
  g_assert_false (dummy->activate_done);
  g_assert_false (wp_session_item_is_configured (item));
}

static void
test_export (TestSessionItemFixture *fixture, gconstpointer data)
{
  g_autoptr (WpSessionItem) item = NULL;
  TestSiDummy *dummy;

  item = g_object_new (si_dummy_get_type (), "core", fixture->base.core, NULL);
  dummy = TEST_SI_DUMMY (item);

  {
    g_autoptr (WpProperties) p = wp_properties_new_empty ();
    wp_properties_setf (p, "fail", "%u", FALSE);
    g_assert_true (wp_session_item_configure (item, g_steal_pointer (&p)));
    g_assert_true (wp_session_item_is_configured (item));
  }

  wp_object_activate (WP_OBJECT (item),
      WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED,
      NULL, (GAsyncReadyCallback) expect_activate_success, fixture->base.loop);
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (wp_object_get_active_features (WP_OBJECT (item)), ==,
      WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED);
  g_assert_true (dummy->activate_done);
  g_assert_true (dummy->export_done);

  wp_object_deactivate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_EXPORTED);
  g_assert_cmpint (wp_object_get_active_features (WP_OBJECT (item)), ==,
      WP_SESSION_ITEM_FEATURE_ACTIVE);
  g_assert_true (dummy->activate_done);
  g_assert_false (dummy->export_done);

  wp_session_item_reset (item);
  g_assert_false (dummy->activate_done);
  g_assert_false (wp_session_item_is_configured (item));
}

static void
test_export_error (TestSessionItemFixture *fixture, gconstpointer data)
{
  g_autoptr (WpSessionItem) item = NULL;
  TestSiDummy *dummy;

  item = g_object_new (si_dummy_get_type (), "core", fixture->base.core, NULL);
  dummy = TEST_SI_DUMMY (item);

  {
    g_autoptr (WpProperties) p = wp_properties_new_empty ();
    wp_properties_setf (p, "fail", "%u", TRUE);
    g_assert_true (wp_session_item_configure (item, g_steal_pointer (&p)));
    g_assert_true (wp_session_item_is_configured (item));
  }

  wp_object_activate (WP_OBJECT (item),
      WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED,
      NULL, (GAsyncReadyCallback) expect_activate_failure, fixture->base.loop);
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (wp_object_get_active_features (WP_OBJECT (item)), ==, 0);
  g_assert_false (dummy->activate_done);
  g_assert_false (dummy->export_done);

  wp_object_deactivate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_EXPORTED);
  g_assert_cmpint (wp_object_get_active_features (WP_OBJECT (item)), ==, 0);
  g_assert_true (dummy->fail);
  g_assert_false (dummy->activate_done);
  g_assert_false (dummy->export_done);

  wp_session_item_reset (item);
  g_assert_false (dummy->fail);
  g_assert_false (dummy->activate_done);
  g_assert_false (dummy->export_done);
  g_assert_false (wp_session_item_is_configured (item));
}

static void
test_session_item_added (WpObjectManager *om, WpSessionItem *item,
    TestSessionItemFixture *fixture)
{
  g_assert_true (WP_IS_SESSION_ITEM (item));
  fixture->n_items++;
  g_main_loop_quit (fixture->base.loop);
}

static void
test_session_item_removed (WpObjectManager *om, WpSessionItem *item,
    TestSessionItemFixture *fixture)
{
  g_assert_true (WP_IS_SESSION_ITEM (item));
  fixture->n_items--;
  g_main_loop_quit (fixture->base.loop);
}

static gboolean
idle_register (gpointer data)
{
  WpSessionItem *item = data;
  wp_session_item_register (g_object_ref (item));
  return G_SOURCE_REMOVE;
}

static gboolean
idle_remove (gpointer data)
{
  WpSessionItem *item = data;
  wp_session_item_remove (item);
  return G_SOURCE_REMOVE;
}

static void
test_registration (TestSessionItemFixture *fixture, gconstpointer data)
{
  g_autoptr (WpSessionItem) item = NULL;
  g_autoptr (WpSessionItem) item2 = NULL;

  g_signal_connect (fixture->si_om, "object-added",
      (GCallback) test_session_item_added, fixture);
  g_signal_connect (fixture->si_om, "object-removed",
      (GCallback) test_session_item_removed, fixture);
  wp_object_manager_add_interest (fixture->si_om, WP_TYPE_SESSION_ITEM, NULL);
  wp_core_install_object_manager (fixture->base.core, fixture->si_om);

  /* create, configure and export first session item */
  item = g_object_new (si_dummy_get_type (), "core", fixture->base.core, NULL);
  g_assert_nonnull (item);
  {
    g_autoptr (WpProperties) p = wp_properties_new_empty ();
    wp_properties_setf (p, "fail", "%u", FALSE);
    g_assert_true (wp_session_item_configure (item, g_steal_pointer (&p)));
    g_assert_true (wp_session_item_is_configured (item));
  }

  /* create and configure second session item */
  item2 = g_object_new (si_dummy_get_type (), "core", fixture->base.core, NULL);
  g_assert_nonnull (item2);
  {
    g_autoptr (WpProperties) p = wp_properties_new_empty ();
    wp_properties_setf (p, "fail", "%u", TRUE);
    g_assert_true (wp_session_item_configure (item2, g_steal_pointer (&p)));
    g_assert_true (wp_session_item_is_configured (item2));
  }

  /* register item */
  wp_core_idle_add (fixture->base.core, NULL, idle_register, item, NULL);
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (fixture->n_items, ==, 1);

  /* register item2 */
  wp_core_idle_add (fixture->base.core, NULL, idle_register, item2, NULL);
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (fixture->n_items, ==, 2);

  /* lookup item, which has fail set to false */
  {
    g_autoptr (WpSessionItem) si = wp_object_manager_lookup (
        fixture->si_om, WP_TYPE_SESSION_ITEM,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "fail", "=b", FALSE, NULL);
    g_assert_nonnull (si);
    g_assert_true (si == item);
  }

  /* lookup item2, which has fail set to true */
  {
    g_autoptr (WpSessionItem) si = wp_object_manager_lookup (
        fixture->si_om, WP_TYPE_SESSION_ITEM,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "fail", "=b", TRUE, NULL);
    g_assert_nonnull (si);
    g_assert_true (si == item2);
  }

  /* remove item */
  wp_core_idle_add (fixture->base.core, NULL, idle_remove, item, NULL);
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (fixture->n_items, ==, 1);

  /* make sure item cannot be found */
  {
    g_autoptr (WpSessionItem) si = wp_object_manager_lookup (
        fixture->si_om, WP_TYPE_SESSION_ITEM,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "fail", "=b", FALSE, NULL);
    g_assert_null (si);
  }

  /* make sure item2 is still there */
  {
    g_autoptr (WpSessionItem) si = wp_object_manager_lookup (
        fixture->si_om, WP_TYPE_SESSION_ITEM,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "fail", "=b", TRUE, NULL);
    g_assert_nonnull (si);
    g_assert_true (si == item2);
  }

  /* remove item2 */
  wp_core_idle_add (fixture->base.core, NULL, idle_remove, item2, NULL);
  g_main_loop_run (fixture->base.loop);
  g_assert_cmpint (fixture->n_items, ==, 0);

  /* make sure item cannot be found */
  {
    g_autoptr (WpSessionItem) si = wp_object_manager_lookup (
        fixture->si_om, WP_TYPE_SESSION_ITEM,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "fail", "=b", TRUE, NULL);
    g_assert_null (si);
  }
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/session-item/configuration", TestSessionItemFixture, NULL,
      test_session_item_setup, test_configuration, test_session_item_teardown);
  g_test_add ("/wp/session-item/activation", TestSessionItemFixture, NULL,
      test_session_item_setup, test_activation, test_session_item_teardown);
  g_test_add ("/wp/session-item/activation-error", TestSessionItemFixture, NULL,
      test_session_item_setup, test_activation_error, test_session_item_teardown);
  g_test_add ("/wp/session-item/export", TestSessionItemFixture, NULL,
      test_session_item_setup, test_export, test_session_item_teardown);
  g_test_add ("/wp/session-item/export-error", TestSessionItemFixture, NULL,
      test_session_item_setup, test_export_error, test_session_item_teardown);
  g_test_add ("/wp/session-item/registration", TestSessionItemFixture, NULL,
      test_session_item_setup, test_registration, test_session_item_teardown);

  return g_test_run ();
}
 070701000001EB000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002400000000wireplumber-0.5.0/tests/wp/settings   070701000001EC000081A400000000000000000000000165F863040000A8D6000000000000000000000000000000000000002600000000wireplumber-0.5.0/tests/wp/settings.c /* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */
#include "../common/base-test-fixture.h"

typedef struct {
  WpBaseTestFixture base;

  WpProperties *loaded_settings;
  WpProperties *loaded_schema;

  WpImplMetadata *metadata;
  WpImplMetadata *metadata_schema;
  WpImplMetadata *metadata_persistent;

  WpSettings *settings;

  gchar *triggered_setting;
  WpSpaJson *triggered_setting_value;
  gboolean triggered_callback;
} TestSettingsFixture;

static void
test_conf_file_setup (TestSettingsFixture *self, gconstpointer user_data)
{
  self->base.conf_file = g_strdup_printf ("%s/settings/wireplumber.conf",
      g_getenv ("G_TEST_SRCDIR"));

  wp_base_test_fixture_setup (&self->base, WP_BASE_TEST_FLAG_CLIENT_CORE);
}

static void
test_conf_file_teardown (TestSettingsFixture *self, gconstpointer user_data)
{
  wp_base_test_fixture_teardown (&self->base);
}

static WpProperties *
do_parse_section (WpSpaJson *json)
{
  g_autoptr (WpProperties) settings = wp_properties_new_empty ();
  g_autoptr (WpIterator) iter = wp_spa_json_new_iterator (json);
  g_auto (GValue) item = G_VALUE_INIT;

  if (!wp_spa_json_is_object (json))
    return NULL;

  while (wp_iterator_next (iter, &item)) {
    WpSpaJson *j = g_value_get_boxed (&item);
    g_autofree gchar *name = wp_spa_json_parse_string (j);
    g_autofree gchar *value = NULL;

    g_value_unset (&item);
    g_assert_true (wp_iterator_next (iter, &item));
    j = g_value_get_boxed (&item);

    value = wp_spa_json_to_string (j);
    g_value_unset (&item);

    if (name && value)
      wp_properties_set (settings, name, value);
  }

  return g_steal_pointer (&settings);
}

static void
test_parsing_setup (TestSettingsFixture *self, gconstpointer user_data)
{
  test_conf_file_setup (self, user_data);

  g_autoptr (WpConf) conf = wp_core_get_conf (self->base.core);
  g_assert_nonnull (conf);

  {
    g_autoptr (WpSpaJson) json = wp_conf_get_section (conf,
        "wireplumber.settings");
    g_assert_nonnull (json);
    self->loaded_settings = do_parse_section (json);
    g_assert_nonnull (self->loaded_settings);
  }

  {
    g_autoptr (WpSpaJson) json = wp_conf_get_section (conf,
        "wireplumber.settings.schema");
    self->loaded_schema = do_parse_section (json);
    g_assert_nonnull (self->loaded_schema);
  }
}

static void
test_parsing_teardown (TestSettingsFixture *self, gconstpointer user_data)
{
  g_clear_pointer (&self->loaded_settings, wp_properties_unref);
  g_clear_pointer (&self->loaded_schema, wp_properties_unref);

  test_conf_file_teardown (self, user_data);
}

static void
on_metadata_persistent_activated (WpMetadata * m, GAsyncResult * res,
    gpointer user_data)
{
  TestSettingsFixture *self = user_data;

  g_assert_true (wp_object_activate_finish (WP_OBJECT (m), res, NULL));

  g_main_loop_quit (self->base.loop);
}

static void
on_metadata_schema_activated (WpMetadata * m, GAsyncResult * res,
    gpointer user_data)
{
  TestSettingsFixture *self = user_data;
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;

  g_assert_true (wp_object_activate_finish (WP_OBJECT (m), res, NULL));

  for (it = wp_properties_new_iterator (self->loaded_schema);
      wp_iterator_next (it, &item);
      g_value_unset (&item)) {
    WpPropertiesItem *pi = g_value_get_boxed (&item);
    const gchar *key = wp_properties_item_get_key (pi);
    const gchar *value = wp_properties_item_get_value (pi);

    wp_metadata_set (m, 0, key, "Spa:String:JSON", value);
  }

  self->metadata_persistent = wp_impl_metadata_new_full (self->base.core,
      WP_SETTINGS_PERSISTENT_METADATA_NAME_PREFIX "sm-settings", NULL);

  wp_object_activate (WP_OBJECT (self->metadata_persistent),
      WP_OBJECT_FEATURES_ALL,
      NULL,
      (GAsyncReadyCallback)on_metadata_persistent_activated,
      self);
}

static void
on_metadata_activated (WpMetadata * m, GAsyncResult * res, gpointer user_data)
{
  TestSettingsFixture *self = user_data;
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;

  g_assert_true (wp_object_activate_finish (WP_OBJECT (m), res, NULL));

  for (it = wp_properties_new_iterator (self->loaded_settings);
        wp_iterator_next (it, &item);
        g_value_unset (&item)) {
    WpPropertiesItem *pi = g_value_get_boxed (&item);
    const gchar *key = wp_properties_item_get_key (pi);
    const gchar *value = wp_properties_item_get_value (pi);

    wp_metadata_set (m, 0, key, "Spa:String:JSON", value);
  }

  self->metadata_schema = wp_impl_metadata_new_full (self->base.core,
      WP_SETTINGS_SCHEMA_METADATA_NAME_PREFIX "sm-settings", NULL);

  wp_object_activate (WP_OBJECT (self->metadata_schema),
      WP_OBJECT_FEATURES_ALL,
      NULL,
      (GAsyncReadyCallback)on_metadata_schema_activated,
      self);
}

static void
test_metadata_setup (TestSettingsFixture *self, gconstpointer user_data)
{
  test_parsing_setup (self, user_data);

  self->metadata = wp_impl_metadata_new_full (self->base.core, "sm-settings",
      NULL);

  wp_object_activate (WP_OBJECT (self->metadata),
      WP_OBJECT_FEATURES_ALL,
      NULL,
      (GAsyncReadyCallback)on_metadata_activated,
      self);

  g_main_loop_run (self->base.loop);
}

static void
test_metadata_teardown (TestSettingsFixture *self, gconstpointer user_data)
{
  test_parsing_teardown (self, user_data);

  g_clear_object (&self->metadata);
  g_clear_object (&self->metadata_schema);
  g_clear_object (&self->metadata_persistent);
}

static void
on_settings_ready (WpSettings *s, GAsyncResult *res, gpointer data)
{
  TestSettingsFixture *self = data;

  g_assert_true (wp_object_activate_finish (WP_OBJECT (s), res, NULL));

  wp_core_register_object (self->base.core, g_object_ref (s));

  g_main_loop_quit (self->base.loop);
}

static void
test_settings_setup (TestSettingsFixture *self, gconstpointer user_data)
{
  test_metadata_setup (self, user_data);

  self->settings = wp_settings_new (self->base.core, "sm-settings");
  wp_object_activate (WP_OBJECT (self->settings),
      WP_OBJECT_FEATURES_ALL,
      NULL,
      (GAsyncReadyCallback)on_settings_ready,
      self);
  g_main_loop_run (self->base.loop);
}

static void
test_settings_teardown (TestSettingsFixture *self, gconstpointer user_data)
{
  test_metadata_teardown (self, user_data);
  g_clear_object (&self->settings);
}

static void
test_basic (TestSettingsFixture *self, gconstpointer data)
{
  /* Find */
  {
    g_autoptr (WpSettings) s1 =
        wp_settings_find (self->base.core, NULL);
    g_autoptr (WpSettings) s2 =
        wp_settings_find (self->base.core, "sm-settings");
    g_autoptr (WpSettings) s3 =
        wp_settings_find (self->base.core, "blah-blah");

    g_assert_true (self->settings == s1);
    g_assert_true (s1 == s2);
    g_assert_false (s1 == s3);
    g_assert_null (s3);

    g_autoptr (WpSettings) s4 = wp_settings_find (self->base.core, NULL);
    g_auto (GValue) value = G_VALUE_INIT;
    g_object_get_property (G_OBJECT(s4), "metadata-name", &value);
    g_assert_cmpstr (g_value_get_string (&value), ==, "sm-settings");
  }

  /* Iterator */
  {
    g_autoptr (WpProperties) settings = wp_properties_new_empty ();
    g_autoptr (WpIterator) it = wp_settings_new_iterator (self->settings);
    g_auto (GValue) val = G_VALUE_INIT;
    for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpSettingsItem *si = g_value_get_boxed (&val);
      const gchar *key = wp_settings_item_get_key (si);
      g_autoptr (WpSpaJson) value = wp_settings_item_get_value (si);
      wp_properties_set (settings, key, wp_spa_json_get_data (value));
    }
    g_assert_true (wp_properties_matches (self->loaded_settings, settings));
  }
}

static void
test_get_set_save_reset_delete (TestSettingsFixture *self, gconstpointer data)
{
  WpSettings *s = self->settings;
  g_autoptr (WpSettingsSpec) spec = NULL;
  const gchar *desc = NULL;
  g_autoptr (WpSpaJson) def = NULL;
  g_autoptr (WpSpaJson) min = NULL;
  g_autoptr (WpSpaJson) max = NULL;
  g_autoptr (WpSpaJson) j = NULL;

  /* Undefined */
  {
    spec = wp_settings_get_spec (s, "test-setting-undefined");
    g_assert_null (spec);

    j = wp_settings_get (s, "test-setting-undefined");
    g_assert_null (j);

    j = wp_spa_json_new_null ();
    g_assert_false (wp_settings_set (s, "test-setting-undefined", j));
    g_clear_pointer (&j, wp_spa_json_unref);
  }

  /* Boolean */
  {
    gboolean value = FALSE;

    spec = wp_settings_get_spec (s, "test-setting-bool");
    desc = wp_settings_spec_get_description (spec);
    g_assert_nonnull (desc);
    g_assert_cmpstr (desc, ==, "test-setting-bool description");
    g_assert_true (
        wp_settings_spec_get_value_type (spec) == WP_SETTINGS_SPEC_TYPE_BOOL);
    def = wp_settings_spec_get_default_value (spec);
    g_assert_nonnull (def);
    g_assert_true (wp_spa_json_parse_boolean (def, &value));
    g_assert_false (value);
    g_clear_pointer (&def, wp_spa_json_unref);
    g_clear_pointer (&spec, wp_settings_spec_unref);

    j = wp_settings_get (s, "test-setting-bool");
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_parse_boolean (j, &value));
    g_assert_true (value);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_boolean (FALSE);
    g_assert_true (wp_settings_set (s, "test-setting-bool", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-bool");
    g_assert_true (wp_spa_json_parse_boolean (j, &value));
    g_assert_false (value);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_boolean (TRUE);
    g_assert_true (wp_settings_set (s, "test-setting-bool", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-bool");
    g_assert_true (wp_spa_json_parse_boolean (j, &value));
    g_assert_true (value);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_int (1);
    g_assert_false (wp_settings_set (s, "test-setting-bool", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-bool");
    g_assert_true (wp_spa_json_parse_boolean (j, &value));
    g_assert_true (value);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_settings_get_saved (s, "test-setting-bool");
    g_assert_null (j);
    g_assert_true (wp_settings_save (s, "test-setting-bool"));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get_saved (s, "test-setting-bool");
    g_assert_true (wp_spa_json_parse_boolean (j, &value));
    g_assert_true (value);
    g_clear_pointer (&j, wp_spa_json_unref);
    g_assert_true (wp_settings_delete (s, "test-setting-bool"));
    j = wp_settings_get_saved (s, "test-setting-bool");
    g_assert_null (j);
    g_assert_true (wp_settings_reset (s, "test-setting-bool"));
    j = wp_settings_get (s, "test-setting-bool");
    g_assert_true (wp_spa_json_parse_boolean (j, &value));
    g_assert_false (value);
    g_clear_pointer (&j, wp_spa_json_unref);
  }

  /* Int */
  {
    gint value = 0;

    spec = wp_settings_get_spec (s, "test-setting-int");
    desc = wp_settings_spec_get_description (spec);
    g_assert_nonnull (desc);
    g_assert_cmpstr (desc, ==, "test-setting-int description");
    g_assert_true (
        wp_settings_spec_get_value_type (spec) == WP_SETTINGS_SPEC_TYPE_INT);
    def = wp_settings_spec_get_default_value (spec);
    min = wp_settings_spec_get_min_value (spec);
    max = wp_settings_spec_get_max_value (spec);
    g_assert_nonnull (def);
    g_assert_nonnull (min);
    g_assert_nonnull (max);
    g_assert_true (wp_spa_json_parse_int (def, &value));
    g_assert_cmpint (value, ==, 0);
    g_assert_true (wp_spa_json_parse_int (min, &value));
    g_assert_cmpint (value, ==, -100);
    g_assert_true (wp_spa_json_parse_int (max, &value));
    g_assert_cmpint (value, ==, 100);
    g_clear_pointer (&def, wp_spa_json_unref);
    g_clear_pointer (&min, wp_spa_json_unref);
    g_clear_pointer (&max, wp_spa_json_unref);
    g_clear_pointer (&spec, wp_settings_spec_unref);

    j = wp_settings_get (s, "test-setting-int");
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_parse_int (j, &value));
    g_assert_cmpint (value, ==, -20);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_int (3);
    g_assert_true (wp_settings_set (s, "test-setting-int", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-int");
    g_assert_true (wp_spa_json_parse_int (j, &value));
    g_assert_cmpint (value, ==, 3);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_int (1000);
    g_assert_false (wp_settings_set (s, "test-setting-int", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-int");
    g_assert_true (wp_spa_json_parse_int (j, &value));
    g_assert_cmpint (value, ==, 3);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_int (-1000);
    g_assert_false (wp_settings_set (s, "test-setting-int", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-int");
    g_assert_true (wp_spa_json_parse_int (j, &value));
    g_assert_cmpint (value, ==, 3);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_float (50.5);
    g_assert_false (wp_settings_set (s, "test-setting-int", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-int");
    g_assert_true (wp_spa_json_parse_int (j, &value));
    g_assert_cmpint (value, ==, 3);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_settings_get_saved (s, "test-setting-int");
    g_assert_null (j);
    g_assert_true (wp_settings_save (s, "test-setting-int"));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get_saved (s, "test-setting-int");
    g_assert_true (wp_spa_json_parse_int (j, &value));
    g_assert_cmpint (value, ==, 3);
    g_clear_pointer (&j, wp_spa_json_unref);
    g_assert_true (wp_settings_delete (s, "test-setting-int"));
    j = wp_settings_get_saved (s, "test-setting-int");
    g_assert_null (j);
    g_assert_true (wp_settings_reset (s, "test-setting-int"));
    j = wp_settings_get (s, "test-setting-int");
    g_assert_true (wp_spa_json_parse_int (j, &value));
    g_assert_cmpint (value, ==, 0);
    g_clear_pointer (&j, wp_spa_json_unref);
  }

  /* Float */
  {
    gfloat value = 0.0;

    spec = wp_settings_get_spec (s, "test-setting-float");
    desc = wp_settings_spec_get_description (spec);
    g_assert_nonnull (desc);
    g_assert_cmpstr (desc, ==, "test-setting-float description");
    g_assert_true (
        wp_settings_spec_get_value_type (spec) == WP_SETTINGS_SPEC_TYPE_FLOAT);
    def = wp_settings_spec_get_default_value (spec);
    min = wp_settings_spec_get_min_value (spec);
    max = wp_settings_spec_get_max_value (spec);
    g_assert_nonnull (def);
    g_assert_nonnull (min);
    g_assert_nonnull (max);
    g_assert_true (wp_spa_json_parse_float (def, &value));
    g_assert_cmpfloat_with_epsilon (value, 0.0, 0.001);
    g_assert_true (wp_spa_json_parse_float (min, &value));
    g_assert_cmpfloat_with_epsilon (value, -100.0, 0.001);
    g_assert_true (wp_spa_json_parse_float (max, &value));
    g_assert_cmpfloat_with_epsilon (value, 100.0, 0.001);
    g_clear_pointer (&def, wp_spa_json_unref);
    g_clear_pointer (&min, wp_spa_json_unref);
    g_clear_pointer (&max, wp_spa_json_unref);
    g_clear_pointer (&spec, wp_settings_spec_unref);

    j = wp_settings_get (s, "test-setting-float");
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_parse_float (j, &value));
    g_assert_cmpfloat_with_epsilon (value, 3.14, 0.001);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_float (99.5);
    g_assert_true (wp_settings_set (s, "test-setting-float", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-float");
    g_assert_true (wp_spa_json_parse_float (j, &value));
    g_assert_cmpfloat_with_epsilon (value, 99.5, 0.001);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_float (150.5);
    g_assert_false (wp_settings_set (s, "test-setting-float", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-float");
    g_assert_true (wp_spa_json_parse_float (j, &value));
    g_assert_cmpfloat_with_epsilon (value, 99.5, 0.001);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_float (-150.5);
    g_assert_false (wp_settings_set (s, "test-setting-float", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-float");
    g_assert_true (wp_spa_json_parse_float (j, &value));
    g_assert_cmpfloat_with_epsilon (value, 99.5, 0.001);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_int (50);
    g_assert_false (wp_settings_set (s, "test-setting-float", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-float");
    g_assert_true (wp_spa_json_parse_float (j, &value));
    g_assert_cmpfloat_with_epsilon (value, 99.5, 0.001);
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_settings_get_saved (s, "test-setting-float");
    g_assert_null (j);
    g_assert_true (wp_settings_save (s, "test-setting-float"));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get_saved (s, "test-setting-float");
    g_assert_true (wp_spa_json_parse_float (j, &value));
    g_assert_cmpfloat_with_epsilon (value, 99.5, 0.001);
    g_clear_pointer (&j, wp_spa_json_unref);
    g_assert_true (wp_settings_delete (s, "test-setting-float"));
    j = wp_settings_get_saved (s, "test-setting-float");
    g_assert_null (j);
    g_assert_true (wp_settings_reset (s, "test-setting-float"));
    j = wp_settings_get (s, "test-setting-float");
    g_assert_true (wp_spa_json_parse_float (j, &value));
    g_assert_cmpfloat_with_epsilon (value, 0.0, 0.001);
    g_clear_pointer (&j, wp_spa_json_unref);
  }

  /* String */
  {
    g_autofree gchar *value = NULL;

    {
      spec = wp_settings_get_spec (s, "test-setting-string");
      desc = wp_settings_spec_get_description (spec);
      g_assert_nonnull (desc);
      g_assert_cmpstr (desc, ==, "test-setting-string description");
      g_assert_true (wp_settings_spec_get_value_type (spec) ==
          WP_SETTINGS_SPEC_TYPE_STRING);
      def = wp_settings_spec_get_default_value (spec);
      g_assert_nonnull (def);
      value = wp_spa_json_parse_string (def);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "default");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&def, wp_spa_json_unref);
      g_clear_pointer (&spec, wp_settings_spec_unref);

      j = wp_settings_get (s, "test-setting-string");
      g_assert_nonnull (j);
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "blahblah");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_string ("new-string-value");
      g_assert_true (wp_settings_set (s, "test-setting-string", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-string");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "new-string-value");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_int (99);
      g_assert_false (wp_settings_set (s, "test-setting-string", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-string");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "new-string-value");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_string ("99");
      g_assert_true (wp_settings_set (s, "test-setting-string", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-string");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "99");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_settings_get_saved (s, "test-setting-string");
      g_assert_null (j);
      g_assert_true (wp_settings_save (s, "test-setting-string"));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get_saved (s, "test-setting-string");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "99");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);
      g_assert_true (wp_settings_delete (s, "test-setting-string"));
      j = wp_settings_get_saved (s, "test-setting-string");
      g_assert_null (j);
      g_assert_true (wp_settings_reset (s, "test-setting-string"));
      j = wp_settings_get (s, "test-setting-string");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "default");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);
    }

    {
      spec = wp_settings_get_spec (s, "test-setting-string");
      desc = wp_settings_spec_get_description (spec);
      g_assert_nonnull (desc);
      g_assert_cmpstr (desc, ==, "test-setting-string description");
      g_assert_true (wp_settings_spec_get_value_type (spec) ==
          WP_SETTINGS_SPEC_TYPE_STRING);
      def = wp_settings_spec_get_default_value (spec);
      g_assert_nonnull (def);
      value = wp_spa_json_parse_string (def);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "default");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&def, wp_spa_json_unref);
      g_clear_pointer (&spec, wp_settings_spec_unref);

      j = wp_settings_get (s, "test-setting-string2");
      g_assert_nonnull (j);
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "a string with \"quotes\"");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_string ("a new string with \"quotes\"");
      g_assert_true (wp_settings_set (s, "test-setting-string2", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-string2");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "a new string with \"quotes\"");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_float (99.5);
      g_assert_false (wp_settings_set (s, "test-setting-string2", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-string2");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "a new string with \"quotes\"");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_string ("99.5");
      g_assert_true (wp_settings_set (s, "test-setting-string2", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-string2");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "99.5");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_settings_get_saved (s, "test-setting-string2");
      g_assert_null (j);
      g_assert_true (wp_settings_save (s, "test-setting-string2"));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get_saved (s, "test-setting-string2");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "99.5");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);
      g_assert_true (wp_settings_delete (s, "test-setting-string2"));
      j = wp_settings_get_saved (s, "test-setting-string2");
      g_assert_null (j);
      g_assert_true (wp_settings_reset (s, "test-setting-string2"));
      j = wp_settings_get (s, "test-setting-string2");
      value = wp_spa_json_parse_string (j);
      g_assert_nonnull (value);
      g_assert_cmpstr (value, ==, "default");
      g_clear_pointer (&value, g_free);
      g_clear_pointer (&j, wp_spa_json_unref);
    }
  }

  /* Array */
  {
    {
      spec = wp_settings_get_spec (s, "test-setting-array");
      desc = wp_settings_spec_get_description (spec);
      g_assert_nonnull (desc);
      g_assert_cmpstr (desc, ==, "test-setting-array description");
      g_assert_true (wp_settings_spec_get_value_type (spec) ==
          WP_SETTINGS_SPEC_TYPE_ARRAY);
      def = wp_settings_spec_get_default_value (spec);
      g_assert_nonnull (def);
      g_assert_cmpstr (wp_spa_json_get_data (def), ==, "[]");
      g_clear_pointer (&def, wp_spa_json_unref);
      g_clear_pointer (&spec, wp_settings_spec_unref);

      j = wp_settings_get (s, "test-setting-array");
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_array (j));
      g_assert_cmpstr (wp_spa_json_get_data (j), ==, "[1, 2, 3]");
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_array ("i", 4, "i", 5, NULL);
      g_assert_true (wp_settings_set (s, "test-setting-array", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-array");
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_array (j));
      g_assert_cmpstr (wp_spa_json_get_data (j), ==, "[4, 5]");
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_int (6);
      g_assert_false (wp_settings_set (s, "test-setting-array", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-array");
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_array (j));
      g_assert_cmpstr (wp_spa_json_get_data (j), ==, "[4, 5]");
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_settings_get_saved (s, "test-setting-array");
      g_assert_null (j);
      g_assert_true (wp_settings_save (s, "test-setting-array"));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get_saved (s, "test-setting-array");
      g_assert_cmpstr (wp_spa_json_get_data (j), ==, "[4, 5]");
      g_clear_pointer (&j, wp_spa_json_unref);
      g_assert_true (wp_settings_delete (s, "test-setting-array"));
      j = wp_settings_get_saved (s, "test-setting-array");
      g_assert_null (j);
      g_assert_true (wp_settings_reset (s, "test-setting-array"));
      j = wp_settings_get (s, "test-setting-array");
      g_assert_cmpstr (wp_spa_json_get_data (j), ==, "[]");
      g_clear_pointer (&j, wp_spa_json_unref);
    }

    {
      spec = wp_settings_get_spec (s, "test-setting-array2");
      desc = wp_settings_spec_get_description (spec);
      g_assert_nonnull (desc);
      g_assert_cmpstr (desc, ==, "test-setting-array2 description");
      g_assert_true (wp_settings_spec_get_value_type (spec) ==
          WP_SETTINGS_SPEC_TYPE_ARRAY);
      def = wp_settings_spec_get_default_value (spec);
      g_assert_nonnull (def);
      g_assert_cmpstr (wp_spa_json_get_data (def), ==, "[]");
      g_clear_pointer (&def, wp_spa_json_unref);
      g_clear_pointer (&spec, wp_settings_spec_unref);

      j = wp_settings_get (s, "test-setting-array2");
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_array (j));
      g_assert_cmpstr (wp_spa_json_get_data (j), ==,
          "[\"test1\", \"test 2\", \"test three\", \"test-four\"]");
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_array ("s", "foo", "s", "bar", NULL);
      g_assert_true (wp_settings_set (s, "test-setting-array2", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-array2");
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_array (j));
      g_assert_cmpstr (wp_spa_json_get_data (j), ==, "[\"foo\", \"bar\"]");
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_spa_json_new_string ("value");
      g_assert_false (wp_settings_set (s, "test-setting-array2", j));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get (s, "test-setting-array2");
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_array (j));
      g_assert_cmpstr (wp_spa_json_get_data (j), ==, "[\"foo\", \"bar\"]");
      g_clear_pointer (&j, wp_spa_json_unref);

      j = wp_settings_get_saved (s, "test-setting-array2");
      g_assert_null (j);
      g_assert_true (wp_settings_save (s, "test-setting-array2"));
      g_clear_pointer (&j, wp_spa_json_unref);
      j = wp_settings_get_saved (s, "test-setting-array2");
      g_assert_cmpstr (wp_spa_json_get_data (j), ==, "[\"foo\", \"bar\"]");
      g_clear_pointer (&j, wp_spa_json_unref);
      g_assert_true (wp_settings_delete (s, "test-setting-array2"));
      j = wp_settings_get_saved (s, "test-setting-array2");
      g_assert_null (j);
      g_assert_true (wp_settings_reset (s, "test-setting-array2"));
      j = wp_settings_get (s, "test-setting-array2");
      g_assert_cmpstr (wp_spa_json_get_data (j), ==, "[]");
      g_clear_pointer (&j, wp_spa_json_unref);
    }
  }

  /* Object */
  {
    spec = wp_settings_get_spec (s, "test-setting-object");
    desc = wp_settings_spec_get_description (spec);
    g_assert_nonnull (desc);
    g_assert_cmpstr (desc, ==, "test-setting-object description");
    g_assert_true (wp_settings_spec_get_value_type (spec) ==
        WP_SETTINGS_SPEC_TYPE_OBJECT);
    def = wp_settings_spec_get_default_value (spec);
    g_assert_nonnull (def);
    g_assert_cmpstr (wp_spa_json_get_data (def), ==, "{}");
    g_clear_pointer (&def, wp_spa_json_unref);
    g_clear_pointer (&spec, wp_settings_spec_unref);

    j = wp_settings_get (s, "test-setting-object");
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_object (j));
    g_assert_cmpstr (wp_spa_json_get_data(j), ==,
      "{ key1: \"value\", key2: 2, key3: true }");
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_object (
        "key1", "s", "new-value",
        "key2", "i", 5,
        "key4", "b", FALSE,
        NULL);
    g_assert_true (wp_settings_set (s, "test-setting-object", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-object");
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_object (j));
    g_assert_cmpstr (wp_spa_json_get_data (j), ==,
        "{\"key1\":\"new-value\", \"key2\":5, \"key4\":false}");
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_spa_json_new_string ("value");
    g_assert_false (wp_settings_set (s, "test-setting-object", j));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get (s, "test-setting-object");
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_object (j));
    g_assert_cmpstr (wp_spa_json_get_data (j), ==,
        "{\"key1\":\"new-value\", \"key2\":5, \"key4\":false}");
    g_clear_pointer (&j, wp_spa_json_unref);

    j = wp_settings_get_saved (s, "test-setting-object");
    g_assert_null (j);
    g_assert_true (wp_settings_save (s, "test-setting-object"));
    g_clear_pointer (&j, wp_spa_json_unref);
    j = wp_settings_get_saved (s, "test-setting-object");
    g_assert_cmpstr (wp_spa_json_get_data (j), ==,
        "{\"key1\":\"new-value\", \"key2\":5, \"key4\":false}");
    g_clear_pointer (&j, wp_spa_json_unref);
    g_assert_true (wp_settings_delete (s, "test-setting-object"));
    j = wp_settings_get_saved (s, "test-setting-object");
    g_assert_null (j);
    g_assert_true (wp_settings_reset (s, "test-setting-object"));
    j = wp_settings_get (s, "test-setting-object");
    g_assert_cmpstr (wp_spa_json_get_data (j), ==, "{}");
    g_clear_pointer (&j, wp_spa_json_unref);
  }
}

static void
test_save_reset_delete_all (TestSettingsFixture *self, gconstpointer data)
{
  WpSettings *s = self->settings;

  /* Check settings are the same as the loaded ones, and not saved */
  {
    g_autoptr (WpProperties) props = wp_properties_new_empty ();
    g_autoptr (WpIterator) it = wp_settings_new_iterator (s);
    g_auto (GValue) val = G_VALUE_INIT;
    for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpSettingsItem *si = g_value_get_boxed (&val);
      const gchar *key = wp_settings_item_get_key (si);
      g_autoptr (WpSpaJson) value = wp_settings_item_get_value (si);
      g_autoptr (WpSpaJson) saved = wp_settings_get_saved (s, key);
      g_assert_null (saved);
      wp_properties_set (props, key, wp_spa_json_get_data (value));
    }
    g_assert_true (wp_properties_matches (self->loaded_settings, props));
  }

  /* Reset all settings to default value */
  wp_settings_reset_all (self->settings);

  /* Check all settings are set to their default values */
  {
    gint n_settings = 0;
    g_autoptr (WpIterator) it = wp_settings_new_iterator (s);
    g_auto (GValue) val = G_VALUE_INIT;
    for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpSettingsItem *si = g_value_get_boxed (&val);
      const gchar *key = wp_settings_item_get_key (si);
      g_autoptr (WpSpaJson) value = wp_settings_item_get_value (si);
      g_autoptr (WpSettingsSpec) spec = NULL;
      g_autoptr (WpSpaJson) def = NULL;
      spec = wp_settings_get_spec (s, key);
      g_assert_nonnull (spec);
      def = wp_settings_spec_get_default_value (spec);
      g_assert_cmpstr (wp_spa_json_get_data (def), ==,
          wp_spa_json_get_data (value));
      n_settings++;
    }
    g_assert_cmpint (n_settings, ==,
        wp_properties_get_count (self->loaded_settings));
  }

  /* Save all settings */
  wp_settings_save_all (self->settings);

  /* Check all settings are saved */
  {
    g_autoptr (WpIterator) it = wp_settings_new_iterator (s);
    g_auto (GValue) val = G_VALUE_INIT;
    for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpSettingsItem *si = g_value_get_boxed (&val);
      const gchar *key = wp_settings_item_get_key (si);
      g_autoptr (WpSpaJson) saved = wp_settings_get_saved (s, key);
      g_assert_nonnull (saved);
    }
  }

  /* Delete all saved settings */
  wp_settings_delete_all (self->settings);

  /* Check all settings are not saved anymore */
  {
    g_autoptr (WpIterator) it = wp_settings_new_iterator (s);
    g_auto (GValue) val = G_VALUE_INIT;
    for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
      WpSettingsItem *si = g_value_get_boxed (&val);
      const gchar *key = wp_settings_item_get_key (si);
      g_autoptr (WpSpaJson) saved = wp_settings_get_saved (s, key);
      g_assert_null (saved);
    }
  }
}

static void
wp_settings_changed_callback (WpSettings *s, const gchar *key,
    WpSpaJson *value, gpointer user_data)
{
  TestSettingsFixture *self = user_data;
  g_autoptr (WpSettingsSpec) spec = NULL;

  g_assert_cmpstr (key, ==, self->triggered_setting);
  g_assert_nonnull (value);

  self->triggered_callback = TRUE;

  spec = wp_settings_get_spec (s, key);
  g_assert_nonnull (spec);

  switch (wp_settings_spec_get_value_type (spec)) {
    case WP_SETTINGS_SPEC_TYPE_BOOL: {
      gboolean val = FALSE, expected = FALSE;
      g_assert_true (wp_spa_json_parse_boolean (value, &val));
      g_assert_true (wp_spa_json_parse_boolean (self->triggered_setting_value,
          &expected));
      g_assert_true (val == expected);
      break;
    }
    case WP_SETTINGS_SPEC_TYPE_INT: {
      gint val = 0, expected = 0;
      g_assert_true (wp_spa_json_parse_int (value, &val));
      g_assert_true (wp_spa_json_parse_int (self->triggered_setting_value,
          &expected));
      g_assert_cmpint (val, ==, expected);
      break;
    }
    case WP_SETTINGS_SPEC_TYPE_FLOAT: {
      gfloat val = 0.0, expected = 0.0;
      g_assert_true (wp_spa_json_parse_float (value, &val));
      g_assert_true (wp_spa_json_parse_float (self->triggered_setting_value,
          &expected));
      g_assert_cmpfloat_with_epsilon (val, expected, 0.001);
      break;
    }
    case WP_SETTINGS_SPEC_TYPE_STRING: {
      g_autofree gchar *val = wp_spa_json_parse_string (value);
      g_autofree gchar *expected = wp_spa_json_parse_string (
          self->triggered_setting_value);
      g_assert_nonnull (value);
      g_assert_nonnull (expected);
      g_assert_cmpstr (val, ==, expected);
      break;
    }
    case WP_SETTINGS_SPEC_TYPE_ARRAY: {
      g_assert_cmpstr (wp_spa_json_get_data (value), ==,
          wp_spa_json_get_data (self->triggered_setting_value));
      break;
    }
    case WP_SETTINGS_SPEC_TYPE_OBJECT: {
      g_assert_cmpstr (wp_spa_json_get_data (value), ==,
          wp_spa_json_get_data (self->triggered_setting_value));
      break;
    }
    default:
      g_assert_not_reached ();
  }
}

static void
test_subscribe_unsibscribe (TestSettingsFixture *self, gconstpointer data)
{
  WpSettings *s = self->settings;
  g_autoptr (WpSpaJson) json = NULL;
  guintptr sub_id;

  /* Boolean */
  {
    sub_id = wp_settings_subscribe (s, "test*",
        wp_settings_changed_callback, (gpointer)self);

    json = wp_spa_json_new_boolean (FALSE);
    self->triggered_setting = "test-setting-bool";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_true (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);

    wp_settings_unsubscribe (s, sub_id);

    json = wp_spa_json_new_boolean (TRUE);
    self->triggered_setting = "test-setting-bool";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    wp_settings_set (s, self->triggered_setting, json);
    g_assert_false (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);
  }

  /* Int */
  {
    sub_id = wp_settings_subscribe (s, "test*",
        wp_settings_changed_callback, (gpointer)self);

    json = wp_spa_json_new_int (99);
    self->triggered_setting = "test-setting-int";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_true (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);

    wp_settings_unsubscribe (s, sub_id);

    json = wp_spa_json_new_int (90);
    self->triggered_setting = "test-setting-int";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_false (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);
  }

  /* Float */
  {
    sub_id = wp_settings_subscribe (s, "test*",
        wp_settings_changed_callback, (gpointer)self);

    json = wp_spa_json_new_float (45.5);
    self->triggered_setting = "test-setting-float";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_true (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);

    wp_settings_unsubscribe (s, sub_id);

    json = wp_spa_json_new_float (30.5);
    self->triggered_setting = "test-setting-float";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_false (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);
  }

  /* String */
  {
    sub_id = wp_settings_subscribe (s, "test*",
        wp_settings_changed_callback, (gpointer)self);

    json = wp_spa_json_new_string ("lets not blabber");
    self->triggered_setting = "test-setting-string";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_true (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);

    wp_settings_unsubscribe (s, sub_id);

    json = wp_spa_json_new_string ("lets blabber");
    self->triggered_setting = "test-setting-string";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_false (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);
  }


  /* Array */
  {
    sub_id = wp_settings_subscribe (s, "test*",
        wp_settings_changed_callback, (gpointer)self);

    json = wp_spa_json_new_array ("i", 4, "i", 5, NULL);
    self->triggered_setting = "test-setting-array";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_true (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);

    wp_settings_unsubscribe (s, sub_id);

    json = wp_spa_json_new_array ("i", 6, "i", 7, NULL);
    self->triggered_setting = "test-setting-array";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_false (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);
  }

  /* Object */
  {
    sub_id = wp_settings_subscribe (s, "test*",
        wp_settings_changed_callback, (gpointer)self);

    json = wp_spa_json_new_object (
        "key1", "s", "value",
        "key2", "i", 3,
        "key4", "b", TRUE,
        NULL);
    self->triggered_setting = "test-setting-object";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_true (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);

    wp_settings_unsubscribe (s, sub_id);

    json = wp_spa_json_new_object (
        "key1", "f", 1.2,
        NULL);
    self->triggered_setting = "test-setting-object";
    self->triggered_setting_value = json;
    self->triggered_callback = FALSE;
    g_assert_true (wp_settings_set (s, self->triggered_setting, json));
    g_assert_false (self->triggered_callback);
    g_clear_pointer (&json, wp_spa_json_unref);
  }
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add ("/wp/settings/basic", TestSettingsFixture, NULL,
      test_settings_setup, test_basic, test_settings_teardown);
  g_test_add ("/wp/settings/get-set-save-reset-delete", TestSettingsFixture, NULL,
      test_settings_setup, test_get_set_save_reset_delete, test_settings_teardown);
  g_test_add ("/wp/settings/save-reset-delete-all", TestSettingsFixture, NULL,
      test_settings_setup, test_save_reset_delete_all, test_settings_teardown);
  g_test_add ("/wp/settings/subscribe-unsubscribe", TestSettingsFixture, NULL,
      test_settings_setup, test_subscribe_unsibscribe, test_settings_teardown);

  return g_test_run ();
}
  070701000001ED000081A400000000000000000000000165F86304000005F3000000000000000000000000000000000000003500000000wireplumber-0.5.0/tests/wp/settings/wireplumber.conf  context.modules = [
    { name = libpipewire-module-protocol-native }
    { name = libpipewire-module-metadata }
]

wireplumber.settings.schema = {
  test-setting-bool = {
    description = "test-setting-bool description"
    type = "bool"
    default = false
  }
  test-setting-int = {
    description = "test-setting-int description"
    type = "int"
    default = 0
    min = -100
    max = 100
  }
  test-setting-float = {
    description = "test-setting-float description"
    type = "float"
    default = 0.0
    min = -100.0
    max = 100.0
  }
  test-setting-string = {
    description = "test-setting-string description"
    type = "string"
    default = "default"
  }
  test-setting-string2 = {
    description = "test-setting-string2 description"
    type = "string"
    default = "default"
  }
  test-setting-array = {
    description = "test-setting-array description"
    type = "array"
    default = []
  }
  test-setting-array2 = {
    description = "test-setting-array2 description"
    type = "array"
    default = []
  }
  test-setting-object = {
    description = "test-setting-object description"
    type = "object"
    default = {}
  }
}

wireplumber.settings = {
  test-setting-bool = true
  test-setting-int = -20
  test-setting-float = 3.14
  test-setting-string = "blahblah"
  test-setting-string2 = "a string with \"quotes\""
  test-setting-array = [1, 2, 3]
  test-setting-array2 = ["test1", "test 2", "test three", "test-four"]
  test-setting-object = { key1: "value", key2: 2, key3: true }
}
 070701000001EE000081A400000000000000000000000165F863040000A73C000000000000000000000000000000000000002600000000wireplumber-0.5.0/tests/wp/spa-json.c ﻿/* WirePlumber
 *
 * Copyright © 2021 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"

static void
test_spa_json_basic (void)
{
  /* Null */
  {
    g_autoptr (WpSpaJson) json = wp_spa_json_new_null ();
    g_assert_nonnull (json);
    g_assert_true (wp_spa_json_is_null (json));
    g_assert_cmpmem (wp_spa_json_get_data (json), wp_spa_json_get_size (json),
        "null", 4);
  }

  /* Boolean */
  {
    g_autoptr (WpSpaJson) json = wp_spa_json_new_boolean (TRUE);
    g_assert_nonnull (json);
    g_assert_true (wp_spa_json_is_boolean (json));
    gboolean v = FALSE;
    g_assert_true (wp_spa_json_parse_boolean (json, &v));
    g_assert_true (v);
    g_assert_cmpmem (wp_spa_json_get_data (json), wp_spa_json_get_size (json),
        "true", 4);
  }

  /* Int */
  {
    g_autoptr (WpSpaJson) json = wp_spa_json_new_int (8);
    g_assert_nonnull (json);
    g_assert_true (wp_spa_json_is_int (json));
    gint32 v = 0;
    g_assert_true (wp_spa_json_parse_int (json, &v));
    g_assert_cmpint (v, ==, 8);
    g_assert_cmpmem (wp_spa_json_get_data (json), wp_spa_json_get_size (json),
        "8", 1);
  }

  /* Float */
  {
    g_autoptr (WpSpaJson) json = wp_spa_json_new_float (3.14f);
    g_assert_nonnull (json);
    g_assert_true (wp_spa_json_is_float (json));
    float v = 0;
    g_assert_true (wp_spa_json_parse_float (json, &v));
    g_assert_cmpfloat_with_epsilon (v, 3.14f, 0.001f);
  }

  /* String */
  {
    g_autoptr (WpSpaJson) json = wp_spa_json_new_string ("wireplumber");
    g_assert_nonnull (json);
    g_assert_true (wp_spa_json_is_string (json));
    g_autofree gchar *v1 = wp_spa_json_parse_string (json);
    g_assert_nonnull (v1);
    g_assert_cmpstr (v1, ==, "wireplumber");

    g_autoptr (WpSpaJson) jsone = wp_spa_json_new_string ("");
    g_assert_nonnull (jsone);
    g_assert_true (wp_spa_json_is_string (jsone));
    g_autofree gchar *v2 = wp_spa_json_parse_string (jsone);
    g_assert_nonnull (v2);
    g_assert_cmpstr (v2, ==, "");

    g_autoptr (WpSpaJson) jsonl = wp_spa_json_new_string (
        "looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong");
    g_assert_nonnull (jsonl);
    g_assert_true (wp_spa_json_is_string (jsonl));
    g_autofree gchar *v3 = wp_spa_json_parse_string (jsonl);
    g_assert_nonnull (v3);
    g_assert_cmpstr (v3, ==,
        "looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong");

    g_autoptr (WpSpaJson) jsons = wp_spa_json_new_string ("\v\v\v\v");
    g_assert_nonnull (jsons);
    g_assert_true (wp_spa_json_is_string (jsons));
    g_autofree gchar *v4 = wp_spa_json_parse_string (jsons);
    g_assert_nonnull (v4);
    g_assert_cmpstr (v4, ==, "\v\v\v\v");
  }

  /* Array */
  {
    g_autoptr (WpSpaJson) empty = wp_spa_json_new_array (NULL, NULL);
    g_assert_nonnull (empty);
    g_assert_true (wp_spa_json_is_array (empty));
    g_assert_cmpmem (wp_spa_json_get_data (empty), wp_spa_json_get_size (empty),
        "[]", 2);

    g_autoptr (WpSpaJson) json = wp_spa_json_new_array ("i", 1, "i", 2, NULL);
    g_assert_nonnull (json);
    g_assert_true (wp_spa_json_is_array (json));
    gint32 v1 = 0, v2 = 0;
    g_assert_true (wp_spa_json_parse_array (json, "i", &v1, "i", &v2, NULL));
    g_assert_cmpint (v1, ==, 1);
    g_assert_cmpint (v2, ==, 2);
    g_assert_cmpmem (wp_spa_json_get_data (json), wp_spa_json_get_size (json),
        "[1, 2]", 6);
  }

  /* Object */
  {
    g_autoptr (WpSpaJson) empty = wp_spa_json_new_object (NULL, NULL, NULL);
    g_assert_nonnull (empty);
    g_assert_true (wp_spa_json_is_object (empty));
    g_assert_cmpmem (wp_spa_json_get_data (empty), wp_spa_json_get_size (empty),
        "{}", 2);

    g_autoptr (WpSpaJson) subjson = wp_spa_json_new_array ("b", TRUE, NULL);
    g_autoptr (WpSpaJson) json = wp_spa_json_new_object (
        "key1", "n",
        "key2", "b", TRUE,
        "key3", "i", 3,
        "key4", "f", 2.72f,
        "key5", "s", "str",
        "key6", "J", subjson,
        NULL);
    g_assert_nonnull (json);
    g_assert_true (wp_spa_json_is_object (json));

    {
      g_autofree gchar *key1 = NULL, *key2 = NULL, *key3 = NULL, *key4 = NULL,
          *key5 = NULL, *key6 = NULL;
      gboolean v2 = FALSE;
      gint32 v3 = 0;
      float v4 = 0.0f;
      g_autofree gchar *v5 = NULL;
      g_autoptr (WpSpaJson) v6 = NULL;
      g_assert_true (wp_spa_json_parse_object (json,
          &key1, "n",
          &key2, "b", &v2,
          &key3, "i", &v3,
          &key4, "f", &v4,
          &key5, "s", &v5,
          &key6, "J", &v6,
          NULL));
      g_assert_cmpstr (key1, ==, "key1");
      g_assert_cmpstr (key2, ==, "key2");
      g_assert_true (v2);
      g_assert_cmpstr (key3, ==, "key3");
      g_assert_cmpint (v3, ==, 3);
      g_assert_cmpstr (key4, ==, "key4");
      g_assert_cmpfloat_with_epsilon (v4, 2.72f, 0.001f);
      g_assert_cmpstr (key5, ==, "key5");
      g_assert_cmpstr (v5, ==, "str");
      g_assert_cmpstr (key6, ==, "key6");
      g_assert_nonnull (v6);
      g_assert_cmpmem (wp_spa_json_get_data (v6), wp_spa_json_get_size (v6),
          "[true]", 6);
    }

    {
      gboolean v2 = FALSE;
      gint32 v3 = 0;
      float v4 = 0.0f;
      g_autofree gchar *v5 = NULL;
      g_autoptr (WpSpaJson) v6 = NULL;
      g_assert_true (wp_spa_json_object_get (json,
          "key6", "J", &v6,
          "key3", "i", &v3,
          "key5", "s", &v5,
          "key1", "n",
          "key2", "b", &v2,
          "key4", "f", &v4,
          NULL));
      g_assert_true (v2);
      g_assert_cmpint (v3, ==, 3);
      g_assert_cmpfloat_with_epsilon (v4, 2.72f, 0.001f);
      g_assert_cmpstr (v5, ==, "str");
      g_assert_nonnull (v6);
      g_assert_cmpmem (wp_spa_json_get_data (v6), wp_spa_json_get_size (v6),
          "[true]", 6);
    }
  }
}

static void
test_spa_json_array_builder_parser_iterator (void)
{
  g_autoptr (WpSpaJson) json = NULL;

  {
    g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_array ();
    g_assert_nonnull (b);
    wp_spa_json_builder_add_int (b, 1);
    wp_spa_json_builder_add_int (b, 2);
    wp_spa_json_builder_add_int (b, 3);
    json = wp_spa_json_builder_end (b);
  }

  g_assert_true (wp_spa_json_is_array (json));
  g_assert_cmpmem (wp_spa_json_get_data (json), wp_spa_json_get_size (json),
      "[1, 2, 3]", 9);

  {
    g_autoptr (WpSpaJsonParser) p = wp_spa_json_parser_new_array (json);
    g_assert_nonnull (p);
    gint32 v = 0;
    g_assert_true (wp_spa_json_parser_get_int (p, &v));
    g_assert_cmpint (v, ==, 1);
    g_assert_true (wp_spa_json_parser_get_int (p, &v));
    g_assert_cmpint (v, ==, 2);
    g_assert_true (wp_spa_json_parser_get_int (p, &v));
    g_assert_cmpint (v, ==, 3);
    wp_spa_json_parser_end (p);
    g_assert_false (wp_spa_json_parser_get_null (p));
  }

  g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
  g_assert_nonnull (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_int (j));
    gint32 v = 0;
    g_assert_true (wp_spa_json_parse_int (j, &v));
    g_assert_cmpint (v, ==, 1);
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_int (j));
    gint32 v = 0;
    g_assert_true (wp_spa_json_parse_int (j, &v));
    g_assert_cmpint (v, ==, 2);
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_int (j));
    gint32 v = 0;
    g_assert_true (wp_spa_json_parse_int (j, &v));
    g_assert_cmpint (v, ==, 3);
    g_value_unset (&next);
  }

  g_assert_false (wp_iterator_next (it, NULL));
  wp_iterator_reset (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_int (j));
    gint32 v = 0;
    g_assert_true (wp_spa_json_parse_int (j, &v));
    g_assert_cmpint (v, ==, 1);
    g_value_unset (&next);
  }
}

static void
test_spa_json_object_builder_parser_iterator (void)
{
  g_autoptr (WpSpaJson) json = NULL;

  {
    g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_object ();
    g_assert_nonnull (b);
    wp_spa_json_builder_add_property (b, "key-null");
    wp_spa_json_builder_add_null (b);
    wp_spa_json_builder_add_property (b, "key-boolean");
    wp_spa_json_builder_add_boolean (b, TRUE);
    wp_spa_json_builder_add_property (b, "key-int");
    wp_spa_json_builder_add_int (b, 7);
    wp_spa_json_builder_add_property (b, "key-float");
    wp_spa_json_builder_add_float (b, 0.12f);
    wp_spa_json_builder_add_property (b, "key-string");
    wp_spa_json_builder_add_string (b, "str");
    wp_spa_json_builder_add_property (b, "key-empty-string");
    wp_spa_json_builder_add_string (b, "");
    wp_spa_json_builder_add_property (b, "key-special-char-string");
    wp_spa_json_builder_add_string (b, "\v\v\v\v");
    json = wp_spa_json_builder_end (b);
  }

  g_assert_true (wp_spa_json_is_object (json));

  {
    g_autoptr (WpSpaJsonParser) p = wp_spa_json_parser_new_object (json);
    g_assert_nonnull (p);

    g_autofree gchar *key_null = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (key_null);
    g_assert_cmpstr (key_null, ==, "key-null");
    g_assert_true (wp_spa_json_parser_get_null (p));

    g_autofree gchar *key_boolean = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (key_boolean);
    g_assert_cmpstr (key_boolean, ==, "key-boolean");
    gboolean v_boolean = FALSE;
    g_assert_true (wp_spa_json_parser_get_boolean (p, &v_boolean));
    g_assert_true (v_boolean);

    g_autofree gchar *key_int = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (key_int);
    g_assert_cmpstr (key_int, ==, "key-int");
    gint32 v_int = 0;
    g_assert_true (wp_spa_json_parser_get_int (p, &v_int));
    g_assert_cmpint (v_int, ==, 7);

    g_autofree gchar *key_float = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (key_float);
    g_assert_cmpstr (key_float, ==, "key-float");
    float v_float = 0.0f;
    g_assert_true (wp_spa_json_parser_get_float (p, &v_float));
    g_assert_cmpfloat_with_epsilon (v_float, 0.12f, 0.001f);

    g_autofree gchar *key_string = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (key_string);
    g_assert_cmpstr (key_string, ==, "key-string");
    g_autofree gchar *v_string = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (v_string);
    g_assert_cmpstr (v_string, ==, "str");

    g_autofree gchar *key_empty_string = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (key_empty_string);
    g_assert_cmpstr (key_empty_string, ==, "key-empty-string");
    g_autofree gchar *v_empty_string = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (v_empty_string);
    g_assert_cmpstr (v_empty_string, ==, "");

    g_autofree gchar *key_special_char_string = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (key_special_char_string);
    g_assert_cmpstr (key_special_char_string, ==, "key-special-char-string");
    g_autofree gchar *v_special_char_string = wp_spa_json_parser_get_string (p);
    g_assert_nonnull (v_special_char_string);
    g_assert_cmpstr (v_special_char_string, ==, "\v\v\v\v");

    wp_spa_json_parser_end (p);
    g_assert_false (wp_spa_json_parser_get_null (p));
  }

  g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
  g_assert_nonnull (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-null");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_null (j));
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-boolean");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_boolean (j));
    gboolean v = FALSE;
    g_assert_true (wp_spa_json_parse_boolean (j, &v));
    g_assert_true (v);
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-int");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_int (j));
    gint32 v = 0;
    g_assert_true (wp_spa_json_parse_int (j, &v));
    g_assert_cmpint (v, ==, 7);
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-float");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_float (j));
    float v = 0;
    g_assert_true (wp_spa_json_parse_float (j, &v));
    g_assert_cmpfloat_with_epsilon (v, 0.12f, 0.001f);
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-string");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "str");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-empty-string");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-special-char-string");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "\v\v\v\v");
    g_value_unset (&next);
  }

  g_assert_false (wp_iterator_next (it, NULL));
  wp_iterator_reset (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-null");
    g_value_unset (&next);
  }
}

static void
test_spa_json_nested (void)
{
  g_autoptr (WpSpaJson) array = NULL;
  g_autoptr (WpSpaJson) array2 = NULL;
  g_autoptr (WpSpaJson) object = NULL;
  g_autoptr (WpSpaJson) json = NULL;

  {
    g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_array ();
    g_assert_nonnull (b);
    wp_spa_json_builder_add_int (b, 5);
    wp_spa_json_builder_add_int (b, 10);
    wp_spa_json_builder_add_int (b, 15);
    array = wp_spa_json_builder_end (b);
  }
  g_assert_true (wp_spa_json_is_array (array));
  g_assert_cmpuint (wp_spa_json_get_size (array), ==, 11);
  g_assert_cmpmem (wp_spa_json_get_data (array), wp_spa_json_get_size (array),
      "[5, 10, 15]", 11);

  {
    g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_array ();
    g_assert_nonnull (b);
    wp_spa_json_builder_add_int (b, 2);
    wp_spa_json_builder_add_int (b, 4);
    array2 = wp_spa_json_builder_end (b);
  }
  g_assert_true (wp_spa_json_is_array (array2));
  g_assert_cmpuint (wp_spa_json_get_size (array2), ==, 6);
  g_assert_cmpmem (wp_spa_json_get_data (array2), wp_spa_json_get_size (array2),
      "[2, 4]", 6);

  {
    g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_object ();
    g_assert_nonnull (b);
    wp_spa_json_builder_add_property (b, "key-boolean");
    wp_spa_json_builder_add_boolean (b, FALSE);
    wp_spa_json_builder_add_property (b, "key-int");
    wp_spa_json_builder_add_int (b, 8);
    wp_spa_json_builder_add_property (b, "key-array");
    wp_spa_json_builder_add_json (b, array2);
    object = wp_spa_json_builder_end (b);
  }
  g_assert_true (wp_spa_json_is_object (object));
  g_assert_cmpuint (wp_spa_json_get_size (object), ==, 54);
  g_assert_cmpmem (wp_spa_json_get_data (object), wp_spa_json_get_size (object),
      "{\"key-boolean\":false, \"key-int\":8, \"key-array\":[2, 4]}", 54);

  {
    g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_object ();
    g_assert_nonnull (b);
    wp_spa_json_builder_add_property (b, "key-array");
    wp_spa_json_builder_add_json (b, array);
    wp_spa_json_builder_add_property (b, "key-object");
    wp_spa_json_builder_add_json (b, object);
    json = wp_spa_json_builder_end (b);
  }
  g_assert_true (wp_spa_json_is_object (json));
  g_assert_cmpuint (wp_spa_json_get_size (json), ==, 94);
  g_assert_cmpmem (wp_spa_json_get_data (json), wp_spa_json_get_size (json),
      "{\"key-array\":[5, 10, 15], \"key-object\":{\"key-boolean\":false, "
      "\"key-int\":8, \"key-array\":[2, 4]}}", 94);
  g_assert_cmpstr (wp_spa_json_get_data (json), ==,
      "{\"key-array\":[5, 10, 15], \"key-object\":{\"key-boolean\":false, "
      "\"key-int\":8, \"key-array\":[2, 4]}}");

  g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
  g_assert_nonnull (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-array");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_array (j));
    g_assert_cmpuint (wp_spa_json_get_size (j), ==, 11);
    g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
        "[5, 10, 15]", 11);

    g_autoptr (WpIterator) it2 = wp_spa_json_new_iterator (j);
    g_assert_nonnull (it2);

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_int (j));
      gint32 v = 0;
      g_assert_true (wp_spa_json_parse_int (j, &v));
      g_assert_cmpint (v, ==, 5);
      g_value_unset (&next2);
    }

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_int (j));
      gint32 v = 0;
      g_assert_true (wp_spa_json_parse_int (j, &v));
      g_assert_cmpint (v, ==, 10);
      g_value_unset (&next2);
    }

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_int (j));
      gint32 v = 0;
      g_assert_true (wp_spa_json_parse_int (j, &v));
      g_assert_cmpint (v, ==, 15);
      g_value_unset (&next2);
    }

    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-object");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_object (j));
    g_assert_cmpuint (wp_spa_json_get_size (j), ==, 54);
    g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
        "{\"key-boolean\":false, \"key-int\":8, \"key-array\":[2, 4]}", 54);

    g_autoptr (WpIterator) it2 = wp_spa_json_new_iterator (j);
    g_assert_nonnull (it2);

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_string (j));
      g_autofree gchar *v = wp_spa_json_parse_string (j);
      g_assert_nonnull (v);
      g_assert_cmpstr (v, ==, "key-boolean");
      g_value_unset (&next2);
    }

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_boolean (j));
      gboolean v = TRUE;
      g_assert_true (wp_spa_json_parse_boolean (j, &v));
      g_assert_false (v);
      g_value_unset (&next2);
    }

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_string (j));
      g_autofree gchar *v = wp_spa_json_parse_string (j);
      g_assert_nonnull (v);
      g_assert_cmpstr (v, ==, "key-int");
      g_value_unset (&next2);
    }

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_int (j));
      gint32 v = 0;
      g_assert_true (wp_spa_json_parse_int (j, &v));
      g_assert_cmpint (v, ==, 8);
      g_value_unset (&next2);
    }

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_string (j));
      g_autofree gchar *v = wp_spa_json_parse_string (j);
      g_assert_nonnull (v);
      g_assert_cmpstr (v, ==, "key-array");
      g_value_unset (&next2);
    }

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_array (array2));
      g_assert_cmpuint (wp_spa_json_get_size (array2), ==, 6);
      g_assert_cmpmem (wp_spa_json_get_data (array2),
          wp_spa_json_get_size (array2), "[2, 4]", 6);

      g_autoptr (WpIterator) it3 = wp_spa_json_new_iterator (j);
      g_assert_nonnull (it3);

      {
        GValue next3 = G_VALUE_INIT;
        g_assert_true (wp_iterator_next (it3, &next3));
        WpSpaJson *j = g_value_get_boxed (&next3);
        g_assert_nonnull (j);
        g_assert_true (wp_spa_json_is_int (j));
        gint32 v = 0;
        g_assert_true (wp_spa_json_parse_int (j, &v));
        g_assert_cmpint (v, ==, 2);
        g_value_unset (&next3);
      }

      {
        GValue next3 = G_VALUE_INIT;
        g_assert_true (wp_iterator_next (it3, &next3));
        WpSpaJson *j = g_value_get_boxed (&next3);
        g_assert_nonnull (j);
        g_assert_true (wp_spa_json_is_int (j));
        gint32 v = 0;
        g_assert_true (wp_spa_json_parse_int (j, &v));
        g_assert_cmpint (v, ==, 4);
        g_value_unset (&next3);
      }

      g_value_unset (&next2);
    }

    g_value_unset (&next);
  }

  g_assert_false (wp_iterator_next (it, NULL));
  wp_iterator_reset (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "key-array");
    g_value_unset (&next);
  }
}

static void
test_spa_json_nested2 (void)
{
  const gchar json_str[] = "[[[[1], [2]], [3]], [4]]";
  g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_string (json_str);

  g_assert_true (wp_spa_json_is_array (json));
  g_assert_cmpmem (wp_spa_json_get_data (json), wp_spa_json_get_size (json),
        "[[[[1], [2]], [3]], [4]]", 24);

  {
    g_autoptr (WpSpaJsonParser) p = wp_spa_json_parser_new_array (json);
    g_assert_nonnull (p);
    g_autoptr (WpSpaJson) j0 = wp_spa_json_parser_get_json (p);
    g_assert_nonnull (j0);
    g_assert_cmpmem (wp_spa_json_get_data (j0), wp_spa_json_get_size (j0),
        "[[[1], [2]], [3]]", 17);
    g_autoptr (WpSpaJson) j1 = wp_spa_json_parser_get_json (p);
    g_assert_nonnull (j1);
    g_assert_cmpmem (wp_spa_json_get_data (j1), wp_spa_json_get_size (j1),
        "[4]", 3);
    wp_spa_json_parser_end (p);
    g_assert_false (wp_spa_json_parser_get_null (p));
  }

  g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
  g_assert_nonnull (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_array (j));
    g_assert_cmpuint (wp_spa_json_get_size (j), ==, 17);
    g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
        "[[[1], [2]], [3]]", 17);

    g_autoptr (WpIterator) it2 = wp_spa_json_new_iterator (j);
    g_assert_nonnull (it2);

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_array (j));
      g_assert_cmpuint (wp_spa_json_get_size (j), ==, 10);
      g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
          "[[1], [2]]", 10);

      g_autoptr (WpIterator) it3 = wp_spa_json_new_iterator (j);
      g_assert_nonnull (it3);

      {
        GValue next3 = G_VALUE_INIT;
        g_assert_true (wp_iterator_next (it3, &next3));
        WpSpaJson *j = g_value_get_boxed (&next3);
        g_assert_nonnull (j);
        g_assert_true (wp_spa_json_is_array (j));
        g_assert_cmpuint (wp_spa_json_get_size (j), ==, 3);
        g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
            "[1]", 3);

        g_autoptr (WpIterator) it4 = wp_spa_json_new_iterator (j);
        g_assert_nonnull (it4);

        {
          GValue next4 = G_VALUE_INIT;
          g_assert_true (wp_iterator_next (it4, &next4));
          WpSpaJson *j = g_value_get_boxed (&next4);
          g_assert_nonnull (j);
          g_assert_true (wp_spa_json_is_int (j));
          g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
              "1", 1);
          g_value_unset (&next4);
        }

        g_value_unset (&next3);
      }

      {
        GValue next3 = G_VALUE_INIT;
        g_assert_true (wp_iterator_next (it3, &next3));
        WpSpaJson *j = g_value_get_boxed (&next3);
        g_assert_nonnull (j);
        g_assert_true (wp_spa_json_is_array (j));
        g_assert_cmpuint (wp_spa_json_get_size (j), ==, 3);
        g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
            "[2]", 3);

        g_autoptr (WpIterator) it4 = wp_spa_json_new_iterator (j);
        g_assert_nonnull (it4);

        {
          GValue next4 = G_VALUE_INIT;
          g_assert_true (wp_iterator_next (it4, &next4));
          WpSpaJson *j = g_value_get_boxed (&next4);
          g_assert_nonnull (j);
          g_assert_true (wp_spa_json_is_int (j));
          g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
              "2", 1);
          g_value_unset (&next4);
        }

        g_value_unset (&next3);
      }

      g_value_unset (&next2);
    }

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_array (j));
      g_assert_cmpuint (wp_spa_json_get_size (j), ==, 3);
      g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
          "[3]", 3);

      g_autoptr (WpIterator) it3 = wp_spa_json_new_iterator (j);
      g_assert_nonnull (it3);

      {
        GValue next3 = G_VALUE_INIT;
        g_assert_true (wp_iterator_next (it3, &next3));
        WpSpaJson *j = g_value_get_boxed (&next3);
        g_assert_nonnull (j);
        g_assert_true (wp_spa_json_is_int (j));
        g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
            "3", 1);
        g_value_unset (&next3);
      }

      g_value_unset (&next2);
    }

    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_array (j));
    g_assert_cmpuint (wp_spa_json_get_size (j), ==, 3);
    g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
        "[4]", 3);

    g_autoptr (WpIterator) it2 = wp_spa_json_new_iterator (j);
    g_assert_nonnull (it2);

    {
      GValue next2 = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it2, &next2));
      WpSpaJson *j = g_value_get_boxed (&next2);
      g_assert_nonnull (j);
      g_assert_true (wp_spa_json_is_int (j));
      g_assert_cmpmem (wp_spa_json_get_data (j), wp_spa_json_get_size (j),
          "4", 1);
      g_value_unset (&next2);
    }

    g_value_unset (&next);
  }
}

static void
test_spa_json_nested3 (void)
{
  const gchar json_str[] =
      "{ test-setting-json3: { key1: \"value\", key2: 2, key3: true } }";
  g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_string (json_str);
  g_assert_nonnull (json);
  g_assert_true (wp_spa_json_is_object (json));

  g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
  g_assert_nonnull (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_cmpstr (v, ==, "test-setting-json3");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_object (j));
    g_autofree gchar *v = wp_spa_json_to_string (j);
    g_assert_cmpstr (v, ==, "{ key1: \"value\", key2: 2, key3: true }");
    g_value_unset (&next);
  }
}

static void
test_spa_json_ownership (void)
{
  g_autoptr (WpSpaJson) json = NULL;

  {
    const gchar json_str[] = "{\"name\":\"John\", \"age\":30, \"car\":null}";
    json = wp_spa_json_new_wrap_string (json_str);
    g_assert_nonnull (json);

    g_assert_false (wp_spa_json_is_unique_owner (json));

    g_assert_true (wp_spa_json_is_object (json));
    g_assert_cmpmem (wp_spa_json_get_data (json), wp_spa_json_get_size (json),
        "{\"name\":\"John\", \"age\":30, \"car\":null}", 37);

    json = wp_spa_json_ensure_unique_owner (json);
    g_assert_nonnull (json);
    g_assert_true (wp_spa_json_is_unique_owner (json));
  }

  g_assert_true (wp_spa_json_is_object (json));
  g_assert_cmpmem (wp_spa_json_get_data (json), wp_spa_json_get_size (json),
      "{\"name\":\"John\", \"age\":30, \"car\":null}", 37);

  g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
  g_assert_nonnull (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "name");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "John");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "age");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_int (j));
    gint32 v = 0;
    g_assert_true (wp_spa_json_parse_int (j, &v));
    g_assert_cmpint (v, ==, 30);
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "car");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_null (j));
    g_value_unset (&next);
  }
}

static void
test_spa_json_spa_format (void)
{
  g_autoptr (WpSpaJson) json = NULL;

  const gchar json_str[] = "{ name = John age:30, \"car\" null }";
  json = wp_spa_json_new_wrap_string (json_str);
  g_assert_nonnull (json);

  g_assert_true (wp_spa_json_is_object (json));

  g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
  g_assert_nonnull (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_false (wp_spa_json_is_string (j));  // FALSE because no quotes
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "name");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_false (wp_spa_json_is_string (j));  // FALSE because no quotes
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "John");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_false (wp_spa_json_is_string (j));  // FALSE because no quotes
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "age");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_int (j));
    gint32 v = 0;
    g_assert_true (wp_spa_json_parse_int (j, &v));
    g_assert_cmpint (v, ==, 30);
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_string (j));
    g_autofree gchar *v = wp_spa_json_parse_string (j);
    g_assert_nonnull (v);
    g_assert_cmpstr (v, ==, "car");
    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *j = g_value_get_boxed (&next);
    g_assert_nonnull (j);
    g_assert_true (wp_spa_json_is_null (j));
    g_value_unset (&next);
  }
}

static void
test_spa_json_to_string (void)
{
  const gchar json_str[] = "[{\"key0\":\"val0\"}, {\"key1\":\"val1\"}]";
  g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_string (json_str);
  g_assert_nonnull (json);

  {
    g_autofree gchar *str = wp_spa_json_to_string (json);
    g_assert_cmpstr (str, ==, wp_spa_json_get_data (json));
    g_assert_cmpstr (str, ==, json_str);
  }

  g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json);
  g_assert_nonnull (it);

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *o = g_value_get_boxed (&next);
    g_assert_nonnull (o);
    g_assert_true (wp_spa_json_is_object (o));
    g_autofree gchar *str = wp_spa_json_to_string (o);
    g_assert_cmpstr (str, ==, "{\"key0\":\"val0\"}");
    g_assert_cmpstr (str, !=, wp_spa_json_get_data (o));

    g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_array ();
    wp_spa_json_builder_add_json (b, o);
    g_autoptr (WpSpaJson) json2 = wp_spa_json_builder_end (b);
    g_autofree gchar *str2 = wp_spa_json_to_string (json2);
    g_assert_cmpstr (str2, ==, wp_spa_json_get_data (json2));
    g_assert_cmpstr (str2, ==, "[{\"key0\":\"val0\"}]");

    g_value_unset (&next);
  }

  {
    GValue next = G_VALUE_INIT;
    g_assert_true (wp_iterator_next (it, &next));
    WpSpaJson *o = g_value_get_boxed (&next);
    g_assert_nonnull (o);
    g_assert_true (wp_spa_json_is_object (o));
    g_autofree gchar *str = wp_spa_json_to_string (o);
    g_assert_cmpstr (str, ==, "{\"key1\":\"val1\"}");
    g_assert_cmpstr (str, !=, wp_spa_json_get_data (o));

    g_autoptr (WpSpaJsonBuilder) b = wp_spa_json_builder_new_array ();
    wp_spa_json_builder_add_json (b, o);
    g_autoptr (WpSpaJson) json2 = wp_spa_json_builder_end (b);
    g_autofree gchar *str2 = wp_spa_json_to_string (json2);
    g_assert_cmpstr (str2, ==, wp_spa_json_get_data (json2));
    g_assert_cmpstr (str2, ==, "[{\"key1\":\"val1\"}]");

    g_value_unset (&next);
  }
}

static void
test_spa_json_undefined_parser (void)
{
  const gchar json_str[] = "key0 = val0, key.array = [ val1 val2 ], "
      "key.object = { key-boolean = false, key-int = 8, key-array = [ 2 4 ] }";
  g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_string (json_str);
  g_assert_nonnull (json);

  g_assert_false (wp_spa_json_is_container (json));

  g_autoptr (WpSpaJsonParser) p = wp_spa_json_parser_new_undefined (json);
  g_assert_nonnull (p);

  {
    g_autofree gchar *k = wp_spa_json_parser_get_string (p);
    g_assert_cmpstr (k, ==, "key0");
  }
  {
    g_autofree gchar *v = wp_spa_json_parser_get_string (p);
    g_assert_cmpstr (v, ==, "val0");
  }
  {
    g_autofree gchar *k = wp_spa_json_parser_get_string (p);
    g_assert_cmpstr (k, ==, "key.array");
  }
  {
    g_autoptr (WpSpaJson) v = wp_spa_json_parser_get_json (p);
    g_autofree gchar *str = wp_spa_json_to_string (v);
    g_assert_cmpstr (str, ==, "[ val1 val2 ]");
    g_assert_true (wp_spa_json_is_array (v));
  }
  {
    g_autofree gchar *k = wp_spa_json_parser_get_string (p);
    g_assert_cmpstr (k, ==, "key.object");
  }
  {
    g_autoptr (WpSpaJson) v = wp_spa_json_parser_get_json (p);
    g_autofree gchar *str = wp_spa_json_to_string (v);
    g_assert_cmpstr (str, ==, "{ key-boolean = false, key-int = 8, key-array = [ 2 4 ] }");
    g_assert_true (wp_spa_json_is_object (v));
  }
  {
    g_autofree gchar *k = wp_spa_json_parser_get_string (p);
    g_assert_null (k);
  }
  {
    g_autofree gchar *k = wp_spa_json_parser_get_string (p);
    g_assert_null (k);
  }
}

int
main (int argc, char *argv[])
{
  g_test_init (&argc, &argv, NULL);
  g_log_set_writer_func (wp_log_writer_default, NULL, NULL);

  g_test_add_func ("/wp/spa-json/basic", test_spa_json_basic);
  g_test_add_func ("/wp/spa-json/array-builder-parser-iterator",
      test_spa_json_array_builder_parser_iterator);
  g_test_add_func ("/wp/spa-json/object-builder-parser-iterator",
      test_spa_json_object_builder_parser_iterator);
  g_test_add_func ("/wp/spa-json/nested", test_spa_json_nested);
  g_test_add_func ("/wp/spa-json/nested2", test_spa_json_nested2);
  g_test_add_func ("/wp/spa-json/nested3", test_spa_json_nested3);
  g_test_add_func ("/wp/spa-json/ownership", test_spa_json_ownership);
  g_test_add_func ("/wp/spa-json/spa-format", test_spa_json_spa_format);
  g_test_add_func ("/wp/spa-json/to-string", test_spa_json_to_string);
  g_test_add_func ("/wp/spa-json/undefined-parser",
      test_spa_json_undefined_parser);

  return g_test_run ();
}
070701000001EF000081A400000000000000000000000165F86304000091E5000000000000000000000000000000000000002500000000wireplumber-0.5.0/tests/wp/spa-pod.c  /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"

static void
test_spa_pod_basic (void)
{
  /* None */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_none ();
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_none (pod));
    g_assert_false (wp_spa_pod_is_id (pod));
    g_assert_cmpstr ("Spa:None", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_none ();
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Boolean */
  {
    g_autoptr (WpSpaPod) copy = NULL;
    g_assert_null (copy);

    {
      g_autoptr (WpSpaPod) pod = wp_spa_pod_new_boolean (TRUE);
      g_assert_nonnull (pod);
      g_assert_true (wp_spa_pod_is_boolean (pod));
      gboolean value = FALSE;
      g_assert_true (wp_spa_pod_get_boolean (pod, &value));
      g_assert_true (value);
      g_assert_cmpstr ("Spa:Bool", ==,
          wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
      g_assert_true (wp_spa_pod_set_boolean (pod, FALSE));
      g_assert_true (wp_spa_pod_get_boolean (pod, &value));
      g_assert_false (value);

      copy = wp_spa_pod_copy (pod);
    }

    g_assert_nonnull (copy);
    g_assert_true (wp_spa_pod_is_boolean (copy));
    gboolean value = FALSE;
    g_assert_true (wp_spa_pod_get_boolean (copy, &value));
    g_assert_false (value);
    g_assert_cmpstr ("Spa:Bool", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (copy)));
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_boolean (TRUE);
    g_assert_true (wp_spa_pod_set_pod (copy, other));
    g_assert_true (wp_spa_pod_get_boolean (copy, &value));
    g_assert_true (value);
    g_assert_true (wp_spa_pod_equal (copy, other));
  }

  /* Id */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_id (5);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_id (pod));
    guint32 value = 0;
    g_assert_true (wp_spa_pod_get_id (pod, &value));
    g_assert_cmpuint (value, ==, 5);
    g_assert_cmpstr ("Spa:Id", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_true (wp_spa_pod_set_id (pod, 10));
    g_assert_true (wp_spa_pod_get_id (pod, &value));
    g_assert_cmpuint (value, ==, 10);
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_id (20);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_id (pod, &value));
    g_assert_cmpuint (value, ==, 20);
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Int */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_int (-12);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_int (pod));
    gint32 value = 0;
    g_assert_true (wp_spa_pod_get_int (pod, &value));
    g_assert_cmpint (value, ==, -12);
    g_assert_cmpstr ("Spa:Int", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_true (wp_spa_pod_set_int (pod, 9999));
    g_assert_true (wp_spa_pod_get_int (pod, &value));
    g_assert_cmpint (value, ==, 9999);
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_int (1000);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_int (pod, &value));
    g_assert_cmpuint (value, ==, 1000);
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Long */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_long (LONG_MAX);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_long (pod));
    gint64 value = 0;
    g_assert_true (wp_spa_pod_get_long (pod, &value));
    g_assert_cmpint (value, ==, LONG_MAX);
    g_assert_cmpstr ("Spa:Long", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_true (wp_spa_pod_set_long (pod, LONG_MIN));
    g_assert_true (wp_spa_pod_get_long (pod, &value));
    g_assert_cmpuint (value, ==, LONG_MIN);
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_long (0);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_long (pod, &value));
    g_assert_cmpuint (value, ==, 0);
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Float */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_float (3.14);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_float (pod));
    float value = 0;
    g_assert_true (wp_spa_pod_get_float (pod, &value));
    g_assert_cmpfloat_with_epsilon (value, 3.14, 0.001);
    g_assert_cmpstr ("Spa:Float", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_true (wp_spa_pod_set_float (pod, 1.0));
    g_assert_true (wp_spa_pod_get_float (pod, &value));
    g_assert_cmpfloat_with_epsilon (value, 1.0, 0.001);
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_float (-3.14);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_float (pod, &value));
    g_assert_cmpfloat_with_epsilon (value, -3.14, 0.001);
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Double */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_double (2.718281828);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_double (pod));
    double value = 0;
    g_assert_true (wp_spa_pod_get_double (pod, &value));
    g_assert_cmpfloat_with_epsilon (value, 2.718281828, 0.0000000001);
    g_assert_cmpstr ("Spa:Double", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_true (wp_spa_pod_set_double (pod, 2.0));
    g_assert_true (wp_spa_pod_get_double (pod, &value));
    g_assert_cmpfloat_with_epsilon (value, 2.0, 0.0000000001);
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_double (3.0);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_double (pod, &value));
    g_assert_cmpfloat_with_epsilon (value, 3.0, 0.0000000001);
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* String */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_string ("WirePlumber");
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_string (pod));
    const char *value = NULL;
    g_assert_true (wp_spa_pod_get_string (pod, &value));
    g_assert_nonnull (value);
    g_assert_cmpstr (value, ==, "WirePlumber");
    g_assert_cmpstr ("Spa:String", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_string ("Other");
    g_assert_nonnull (other);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_string (pod, &value));
    g_assert_nonnull (value);
    g_assert_cmpstr (value, ==, "Other");
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Bytes */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_bytes ("bytes", 5);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_bytes (pod));
    gconstpointer value = NULL;
    guint32 len = 0;
    g_assert_true (wp_spa_pod_get_bytes (pod, &value, &len));
    g_assert_nonnull (value);
    g_assert_cmpmem (value, len, "bytes", 5);
    g_assert_cmpuint (len, ==, 5);
    g_assert_cmpstr ("Spa:Bytes", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_bytes ("pod", 3);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_bytes (pod, &value, &len));
    g_assert_nonnull (value);
    g_assert_cmpmem (value, len, "pod", 3);
    g_assert_cmpuint (len, ==, 3);
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Pointer */
  {
    gint i = 3;
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_pointer ("Spa:Pointer:Buffer", &i);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_pointer (pod));
    gconstpointer p = NULL;
    g_assert_true (wp_spa_pod_get_pointer (pod, &p));
    g_assert_nonnull (p);
    g_assert_true (p == &i);
    g_assert_cmpint (*(gint *)p, ==, 3);
    g_assert_cmpstr ("Spa:Pointer:Buffer", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));

    float f = 1.1;
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_pointer ("Spa:Pointer:Meta", &f);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_pointer (pod, &p));
    g_assert_nonnull (p);
    g_assert_true (p == &f);
    g_assert_cmpfloat_with_epsilon (*(float *)p, 1.1, 0.01);
    g_assert_cmpstr ("Spa:Pointer:Meta", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Fd */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_fd (4);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_fd (pod));
    gint64 value = 0;
    g_assert_true (wp_spa_pod_get_fd (pod, &value));
    g_assert_cmpint (value, ==, 4);
    g_assert_cmpstr ("Spa:Fd", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_true (wp_spa_pod_set_fd (pod, 1));
    g_assert_true (wp_spa_pod_get_fd (pod, &value));
    g_assert_cmpuint (value, ==, 1);
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_fd (10);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_fd (pod, &value));
    g_assert_cmpuint (value, ==, 10);
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Rectangle */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_rectangle (1920, 1080);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_rectangle (pod));
    guint32 width = 0;
    guint32 height = 0;
    g_assert_true (wp_spa_pod_get_rectangle (pod, &width, &height));
    g_assert_cmpint (width, ==, 1920);
    g_assert_cmpint (height, ==, 1080);
    g_assert_cmpstr ("Spa:Rectangle", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_true (wp_spa_pod_set_rectangle (pod, 640, 480));
    g_assert_true (wp_spa_pod_get_rectangle (pod, &width, &height));
    g_assert_cmpint (width, ==, 640);
    g_assert_cmpint (height, ==, 480);
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_rectangle (200, 100);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_rectangle (pod, &width, &height));
    g_assert_cmpint (width, ==, 200);
    g_assert_cmpint (height, ==, 100);
    g_assert_true (wp_spa_pod_equal (pod, other));
  }

  /* Fraction */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_fraction (16, 9);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_fraction (pod));
    guint32 num = 0;
    guint32 denom = 0;
    g_assert_true (wp_spa_pod_get_fraction (pod, &num, &denom));
    g_assert_cmpint (num, ==, 16);
    g_assert_cmpint (denom, ==, 9);
    g_assert_cmpstr ("Spa:Fraction", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_true (wp_spa_pod_set_fraction (pod, 4, 3));
    g_assert_true (wp_spa_pod_get_fraction (pod, &num, &denom));
    g_assert_cmpint (num, ==, 4);
    g_assert_cmpint (denom, ==, 3);
    g_autoptr (WpSpaPod) other = wp_spa_pod_new_fraction (2, 1);
    g_assert_true (wp_spa_pod_set_pod (pod, other));
    g_assert_true (wp_spa_pod_get_fraction (pod, &num, &denom));
    g_assert_cmpint (num, ==, 2);
    g_assert_cmpint (denom, ==, 1);
    g_assert_true (wp_spa_pod_equal (pod, other));
  }
}

static void
test_spa_pod_choice (void)
{
  /* Static Enum */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_choice (
        "Enum", "i", 0, "i", 1, "i", 2, NULL);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_choice (pod));
    g_assert_cmpstr ("Spa:Pod:Choice", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    g_assert_cmpstr ("Enum", ==,
        wp_spa_id_value_short_name (wp_spa_pod_get_choice_type (pod)));

    g_autoptr (WpSpaPod) child = wp_spa_pod_get_choice_child (pod);
    g_assert_nonnull (child);
    g_assert_cmpstr ("Spa:Int", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (child)));
    gint32 value = 1;
    g_assert_true (wp_spa_pod_get_int (child, &value));
    g_assert_cmpint (value, ==, 0);
    g_assert_true (wp_spa_pod_set_int (child, 3));
    g_assert_true (wp_spa_pod_get_int (child, &value));
    g_assert_cmpint (value, ==, 3);
  }

  /* Static None */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_choice ("None", "s",
        "default value", NULL);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_choice (pod));
    g_assert_cmpstr ("Spa:Pod:Choice", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));

    {
      g_autoptr (WpSpaPod) child = wp_spa_pod_get_choice_child (pod);
      g_assert_nonnull (child);
      g_assert_cmpstr ("Spa:String", ==,
          wp_spa_type_name (wp_spa_pod_get_spa_type (child)));
      const char *value = NULL;
      g_assert_true (wp_spa_pod_get_string (child, &value));
      g_assert_nonnull (value);
      g_assert_cmpstr ("default value", ==, value);
      g_autoptr (WpSpaPod) str_pod = wp_spa_pod_new_string ("new value");
      g_assert_true (wp_spa_pod_set_pod (child, str_pod));
      g_assert_true (wp_spa_pod_get_string (child, &value));
      g_assert_cmpstr ("new value", ==, value);
    }

    {
      g_autoptr (WpSpaPod) child = wp_spa_pod_get_choice_child (pod);
      g_assert_nonnull (child);
      g_assert_cmpstr ("Spa:String", ==,
          wp_spa_type_name (wp_spa_pod_get_spa_type (child)));
      const char *value = NULL;
      g_assert_true (wp_spa_pod_get_string (child, &value));
      g_assert_nonnull (value);
      g_assert_cmpstr ("new value", ==, value);
    }
  }

  /* Dynamic */
  {
    g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_choice ("Enum");
    wp_spa_pod_builder_add (b, "i", 0, NULL);
    wp_spa_pod_builder_add (b, "i", 1, NULL);
    wp_spa_pod_builder_add (b, "i", 2, NULL);
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_choice (pod));
    g_assert_cmpstr ("Spa:Pod:Choice", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
  }

  /* It is not possible to use the parser to get the contents of a choice, you
   * need to use the iterator API to achieve that. This is because there is no
   * `spa_pod_parser_get_choice` API in the SPA library */
}

static void
test_spa_pod_array (void)
{
  /* Dynamic */
  {
    WpSpaPodBuilder *b = wp_spa_pod_builder_new_array ();
    wp_spa_pod_builder_add (b, "b", FALSE, NULL);
    wp_spa_pod_builder_add (b, "b", TRUE, NULL);
    wp_spa_pod_builder_add (b, "b", TRUE, NULL);
    wp_spa_pod_builder_add (b, "b", FALSE, NULL);
    wp_spa_pod_builder_add (b, "b", TRUE, NULL);
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_array (pod));
    g_assert_cmpstr ("Spa:Array", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
    wp_spa_pod_builder_unref (b);
    g_assert_true (wp_spa_pod_is_array (pod));

    g_autoptr (WpSpaPod) child = wp_spa_pod_get_array_child (pod);
    g_assert_nonnull (child);
    g_assert_cmpstr ("Spa:Bool", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (child)));
    gboolean value = TRUE;
    g_assert_true (wp_spa_pod_get_boolean (child, &value));
    g_assert_false (value);
  }

  /* It is not possible to use the parser to get the contents of an array, you
   * need to use the iterator API to achieve that. This is because there is no
   * `spa_pod_parser_get_array` API in the SPA library. */
}

static void
test_spa_pod_object (void)
{
  /* Static */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_object (
        "Spa:Pod:Object:Param:Props", "Props",
        "mute", "b", FALSE,
        "volume", "f", 0.5,
        "frequency", "i", 440,
        "device", "s", "device-name",
        "deviceFd", "h", 5,
        "id-01000000", "b", TRUE,
        NULL);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_object (pod));
    g_assert_cmpstr ("Spa:Pod:Object:Param:Props", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));

    const char *id_name;
    gboolean mute = TRUE;
    float vol = 0.0;
    gint32 frequency;
    const char *device;
    gint64 device_fd;
    gboolean custom = FALSE;
    g_assert_true (wp_spa_pod_get_object (pod,
        &id_name,
        "mute", "b", &mute,
        "volume", "f", &vol,
        "frequency", "i", &frequency,
        "device", "s", &device,
        "deviceFd", "h", &device_fd,
        "id-01000000", "b", &custom,
        NULL));
    g_assert_cmpstr (id_name, ==, "Props");
    g_assert_false (mute);
    g_assert_cmpfloat_with_epsilon (vol, 0.5, 0.01);
    g_assert_cmpint (frequency, ==, 440);
    g_assert_cmpstr (device, ==, "device-name");
    g_assert_cmpint (device_fd, ==, 5);
    g_assert_true (custom);
  }

  /* Dynamic */
  {
    g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_object (
        "Spa:Pod:Object:Param:Props", "Props");
    wp_spa_pod_builder_add_property (b, "mute");
    wp_spa_pod_builder_add_boolean (b, FALSE);
    wp_spa_pod_builder_add_property (b, "volume");
    wp_spa_pod_builder_add_float (b, 0.5);
    wp_spa_pod_builder_add_property (b, "frequency");
    wp_spa_pod_builder_add_int (b, 440);
    wp_spa_pod_builder_add_property (b, "device");
    wp_spa_pod_builder_add_string (b, "device-name");
    wp_spa_pod_builder_add_property (b, "deviceFd");
    wp_spa_pod_builder_add_fd (b, 5);
    wp_spa_pod_builder_add_property (b, "id-01000000");
    wp_spa_pod_builder_add_boolean (b, TRUE);
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_object (pod));
    g_assert_cmpstr ("Spa:Pod:Object:Param:Props", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));

    const char *id_name;
    gboolean mute = TRUE;
    float vol = 0.0;
    gint32 frequency;
    const char *device;
    gint64 device_fd;
    gboolean custom = FALSE;
    g_autoptr (WpSpaPodParser) p = wp_spa_pod_parser_new_object (pod, &id_name);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_parser_get (p, "mute", "b", &mute, NULL));
    g_assert_true (wp_spa_pod_parser_get (p, "volume", "f", &vol, NULL));
    g_assert_true (wp_spa_pod_parser_get (p, "frequency", "i", &frequency, NULL));
    g_assert_true (wp_spa_pod_parser_get (p, "device", "s", &device, NULL));
    g_assert_true (wp_spa_pod_parser_get (p, "deviceFd", "h", &device_fd, NULL));
    g_assert_true (wp_spa_pod_parser_get (p, "id-01000000", "b", &custom, NULL));
    wp_spa_pod_parser_end (p);
    g_assert_cmpstr (id_name, ==, "Props");
    g_assert_false (mute);
    g_assert_cmpfloat_with_epsilon (vol, 0.5, 0.01);
    g_assert_cmpint (frequency, ==, 440);
    g_assert_cmpstr (device, ==, "device-name");
    g_assert_cmpint (device_fd, ==, 5);
    g_assert_true (custom);
  }
}

static void
test_spa_pod_struct (void)
{
  /* Dynamic */
  {
    g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_struct ();
    wp_spa_pod_builder_add_boolean (b, TRUE);
    wp_spa_pod_builder_add_id (b, 2);
    wp_spa_pod_builder_add_int (b, 8);
    wp_spa_pod_builder_add_long (b, 64);
    wp_spa_pod_builder_add_float (b, 3.14);
    wp_spa_pod_builder_add_double (b, 2.718281828);
    wp_spa_pod_builder_add_string (b, "WirePlumber");
    wp_spa_pod_builder_add_bytes (b, "bytes", 5);
    wp_spa_pod_builder_add_pointer (b, "Spa:Pointer:Buffer", b);
    wp_spa_pod_builder_add_fd (b, 4);
    wp_spa_pod_builder_add_rectangle (b, 1920, 1080);
    wp_spa_pod_builder_add_fraction (b, 16, 9);
    {
      g_autoptr (WpSpaPod) pod = wp_spa_pod_new_int (35254);
      wp_spa_pod_builder_add_pod (b, pod);
    }
    {
      g_autoptr (WpSpaPod) pod = wp_spa_pod_new_object (
        "Spa:Pod:Object:Param:Props", "Props",
        "mute", "b", FALSE,
        NULL);
      wp_spa_pod_builder_add (b, "P", pod, NULL);
    }
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_struct (pod));
    g_assert_cmpstr ("Spa:Pod:Struct", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));

    g_autoptr (WpSpaPodParser) p = wp_spa_pod_parser_new_struct (pod);
    g_assert_nonnull (pod);

    gboolean value_boolean;
    g_assert_true (wp_spa_pod_parser_get_boolean (p, &value_boolean));
    g_assert_true (value_boolean);

    guint32 value_id;
    g_assert_true (wp_spa_pod_parser_get_id (p, &value_id));
    g_assert_cmpuint (value_id, ==, 2);

    gint32 value_int;
    g_assert_true (wp_spa_pod_parser_get_int (p, &value_int));
    g_assert_cmpint (value_int, ==, 8);

    gint64 value_long;
    g_assert_true (wp_spa_pod_parser_get_long (p, &value_long));
    g_assert_cmpint (value_long, ==, 64);

    float value_float;
    g_assert_true (wp_spa_pod_parser_get_float (p, &value_float));
    g_assert_cmpfloat_with_epsilon (value_float, 3.14, 0.001);

    double value_double;
    g_assert_true (wp_spa_pod_parser_get_double (p, &value_double));
    g_assert_cmpfloat_with_epsilon (value_double, 2.718281828, 0.0000000001);

    const char *value_string;
    g_assert_true (wp_spa_pod_parser_get_string (p, &value_string));
    g_assert_cmpstr (value_string, ==, "WirePlumber");

    gconstpointer value_bytes;
    guint32 len_bytes;
    g_assert_true (wp_spa_pod_parser_get_bytes (p, &value_bytes, &len_bytes));
    g_assert_cmpmem (value_bytes, len_bytes, "bytes", 5);
    g_assert_cmpuint (len_bytes, ==, 5);

    gconstpointer value_pointer;
    g_assert_true (wp_spa_pod_parser_get_pointer (p, &value_pointer));
    g_assert_nonnull (value_pointer);
    g_assert_true (value_pointer == b);

    gint64 value_fd;
    g_assert_true (wp_spa_pod_parser_get_fd (p, &value_fd));
    g_assert_cmpint (value_fd, ==, 4);

    guint32 value_width;
    guint32 value_height;
    g_assert_true (wp_spa_pod_parser_get_rectangle (p, &value_width, &value_height));
    g_assert_cmpuint (value_width, ==, 1920);
    g_assert_cmpuint (value_height, ==, 1080);

    guint32 value_num;
    guint32 value_denom;
    g_assert_true (wp_spa_pod_parser_get_fraction (p, &value_num, &value_denom));
    g_assert_cmpuint (value_num, ==, 16);
    g_assert_cmpuint (value_denom, ==, 9);

    g_autoptr (WpSpaPod) value_pod = wp_spa_pod_parser_get_pod (p);
    g_assert_nonnull (value_pod);
    gint value_pod_int;
    g_assert_true (wp_spa_pod_get_int (value_pod, &value_pod_int));
    g_assert_cmpint (value_pod_int, ==, 35254);

    g_autoptr (WpSpaPod) value_object = NULL;
    g_assert_true (wp_spa_pod_parser_get (p, "P", &value_object, NULL));
    g_assert_nonnull (value_object);
    const char *id_name;
    gboolean mute = TRUE;

    g_assert_true (wp_spa_pod_get_object (value_object,
        &id_name,
        "mute", "b", &mute,
        NULL));
    g_assert_cmpstr (id_name, ==, "Props");
    g_assert_false (mute);
  }
}

static void
test_spa_pod_sequence (void)
{
  /* Static */
  {
    g_autoptr (WpSpaPod) pod = wp_spa_pod_new_sequence (0,
        10, "Properties", "l", G_GINT64_CONSTANT (9999), NULL);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_sequence (pod));
    g_assert_cmpstr ("Spa:Pod:Sequence", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
  }

  /* Dynamic */
  {
    g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_sequence (0);
    wp_spa_pod_builder_add_control (b, 10, "Properties");
    wp_spa_pod_builder_add_long (b, G_GINT64_CONSTANT (9999));
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);
    g_assert_true (wp_spa_pod_is_sequence (pod));
    g_assert_cmpstr ("Spa:Pod:Sequence", ==,
        wp_spa_type_name (wp_spa_pod_get_spa_type (pod)));
  }

  /* It is not possible to use the parser to get the contents of a sequence, you
   * need to use the iterator API to achieve that. This is because there is no
   * `spa_pod_parser_get_sequence` API in the SPA library. */
}

static void
choice_foreach (const GValue *item, gpointer data)
{
  gint32 *total = data;
  const gint32 *value = g_value_get_pointer (item);
  *total += *value;
}

static void
array_foreach (const GValue *item, gpointer data)
{
  gint32 *total = data;
  const gint32 *value = g_value_get_pointer (item);
  *total += *value;
}

static void
object_foreach (const GValue *item, gpointer data)
{
  guint32 *total_props = data;
  WpSpaPod *prop = g_value_get_boxed (item);
  g_assert_true (wp_spa_pod_is_property (prop));
  *total_props += 1;
}

static void
struct_foreach (const GValue *item, gpointer data)
{
  guint32 *total_fields = data;
  *total_fields += 1;
}

static void
sequence_foreach (const GValue *item, gpointer data)
{
  guint32 *offset_total = data;
  WpSpaPod *control = g_value_get_boxed (item);
  g_assert_true (wp_spa_pod_is_control (control));
  guint32 offset = 0;
  g_assert_true (wp_spa_pod_get_control (control, &offset, NULL, NULL));
  *offset_total += offset;
}

static void
test_spa_pod_iterator (void)
{
  /* Choice */
  {
    g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_choice ("Enum");
    wp_spa_pod_builder_add (b, "i", 0, NULL);
    wp_spa_pod_builder_add (b, "i", 1, NULL);
    wp_spa_pod_builder_add (b, "i", 2, NULL);
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);

    g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (pod);
    g_assert_nonnull (it);

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      gpointer p = g_value_get_pointer (&next);
      g_assert_nonnull (p);
      g_assert_cmpint (*(gint *)p, ==, 0);
      g_value_unset (&next);
    }

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      gpointer p = g_value_get_pointer (&next);
      g_assert_nonnull (p);
      g_assert_cmpint (*(gint *)p, ==, 1);
      g_value_unset (&next);
    }

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      gpointer p = g_value_get_pointer (&next);
      g_assert_nonnull (p);
      g_assert_cmpint (*(gint *)p, ==, 2);
      g_value_unset (&next);
    }

    {
      g_assert_false (wp_iterator_next (it, NULL));
    }

    gint32 total = 0;
    g_assert_true (wp_iterator_foreach (it, choice_foreach, &total));
    g_assert_cmpint (total, ==, 3);

  }

  /* Array */
  {
    g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_array ();
    wp_spa_pod_builder_add_int (b, 1);
    wp_spa_pod_builder_add_int (b, 2);
    wp_spa_pod_builder_add_int (b, 3);
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);

    g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (pod);
    g_assert_nonnull (it);

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      gpointer p = g_value_get_pointer (&next);
      g_assert_nonnull (p);
      g_assert_cmpint (*(gint *)p, ==, 1);
      g_value_unset (&next);
    }

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      gpointer p = g_value_get_pointer (&next);
      g_assert_nonnull (p);
      g_assert_cmpint (*(gint *)p, ==, 2);
      g_value_unset (&next);
    }

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      gpointer p = g_value_get_pointer (&next);
      g_assert_nonnull (p);
      g_assert_cmpint (*(gint *)p, ==, 3);
      g_value_unset (&next);
    }

    {
      g_assert_false (wp_iterator_next (it, NULL));
    }

    gint total = 0;
    g_assert_true (wp_iterator_foreach (it, array_foreach, &total));
    g_assert_cmpint (total, ==, 6);
  }

  /* Object */
  {
    g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_object (
        "Spa:Pod:Object:Param:Props", "Props");
    wp_spa_pod_builder_add_property (b, "mute");
    wp_spa_pod_builder_add_boolean (b, FALSE);
    wp_spa_pod_builder_add_property (b, "device");
    wp_spa_pod_builder_add_string (b, "device-name");
    wp_spa_pod_builder_add_property (b, "id-01000000");
    wp_spa_pod_builder_add_boolean (b, TRUE);
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);

    g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (pod);
    g_assert_nonnull (it);

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      WpSpaPod *p = g_value_get_boxed (&next);
      g_assert_nonnull (p);
      g_assert_true (wp_spa_pod_is_property (p));
      const char *key = NULL;
      g_autoptr (WpSpaPod) value = NULL;
      g_assert_true (wp_spa_pod_get_property (p, &key, &value));
      g_assert_cmpstr (key, ==, "mute");
      gboolean b = TRUE;
      g_assert_true (wp_spa_pod_get_boolean (value, &b));
      g_assert_false (b);
      g_value_unset (&next);
    }

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      WpSpaPod *p = g_value_get_boxed (&next);
      g_assert_nonnull (p);
      g_assert_true (wp_spa_pod_is_property (p));
      const char *key = NULL;
      g_autoptr (WpSpaPod) value = NULL;
      g_assert_true (wp_spa_pod_get_property (p, &key, &value));
      g_assert_cmpstr (key, ==, "device");
      const char *s = NULL;
      g_assert_true (wp_spa_pod_get_string (value, &s));
      g_assert_cmpstr (s, ==, "device-name");
      g_value_unset (&next);
    }

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      WpSpaPod *p = g_value_get_boxed (&next);
      g_assert_nonnull (p);
      g_assert_true (wp_spa_pod_is_property (p));
      const char *key = NULL;
      g_autoptr (WpSpaPod) value = NULL;
      g_assert_true (wp_spa_pod_get_property (p, &key, &value));
      g_assert_cmpstr (key, ==, "id-01000000");
      gboolean b = FALSE;
      g_assert_true (wp_spa_pod_get_boolean (value, &b));
      g_assert_true (b);
      g_value_unset (&next);
    }

    {
      g_assert_false (wp_iterator_next (it, NULL));
    }

    guint32 total_props = 0;
    g_assert_true (wp_iterator_foreach (it, object_foreach, &total_props));
    g_assert_cmpuint (total_props, ==, 3);
  }

  /* Struct */
  {
    g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_struct ();
    wp_spa_pod_builder_add_boolean (b, TRUE);
    wp_spa_pod_builder_add_id (b, 2);
    wp_spa_pod_builder_add_int (b, 8);
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);

    g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (pod);
    g_assert_nonnull (it);

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      WpSpaPod *p = g_value_get_boxed (&next);
      g_assert_nonnull (p);
      gboolean v = FALSE;
      g_assert_true (wp_spa_pod_get_boolean (p, &v));
      g_assert_true (v);
      g_value_unset (&next);
    }

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      WpSpaPod *p = g_value_get_boxed (&next);
      g_assert_nonnull (p);
      guint32 v = 0;
      g_assert_true (wp_spa_pod_get_id (p, &v));
      g_assert_cmpuint (v, ==, 2);
      g_value_unset (&next);
    }

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      WpSpaPod *p = g_value_get_boxed (&next);
      g_assert_nonnull (p);
      gint v = 0;
      g_assert_true (wp_spa_pod_get_int (p, &v));
      g_assert_cmpint (v, ==, 8);
      g_value_unset (&next);
    }

    {
      g_assert_false (wp_iterator_next (it, NULL));
    }

    guint32 total_fields = 0;
    g_assert_true (wp_iterator_foreach (it, struct_foreach, &total_fields));
    g_assert_cmpuint (total_fields, ==, 3);
  }

  /* Sequence */
  {
    g_autoptr (WpSpaPodBuilder) b = wp_spa_pod_builder_new_sequence (0);
    wp_spa_pod_builder_add_control (b, 10, "Properties");
    wp_spa_pod_builder_add_float (b, 0.33);
    wp_spa_pod_builder_add_control (b, 40, "Properties");
    wp_spa_pod_builder_add_float (b, 0.66);
    g_autoptr (WpSpaPod) pod = wp_spa_pod_builder_end (b);
    g_assert_nonnull (pod);

    g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (pod);
    g_assert_nonnull (it);

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      WpSpaPod *p = g_value_get_boxed (&next);
      g_assert_nonnull (p);
      g_assert_true (wp_spa_pod_is_control (p));
      guint32 offset = 0;
      const char *type_name = NULL;
      g_autoptr (WpSpaPod) value = NULL;
      g_assert_true (wp_spa_pod_get_control (p, &offset, &type_name, &value));
      g_assert_cmpuint (offset, ==, 10);
      g_assert_cmpstr (type_name, ==, "Properties");
      float f = 0;
      g_assert_true (wp_spa_pod_get_float (value, &f));
      g_assert_cmpfloat_with_epsilon (f, 0.33, 0.001);
      g_value_unset (&next);
    }

    {
      GValue next = G_VALUE_INIT;
      g_assert_true (wp_iterator_next (it, &next));
      WpSpaPod *p = g_value_get_boxed (&next);
      g_assert_nonnull (p);
      g_assert_true (wp_spa_pod_is_control (p));
      guint32 offset = 0;
      const char *type_name = NULL;
      g_autoptr (WpSpaPod) value = NULL;
      g_assert_true (wp_spa_pod_get_control (p, &offset, &type_name, &value));
      g_assert_cmpuint (offset, ==, 40);
      g_assert_cmpstr (type_name, ==, "Properties");
      float f = 0;
      g_assert_true (wp_spa_pod_get_float (value, &f));
      g_assert_cmpfloat_with_epsilon (f, 0.66, 0.001);
      g_value_unset (&next);
    }

    {
      g_assert_false (wp_iterator_next (it, NULL));
    }

    guint32 offset_total = 0;
    g_assert_true (wp_iterator_foreach (it, sequence_foreach, &offset_total));
    g_assert_cmpuint (offset_total, ==, 50);
  }
}

static void
test_spa_pod_unique_owner (void)
{
  /* Create an object */
  WpSpaPod *pod = wp_spa_pod_new_object (
        "Spa:Pod:Object:Param:PropInfo", "PropInfo",
        "id", "K", "unknown",
        "name", "s", "prop-info-name",
        NULL);
  g_assert_nonnull (pod);
  g_assert_true (wp_spa_pod_is_unique_owner (pod));

  /* Get the first property using an iterator */
  GValue next = G_VALUE_INIT;
  g_autoptr (WpSpaPod) property = NULL;
  {
    g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (pod);
    g_assert_nonnull (it);
    g_assert_true (wp_iterator_next (it, &next));
    property = g_value_dup_boxed (&next);
  }
  g_assert_nonnull (property);
  g_assert_true (wp_spa_pod_is_property (property));
  {
    g_autoptr (WpSpaPod) value = NULL;
    const char *key = NULL;
    g_assert_true (wp_spa_pod_get_property (property, &key, &value));
    g_assert_nonnull (key);
    g_assert_cmpstr (key, ==, "id");
    g_assert_nonnull (value);
    guint32 id = 0;
    g_assert_true (wp_spa_pod_get_id (value, &id));
    g_assert_cmpuint (id, ==, 1);
  }

  /* Own the data */
  g_assert_true (wp_spa_pod_is_unique_owner (pod));
  g_assert_false (wp_spa_pod_is_unique_owner (property));
  property = wp_spa_pod_ensure_unique_owner (property);
  g_assert_true (wp_spa_pod_is_unique_owner (pod));
  g_assert_true (wp_spa_pod_is_unique_owner (property));

  /* Destroy the object */
  wp_spa_pod_unref (pod);
  g_assert_true (wp_spa_pod_is_unique_owner (property));

  /* Make sure the property data is still valid */
  {
    g_autoptr (WpSpaPod) value = NULL;
    const char *key = NULL;
    g_assert_true (wp_spa_pod_get_property (property, &key, &value));
    g_assert_nonnull (key);
    g_assert_cmpstr (key, ==, "id");
    g_assert_nonnull (value);
    guint32 id = 0;
    g_assert_true (wp_spa_pod_get_id (value, &id));
    g_assert_cmpuint (id, ==, 1);
  }

  /* Destroy the property */
  g_value_unset (&next);
}

static void
test_spa_pod_port_config (void)
{
  const gint32 rate = 48000;
  const gint32 channels = 2;

  /* Build the format to make sure the types exist */
  g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_object (
     "Spa:Pod:Object:Param:Format", "Format");
  wp_spa_pod_builder_add (builder,
     "mediaType",    "K", "audio",
     "mediaSubtype", "K", "raw",
     "format",       "K", "S16LE",
     "rate",         "i", rate,
     "channels",     "i", channels,
     NULL);
  g_autoptr (WpSpaPodBuilder) position_builder = wp_spa_pod_builder_new_array ();
  for (gint i = 0; i < channels; i++)
    wp_spa_pod_builder_add_id (position_builder, 0);
  wp_spa_pod_builder_add_property (builder, "position");
  g_autoptr (WpSpaPod) position = wp_spa_pod_builder_end (position_builder);
  wp_spa_pod_builder_add_pod (builder, position);
  g_autoptr (WpSpaPod) format = wp_spa_pod_builder_end (builder);
  g_assert_nonnull (format);

  /* Build the port config to make sure the types exist */
  g_autoptr (WpSpaPod) pod = wp_spa_pod_new_object (
      "Spa:Pod:Object:Param:PortConfig", "PortConfig",
      "direction",  "K", "Input",
      "mode",       "K", "dsp",
      "monitor",    "b", FALSE,
      "control",    "b", FALSE,
      "format",     "P", format,
      NULL);
  g_assert_nonnull (pod);
}

int
main (int argc, char *argv[])
{
  g_test_init (&argc, &argv, NULL);
  g_log_set_writer_func (wp_log_writer_default, NULL, NULL);

  g_test_add_func ("/wp/spa-pod/basic", test_spa_pod_basic);
  g_test_add_func ("/wp/spa-pod/choice", test_spa_pod_choice);
  g_test_add_func ("/wp/spa-pod/array", test_spa_pod_array);
  g_test_add_func ("/wp/spa-pod/object", test_spa_pod_object);
  g_test_add_func ("/wp/spa-pod/struct", test_spa_pod_struct);
  g_test_add_func ("/wp/spa-pod/sequence", test_spa_pod_sequence);
  g_test_add_func ("/wp/spa-pod/iterator", test_spa_pod_iterator);
  g_test_add_func ("/wp/spa-pod/unique-owner", test_spa_pod_unique_owner);
  g_test_add_func ("/wp/spa-pod/port-config", test_spa_pod_port_config);

  return g_test_run ();
}
   070701000001F0000081A400000000000000000000000165F8630400003CAC000000000000000000000000000000000000002600000000wireplumber-0.5.0/tests/wp/spa-type.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"
#include <spa/utils/type-info.h>

static void
test_spa_type_basic (void)
{
  g_assert_cmpuint (WP_SPA_TYPE_INVALID, ==, SPA_ID_INVALID);

  {
    WpSpaType type = SPA_TYPE_Int;
    g_assert_cmpstr (wp_spa_type_name (type), ==, "Spa:Int");
    g_assert_true (wp_spa_type_is_fundamental (type));
    g_assert_cmpuint (wp_spa_type_parent (type), ==, SPA_TYPE_Int);
  }

  {
    WpSpaType type = wp_spa_type_from_name ("Spa:Enum:ParamId");
    g_assert_cmpuint (type, ==, WP_SPA_TYPE_INVALID);

    WpSpaIdTable table = wp_spa_id_table_from_name ("Spa:Enum:ParamId");
    g_assert_nonnull (table);
  }

  {
    WpSpaType type = SPA_TYPE_OBJECT_Props;
    g_assert_cmpstr (wp_spa_type_name (type), ==, "Spa:Pod:Object:Param:Props");
    g_assert_cmpuint (wp_spa_type_from_name (SPA_TYPE_INFO_Props), ==, type);
    g_assert_true (wp_spa_type_is_object (type));
    g_assert_false (wp_spa_type_is_fundamental (type));
    g_assert_cmpuint (wp_spa_type_parent (type), ==, SPA_TYPE_Object);
    g_assert_nonnull (wp_spa_type_get_object_id_values_table (type));
    g_assert_true (wp_spa_type_get_object_id_values_table (type) ==
        wp_spa_id_table_from_name ("Spa:Enum:ParamId"));
  }

  /* enums */
  {
    WpSpaIdValue id = wp_spa_id_value_from_name ("Spa:Enum:ParamId:Props");
    g_assert_nonnull (id);
    g_assert_cmpstr (wp_spa_id_value_name (id), ==, "Spa:Enum:ParamId:Props");
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "Props");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PARAM_Props);

    g_assert_true (id == wp_spa_id_value_from_short_name (
            "Spa:Enum:ParamId", "Props"));
    g_assert_true (id == wp_spa_id_value_from_number (
            "Spa:Enum:ParamId", SPA_PARAM_Props));
  }

  {
    WpSpaIdValue id =
        wp_spa_id_value_from_name ("Spa:Enum:Control:Properties");
    g_assert_nonnull (id);
    g_assert_cmpstr (wp_spa_id_value_name (id), ==,
        "Spa:Enum:Control:Properties");
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "Properties");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_CONTROL_Properties);

    g_assert_true (id == wp_spa_id_value_from_short_name (
            "Spa:Enum:Control", "Properties"));
    g_assert_true (id == wp_spa_id_value_from_number (
            "Spa:Enum:Control", SPA_CONTROL_Properties));
  }

  {
    WpSpaIdValue id = wp_spa_id_value_from_name ("Spa:Enum:Choice:Enum");
    g_assert_nonnull (id);
    g_assert_cmpstr (wp_spa_id_value_name (id), ==, "Spa:Enum:Choice:Enum");
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "Enum");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_CHOICE_Enum);

    g_assert_true (id == wp_spa_id_value_from_short_name (
            "Spa:Enum:Choice", "Enum"));
    g_assert_true (id == wp_spa_id_value_from_number (
            "Spa:Enum:Choice", SPA_CHOICE_Enum));
  }

  /* objects */
  {
    WpSpaIdValue id =
        wp_spa_id_value_from_name ("Spa:Pod:Object:Param:Props:mute");
    g_assert_nonnull (id);
    g_assert_cmpstr (wp_spa_id_value_name (id), ==,
        "Spa:Pod:Object:Param:Props:mute");
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "mute");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_mute);

    g_assert_true (id == wp_spa_id_value_from_short_name (
            SPA_TYPE_INFO_Props, "mute"));
    g_assert_true (id == wp_spa_id_value_from_number (
            SPA_TYPE_INFO_Props, SPA_PROP_mute));
  }

  {
    WpSpaIdValue id =
        wp_spa_id_value_from_name ("Spa:Pod:Object:Param:PropInfo:id");
    g_assert_nonnull (id);
    g_assert_cmpstr (wp_spa_id_value_name (id), ==,
        "Spa:Pod:Object:Param:PropInfo:id");
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "id");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_INFO_id);

    /* WpSpaIdValue is a pointer to static spa_type_info,
       so it should be the same on all queries */
    g_assert_true (id == wp_spa_id_value_from_short_name (
            SPA_TYPE_INFO_PropInfo, "id"));
    g_assert_true (id == wp_spa_id_value_from_number (
            SPA_TYPE_INFO_PropInfo, SPA_PROP_INFO_id));
  }

  /* array value type check */
  {
    WpSpaIdValue id =
        wp_spa_id_value_from_name ("Spa:Pod:Object:Param:Props:channelVolumes");
    g_assert_nonnull (id);
    g_assert_cmpstr (wp_spa_id_value_name (id), ==,
        "Spa:Pod:Object:Param:Props:channelVolumes");
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "channelVolumes");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_channelVolumes);

    g_assert_cmpuint (wp_spa_id_value_array_get_item_type (id, NULL), ==,
        SPA_TYPE_Float);
  }

  {
    WpSpaIdValue id =
        wp_spa_id_value_from_name ("Spa:Pod:Object:Param:Props:channelMap");
    g_assert_nonnull (id);
    g_assert_cmpstr (wp_spa_id_value_name (id), ==,
        "Spa:Pod:Object:Param:Props:channelMap");
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "channelMap");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_channelMap);

    WpSpaIdTable table = NULL;
    g_assert_cmpuint (wp_spa_id_value_array_get_item_type (id, &table), ==,
        SPA_TYPE_Id);
    g_assert_nonnull (table);
    g_assert_true (table == wp_spa_id_table_from_name ("Spa:Enum:AudioChannel"));
  }
}

static void
test_spa_type_iterate (void)
{
  {
    WpSpaType type = wp_spa_type_from_name (SPA_TYPE_INFO_PropInfo);
    g_assert_cmpuint (type, !=, WP_SPA_TYPE_INVALID);
    g_assert_true (wp_spa_type_is_object (type));

    WpSpaIdTable table = wp_spa_type_get_values_table (type);
    g_autoptr (WpIterator) it = wp_spa_id_table_new_iterator (table);
    g_auto (GValue) value = G_VALUE_INIT;
    WpSpaIdValue id = NULL;

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_INFO_START);
    table = NULL;
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, &table), ==, SPA_TYPE_Id);
    g_assert_nonnull (table);
    g_assert_true (table == wp_spa_id_table_from_name ("Spa:Enum:ParamId"));
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "id");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_INFO_id);
    table = NULL;
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, &table), ==, SPA_TYPE_Id);
    g_assert_nonnull (table);
    g_assert_true (table ==
        wp_spa_id_table_from_name ("Spa:Pod:Object:Param:Props"));
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "name");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_INFO_name);
    table = NULL;
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, &table), ==, SPA_TYPE_String);
    g_assert_null (table);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "type");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_INFO_type);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, &table), ==, SPA_TYPE_Pod);
    g_assert_null (table);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "labels");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_INFO_labels);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, &table), ==, SPA_TYPE_Struct);
    g_assert_null (table);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "container");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_PROP_INFO_container);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, &table), ==, SPA_TYPE_Id);
    g_assert_null (table);
    g_value_unset (&value);
  }

  {
    WpSpaIdTable table = wp_spa_id_table_from_name ("Spa:Enum:Choice");
    g_assert_nonnull (table);

    g_autoptr (WpIterator) it = wp_spa_id_table_new_iterator (table);
    g_auto (GValue) value = G_VALUE_INIT;
    WpSpaIdValue id = NULL;

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "None");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_CHOICE_None);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Int);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "Range");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_CHOICE_Range);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Int);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "Step");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_CHOICE_Step);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Int);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "Enum");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_CHOICE_Enum);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Int);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "Flags");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, SPA_CHOICE_Flags);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Int);
    g_value_unset (&value);
  }
}

static void
test_spa_type_register (void)
{
  static const struct spa_type_info custom_enum_info[] = {
    { 0, SPA_TYPE_Int, "Spa:Enum:CustomEnum:Invalid", NULL  },
    { 1, SPA_TYPE_Int, "Spa:Enum:CustomEnum:Valid", NULL  },
    { 0, 0, NULL, NULL }
  };

  static const struct spa_type_info custom_obj_info[] = {
    { 0, SPA_TYPE_Id, "Spa:Pod:Object:CustomObj:", custom_enum_info },
    { 1, SPA_TYPE_Int, "Spa:Pod:Object:CustomObj:id", NULL },
    { 2, SPA_TYPE_String, "Spa:Pod:Object:CustomObj:name", NULL },
    { 3, SPA_TYPE_Float, "Spa:Pod:Object:CustomObj:volume", NULL },
    { 4, SPA_TYPE_Rectangle, "Spa:Pod:Object:CustomObj:box", NULL },
    { 5, SPA_TYPE_Bytes, "Spa:Pod:Object:CustomObj:data", NULL },
    { 0, 0, NULL, NULL },
  };

  wp_spa_dynamic_type_init ();

  WpSpaIdTable enum_table =
      wp_spa_dynamic_id_table_register ("Spa:Enum:CustomEnum", custom_enum_info);
  WpSpaType obj_type = wp_spa_dynamic_type_register ("Spa:Pod:Object:CustomObj",
      SPA_TYPE_Object, custom_obj_info);

  g_assert_nonnull (enum_table);
  g_assert_true (obj_type != WP_SPA_TYPE_INVALID);

  g_assert_true (enum_table ==
      wp_spa_id_table_from_name ("Spa:Enum:CustomEnum"));

  {
    g_autoptr (WpIterator) it = wp_spa_id_table_new_iterator (enum_table);
    g_auto (GValue) value = G_VALUE_INIT;
    WpSpaIdValue id = NULL;

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "Invalid");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, 0);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Int);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "Valid");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, 1);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Int);
    g_value_unset (&value);

    g_assert_false (wp_iterator_next (it, &value));
  }

  g_assert_cmpstr (wp_spa_type_name (obj_type), ==, "Spa:Pod:Object:CustomObj");
  g_assert_true (wp_spa_type_is_object (obj_type));
  g_assert_false (wp_spa_type_is_fundamental (obj_type));
  g_assert_cmpuint (wp_spa_type_parent (obj_type), ==, SPA_TYPE_Object);
  g_assert_cmpuint (obj_type, ==,
      wp_spa_type_from_name ("Spa:Pod:Object:CustomObj"));
  g_assert_true (enum_table ==
      wp_spa_type_get_object_id_values_table (obj_type));

  {
    WpSpaIdTable table = wp_spa_type_get_values_table (obj_type);
    g_autoptr (WpIterator) it = wp_spa_id_table_new_iterator (table);
    g_auto (GValue) value = G_VALUE_INIT;
    WpSpaIdValue id = NULL;

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, 0);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Id);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "id");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, 1);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Int);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "name");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, 2);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_String);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "volume");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, 3);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Float);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "box");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, 4);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Rectangle);
    g_value_unset (&value);

    g_assert_true (wp_iterator_next (it, &value));
    id = g_value_get_pointer (&value);
    g_assert_cmpstr (wp_spa_id_value_short_name (id), ==, "data");
    g_assert_cmpuint (wp_spa_id_value_number (id), ==, 5);
    g_assert_cmpuint (wp_spa_id_value_get_value_type (id, NULL), ==, SPA_TYPE_Bytes);
    g_value_unset (&value);

    g_assert_false (wp_iterator_next (it, &value));
  }

  wp_spa_dynamic_type_deinit ();
}

int
main (int argc, char *argv[])
{
  g_test_init (&argc, &argv, NULL);
  g_log_set_writer_func (wp_log_writer_default, NULL, NULL);

  g_test_add_func ("/wp/spa-type/basic", test_spa_type_basic);
  g_test_add_func ("/wp/spa-type/iterate", test_spa_type_iterate);
  g_test_add_func ("/wp/spa-type/register", test_spa_type_register);

  return g_test_run ();
}
070701000001F1000081A400000000000000000000000165F8630400001B91000000000000000000000000000000000000002300000000wireplumber-0.5.0/tests/wp/state.c    /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"

static void
test_state_basic (void)
{
  g_autoptr (GError) error = NULL;
  g_autoptr (WpState) state = wp_state_new ("basic");
  g_assert_nonnull (state);

  g_assert_cmpstr (wp_state_get_name (state), ==, "basic");
  g_assert_true (g_str_has_suffix (wp_state_get_location (state), "basic"));

  /* Save */
  {
    g_autoptr (WpProperties) props = wp_properties_new_empty ();
    wp_properties_set (props, "key1", "value1");
    wp_properties_set (props, "key2", "value2");
    wp_properties_set (props, "key3", "value3");
    g_assert_true (wp_state_save (state, props, &error));
    g_assert_no_error (error);
  }

  /* Load */
  {
    g_autoptr (WpProperties) props = wp_state_load (state);
    g_assert_nonnull (props);
    g_assert_cmpstr (wp_properties_get (props, "key1"), ==, "value1");
    g_assert_cmpstr (wp_properties_get (props, "key2"), ==, "value2");
    g_assert_cmpstr (wp_properties_get (props, "key2"), ==, "value2");
    g_assert_null (wp_properties_get (props, "invalid"));
  }

  /* Re-Save */
  {
    g_autoptr (WpProperties) props = wp_properties_new_empty ();
    wp_properties_set (props, "new-key", "new-value");
    g_assert_true (wp_state_save (state, props, &error));
    g_assert_no_error (error);
  }

  /* Re-Load */
  {
    g_autoptr (WpProperties) props = wp_state_load (state);
    g_assert_nonnull (props);
    g_assert_cmpstr (wp_properties_get (props, "new-key"), ==, "new-value");
    g_assert_null (wp_properties_get (props, "key1"));
    g_assert_null (wp_properties_get (props, "key2"));
    g_assert_null (wp_properties_get (props, "key3"));
  }

  wp_state_clear (state);

  /* Load empty */
  {
    g_autoptr (WpProperties) props = wp_state_load (state);
    g_assert_nonnull (props);
    g_assert_null (wp_properties_get (props, "new-key"));
    g_assert_null (wp_properties_get (props, "key1"));
    g_assert_null (wp_properties_get (props, "key2"));
    g_assert_null (wp_properties_get (props, "key3"));
  }

  wp_state_clear (state);
}

static void
test_state_empty (void)
{
  g_autoptr (GError) error = NULL;
  g_autoptr (WpState) state = wp_state_new ("empty");
  g_assert_nonnull (state);

  /* Save */
  {
    g_autoptr (WpProperties) props = wp_properties_new_empty ();
    wp_properties_set (props, "key", "value");
    g_assert_true (wp_state_save (state, props, &error));
    g_assert_no_error (error);
  }

  /* Load */
  {
    g_autoptr (WpProperties) props = wp_state_load (state);
    g_assert_nonnull (props);
    g_assert_cmpstr (wp_properties_get (props, "key"), ==, "value");
  }

  /* Save empty */
  {
    g_autoptr (WpProperties) props = wp_properties_new_empty ();
    g_assert_true (wp_state_save (state, props, &error));
    g_assert_no_error (error);
  }

  /* Load empty */
  {
    g_autoptr (WpProperties) props = wp_state_load (state);
    g_assert_nonnull (props);
    g_assert_null (wp_properties_get (props, "key"));
  }

  wp_state_clear (state);
}

static void
test_state_spaces (void)
{
  g_autoptr (GError) error = NULL;
  g_autoptr (WpState) state = wp_state_new ("spaces");
  g_assert_nonnull (state);

  /* Save */
  {
    g_autoptr (WpProperties) props = wp_properties_new_empty ();
    wp_properties_set (props, "key", "value with spaces");
    g_assert_true (wp_state_save (state, props, &error));
    g_assert_no_error (error);
  }

  /* Load */
  {
    g_autoptr (WpProperties) props = wp_state_load (state);
    g_assert_nonnull (props);
    g_assert_cmpstr (wp_properties_get (props, "key"), ==, "value with spaces");
  }

  wp_state_clear (state);
}

static void
test_state_escaped (void)
{
  g_autoptr (GError) error = NULL;
  g_autoptr (WpState) state = wp_state_new ("escaped");
  g_assert_nonnull (state);

  /* Save */
  {
    g_autoptr (WpProperties) props = wp_properties_new_empty ();
    wp_properties_set (props, "[]", "v0");
    wp_properties_set (props, "[ ]", "v1");
    wp_properties_set (props, "[=]", "v2");
    wp_properties_set (props, " [=]", "v3");
    wp_properties_set (props, "[=] ", "v4");
    wp_properties_set (props, " [=] ", "v5");
    wp_properties_set (props, " [ =] ", "v6");
    wp_properties_set (props, " [= ] ", "v7");
    wp_properties_set (props, " [ = ] ", "v8");
    wp_properties_set (props, " [", "v9");
    wp_properties_set (props, "[ ", "v10");
    wp_properties_set (props, " [ ", "v11");
    wp_properties_set (props, " ]", "v12");
    wp_properties_set (props, "] ", "v13");
    wp_properties_set (props, " ] ", "v14");
    wp_properties_set (props, " ", "v15");
    wp_properties_set (props, "=", "v16");
    wp_properties_set (props, "\\", "v17");
    wp_properties_set (props, "\\[", "v18");
    wp_properties_set (props, "\\a", "v19");
    wp_properties_set (props, "\\\\", "v20");
    wp_properties_set (props, "[][", "][]");
    g_assert_true (wp_state_save (state, props, &error));
    g_assert_no_error (error);
  }

  /* Load */
  {
    g_autoptr (WpProperties) props = wp_state_load (state);
    g_assert_nonnull (props);
    g_assert_cmpstr (wp_properties_get (props, "[]"), ==, "v0");
    g_assert_cmpstr (wp_properties_get (props, "[ ]"), ==, "v1");
    g_assert_cmpstr (wp_properties_get (props, "[=]"), ==, "v2");
    g_assert_cmpstr (wp_properties_get (props, " [=]"), ==, "v3");
    g_assert_cmpstr (wp_properties_get (props, "[=] "), ==, "v4");
    g_assert_cmpstr (wp_properties_get (props, " [=] "), ==, "v5");
    g_assert_cmpstr (wp_properties_get (props, " [ =] "), ==, "v6");
    g_assert_cmpstr (wp_properties_get (props, " [= ] "), ==, "v7");
    g_assert_cmpstr (wp_properties_get (props, " [ = ] "), ==, "v8");
    g_assert_cmpstr (wp_properties_get (props, " ["), ==, "v9");
    g_assert_cmpstr (wp_properties_get (props, "[ "), ==, "v10");
    g_assert_cmpstr (wp_properties_get (props, " [ "), ==, "v11");
    g_assert_cmpstr (wp_properties_get (props, " ]"), ==, "v12");
    g_assert_cmpstr (wp_properties_get (props, "] "), ==, "v13");
    g_assert_cmpstr (wp_properties_get (props, " ] "), ==, "v14");
    g_assert_cmpstr (wp_properties_get (props, " "), ==, "v15");
    g_assert_cmpstr (wp_properties_get (props, "="), ==, "v16");
    g_assert_cmpstr (wp_properties_get (props, "\\"), ==, "v17");
    g_assert_cmpstr (wp_properties_get (props, "\\["), ==, "v18");
    g_assert_cmpstr (wp_properties_get (props, "\\a"), ==, "v19");
    g_assert_cmpstr (wp_properties_get (props, "\\\\"), ==, "v20");
    g_assert_cmpstr (wp_properties_get (props, "[]["), ==, "][]");
  }

  wp_state_clear (state);
}

int
main (int argc, char *argv[])
{
  g_test_init (&argc, &argv, NULL);
  g_log_set_writer_func (wp_log_writer_default, NULL, NULL);

  g_test_add_func ("/wp/state/basic", test_state_basic);
  g_test_add_func ("/wp/state/empty", test_state_empty);
  g_test_add_func ("/wp/state/spaces", test_state_spaces);
  g_test_add_func ("/wp/state/escaped", test_state_escaped);

  return g_test_run ();
}
   070701000001F2000081A400000000000000000000000165F863040000213D000000000000000000000000000000000000002800000000wireplumber-0.5.0/tests/wp/transition.c   /* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"

enum {
  STEP_FIRST = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_SECOND,
  STEP_THIRD,
  STEP_FINISH,
};

struct data
{
  gboolean destroyed;

  /* steps that appeared in get_next_step */
  guint sta[10];
  guint sta_i;

  /* steps executed */
  guint ste[10];
  guint ste_i;
};

struct _WpTestTransition
{
  WpTransition parent;
  gboolean step_third_wait;
  gboolean step_third_error;
};

G_DECLARE_FINAL_TYPE (WpTestTransition, wp_test_transition, WP, TEST_TRANSITION, WpTransition)
G_DEFINE_TYPE (WpTestTransition, wp_test_transition, WP_TYPE_TRANSITION)

static void
data_destroy (gpointer data)
{
  struct data *d = data;
  d->destroyed = TRUE;
}

static gboolean
advance_on_idle (gpointer data)
{
  WpTransition * self = WP_TRANSITION (data);
  wp_transition_advance (self);
  return G_SOURCE_REMOVE;
}

static void
wp_test_transition_init (WpTestTransition *self)
{
  self->step_third_wait = TRUE;
  self->step_third_error = FALSE;
}

static guint
wp_test_transition_get_next_step (WpTransition * transition, guint step)
{
  WpTestTransition * self = WP_TEST_TRANSITION (transition);
  struct data *d = wp_transition_get_data (transition);

  g_assert_nonnull (d);
  g_assert_cmpint (d->sta_i, <, 10);
  d->sta[d->sta_i++] = step;

  switch (step) {
    case WP_TRANSITION_STEP_NONE:
      return STEP_FIRST;

    case STEP_FIRST:
    case STEP_SECOND:
      return step + 1;

    case STEP_THIRD:
      if (self->step_third_wait) {
        self->step_third_wait = FALSE;
        g_idle_add (advance_on_idle, transition);
        return STEP_THIRD;
      }
      return STEP_FINISH;

    case STEP_FINISH:
      return WP_TRANSITION_STEP_NONE;

    default:
      g_assert_not_reached ();
  }

  return WP_TRANSITION_STEP_ERROR;
}

static void
wp_test_transition_execute_step (WpTransition * transition, guint step)
{
  WpTestTransition * self = WP_TEST_TRANSITION (transition);
  struct data *d = wp_transition_get_data (transition);

  if (step != WP_TRANSITION_STEP_ERROR) {
    g_assert_cmpint (step, >=, STEP_FIRST);
    g_assert_cmpint (step, <=, STEP_FINISH);
  }

  g_assert_nonnull (d);
  g_assert_cmpint (d->ste_i, <, 10);
  d->ste[d->ste_i++] = step;

  if (step == STEP_THIRD && self->step_third_error) {
    wp_transition_return_error (transition,
        g_error_new (WP_DOMAIN_LIBRARY, 100, "error"));
    return;
  }

  if (step != WP_TRANSITION_STEP_ERROR)
    g_idle_add (advance_on_idle, transition);
}

static void
wp_test_transition_class_init (WpTestTransitionClass *klass)
{
  WpTransitionClass *transition_class = (WpTransitionClass *) klass;

  transition_class->get_next_step = wp_test_transition_get_next_step;
  transition_class->execute_step = wp_test_transition_execute_step;
}

static void test_transition_basic (void);

static void
test_transition_basic_done (GObject *source, GAsyncResult *res, gpointer data)
{
  g_autoptr (GError) error = NULL;

  g_assert_true (WP_IS_TEST_TRANSITION (res));
  g_assert_true (g_async_result_is_tagged (res, test_transition_basic));
  g_assert_true (wp_transition_get_completed (WP_TRANSITION (res)));
  g_assert_false (wp_transition_had_error (WP_TRANSITION (res)));
  g_assert_true (wp_transition_finish (res, &error));
  g_assert_no_error (error);

  g_main_loop_quit ((GMainLoop *) data);
}

static void
test_transition_basic (void)
{
  struct data data = {0};
  g_autoptr (GMainLoop) loop = g_main_loop_new (NULL, FALSE);
  g_autoptr (GObject) source_object = g_object_new (G_TYPE_OBJECT, NULL);

  WpTransition *t = wp_transition_new (wp_test_transition_get_type (),
      source_object, NULL, test_transition_basic_done, loop);
  g_assert_nonnull (t);
  g_assert_true (WP_IS_TRANSITION (t));
  g_assert_true (WP_IS_TEST_TRANSITION (t));
  g_assert_true (G_IS_ASYNC_RESULT (t));
  g_assert_true (wp_transition_get_source_object (t) == source_object);
  {
    g_autoptr (GObject) so = g_async_result_get_source_object (G_ASYNC_RESULT (t));
    g_assert_true (so == source_object);
  }
  g_assert_cmpint (source_object->ref_count, ==, 2);

  g_assert_null (wp_transition_get_data (t));
  wp_transition_set_data (t, &data, data_destroy);
  g_assert_true (wp_transition_get_data (t) == &data);
  g_assert_true (g_async_result_get_user_data (G_ASYNC_RESULT (t)) == &data);

  g_assert_null (wp_transition_get_source_tag (t));
  wp_transition_set_source_tag (t, test_transition_basic);
  g_assert_true (wp_transition_get_source_tag (t) == test_transition_basic);
  g_assert_true (wp_transition_is_tagged (t, test_transition_basic));

  g_object_ref (t);
  wp_transition_advance (t);
  g_assert_false (wp_transition_get_completed (t));
  g_assert_false (wp_transition_had_error (t));
  g_object_unref (t);

  g_main_loop_run (loop);

  g_assert_cmpint (source_object->ref_count, ==, 1);
  g_assert_cmpint (data.sta[0], ==, WP_TRANSITION_STEP_NONE);
  g_assert_cmpint (data.sta[1], ==, STEP_FIRST);
  g_assert_cmpint (data.sta[2], ==, STEP_SECOND);
  g_assert_cmpint (data.sta[3], ==, STEP_THIRD);
  g_assert_cmpint (data.sta[4], ==, STEP_THIRD);
  g_assert_cmpint (data.sta[5], ==, STEP_FINISH);
  g_assert_cmpint (data.sta_i, ==, 6);
  g_assert_cmpint (data.ste[0], ==, STEP_FIRST);
  g_assert_cmpint (data.ste[1], ==, STEP_SECOND);
  g_assert_cmpint (data.ste[2], ==, STEP_THIRD);
  g_assert_cmpint (data.ste[3], ==, STEP_FINISH);
  g_assert_cmpint (data.ste_i, ==, 4);
  g_assert_true (data.destroyed);
}

static void test_transition_error (void);

static void
test_transition_error_done (GObject *source, GAsyncResult *res, gpointer data)
{
  g_autoptr (GError) error = NULL;

  g_assert_true (WP_IS_TEST_TRANSITION (res));
  g_assert_true (g_async_result_is_tagged (res, test_transition_error));
  g_assert_true (wp_transition_get_completed (WP_TRANSITION (res)));
  g_assert_true (wp_transition_had_error (WP_TRANSITION (res)));
  g_assert_false (wp_transition_finish (res, &error));
  g_assert_error (error, WP_DOMAIN_LIBRARY, 100);

  g_main_loop_quit ((GMainLoop *) data);
}

static void
test_transition_error (void)
{
  struct data data = {0};
  g_autoptr (GMainLoop) loop = g_main_loop_new (NULL, FALSE);
  g_autoptr (GObject) source_object = g_object_new (G_TYPE_OBJECT, NULL);

  WpTransition *t = wp_transition_new (wp_test_transition_get_type (),
      source_object, NULL, test_transition_error_done, loop);
  g_assert_nonnull (t);
  g_assert_true (WP_IS_TRANSITION (t));
  g_assert_true (WP_IS_TEST_TRANSITION (t));
  g_assert_true (G_IS_ASYNC_RESULT (t));
  g_assert_true (wp_transition_get_source_object (t) == source_object);
  {
    g_autoptr (GObject) so = g_async_result_get_source_object (G_ASYNC_RESULT (t));
    g_assert_true (so == source_object);
  }
  g_assert_cmpint (source_object->ref_count, ==, 2);

  g_assert_null (wp_transition_get_data (t));
  wp_transition_set_data (t, &data, data_destroy);
  g_assert_true (wp_transition_get_data (t) == &data);
  g_assert_true (g_async_result_get_user_data (G_ASYNC_RESULT (t)) == &data);

  g_assert_null (wp_transition_get_source_tag (t));
  wp_transition_set_source_tag (t, test_transition_error);
  g_assert_true (wp_transition_get_source_tag (t) == test_transition_error);
  g_assert_true (wp_transition_is_tagged (t, test_transition_error));

  /* enable error condition */
  ((WpTestTransition *) t)->step_third_error = TRUE;

  g_object_ref (t);
  wp_transition_advance (t);
  g_assert_false (wp_transition_get_completed (t));
  g_assert_false (wp_transition_had_error (t));
  g_object_unref (t);

  g_main_loop_run (loop);

  g_assert_cmpint (source_object->ref_count, ==, 1);
  g_assert_cmpint (data.sta[0], ==, WP_TRANSITION_STEP_NONE);
  g_assert_cmpint (data.sta[1], ==, STEP_FIRST);
  g_assert_cmpint (data.sta[2], ==, STEP_SECOND);
  g_assert_cmpint (data.sta_i, ==, 3);
  g_assert_cmpint (data.ste[0], ==, STEP_FIRST);
  g_assert_cmpint (data.ste[1], ==, STEP_SECOND);
  g_assert_cmpint (data.ste[2], ==, STEP_THIRD);
  g_assert_cmpint (data.ste[3], ==, WP_TRANSITION_STEP_ERROR);
  g_assert_cmpint (data.ste_i, ==, 4);
  g_assert_true (data.destroyed);
}

int
main (int argc, char *argv[])
{
  g_test_init (&argc, &argv, NULL);
  g_log_set_writer_func (wp_log_writer_default, NULL, NULL);

  g_test_add_func ("/wp/transition/basic", test_transition_basic);
  g_test_add_func ("/wp/transition/error", test_transition_error);

  return g_test_run ();
}
   070701000001F3000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000001E00000000wireplumber-0.5.0/tests/wplua 070701000001F4000081A400000000000000000000000165F8630400000569000000000000000000000000000000000000002A00000000wireplumber-0.5.0/tests/wplua/meson.build common_deps = [wplua_dep, pipewire_dep, wp_dep]
common_env = common_test_env
common_env.prepend('WIREPLUMBER_DATA_DIR', meson.current_source_dir())
common_env.set('G_TEST_SRCDIR', meson.current_source_dir())
common_env.set('G_TEST_BUILDDIR', meson.current_build_dir())

test(
  'test-wplua',
  executable('test-wplua', 'wplua.c',
    dependencies: common_deps),
  env: common_env,
)

script_tester = executable('script-tester',
    '..'/'script-tester.c',
    dependencies: common_deps
)

test(
  'test-lua-pod',
  script_tester,
  args: ['lua-api-tests', 'pod.lua'],
  env: common_env,
)
test(
  'test-lua-json',
  script_tester,
  args: ['lua-api-tests', 'json.lua'],
  env: common_env,
)
test(
  'test-lua-json-utils',
  script_tester,
  args: ['lua-api-tests', 'json-utils.lua'],
  env: common_env,
)
test(
  'test-lua-monitor-rules',
  script_tester,
  args: ['lua-api-tests', 'monitor-rules.lua'],
  env: common_env,
)
test(
  'test-lua-require',
  script_tester,
  args: ['lua-api-tests', 'require.lua'],
  env: common_env,
)
test(
  'test-lua-async-activation',
  script_tester,
  args: ['lua-api-tests', 'async-activation.lua'],
  env: common_env,
)
test(
  'test-lua-settings',
  script_tester,
  args: ['lua-api-tests', 'settings.lua'],
  env: common_env,
)
test(
  'test-lua-event-hooks',
  script_tester,
  args: ['lua-api-tests', 'event-hooks.lua'],
  env: common_env,
)
   070701000001F5000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002600000000wireplumber-0.5.0/tests/wplua/scripts 070701000001F6000081A400000000000000000000000165F86304000001FF000000000000000000000000000000000000003B00000000wireplumber-0.5.0/tests/wplua/scripts/async-activation.lua    Script.async_activation = true

tags = {}

function checkpoint(x)
  Log.info(x)
  table.insert(tags, x)
end

Core.timeout_add(100, function()
  checkpoint("timeout1")
  return false
end)

Core.timeout_add(200, function()
  checkpoint("timeout2")
  return false
end)

Core.timeout_add(300, function()
  checkpoint("timeout3")
  assert(#tags == 3)
  assert(tags[1] == "timeout1")
  assert(tags[2] == "timeout2")
  assert(tags[3] == "timeout3")
  Script:finish_activation()
  return false
end)

assert(#tags == 0)
 070701000001F7000081A400000000000000000000000165F863040000076E000000000000000000000000000000000000003600000000wireplumber-0.5.0/tests/wplua/scripts/event-hooks.lua Script.async_activation = true

local tags = {}

local function checkpoint(tag)
  Log.info("at " .. tag)
  table.insert(tags, tag)
end

local function check_results()
  local i = 0
  local function inc()
    i = i+1
    return i
  end

  assert(tags[inc()] == "simple-first")
  assert(tags[inc()] == "simple-1")
  assert(tags[inc()] == "async-start")
  assert(tags[inc()] == "async-start-advance")
  assert(tags[inc()] == "async-step2")
  assert(tags[inc()] == "simple-last")
end

local common_interests = {
  EventInterest {
    Constraint { "event.type", "=", "test-event" },
  },
}

AsyncEventHook {
  name = "test-async-hook",
  before = "test-last-hook" ,
  after = { "test-first-hook", "test-simple-hook" },
  interests = common_interests,
  steps = {
    start = {
      next = "step2",
      execute = function (event, transition)
        checkpoint("async-start")
        Core.idle_add(function ()
          checkpoint("async-start-advance")
          transition:advance()
          return false
        end)
      end,
    },
    step2 = {
      next = "none",
      execute = function (event, transition)
        checkpoint("async-step2")
        transition:advance()
      end,
    },
  },
}:register()

SimpleEventHook {
  name = "test-first-hook",
  before = { "test-simple-hook", "test-last-hook" },
  interests = common_interests,
  execute = function (event)
    checkpoint("simple-first")
  end
}:register()

SimpleEventHook {
  name = "test-simple-hook",
  after = { "test-first-hook" },
  before = {},
  interests = common_interests,
  execute = function (event)
    checkpoint("simple-1")
  end
}:register()

SimpleEventHook {
  name = "test-last-hook",
  interests = common_interests,
  execute = function (event)
    checkpoint("simple-last")
    check_results()
    Script:finish_activation()
  end
}:register()

EventDispatcher.push_event { type = "test-event", priority = 1 }
  070701000001F8000081A400000000000000000000000165F8630400000F32000000000000000000000000000000000000003500000000wireplumber-0.5.0/tests/wplua/scripts/json-utils.lua  rules_json_str = [[
[
  {
    matches = [
      {
        device.name = "~alsa_card.*"
      }
    ]
    actions = {
      update-props = {
        api.alsa.use-acp = true
        api.acp.auto-port = false
      }
    }
  }
  {
    matches = [
      {
        node.name = "alsa_output.0.my-alsa-device"
      }
    ]
    actions = {
      update-props = {
        audio.rate = 96000
        node.description = "My ALSA Node"
        media.class = null
      }
    }
  }
]
]]

match_props = { ["device.name"] = "unmatched-device-name" }
ret_props, ret = JsonUtils.match_rules_update_properties (Json.Raw (rules_json_str), match_props)
assert (ret == 0)
assert (ret_props["device.name"] == match_props["device.name"])

match_props = { ["device.name"] = "alsa_card_0.my-alsa-device" }
ret_props, ret = JsonUtils.match_rules_update_properties (Json.Raw (rules_json_str), match_props)
assert (ret == 2)
assert (ret_props["device.name"] == "alsa_card_0.my-alsa-device")
assert (ret_props["api.alsa.use-acp"] == "true")
assert (ret_props["api.acp.auto-port"] == "false")

match_props = { ["node.name"] = "alsa_output.0.my-alsa-device" }
ret_props, ret = JsonUtils.match_rules_update_properties (Json.Raw (rules_json_str), match_props)
assert (ret == 2)
assert (ret_props["node.name"] == "alsa_output.0.my-alsa-device")
assert (ret_props["audio.rate"] == "96000")
assert (ret_props["node.description"] == "My ALSA Node")
assert (ret_props["media.class"] == nil)

match_props = {
  ["node.name"] = "alsa_output.0.my-alsa-device",
  ["media.class"] = "Audio/Sink",
  ["audio.rate"] = "48000",
  ["node.description"] = "Test",
}
ret_props, ret = JsonUtils.match_rules_update_properties (Json.Raw (rules_json_str), match_props)
assert (ret == 3)
assert (ret_props["node.name"] == "alsa_output.0.my-alsa-device")
assert (ret_props["audio.rate"] == "96000")
assert (ret_props["node.description"] == "My ALSA Node")
assert (ret_props["media.class"] == nil)

rules_json_str = [[
[
  {
    matches = [
      {
        device.name = "~alsa_card.*"
      }
    ]
    actions = {
      update-props = {
        api.acp.auto-port = false
      }
      set-answer = 42
    }
  }
  {
    matches = [
      {
        test.error = true
      }
    ]
    actions = {
      generate-error = "test.error is true"
    }
  }
  {
    matches = [
      {
        device.name = "alsa_card.1"
      }
    ]
    actions = {
      set-description = "My ALSA Device"
    }
  }
]
]]

function match_rules_callback (action, value)
  if action == "update-props" then
    local updates = value:parse ()
    for k,v in pairs (updates) do
      match_props[k] = tostring (v)
    end
  elseif action == "set-answer" then
    local v = value:parse ()
    match_props["answer.universe"] = tostring (v)
  elseif action == "generate-error" then
    local err = value:parse ()
    return false, tostring (err)
  elseif action == "set-description" then
    local str = value:parse ()
    match_props["device.description"] = tostring (str)
  end

  return true
end

match_props = {
  ["device.name"] = "alsa_card.1",
  ["test.error"] = "false",
}
ret, err = JsonUtils.match_rules (Json.Raw (rules_json_str), match_props, match_rules_callback)
assert (ret == true)
assert (err == nil)
assert (match_props["device.name"] == "alsa_card.1")
assert (match_props["api.acp.auto-port"] == "false")
assert (match_props["answer.universe"] == "42")
assert (match_props["device.description"] == "My ALSA Device")

match_props = {
  ["device.name"] = "alsa_card.1",
  ["test.error"] = "true",
}
ret, err = JsonUtils.match_rules (Json.Raw (rules_json_str), match_props, match_rules_callback)
assert (ret == false)
assert (err == "test.error is true")
assert (match_props["device.name"] == "alsa_card.1")
assert (match_props["api.acp.auto-port"] == "false")
assert (match_props["answer.universe"] == "42")
assert (match_props["device.description"] == nil)
  070701000001F9000081A400000000000000000000000165F863040000188E000000000000000000000000000000000000002F00000000wireplumber-0.5.0/tests/wplua/scripts/json.lua    -- Null
json = Json.Null ()
assert (json:is_null())
assert (json:parse() == nil)
assert (json:get_data() == "null")
assert (json:get_size() == 4)
assert (json:get_data() == json:to_string())

-- Boolean
json = Json.Boolean (true)
assert (json:is_boolean())
assert (json:parse())
assert (json:get_data() == "true")
assert (json:get_size() == 4)
assert (json:get_data() == json:to_string())
json = Json.Boolean (false)
assert (json:is_boolean())
assert (not json:parse())
assert (json:get_data() == "false")
assert (json:get_size() == 5)
assert (json:get_data() == json:to_string())

-- Int
json = Json.Int (3)
assert (json:is_int())
assert (json:parse() == 3)
assert (json:get_data() == "3")
assert (json:get_size() == 1)
assert (json:get_data() == json:to_string())

-- Float
json = Json.Float(3.14)
assert (json:is_float())
val = json:parse ()
assert (val > 3.13 and val < 3.15)

-- String
json = Json.String ("wireplumber")
assert (json:is_string())
assert (json:parse() == "wireplumber")
assert (json:get_data() == "\"wireplumber\"")
assert (json:get_size() == 13)
assert (json:get_data() == json:to_string())

-- Array
json = Json.Array { Json.Null (), Json.Null () }
assert (json:is_array())
val = json:parse ()
assert (val[1] == nil)
assert (val[2] == nil)
assert (json:get_data() == "[null, null]")
assert (json:get_size() == 12)
assert (json:get_data() == json:to_string())

json = Json.Array { true, false }
assert (json:is_array())
val = json:parse ()
assert (val[1])
assert (not val[2])
assert (json:get_data() == "[true, false]")
assert (json:get_size() == 13)
assert (json:get_data() == json:to_string())

json = Json.Array {1, 2, 3}
assert (json:is_array())
val = json:parse ()
assert (val[1] == 1)
assert (val[2] == 2)
assert (val[3] == 3)
assert (json:get_data() == "[1, 2, 3]")
assert (json:get_size() == 9)
assert (json:get_data() == json:to_string())

json = Json.Array {1.11, 2.22, 3.33}
assert (json:is_array())
val = json:parse ()
assert (val[1] > 1.10 and val[1] < 1.12)
assert (val[2] > 2.21 and val[2] < 2.23)
assert (val[3] > 3.32 and val[3] < 3.34)

json = Json.Array {"lua", "spa", "json"}
assert (json:is_array())
val = json:parse ()
assert (val[1] == "lua")
assert (val[2] == "spa")
assert (val[3] == "json")
assert (val[4] == nil)
assert (json:get_data() == "[\"lua\", \"spa\", \"json\"]")
assert (json:get_size() == 22)
assert (json:get_data() == json:to_string())

json = Json.Array {}
assert (json:is_array())
val = json:parse ()
assert (#val == 0)
assert (json:get_data() == "[]")

json = Json.Array {
  Json.Array {
    Json.Object {
      key1 = 1
    },
    Json.Object {
      key2 = 2
    },
  }
}
assert (json:is_array())
assert (json:get_data() == "[[{\"key1\":1}, {\"key2\":2}]]")
assert (json:get_data() == json:to_string())
val = json:parse ()
assert (val[1][1].key1 == 1)
assert (val[1][2].key2 == 2)

table = {}
table[1] = 1
table[2] = 2
table[3] = 3
table["4"] = 4
json = Json.Array (table)
assert (json:is_array())
val = json:parse ()
assert (val[1] == 1)
assert (val[2] == 2)
assert (val[3] == 3)
assert (val["4"] == nil)

-- Object
json = Json.Object {
    key1 = Json.Null(),
    key2 = true,
    key3 = 3,
    key4 = 4.44,
    key5 = "foo",
    key6 = Json.Array {5, 6, 7},
    key7 = Json.Object {
      key_nested1 = "nested",
      key_nested2 = 8,
      key_nested3 = Json.Array {false, true, false},
      ["Key with spaces and (special % characters)"] = 50.0,
    }
}
assert (json:is_object())
val = json:parse ()
assert (val.key1 == nil)
assert (val.key2 == true)
assert (val.key3 == 3)
assert (val.key4 > 4.43 and val.key4 < 4.45)
assert (val.key5 == "foo")
assert (val.key6[1] == 5)
assert (val.key6[2] == 6)
assert (val.key6[3] == 7)
assert (val.key7.key_nested1 == "nested")
assert (val.key7.key_nested2 == 8)
assert (not val.key7.key_nested3[1])
assert (val.key7.key_nested3[2])
assert (not val.key7.key_nested3[3])
assert (val.key7["Key with spaces and (special % characters)"] == 50.0)

table = {}
table["1"] = 1
table["2"] = 2
table["3"] = 3
table[4] = 4
json = Json.Object (table)
assert (json:is_object())
val = json:parse ()
assert (val["1"] == 1)
assert (val["2"] == 2)
assert (val["3"] == 3)
assert (val[4] == nil)

json = Json.Object {}
assert (json:is_object())
val = json:parse ()
assert (#val == 0)
assert (val.key1 == nil)
assert (json:get_data() == "{}")

-- Raw
json = Json.Raw ("[\"foo\", \"bar\"]")
assert (json:is_array())
assert (json:get_data() == "[\"foo\", \"bar\"]")
assert (json:get_data() == json:to_string())
val = json:parse ()
assert (val[1] == "foo")
assert (val[2] == "bar")

json = Json.Raw ("[foo, bar]")
assert (json:is_array())
assert (json:get_data() == "[foo, bar]")
assert (json:get_data() == json:to_string())
val = json:parse ()
assert (val[1] == "foo")
assert (val[2] == "bar")

json = Json.Raw ("{\"name\": \"wireplumber\", \"version\": [0, 4, 7]}")
assert (json:is_object())
assert (json:get_data() == "{\"name\": \"wireplumber\", \"version\": [0, 4, 7]}")
assert (json:get_data() == json:to_string())
val = json:parse ()
assert (val.name == "wireplumber")
assert (val.version[1] == 0)
assert (val.version[2] == 4)
assert (val.version[3] == 7)

-- recursion limit
json = Json.Raw ("{ name = wireplumber, version = [0, 4, 15], args = { test = [0, 1] } }")

val = json:parse (0)
assert (type (val) == "string")
assert (val == "{ name = wireplumber, version = [0, 4, 15], args = { test = [0, 1] } }")

val = json:parse (1)
assert (type (val) == "table")
assert (val.name == "wireplumber")
assert (type (val.version) == "string")
assert (val.version == "[0, 4, 15]")
assert (type (val.args) == "string")
assert (val.args == "{ test = [0, 1] }")

val = json:parse(2)
assert (type (val) == "table")
assert (val.name == "wireplumber")
assert (type (val.version) == "table")
assert (val.version[1] == 0)
assert (val.version[2] == 4)
assert (val.version[3] == 15)
assert (type (val.args) == "table")
assert (val.args.test == "[0, 1]")

val = json:parse(3)
assert (type (val) == "table")
assert (val.name == "wireplumber")
assert (type (val.version) == "table")
assert (val.version[1] == 0)
assert (val.version[2] == 4)
assert (val.version[3] == 15)
assert (type (val.args) == "table")
assert (type (val.args.test) == "table")
assert (val.args.test[1] == 0)
assert (val.args.test[2] == 1)
  070701000001FA000041ED00000000000000000000000265F8630400000000000000000000000000000000000000000000002A00000000wireplumber-0.5.0/tests/wplua/scripts/lib 070701000001FB000081A400000000000000000000000165F86304000001AA000000000000000000000000000000000000003600000000wireplumber-0.5.0/tests/wplua/scripts/lib/testlib.lua local testlib = {}

testlib.table1 = {}

testlib.table1 ["test-key"] = "test-value"

function testlib.test_add_ten (x)
  return x + 10
end

function testlib.get_empty_table (key)
  testlib.table1 [key] = {}
  return testlib.table1 [key]
end

function testlib.set_table (key, value)
  testlib.table1 [key] = value
end

function testlib.get_table (key)
  return testlib.table1 [key]
end

Log.info ("in testlib")

return testlib
  070701000001FC000081A400000000000000000000000165F86304000006CD000000000000000000000000000000000000003800000000wireplumber-0.5.0/tests/wplua/scripts/monitor-rules.lua   -- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
--    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT

local config = {}
config.rules = {
  {
    matches = {
      {
        { "device.name", "matches", "bluez_card.*" },
      },
    },
    apply_properties = {
      ["device.nick"] = "My Device",
    },
  },
  {
    matches = {
      {
        { "node.name", "matches", "bluez_input.*" },
      },
      {
        { "node.name", "matches", "bluez_output.*" },
      },
    },
    apply_properties = {
      ["node.pause-on-idle"] = true,
    },
  },
}

for _, r in ipairs(config.rules or {}) do
  r.interests = {}
  for _, i in ipairs(r.matches) do
    local interest_desc = { type = "properties" }
    for _, c in ipairs(i) do
      c.type = "pw"
      table.insert(interest_desc, Constraint(c))
    end
    local interest = Interest(interest_desc)
    table.insert(r.interests, interest)
  end
  r.matches = nil
end

function rulesApplyProperties(properties)
  for _, r in ipairs(config.rules or {}) do
    if r.apply_properties then
      for _, interest in ipairs(r.interests) do
        if interest:matches(properties) then
          for k, v in pairs(r.apply_properties) do
            properties[k] = v
          end
        end
      end
    end
  end
end

local test1 = {
  ["node.name"] = "bluez_output.test1"
}
rulesApplyProperties(test1)
assert(test1["node.pause-on-idle"] == true)

local test2 = {
  ["device.name"] = "bluez_card.test2"
}
rulesApplyProperties(test2)
assert(test2["device.nick"] == "My Device")

local test3 = {
  ["device.name"] = "not_a_match"
}
rulesApplyProperties(test3)
assert(test3["device.nick"] == nil)
assert(test3["node.pause-on-idle"] == nil)
   070701000001FD000081A400000000000000000000000165F8630400001FC8000000000000000000000000000000000000002E00000000wireplumber-0.5.0/tests/wplua/scripts/pod.lua -- None
pod = Pod.None ()
assert (pod:parse() == nil)
assert (pod:get_type_name() == "Spa:None")

-- Bool
pod = Pod.Boolean (true)
assert (pod:parse())
assert (pod:get_type_name() == "Spa:Bool")

-- Id
pod = Pod.Id (4)
assert (pod:parse() == 4)
assert (pod:get_type_name() == "Spa:Id")
pod = Pod.Id("Spa:Enum:AudioChannel", "FL")
assert (pod:parse() == 3)
assert (pod:get_type_name() == "Spa:Id")

-- Int
pod = Pod.Int (64)
assert (pod:parse() == 64)
assert (pod:get_type_name() == "Spa:Int")

-- Long
pod = Pod.Long (1024)
assert (pod:parse() == 1024)
assert (pod:get_type_name() == "Spa:Long")

-- Float
pod = Pod.Float (1.02)
val = pod:parse ()
assert (val > 1.01 and val < 1.03)
assert (pod:get_type_name() == "Spa:Float")

-- Double
pod = Pod.Double (3.14159265359)
val = pod:parse ()
assert (val > 3.14159265358 and val < 3.14159265360)
assert (pod:get_type_name() == "Spa:Double")

-- String
pod = Pod.String ("a pod string")
assert (pod:parse() == "a pod string")
assert (pod:get_type_name() == "Spa:String")

-- Bytes
pod = Pod.Bytes ("16characterbytes")
assert (pod:parse() == "16characterbytes")
assert (pod:get_type_name() == "Spa:Bytes")

-- Pointer
pod = Pod.Pointer ("Spa:Pointer:Buffer", nil)
assert (pod:parse() == nil)
assert (pod:get_type_name() == "Spa:Pointer:Buffer")

-- Fd
pod = Pod.Fd (5)
assert (pod:parse() == 5)
assert (pod:get_type_name() == "Spa:Fd")

-- Rectangle
pod = Pod.Rectangle (1920, 1080)
val = pod:parse()
assert (val.pod_type == "Rectangle")
assert (val.width == 1920 and val.height == 1080)
assert (pod:get_type_name() == "Spa:Rectangle")

-- Fraction
pod = Pod.Fraction (3, 4)
val = pod:parse()
assert (val.pod_type == "Fraction")
assert (val.num == 3 and val.denom == 4)
assert (pod:get_type_name() == "Spa:Fraction")

-- Struct
pod = Pod.Struct { true, 1, "string" }
val = pod:parse()
assert (val.pod_type == "Struct")
assert (val[1] and val[2] == 1 and val[3] == "string")
assert (pod:get_type_name() == "Spa:Pod:Struct")

-- Object
pod = Pod.Object {
  "Spa:Pod:Object:Param:PortConfig", "PortConfig",
  direction = "Input",
  mode = "dsp",
  monitor = true
}
val = pod:parse()
assert (val.pod_type == "Object")
assert (val.object_id == "PortConfig")
assert (val.properties.direction == "Input")
assert (val.properties.mode == "dsp")
assert (val.properties.monitor)
assert (pod:get_type_name() == "Spa:Pod:Object:Param:PortConfig")
pod = Pod.Object {
  "Spa:Pod:Object:Param:Props", "Props",
  device = "hw:Generic",
  deviceName = "",
  cardName = "",
  minLatency = 16,
  maxLatency = 8192,
  ["id-01000000"] = Pod.Boolean (true),
  latencyOffsetNsec = 0
}
val = pod:parse()
assert (val.pod_type == "Object")
assert (val.object_id == "Props")
assert (val.properties.device == "hw:Generic")
assert (val.properties.deviceName == "")
assert (val.properties.cardName == "")
assert (val.properties.minLatency == 16)
assert (val.properties.maxLatency == 8192)
assert (val.properties["id-01000000"] == true)
assert (val.properties.latencyOffsetNsec == 0)
assert (pod:get_type_name() == "Spa:Pod:Object:Param:Props")

-- Sequence
pod = Pod.Sequence {
  {offset = 10, typename = "Properties", value = 1},
  {offset = 20, typename = "Properties", value = 2},
  {offset = 40, typename = "Properties", value = 4},
}
val = pod:parse()
assert (val.pod_type == "Sequence")
assert (val[1].offset == 10 and val[1].typename == "Properties" and val[1].value == 1)
assert (val[2].offset == 20 and val[2].typename == "Properties" and val[2].value == 2)
assert (val[3].offset == 40 and val[3].typename == "Properties" and val[3].value == 4)
assert (pod:get_type_name() == "Spa:Pod:Sequence")

-- Array
pod = Pod.Array { "Spa:Bool", true, false, false, true }
val = pod:parse()
assert (val.pod_type == "Array")
assert (val.value_type == "Spa:Bool")
assert (val[1])
assert (not val[2])
assert (not val[3])
assert (val[4])
assert (pod:get_type_name() == "Spa:Array")
pod = Pod.Array { "Spa:Enum:AudioChannel", "FL", "FR" }
val = pod:parse()
assert (val.pod_type == "Array")
assert (val.value_type == "Spa:Id")
assert (val[1] == 3)
assert (val[2] == 4)

-- Choice
pod = Pod.Choice.None { "Spa:Bool", false }
val = pod:parse()
assert(val.pod_type == "Choice.None")
assert(val.value_type == "Spa:Bool")
assert(not val[1])
pod = Pod.Choice.Range { "Spa:Float", 1.0, 0.0, 1.0 }
val = pod:parse()
assert (pod:get_type_name() == "Spa:Pod:Choice")
assert(val.pod_type == "Choice.Range")
assert(val.value_type == "Spa:Float")
assert(val[1] == 1.0)
assert(val[2] == 0.0)
assert(val[3] == 1.0)
assert (pod:get_type_name() == "Spa:Pod:Choice")
pod = Pod.Choice.Step { "Spa:Int", 5, 10 }
val = pod:parse()
assert(val.pod_type == "Choice.Step")
assert(val.value_type == "Spa:Int")
assert(val[1] == 5)
assert(val[2] == 10)
assert (pod:get_type_name() == "Spa:Pod:Choice")
pod = Pod.Choice.Enum { "Spa:Enum:AudioChannel", "FL", "FL", "FR" }
val = pod:parse()
assert (val.pod_type == "Choice.Enum")
assert (val.value_type == "Spa:Id")
assert (val[1] == 3)
assert (val[2] == 3)
assert (val[3] == 4)
assert (pod:get_type_name() == "Spa:Pod:Choice")
pod = Pod.Choice.Flags { "Spa:Int", 1 << 0, 1 << 2, 1 << 3}
val = pod:parse()
assert (val.pod_type == "Choice.Flags")
assert (val.value_type == "Spa:Int")
assert (val[1] == 1 << 0)
assert (val[2] == 1 << 2)
assert (val[3] == 1 << 3)
assert (pod:get_type_name() == "Spa:Pod:Choice")


-- Nested Pods
pod = Pod.Object {
  "Spa:Pod:Object:Param:PortConfig", "PortConfig",
  direction = "Input",
  mode = "dsp",
  monitor = true,
  format = Pod.Object {
    "Spa:Pod:Object:Param:Format", "Format",
    mediaType = "audio",
    mediaSubtype = "raw",
    rate = 48000,
    format = Pod.Choice.Enum { "Spa:Enum:AudioFormat", "S16LE", "S16LE", "F32LE" },
    channels = Pod.Choice.Range { "Spa:Int", 2, 1, 32 },
    position = Pod.Array { "Spa:Enum:AudioChannel", "FL", "FR" }
  }
}
val = pod:parse()
assert (val.pod_type == "Object")
assert (val.object_id == "PortConfig")
assert (val.properties.direction == "Input")
assert (val.properties.mode == "dsp")
assert (val.properties.monitor)
assert (val.properties.format.pod_type == "Object")
assert (val.properties.format.object_id == "Format")
assert (val.properties.format.properties.mediaType == "audio")
assert (val.properties.format.properties.mediaSubtype == "raw")
assert (val.properties.format.properties.rate == 48000)
assert (val.properties.format.properties.format.pod_type == "Choice.Enum")
assert (val.properties.format.properties.format.value_type == "Spa:Id")
assert (val.properties.format.properties.format[1] == "S16LE")
assert (val.properties.format.properties.format[2] == "S16LE")
assert (val.properties.format.properties.format[3] == "F32LE")
assert (val.properties.format.properties.channels.pod_type == "Choice.Range")
assert (val.properties.format.properties.channels.value_type == "Spa:Int")
assert (val.properties.format.properties.channels[1] == 2)
assert (val.properties.format.properties.channels[2] == 1)
assert (val.properties.format.properties.channels[3] == 32)
assert (val.properties.format.properties.position.pod_type == "Array")
assert (val.properties.format.properties.position.value_type == "Spa:Id")
assert (val.properties.format.properties.position[1] == "FL")
assert (val.properties.format.properties.position[2] == "FR")
assert (pod:get_type_name() == "Spa:Pod:Object:Param:PortConfig")

-- Nested Object Id Properties
pod = Pod.Object {
  "Spa:Pod:Object:Param:Props", "Props",
  device = "my-device",
  ["id-01000000"] = Pod.Int (4),
  ["id-02000000"] = Pod.Object {
    "Spa:Pod:Object:Param:Props", "Props",
    device = "my-sub-device",
    ["id-03000000"] = Pod.Boolean (true),
    ["id-04000000"] = Pod.String ("string")
  }
}
val = pod:parse()
assert (val.pod_type == "Object")
assert (val.object_id == "Props")
assert (val.properties.device == "my-device")
assert (val.properties["id-01000000"] == 4)
assert (val.properties["id-02000000"].properties.device == "my-sub-device")
assert (val.properties["id-02000000"].properties["id-03000000"] == true)
assert (val.properties["id-02000000"].properties["id-04000000"] == "string")
assert (pod:get_type_name() == "Spa:Pod:Object:Param:Props")
070701000001FE000081A400000000000000000000000165F86304000001CF000000000000000000000000000000000000003200000000wireplumber-0.5.0/tests/wplua/scripts/require.lua local testlib = require ("testlib")
assert (package.loaded ["testlib"] == testlib)

assert (type (testlib) == "table")

local x = 1
x = testlib.test_add_ten (x)
assert (x == 11)

local val_table = testlib.get_empty_table ("test-key")
assert (type (val_table) == "table")
val_table ["key"] = "value"
val_table.id = 100

testlib.set_table ("test-key", val_table)

local val1 = testlib.get_table ("test-key")
assert (val1 ["key"] == "value")
assert (val1.id == 100)
 070701000001FF000081A400000000000000000000000165F8630400000EE9000000000000000000000000000000000000003300000000wireplumber-0.5.0/tests/wplua/scripts/settings.lua    
--  tests the lua API of WpSettings, this file tests the settings present in
--  .conf file that is loaded.

Script.async_activation = true

-- Undefined
value = Settings.get ("test-setting-undefined")
assert (value == nil)

-- Bool
value = Settings.get_boolean ("test-setting-bool")
assert ("boolean" == type (value))
assert (value == true)
value = Settings.get_boolean ("test-setting-bool-undefined")
assert ("boolean" == type (value))
assert (value == false)

-- Int
value = Settings.get_int ("test-setting-int")
assert ("number" == type (value))
assert (value == -20)
value = Settings.get_int ("test-setting-int-undefined")
assert ("number" == type (value))
assert (value == 0)

-- Float
value = Settings.get_float ("test-setting-float")
assert ("number" == type (value))
assert ((value - 3.14) < 0.00001)
value = Settings.get_float ("test-setting-float-undefined")
assert ("number" == type (value))
assert ((value - 0.0) < 0.00001)

-- String
value = Settings.get_string ("test-setting-string")
assert ("string" == type (value))
assert (value == "blahblah")
value = Settings.get_string ("test-setting-string2")
assert ("string" == type (value))
assert (value == "a string with \"quotes\"")
value = Settings.get_string ("test-setting-string-undefined")
assert ("string" == type (value))
assert (value == "")

-- Array
value = Settings.get_array ("test-setting-array")
assert (value[1] == 1)
assert (value[2] == 2)
assert (value[3] == 3)
assert (value[4] == nil)
assert (#value == 3)
value = Settings.get_array ("test-setting-array2")
assert (value[1] == "test1")
assert (value[2] == "test 2")
assert (value[3] == "test three")
assert (value[4] == "test-four")
assert (value[5] == nil)
assert (#value == 4)
value = Settings.get_array ("test-setting-array-undefined")
assert (next(value) == nil)

-- Object
value = Settings.get_object ("test-setting-object")
assert (value.key1 == "value")
assert (value.key2 == 2)
assert (value.key3 == true)
value = Settings.get_object ("test-setting-object-undefined")
assert (next(value) == nil)

-- Callbacks
metadata_om = ObjectManager {
  Interest {
    type = "metadata",
    Constraint { "metadata.name", "=", "sm-settings" },
  }
}

metadata_om:activate()

local setting
local setting_value
local callback
local finish_activation

function callback (obj, s, json)
  assert (json ~= nil)

  if (json:is_boolean()) then
    assert (s == setting)
    callback = true
    assert (json:parse() == setting_value:parse())
    assert (setting_value:parse() == Settings.get (s):parse())

  elseif (json:is_int()) then
    assert (s == setting)
    callback = true
    assert (json:parse() == setting_value:parse())
    assert (setting_value:parse() == Settings.get (s):parse())

  elseif (json:is_string()) then
    assert (s == setting)
    callback = true
    assert (json:parse() == setting_value:parse())
    assert (setting_value:parse() == Settings.get (s):parse())
  end

  if (finish_activation) then
    assert (Settings.unsubscribe (sub_id))
    assert (not Settings.unsubscribe (sub_id-1))
    Script:finish_activation()
  end
end

sub_id = Settings.subscribe ("test*", callback)

metadata_om:connect("objects-changed", function (om)
  local metadata = om:lookup()

  if (not metadata) then
    return
  end

  -- test #2
  setting = "test-setting-bool"
  setting_value = Json.Boolean (true)
  callback = false

  assert (Settings.set(setting, setting_value))
  assert (not callback)

  -- test #3
  setting = "test-setting-int"
  setting_value = Json.Int (99)
  callback = false

  assert (Settings.set(setting, setting_value))
  assert (callback)

  -- test #4
  setting = "test-setting-string"
  setting_value = Json.String ("lets not blabber")
  callback = false

  finish_activation = true
  assert (Settings.set(setting, setting_value))
  assert (callback)

end)
   070701000002000000A1FF00000000000000000000000165F082030000000E000000000000000000000000000000000000002700000000wireplumber-0.5.0/tests/wplua/settings    ../wp/settings  07070100000201000081A400000000000000000000000165F8630400005496000000000000000000000000000000000000002600000000wireplumber-0.5.0/tests/wplua/wplua.c /* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "../common/test-log.h"
#include <wplua/wplua.h>

enum {
  PROP_0,
  PROP_TEST_STRING,
  PROP_TEST_INT,
  PROP_TEST_UINT,
  PROP_TEST_INT64,
  PROP_TEST_UINT64,
  PROP_TEST_FLOAT,
  PROP_TEST_DOUBLE,
  PROP_TEST_BOOLEAN,
};

typedef struct _TestObject TestObject;
struct _TestObject
{
  GObject parent;
  gchar *test_string;
  gint test_int;
  guint test_uint;
  gint64 test_int64;
  guint64 test_uint64;
  gfloat test_float;
  gdouble test_double;
  gboolean test_boolean;
};

typedef struct _TestObjectClass TestObjectClass;
struct _TestObjectClass
{
  GObjectClass parent_class;

  void (*change) (TestObject * self, const gchar * str, gint integer);
};

G_DEFINE_TYPE (TestObject, test_object, G_TYPE_OBJECT)

#define TEST_TYPE_OBJECT (test_object_get_type ())
_GLIB_DEFINE_AUTOPTR_CHAINUP (TestObject, GObject)

static inline TestObject * TEST_OBJECT (gpointer ptr) {
  return G_TYPE_CHECK_INSTANCE_CAST (ptr, TEST_TYPE_OBJECT, TestObject);
}

static void
test_object_init (TestObject * self)
{
}

static void
test_object_finalize (GObject * object)
{
  TestObject *self = TEST_OBJECT (object);
  g_free (self->test_string);
  G_OBJECT_CLASS (test_object_parent_class)->finalize (object);
}

static void
test_object_get_property (GObject * object, guint id, GValue * value,
    GParamSpec * pspec)
{
  TestObject *self = TEST_OBJECT (object);

  switch (id) {
    case PROP_TEST_STRING:
      g_value_set_string (value, self->test_string);
      break;
    case PROP_TEST_INT:
      g_value_set_int (value, self->test_int);
      break;
    case PROP_TEST_UINT:
      g_value_set_uint (value, self->test_uint);
      break;
    case PROP_TEST_INT64:
      g_value_set_int64 (value, self->test_int64);
      break;
    case PROP_TEST_UINT64:
      g_value_set_uint64 (value, self->test_uint64);
      break;
    case PROP_TEST_FLOAT:
      g_value_set_float (value, self->test_float);
      break;
    case PROP_TEST_DOUBLE:
      g_value_set_double (value, self->test_double);
      break;
    case PROP_TEST_BOOLEAN:
      g_value_set_boolean (value, self->test_boolean);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, id, pspec);
      break;
  }
}

static void
test_object_set_property (GObject * object, guint id, const GValue * value,
    GParamSpec * pspec)
{
  TestObject *self = TEST_OBJECT (object);

  switch (id) {
    case PROP_TEST_STRING:
      g_free (self->test_string);
      self->test_string = g_value_dup_string (value);
      break;
    case PROP_TEST_INT:
      self->test_int = g_value_get_int (value);
      break;
    case PROP_TEST_UINT:
      self->test_uint = g_value_get_uint (value);
      break;
    case PROP_TEST_INT64:
      self->test_int64 = g_value_get_int64 (value);
      break;
    case PROP_TEST_UINT64:
      self->test_uint64 = g_value_get_uint64 (value);
      break;
    case PROP_TEST_FLOAT:
      self->test_float = g_value_get_float (value);
      break;
    case PROP_TEST_DOUBLE:
      self->test_double = g_value_get_double (value);
      break;
    case PROP_TEST_BOOLEAN:
      self->test_boolean = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, id, pspec);
      break;
  }
}

static void
test_object_change (TestObject * self, const gchar * str, gint integer)
{
  g_free (self->test_string);
  self->test_string = g_strdup_printf ("changed: %s", str);
  g_object_notify (G_OBJECT (self), "test-string");
  self->test_int = integer;
  g_object_notify (G_OBJECT (self), "test-int");

  gint ret = 0;
  g_signal_emit_by_name (self, "acquire", &ret);
  self->test_int64 = ret;
  g_object_notify (G_OBJECT (self), "test-int64");
}

static void
test_object_class_init (TestObjectClass * klass)
{
  GObjectClass *obj_class = (GObjectClass *) klass;

  obj_class->finalize = test_object_finalize;
  obj_class->get_property = test_object_get_property;
  obj_class->set_property = test_object_set_property;

  g_object_class_install_property (obj_class, PROP_TEST_STRING,
      g_param_spec_string ("test-string", "test-string", "blurb", NULL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_INT,
      g_param_spec_int ("test-int", "test-int", "blurb",
          G_MININT, G_MAXINT, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_UINT,
      g_param_spec_uint ("test-uint", "test-uint", "blurb",
          0, G_MAXUINT, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_INT64,
      g_param_spec_int64 ("test-int64", "test-int64", "blurb",
          G_MININT64, G_MAXINT64, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_UINT64,
      g_param_spec_uint64 ("test-uint64", "test-uint64", "blurb",
          0, G_MAXUINT64, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_FLOAT,
      g_param_spec_float ("test-float", "test-float", "blurb",
          -20.0f, 20.0f, 0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_DOUBLE,
      g_param_spec_double ("test-double", "test-double", "blurb",
          -20.0, 20.0, 0.0,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (obj_class, PROP_TEST_BOOLEAN,
      g_param_spec_boolean ("test-boolean", "test-boolean", "blurb", FALSE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_signal_new ("change", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_ACTION | G_SIGNAL_RUN_LAST,
      G_STRUCT_OFFSET (TestObjectClass, change), NULL, NULL, NULL,
      G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_INT);
  klass->change = test_object_change;

  g_signal_new ("acquire", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST,
      0, NULL, NULL, NULL, G_TYPE_INT, 0);
}

static void
test_object_toggle (TestObject * self)
{
  self->test_boolean = !self->test_boolean;
  g_object_notify (G_OBJECT (self), "test-boolean");
}

static int
l_test_object_toggle (lua_State * L)
{
  TestObject * self = wplua_checkobject (L, 1, TEST_TYPE_OBJECT);
  test_object_toggle (self);
  return 0;
}

static const luaL_Reg l_test_object_methods[] = {
  { "toggle", l_test_object_toggle },
  { NULL, NULL }
};

static int
l_test_object_new (lua_State * L)
{
  wplua_pushobject (L, g_object_new (TEST_TYPE_OBJECT, NULL));
  return 1;
}

static gboolean
test_load_and_call (lua_State * L, const gchar *buf, gsize size,
    int nargs, int nres, GError **error)
{
  int argstop = lua_gettop (L);
  int sandbox = wplua_push_sandbox (L);

  if (!wplua_load_buffer (L, buf, size, error)) {
    lua_pop (L, nargs + sandbox);
    return FALSE;
  }

  /* push sandbox() and the chunk below the arguments */
  lua_rotate (L, argstop, -nargs);

  return wplua_pcall (L, nargs + sandbox, nres, error);
}

static void
test_wplua_basic ()
{
  lua_State *L = wplua_new ();
  wplua_unref (L);
}

static void
test_wplua_construct ()
{
  g_autoptr (GObject) obj = NULL;
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  wplua_register_type_methods(L, TEST_TYPE_OBJECT,
      l_test_object_new, l_test_object_methods);

  const gchar code[] =
    "o = TestObject_new()\n"
    "assert (type(o) == 'userdata')\n";
  test_load_and_call (L, code, sizeof (code) - 1, 0, 0, &error);
  g_assert_no_error (error);

  g_assert_cmpint (lua_getglobal (L, "o"), ==, LUA_TUSERDATA);
  g_assert_true (wplua_checkobject (L, -1, TEST_TYPE_OBJECT));
  obj = wplua_toobject (L, -1);
  g_assert_nonnull (obj);
  g_object_ref (obj);
  g_assert_cmpint (obj->ref_count, ==, 2);

  wplua_unref (L);
  g_assert_cmpint (obj->ref_count, ==, 1);
}

static void
test_wplua_properties ()
{
  TestObject *obj = NULL;
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  wplua_register_type_methods(L, TEST_TYPE_OBJECT,
      l_test_object_new, l_test_object_methods);

  const gchar code[] =
    "o = TestObject_new()\n"
    "o['test-string'] = 'string from lua'\n"
    "o['test-int'] = -15\n"
    "o['test-uint'] = 1123456789\n"
    "o['test-int64'] = -5123456789\n"
    "o['test-uint64'] = 15123456789\n"
    "o['test-float'] = 3.1415\n"
    "o['test-double'] = 0.123456789\n"
    "o['test-boolean'] = true\n";
  test_load_and_call (L, code, sizeof (code) - 1, 0, 0, &error);
  g_assert_no_error (error);

  g_assert_cmpint (lua_getglobal (L, "o"), ==, LUA_TUSERDATA);
  g_assert_true (wplua_checkobject (L, -1, TEST_TYPE_OBJECT));
  obj = wplua_toobject (L, -1);
  g_assert_nonnull (obj);

  g_assert_cmpstr (obj->test_string, ==, "string from lua");
  g_assert_cmpint (obj->test_int, ==, -15);
  g_assert_cmpuint (obj->test_uint, ==, 1123456789);
  g_assert_cmpint (obj->test_int64, ==, -5123456789);
  g_assert_cmpuint (obj->test_uint64, ==, 15123456789);
  g_assert_cmpfloat_with_epsilon (obj->test_float, 3.1415, 0.00001);
  g_assert_cmpfloat_with_epsilon (obj->test_double, 0.123456789, 0.0000000001);
  g_assert_true (obj->test_boolean);

  const gchar code2[] =
    "assert (o['test-string'] == 'string from lua')\n"
    "assert (o['test-int'] == -15)\n"
    "assert (o['test-uint'] == 1123456789)\n"
    "assert (o['test-int64'] == -5123456789)\n"
    "assert (o['test-uint64'] == 15123456789)\n"
    "assert (math.abs (o['test-float'] - 3.1415) < 0.00001)\n"
    "assert (math.abs (o['test-double'] - 0.123456789) < 0.0000000001)\n"
    "assert (o['test-boolean'] == true)\n";
  test_load_and_call (L, code2, sizeof (code2) - 1, 0, 0, &error);
  g_assert_no_error (error);

  wplua_unref (L);
}

static void
test_wplua_closure ()
{
  GClosure *closure;
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  lua_pushstring (L, "some string");
  lua_setglobal (L, "expected_str");

  const gchar code[] =
    "f_was_called = false\n"
    "function f(s)\n"
    "  assert(s == expected_str)\n"
    "  f_was_called = true\n"
    "end\n";
  test_load_and_call (L, code, sizeof (code) - 1, 0, 0, &error);
  g_assert_no_error (error);

  lua_getglobal (L, "f");
  closure = wplua_function_to_closure (L, -1);
  g_assert_nonnull (closure);
  g_closure_ref (closure);
  g_closure_sink (closure);
  lua_pop (L, 1);

  {
    GValue s = G_VALUE_INIT;
    g_value_init (&s, G_TYPE_STRING);
    g_value_set_static_string (&s, "some string");
    g_closure_invoke (closure, NULL, 1, &s, NULL);
  }

  lua_getglobal (L, "f_was_called");
  g_assert_true (lua_isboolean (L, -1));
  g_assert_true (lua_toboolean (L, -1));

  wplua_unref (L);

  g_assert_true (closure->is_invalid);
  g_closure_unref (closure);
}

static void
test_wplua_signals ()
{
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  wplua_register_type_methods(L, TEST_TYPE_OBJECT,
      l_test_object_new, l_test_object_methods);

  const gchar code[] =
    "o = TestObject_new()\n"
    "\n"
    "o:connect('acquire', function (obj)\n"
    "    assert(obj == o)\n"
    "    return 42\n"
    "  end)\n"
    "\n"
    "o:connect('notify::test-string', function (obj, pspec)\n"
    "    assert(pspec == 'test-string')\n"
    "    assert(obj[pspec] == 'changed: by Lua')\n"
    "  end)\n"
    "\n"
    "o:call('change', 'by Lua', 55)\n"
    "\n"
    "assert(o['test-string'] == 'changed: by Lua')\n"
    "assert(o['test-int'] == 55)\n"
    "assert(o['test-int64'] == 42)\n"
    "\n"
    "o['test-boolean'] = true\n"
    "o:toggle()\n"
    "assert(o['test-boolean'] == false)\n";
  test_load_and_call (L, code, sizeof (code) - 1, 0, 0, &error);
  g_assert_no_error (error);
  wplua_unref (L);
}

static void
test_wplua_sandbox_script ()
{
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  wplua_register_type_methods(L, TEST_TYPE_OBJECT,
      l_test_object_new, l_test_object_methods);

  const gchar code[] =
    "SANDBOX_EXPORT = {\n"
    "  Test = TestObject_new,\n"
    "  Table = { test = 'foobar' }\n"
    "}\n";
  test_load_and_call (L, code, sizeof (code) - 1, 0, 0, &error);
  g_assert_no_error (error);

  wplua_enable_sandbox (L, WP_LUA_SANDBOX_ISOLATE_ENV);

  const gchar code2[] =
    "o = TestObject_new()\n";
  test_load_and_call (L, code2, sizeof (code2) - 1, 0, 0, &error);
  g_debug ("expected error: %s", error ? error->message : "null");
  g_assert_error (error, WP_DOMAIN_LUA, WP_LUA_ERROR_RUNTIME);
  g_clear_error (&error);

  const gchar code3[] =
    "o = Test()\n"
    "o:toggle()\n";
  test_load_and_call (L, code3, sizeof (code3) - 1, 0, 0, &error);
  g_assert_no_error (error);

  const gchar code4[] =
    "assert(string.len(Table.test) == 6)\n";
  test_load_and_call (L, code4, sizeof (code4) - 1, 0, 0, &error);
  g_assert_no_error (error);

  const gchar code5[] =
    "o:call('change', 'by Lua', 55)\n";
  test_load_and_call (L, code5, sizeof (code5) - 1, 0, 0, &error);
  g_debug ("expected error: %s", error ? error->message : "null");
  g_assert_error (error, WP_DOMAIN_LUA, WP_LUA_ERROR_RUNTIME);
  g_clear_error (&error);

  const gchar code6[] =
    "string.test = 'hello world'\n";
  test_load_and_call (L, code6, sizeof (code6) - 1, 0, 0, &error);
  g_debug ("expected error: %s", error ? error->message : "null");
  g_assert_error (error, WP_DOMAIN_LUA, WP_LUA_ERROR_RUNTIME);
  g_clear_error (&error);

  const gchar code7[] =
    "Table.test = 'hello world'\n";
  test_load_and_call (L, code7, sizeof (code7) - 1, 0, 0, &error);
  g_debug ("expected error: %s", error ? error->message : "null");
  g_assert_error (error, WP_DOMAIN_LUA, WP_LUA_ERROR_RUNTIME);
  g_clear_error (&error);

  wplua_unref (L);
}

static void
test_wplua_sandbox_config ()
{
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  wplua_enable_sandbox (L, 0);

  const gchar code3[] =
    "o = { answer = 42 }\n";
  test_load_and_call (L, code3, sizeof (code3) - 1, 0, 0, &error);
  g_assert_no_error (error);

  /* no assert() in minimal_std mode, resort to other means of failure */
  const gchar code4[] =
    "if (o.answer ~= 42) then\n"
    "  non_existent_function()\n"
    "end\n";
  test_load_and_call (L, code4, sizeof (code4) - 1, 0, 0, &error);
  g_assert_no_error (error);

  /* string.* is protected */
  const gchar code6[] =
    "string.test = 'hello world'\n";
  test_load_and_call (L, code6, sizeof (code6) - 1, 0, 0, &error);
  g_debug ("expected error: %s", error ? error->message : "null");
  g_assert_error (error, WP_DOMAIN_LUA, WP_LUA_ERROR_RUNTIME);
  g_clear_error (&error);

  /* this would be an error if the assert function was exported, but it's not */
  const gchar code7[] =
    "assert = 'hello world'\n";
  test_load_and_call (L, code7, sizeof (code7) - 1, 0, 0, &error);
  g_assert_no_error (error);

  wplua_unref (L);
}

static void
test_wplua_convert_asv ()
{
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  g_autoptr (GVariant) v = g_variant_new_parsed ("@a{sv} { "
      "'test-int': <42>, "
      "'test-double': <3.14>, "
      "'test-string': <'foobar'>, "
      "'test-boolean': <true>, "
      "'nested-table': <@a{sv} { 'string': <'baz'> }> "
      "}");
  wplua_gvariant_to_lua (L, v);
  lua_setglobal (L, "o");

  const gchar code2[] =
    "assert (o['test-string'] == 'foobar')\n"
    "assert (o['test-int'] == 42)\n"
    "assert (math.abs (o['test-double'] - 3.14) < 0.0000000001)\n"
    "assert (o['test-boolean'] == true)\n"
    "assert (o['nested-table']['string'] == 'baz')\n";
  test_load_and_call (L, code2, sizeof (code2) - 1, 0, 0, &error);
  g_assert_no_error (error);

  lua_getglobal (L, "o");
  g_autoptr (GVariant) fromlua = wplua_lua_to_gvariant (L, -1);

  gint64 test_int = 0;
  g_assert_true (g_variant_lookup (fromlua, "test-int", "x", &test_int));
  g_assert_cmpint (test_int, ==, 42);

  gdouble test_double = 0;
  g_assert_true (g_variant_lookup (fromlua, "test-double", "d", &test_double));
  g_assert_cmpfloat_with_epsilon (test_double, 3.14, 0.0000000001);

  const gchar *test_str = NULL;
  g_assert_true (g_variant_lookup (fromlua, "test-string", "&s", &test_str));
  g_assert_cmpstr (test_str, ==, "foobar");

  gboolean test_boolean = FALSE;
  g_assert_true (g_variant_lookup (fromlua, "test-boolean", "b", &test_boolean));
  g_assert_true (test_boolean);

  g_autoptr (GVariant) nested = NULL;
  g_assert_true (g_variant_lookup (fromlua, "nested-table", "@a{sv}", &nested));
  g_assert_nonnull (nested);
  g_assert_true (g_variant_is_of_type (nested, G_VARIANT_TYPE_VARDICT));

  test_str = NULL;
  g_assert_true (g_variant_lookup (nested, "string", "&s", &test_str));
  g_assert_cmpstr (test_str, ==, "baz");

  wplua_unref (L);
}

static void
test_wplua_convert_gvariant_array ()
{
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  g_autoptr (GVariant) v = g_variant_new_parsed ("@av [ "
      "<42>, <3.14>, <'foobar'>, <true>, "
      "<@a{sv} { 'string': <'baz'> }> ]");
  wplua_gvariant_to_lua (L, v);
  lua_setglobal (L, "o");

  const gchar code2[] =
    "assert (o[1] == 42)\n"
    "assert (math.abs (o[2] - 3.14) < 0.0000000001)\n"
    "assert (o[3] == 'foobar')\n"
    "assert (o[4] == true)\n"
    "assert (o[5]['string'] == 'baz')\n";
  test_load_and_call (L, code2, sizeof (code2) - 1, 0, 0, &error);
  g_assert_no_error (error);

  lua_getglobal (L, "o");
  g_autoptr (GVariant) fromlua = wplua_lua_to_gvariant (L, -1);

  gint64 test_int = 0;
  g_assert_true (g_variant_lookup (fromlua, "1", "x", &test_int));
  g_assert_cmpint (test_int, ==, 42);

  gdouble test_double = 0;
  g_assert_true (g_variant_lookup (fromlua, "2", "d", &test_double));
  g_assert_cmpfloat_with_epsilon (test_double, 3.14, 0.0000000001);

  const gchar *test_str = NULL;
  g_assert_true (g_variant_lookup (fromlua, "3", "&s", &test_str));
  g_assert_cmpstr (test_str, ==, "foobar");

  gboolean test_boolean = FALSE;
  g_assert_true (g_variant_lookup (fromlua, "4", "b", &test_boolean));
  g_assert_true (test_boolean);

  g_autoptr (GVariant) nested = NULL;
  g_assert_true (g_variant_lookup (fromlua, "5", "@a{sv}", &nested));
  g_assert_nonnull (nested);
  g_assert_true (g_variant_is_of_type (nested, G_VARIANT_TYPE_VARDICT));

  test_str = NULL;
  g_assert_true (g_variant_lookup (nested, "string", "&s", &test_str));
  g_assert_cmpstr (test_str, ==, "baz");

  wplua_unref (L);
}
static void
test_wplua_convert_wp_properties ()
{
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  const gchar code[] =
    "props = { "
    "  ['test-int'] = 42, "
    "  ['test-double'] = 3.14, "
    "  ['test-string'] = 'foobar', "
    "  ['test-boolean'] = false, "
    "}\n";
  test_load_and_call (L, code, sizeof (code) - 1, 0, 0, &error);
  g_assert_no_error (error);

  lua_getglobal (L, "props");
  g_autoptr (WpProperties) fromlua = wplua_table_to_properties (L, -1);

  g_assert_cmpstr (wp_properties_get (fromlua, "test-int"), ==, "42");
  g_assert_cmpstr (wp_properties_get (fromlua, "test-double"), ==, "3.14");
  g_assert_cmpstr (wp_properties_get (fromlua, "test-string"), ==, "foobar");
  g_assert_cmpstr (wp_properties_get (fromlua, "test-boolean"), ==, "false");

  lua_pop(L, 1);
  wplua_properties_to_table (L, fromlua);
  lua_setglobal (L, "fromc");

  const gchar code2[] =
    "assert (fromc['test-string'] == 'foobar')\n"
    "assert (fromc['test-int'] == '42')\n"
    "assert (fromc['test-double'] == '3.14')\n"
    "assert (fromc['test-boolean'] == 'false')\n";
  test_load_and_call (L, code2, sizeof (code2) - 1, 0, 0, &error);
  g_assert_no_error (error);

  wplua_unref (L);
}

static void
test_wplua_script_arguments ()
{
  g_autoptr (GError) error = NULL;
  lua_State *L = wplua_new ();

  g_autoptr (GVariant) v = g_variant_new_parsed ("@a{sv} { "
      "'test-int': <42>, "
      "'test-double': <3.14>, "
      "'test-string': <'foobar'>, "
      "'test-boolean': <true>, "
      "'nested-table': <@a{sv} { 'string': <'baz'> }> "
      "}");
  wplua_gvariant_to_lua (L, v);

  const gchar code2[] =
    "local o = ...\n"
    "assert (o['test-string'] == 'foobar')\n"
    "assert (o['test-int'] == 42)\n"
    "assert (math.abs (o['test-double'] - 3.14) < 0.0000000001)\n"
    "assert (o['test-boolean'] == true)\n"
    "assert (o['nested-table']['string'] == 'baz')\n";
  test_load_and_call (L, code2, sizeof (code2) - 1, 1, 0, &error);
  g_assert_no_error (error);

  /* same test, but with sandbox enabled */
  wplua_enable_sandbox (L, WP_LUA_SANDBOX_ISOLATE_ENV);
  wplua_gvariant_to_lua (L, v);
  test_load_and_call (L, code2, sizeof (code2) - 1, 1, 0, &error);
  g_assert_no_error (error);

  wplua_unref (L);
}

gint
main (gint argc, gchar *argv[])
{
  g_test_init (&argc, &argv, NULL);
  wp_init (WP_INIT_ALL);

  g_test_add_func ("/wplua/basic", test_wplua_basic);
  g_test_add_func ("/wplua/construct", test_wplua_construct);
  g_test_add_func ("/wplua/properties", test_wplua_properties);
  g_test_add_func ("/wplua/closure", test_wplua_closure);
  g_test_add_func ("/wplua/signals", test_wplua_signals);
  g_test_add_func ("/wplua/sandbox/script", test_wplua_sandbox_script);
  g_test_add_func ("/wplua/sandbox/config", test_wplua_sandbox_config);
  g_test_add_func ("/wplua/convert/asv", test_wplua_convert_asv);
  g_test_add_func ("/wplua/convert/gvariant_array",
      test_wplua_convert_gvariant_array);
  g_test_add_func ("/wplua/convert/wp_properties",
      test_wplua_convert_wp_properties);
  g_test_add_func ("/wplua/script_arguments", test_wplua_script_arguments);

  return g_test_run ();
}
  07070100000202000081ED00000000000000000000000165F86304000004F6000000000000000000000000000000000000002400000000wireplumber-0.5.0/wp-uninstalled.sh   #!/usr/bin/env bash

set -e

# This is unset by meson
# shellcheck disable=SC2157
if [ -z "@MESON@" ]; then
  SOURCEDIR="@MESON_SOURCE_ROOT@"
  BUILDDIR="@MESON_BUILD_ROOT@"
else
  SOURCEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
  BUILDDIR=$(find "${SOURCEDIR}" -maxdepth 2 -name build.ninja -printf "%h\n" -quit 2>/dev/null || echo "${SOURCEDIR}/build")
fi
CONFIGDIR=config

while getopts ":b:c:" opt; do
  case ${opt} in
    b)
      BUILDDIR=${OPTARG}
      ;;
    c)
      CONFIGDIR=${OPTARG}
      ;;
    \?)
      echo "Invalid option: -${OPTARG}"
      exit 1
      ;;
    :)
      echo "Option -${OPTARG} requires an argument"
      exit 1
      ;;
  esac
done

shift $((OPTIND-1))

if [ $# -eq 0 ]; then
  echo "Usage: $(basename "${BASH_SOURCE[0]}") [options] <wireplumber|wpctl|wpexec|...>"
  exit 1
fi

if [ ! -d "${BUILDDIR}" ]; then
  echo "Invalid build directory: ${BUILDDIR}"
  exit 1
fi

echo "Using build directory: ${BUILDDIR}"

export WIREPLUMBER_MODULE_DIR="${BUILDDIR}/modules"
export WIREPLUMBER_CONFIG_DIR="${SOURCEDIR}/src/${CONFIGDIR}"
export WIREPLUMBER_DATA_DIR="${SOURCEDIR}/src"
export PATH="${BUILDDIR}/src:${BUILDDIR}/src/tools:$PATH"
export LD_LIBRARY_PATH="${BUILDDIR}/lib/wp:$LD_LIBRARY_PATH"

exec "$@"
  07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!                                                                                5852 blocks
