diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3953445e5..448bfa06e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,8 +18,8 @@ stages: variables: FDO_UPSTREAM_REPO: 'pipewire/pipewire' -# ci-templates as of Jan 27th 2022 -.templates_sha: &templates_sha 0c312d9c7255f46e741d43bcd1930f09cd12efe7 +# ci-templates as of Mar 25th 2024 +.templates_sha: &templates_sha ef5e4669b7500834a17ffe9277e15fbb6d977fff include: - project: 'freedesktop/ci-templates' @@ -31,12 +31,15 @@ include: - project: 'freedesktop/ci-templates' ref: *templates_sha file: '/templates/alpine.yml' + - project: 'freedesktop/ci-templates' + ref: *templates_sha + file: '/templates/debian.yml' .fedora: variables: # Update this tag when you want to trigger a rebuild - FDO_DISTRIBUTION_TAG: '2024-12-10.0' - FDO_DISTRIBUTION_VERSION: '40' + FDO_DISTRIBUTION_TAG: '2025-10-22.0' + FDO_DISTRIBUTION_VERSION: '42' FDO_DISTRIBUTION_PACKAGES: >- alsa-lib-devel avahi-devel @@ -45,6 +48,7 @@ include: dbus-devel doxygen fdk-aac-free-devel + file findutils gcc gcc-c++ @@ -55,6 +59,7 @@ include: gstreamer1-plugins-base-devel jack-audio-connection-kit-devel libasan + liblc3-devel libcanberra-devel libebur128-devel libffado-devel @@ -64,7 +69,6 @@ include: libubsan libusb1-devel lilv-devel - libv4l-devel libva-devel libX11-devel ModemManager-devel @@ -76,6 +80,7 @@ include: sbc-devel ShellCheck SDL2-devel + spandsp-devel systemd-devel vulkan-loader-devel webrtc-audio-processing-devel @@ -87,6 +92,9 @@ include: openal-soft readline-devel pandoc + fftw-libs-single + fftw-devel + onnxruntime-devel # Uncommenting the following two lines and disabling the meson entry above # will re-enable use of Meson via pip but please consider using a newer distro # image first or making the build system compatible instead! This is because @@ -101,7 +109,7 @@ include: .ubuntu: variables: # Update this tag when you want to trigger a rebuild - FDO_DISTRIBUTION_TAG: '2024-01-12.0' + FDO_DISTRIBUTION_TAG: '2025-05-10.0' FDO_DISTRIBUTION_VERSION: '22.04' FDO_DISTRIBUTION_PACKAGES: >- debhelper-compat @@ -122,7 +130,6 @@ include: libsnapd-glib-dev libudev-dev libva-dev - libv4l-dev libx11-dev meson ninja-build @@ -138,10 +145,34 @@ include: # FDO_DISTRIBUTION_EXEC: >- # pip3 install meson +.debian: + variables: + # Update this tag when you want to trigger a rebuild + BASE_TAG: '2025-08-10.0' + FDO_DISTRIBUTION_VERSION: 'trixie' + FDO_DISTRIBUTION_PACKAGES: >- + build-essential + dpkg-dev + findutils + git + meson + +.debian-archictectures: + parallel: + matrix: + - ARCH: + - amd64 + - arm64 + - armhf + - i386 + - ppc64el + - riscv64 + - s390x + .alpine: variables: # Update this tag when you want to trigger a rebuild - FDO_DISTRIBUTION_TAG: '2024-09-20.0' + FDO_DISTRIBUTION_TAG: '2025-03-25.0' FDO_DISTRIBUTION_VERSION: '3.20' FDO_DISTRIBUTION_PACKAGES: >- alsa-lib-dev @@ -210,7 +241,8 @@ include: - echo "Building with meson options $MESON_OPTIONS" - meson setup "$BUILD_DIR" --prefix="$PREFIX" $MESON_OPTIONS - meson compile -C "$BUILD_DIR" $COMPILE_ARGS - - meson test -C "$BUILD_DIR" --no-rebuild + - echo "Running tests with meson options $MESON_TEST_OPTIONS" + - meson test -C "$BUILD_DIR" --no-rebuild $MESON_TEST_OPTIONS - meson install -C "$BUILD_DIR" --no-rebuild artifacts: name: pipewire-$CI_COMMIT_SHA @@ -226,6 +258,18 @@ container_ubuntu: variables: GIT_STRATEGY: none # no need to pull the whole tree for rebuilding the image +container_debian: + extends: + - .debian + - .debian-archictectures + - .fdo.container-build@debian + stage: container + variables: + GIT_STRATEGY: none # no need to pull the whole tree for rebuilding the image + FDO_DISTRIBUTION_TAG: "$BASE_TAG-$ARCH" + FDO_DISTRIBUTION_EXEC: >- + ./.gitlab/ci/setup-debian-cross-container.sh "$ARCH" + container_fedora: extends: - .fedora @@ -258,9 +302,54 @@ build_on_ubuntu: - .fdo.distribution-image@ubuntu - .build stage: build + needs: + - job: container_ubuntu + artifacts: false variables: MESON_OPTIONS: "-Dsession-managers=[] -Dsnap=enabled" +build_on_debian: + extends: + - .debian + - .debian-archictectures + - .not_coverity + - .fdo.distribution-image@debian + - .build + stage: build + needs: + - job: container_debian + artifacts: false + # ideally + # parallel: + # matrix: + # - ARCH: "$ARCH" + # however https://gitlab.com/gitlab-org/gitlab/-/issues/423553 + # ("Expand variables in `needs:parallel:matrix`") + variables: + FDO_DISTRIBUTION_TAG: "$BASE_TAG-$ARCH" + # see /.gitlab/ci/setup-debian-cross-container.sh for installed packages + MESON_OPTIONS: >- + --cross-file /opt/meson-$ARCH.cross + -D c_args=['-UFASTPATH'] + -D cpp_args=['-UFASTPATH'] + -D auto_features=enabled + -D session-managers=[] + -D bluez5-backend-native-mm=enabled + -D bluez5-codec-lc3plus=disabled + -D bluez5-codec-ldac=disabled + -D bluez5-codec-ldac-dec=disabled + -D libcamera=disabled + -D roc=disabled + -D snap=disabled + -D systemd-user-service=disabled + -D systemd-system-service=disabled + -D onnxruntime=disabled + -D vulkan=enabled + -D ffmpeg=enabled + -D pw-cat-ffmpeg=enabled + MESON_TEST_OPTIONS: >- + --timeout-multiplier=2 + .build_on_fedora: extends: - .fedora @@ -268,6 +357,9 @@ build_on_ubuntu: - .fdo.distribution-image@fedora - .build stage: build + needs: + - job: container_fedora + artifacts: false build_on_fedora: extends: @@ -318,11 +410,13 @@ build_on_fedora_html_docs: -Dsndfile=enabled -Dsession-managers=[] before_script: - - git fetch origin 1.0 1.2 master + - git fetch origin 1.0 1.2 1.4 master - git branch -f 1.0 origin/1.0 - git clone -b 1.0 . branch-1.0 - git branch -f 1.2 origin/1.2 - git clone -b 1.2 . branch-1.2 + - git branch -f 1.4 origin/1.4 + - git clone -b 1.4 . branch-1.4 - git branch -f master origin/master - git clone -b master . branch-master - !reference [.build, before_script] @@ -335,6 +429,10 @@ build_on_fedora_html_docs: - meson setup builddir $MESON_OPTIONS - meson compile -C builddir doc/pipewire-docs - cd .. + - cd branch-1.4 + - meson setup builddir $MESON_OPTIONS + - meson compile -C builddir doc/pipewire-docs + - cd .. - cd branch-master - meson setup builddir $MESON_OPTIONS - meson compile -C builddir doc/pipewire-docs @@ -354,6 +452,9 @@ build_on_alpine: - .fdo.distribution-image@alpine - .build stage: build + needs: + - job: container_alpine + artifacts: false variables: MESON_OPTIONS: "-Dsession-managers=[] -Dsnap=disabled -Dlogind=enabled -Dlogind-provider=libelogind" @@ -362,13 +463,13 @@ build_all: extends: - .build_on_fedora variables: - # Fedora doesn't have libfreeaptx, lc3plus, lc3, or roc + # Fedora doesn't have libfreeaptx, lc3plus, or roc # libcamera has no stable API, so let's not chase that target MESON_OPTIONS: >- -Dauto_features=enabled -Dbluez5-codec-aptx=disabled -Dbluez5-codec-lc3plus=disabled - -Dbluez5-codec-lc3=disabled + -Dbluez5-codec-ldac-dec=disabled -Droc=disabled -Dlibcamera=disabled -Dsession-managers=[] @@ -488,6 +589,9 @@ build_with_coverity: - .fdo.suffixed-image@fedora - .build stage: analysis + needs: + - job: container_coverity + artifacts: false script: - export PATH=/opt/coverity/bin:$PATH - meson setup "$BUILD_DIR" --prefix="$PREFIX" @@ -563,8 +667,9 @@ check_missing_headers: - .not_coverity - .fdo.distribution-image@fedora stage: analysis - dependencies: - - build_on_fedora + needs: + - job: build_on_fedora + artifacts: true script: - export PREFIX=`find -name prefix-*` - ./.gitlab/ci/check_missing_headers.sh @@ -573,18 +678,18 @@ pages: extends: - .not_coverity stage: pages - dependencies: - - build_on_fedora_html_docs + needs: + - job: build_on_fedora_html_docs + artifacts: true script: - - mkdir public public/1.0 public/1.2 public/devel + - mkdir public public/1.0 public/1.2 public/1.4 public/devel - cp -R branch-1.0/builddir/doc/html/* public/1.0/ - cp -R branch-1.2/builddir/doc/html/* public/1.2/ + - cp -R branch-1.4/builddir/doc/html/* public/1.4/ - cp -R branch-master/builddir/doc/html/* public/devel/ - - (cd public && ln -s 1.2/* .) + - (cd public && ln -s 1.4/* .) artifacts: paths: - public rules: - if: $CI_COMMIT_BRANCH == 'master' - - if: $CI_COMMIT_BRANCH == '1.0' - - if: $CI_COMMIT_BRANCH == '1.2' diff --git a/.gitlab/ci/setup-debian-cross-container.sh b/.gitlab/ci/setup-debian-cross-container.sh new file mode 100755 index 000000000..616971803 --- /dev/null +++ b/.gitlab/ci/setup-debian-cross-container.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -ex + +packages=( + # libapparmor-dev + libasound2-dev + libavahi-client-dev + libavcodec-dev + libavfilter-dev + libavformat-dev + libbluetooth-dev + libcanberra-dev + libdbus-1-dev + libebur128-dev + libfdk-aac-dev + libffado-dev + libfftw3-dev + libfreeaptx-dev + libglib2.0-dev + libgstreamer1.0-dev + libgstreamer-plugins-base1.0-dev + libjack-jackd2-dev + liblc3-dev + liblilv-dev + libmysofa-dev + libopus-dev + libpulse-dev + libreadline-dev + libsbc-dev + libsdl2-dev + # libsnapd-glib-dev + libsndfile1-dev + libspandsp-dev + libssl-dev + libsystemd-dev + libudev-dev + libusb-1.0-0-dev + libvulkan-dev + libwebrtc-audio-processing-dev + libx11-dev + modemmanager-dev +) + +arch="$1" + +export DEBIAN_FRONTEND=noninteractive + +sed -i \ + 's/^Components:.*$/Components: main contrib non-free non-free-firmware/' \ + /etc/apt/sources.list.d/debian.sources + +dpkg --add-architecture "$arch" +apt update -y + +pkgs=( "crossbuild-essential-$arch" ) +for pkg in "${packages[@]}"; do + pkgs+=( "$pkg:$arch" ) +done + +apt install -y "${pkgs[@]}" + +meson env2mfile --cross --debarch "$arch" -o "/opt/meson-$arch.cross" diff --git a/Makefile.in b/Makefile.in index 104619316..31cd8eb9e 100644 --- a/Makefile.in +++ b/Makefile.in @@ -23,6 +23,7 @@ run: all PIPEWIRE_CONFIG_DIR=$(BUILD_ROOT)/src/daemon \ ACP_PATHS_DIR=$(SOURCE_ROOT)/spa/plugins/alsa/mixer/paths \ ACP_PROFILES_DIR=$(SOURCE_ROOT)/spa/plugins/alsa/mixer/profile-sets \ + PIPEWIRE_LOG_SYSTEMD=false \ $(DBG) $(BUILD_ROOT)/src/daemon/pipewire-uninstalled run-pulse: all @@ -32,6 +33,7 @@ run-pulse: all PIPEWIRE_CONFIG_DIR=$(BUILD_ROOT)/src/daemon \ ACP_PATHS_DIR=$(SOURCE_ROOT)/spa/plugins/alsa/mixer/paths \ ACP_PROFILES_DIR=$(SOURCE_ROOT)/spa/plugins/alsa/mixer/profile-sets \ + PIPEWIRE_LOG_SYSTEMD=false \ $(DBG) $(BUILD_ROOT)/src/daemon/pipewire-pulse gdb: diff --git a/NEWS b/NEWS index 66ce22fcb..b6eba76cb 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,204 @@ +# PipeWire 1.5.81 (2025-10-16) + +This is the first 1.6 release candidate that is API and ABI +compatible with previous 1.4.x, 1.2.x and 1.0.x releases. + +In addition to all the changes backported to 1.4.x, this release +also contains some new features: + +## Highlights + - The link negotiation code was refactored and improved. + Applications now have more options for selecting the default + values and restricting the available options. The default + negotiation code will now attempt to better match the application + suggested values. + - The loop now has support for locking with priority inversion. Most + of the code was updated to use the locks instead of invoke to + get proper concurrent updates with the loop. The Thread loop + functionality of locks, signal and wait was moved to the SPA loop. + This guarantees better real-time behaviour because inter-thread + synchronization does not have to pass eventfd/epoll. + - The control stream parser was rewritten to be safe against concurrent + updates while parsing, which can occur when parsing shared memory. + It also has extra checks to avoid integer overflows and undefined + behaviour. + - MIDI 2.0 clip support was added to the tools. + - Bluetooth ASHA (Audio Streaming for Hearing Aid) support was added. + - The ALSA node setup was tweaked to provide low latency with the ALSA + Firewire driver. + - Better support for explicit sync. It is now possible to negotiate + extra features to know if a consumer will signal the sync objects and + implement a fallback using a reliable transport. + - Many bug fixes and improvements. + + +## PipeWire + - Avoid process calls in disconnect in pw-stream. (#3314) + - Disable PipeWire services for root. + - The link negotiation was refactored and improved. Drivers now + always have a lower priority in deciding the final format. + - Backwards compatibility with the v0 protocol was removed. + - pw-stream and pw-filter will now refuse to queue a buffer that + was not dequeued before. + - Object properties will now be updated on the global as well. + - The priority of config overrides is correct now. (#4816) + - Async links now correctly report 1 extra quantum of latency. + - node.exclusive and the new port.exclusive flag are now enforced + by PipeWire itself. + - A new timer-queue helper was added to schedule timeouts. + - node.terminal and node.physical properties are now copied to the + ports to make it possible to create virtual sources and sinks + for JACK applications. + - Port properties will now be dynamically updated when the node + properties they depend on are updated. + - Passive leaf nodes are now handled better. Now they will also + run when the peer is active. (#4915) + - Reliable transport has been added for output ports. This can be + used in some cases if the producer wants to ensure buffers are + consumed by a consumer. (#4885) + - Context properties now support rlimit. properties to + configure rlimits. (#4047) + +## Modules + - Close SyncObj fds. + - module-combine-stream has better Latency reporting. + - The JACK tunnel can now optionally connect ports. + - module-loopback has better Latency reporting. + - A Dolby Surround and Dolby Pro Logic II example filter config + was added. + - Filter-chain can now resample to a specific rate before running the + filters. This is useful when the filter-graph needs to run at a + specific rate. + - Avahi-poll now uses the timer-queue to schedule timeouts. + - Modules are ported to timer-queue instead of using timerfd directly + for non-realtime timers. + +## SPA + - The loop now has support for locking with priority inversion. Most + of the code was updated to use the locks instead of invoke to + get proper concurrent updates with the loop. The Thread loop + functionality of locks, signal and wait was moved to the SPA loop. + - UMP to Midi 1.0 conversion was improved, some UMP events are now + converted to multiple Midi 1.0 messages. (#4839) + - The POD filter was refactored and improved. It is now possible to + use the default value of the output by specifying an invalid input + default value. + - The POD parser was made safe for concurrent updates of the memory + it is parsing. This is important when the POD is in shared memory + and the parser should not access invalid memory. + - Some hardcoded channel limits were removed and now use the global + channel limit. More things can dynamically adapt to this global + limit. The max number of channels was then bumped to 128. + - The POD builder is safe to use on shared memory now and tries to + avoid many integer overflows. + - Most debug functions are safe to be used on shared memory. + - User specified Commands and Events are now possible. + - The SPA_IO_CLOCK_FLAG_DISCONT was added to spa_io_clock to signal + a discont in the clock due to clock change. + - AC3, DTS, EAC3, TRUEHD and MPEGH now have helper parser functions. + - H265 was added as a video format. (#4674) + - SPA_PARAM_PeerFormats was added to let a port know about its peer + formats in order to better filter possible conversions. + - More color matrices, transfer functions and color primaries. + - The echo-canceler is enabled now. + - Pro-Audio mode now uses 3 periods by default. This lowers the + latency on some drivers (Firewire). The latency of Firewire is + also reported correctly now. + - The ALSA DLL bandwidth is configurable now. + - The resampler now uses fixed point for the phases and is a little + faster when updating adaptive rates. + - The convolver is a little faster by swapping buffers instead of + copying samples. + - Latency and ProcessLatency support was added to filter-graph. + (#4678) + - Audio channel position support was added to filter-graph. + - A new ffmpeg avfilter plugin was added to filter-graph. + - A new ONNX filter was added to filter-graph. + - A debug, pipe, zeroramp and noisegate filter was added to the + filter-graph. (#4745) + - The filter-graph lv2 plugin now supports options and state. + - videoconvert was greatly improved. + - The v4l2 plugin can negotiate DMABUF with modifiers. + - Colorimetry information was added to v4l2 and libcamera. + - Audioconvert can handle empty buffers more efficiently. + - Improve the POD compare functions for Rectangle. + - There is now a SPA_POD_PROP_FLAG_DROP flag to drop the property when + the property is missing from one side. + - A new FEATURE choice was added that is basically a flags choice with + a FLAG_DROP property. + - Metadata features were added. This is a way to negotiate new features + for the metadata. (#4885) + - DSD playback with pw-cat has been improved. + - Compatibility and xrun prevention for the SOF driver has been + improved. (#4489) + - The filter-graph max plugin can now have 8 input channels. + - Buffer Negotiation between the mixer port and the node ports is much + improved. (#4918) + - An offline AEC benchmark was added. + - Channel positions are now read from HDMI ELD when possible. + - Audioconvert and filter-graph now also support properties of Long + and String types. + +## ACP + - It's possible to disable the pro-audio profile. + - Support for Logitech Z407 PC Speakers was improved. + - Support for Razer BlackShark v3. + - Fix volume rounding down causing mute. (#4890) + +## Tools + - pw-cat can now play and record MIDI 2.0 Clips, which is the + official format for storing MIDI 2.0 UMP data. pw-midi2play + and pw-midi2record were added as aliases. + - pw-cat can now upload sysex files. The pw-sysex alias was + added for this. + - The pw-link tool now has a -t option to list port latencies. + It also has better monitor support. + - pw-top can now clear the ERR column with the c key. + - pw-cli now keeps the types of the variables it stores and avoid + using wrongly typed variables that can crash things. It can now + also list the available variables. + - pw-dump can now output raw JSON and SPA JSON. + - pw-dump has configurable indentation level. + - pw-mididump can be forced to output MIDI 1.0 messages. + - pw-profiler now uses doubles for extra precision. + - pw-top now marks the async nodes with =. + +## Bluetooth + - Telephony improvements. + - ASHA support was added. + - Packet loss concealment was added. + - Improved synchronisation between LE Audio streams in the same group. + - Improved LE Audio device compatibility. + - LC3-24kHz voice codec was added (used by Airpods) + - LDAC decoding support added (requires separate decoder library) + +## Pulse-server + - The SUSPEND event is now correctly generated. fail-on-suspend is + now implemented. + - PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND is now implemented. (#4255) + (#4726) + - RTP streams now have stream.properties for extra configuration. + - Timed out streams are now destroyed instead of lingering. (#4901) + - A new help and pipewire-pulse:list-modules core message was added. + +## JACK + - Port rename callbacks are now emitted correctly. + - Use safe POD parsing for the control sequences. + +## V4l2 + - The wrapper now avoids a race while initializing PipeWire. (#4859) + +## GStreamer + - Colorimetry support was added. + - Cursor metadata is now exposed as ROI metadata. + - Many more updates. + +## Docs + - Document the client-node flow a bit more. + + +Older versions: + # PipeWire 1.4.0 (2025-03-06) This is the 1.4 release that is API and ABI compatible with previous @@ -99,9 +300,6 @@ the 1.2 release last year, including: ## JACK - Add an option to disable the MIDI2 port flags. (#4584) - -Older versions: - # PipeWire 1.3.83 (2025-02-20) This is the third and hopefully last 1.4 release candidate that diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index 4d9dc63bf..f8cdbf528 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -38,6 +38,7 @@ IGNORE_PREFIX = pw_ \ spa_ \ SPA_ GENERATE_TREEVIEW = YES +DISABLE_INDEX = NO SEARCHENGINE = YES GENERATE_LATEX = NO diff --git a/doc/DoxygenLayout.xml b/doc/DoxygenLayout.xml index b5d353a6f..300c8400e 100644 --- a/doc/DoxygenLayout.xml +++ b/doc/DoxygenLayout.xml @@ -41,6 +41,11 @@ + + + + + diff --git a/doc/custom.css b/doc/custom.css index 46e58ddae..59a2a94d4 100644 --- a/doc/custom.css +++ b/doc/custom.css @@ -1,4 +1,4 @@ -:root { +html { /* --page-background-color: #729fcf; */ --primary-color: #729fcf; --primary-dark-color: #729fcf; @@ -8,7 +8,7 @@ } @media (prefers-color-scheme: light) { - :root { + html { --code-background: #f5f5f5; --code-foreground: #333333; --fragment-background: #f5f5f5; diff --git a/doc/dox/api/spa-pod.dox b/doc/dox/api/spa-pod.dox index 0bba2a474..2ce8d6297 100644 --- a/doc/dox/api/spa-pod.dox +++ b/doc/dox/api/spa-pod.dox @@ -33,7 +33,7 @@ POD's can contain a number of basic SPA types: - `SPA_TYPE_Bytes`: A byte array. - `SPA_TYPE_Rectangle`: A rectangle with width and height. - `SPA_TYPE_Fraction`: A fraction with numerator and denominator. -- `SPA_TYPE_Bitmap`: An array of bits. +- `SPA_TYPE_Bitmap`: An array of bits. Deprecated and unused. POD's can be grouped together in these container types: @@ -435,10 +435,11 @@ spa_pod_parser_get_object(&p, \endcode `spa_pod_get_values()` is a useful function. It returns a -`struct spa_pod*` with and array of values. For normal POD's -and choice none values, it simply returns the POD and one value. -For other choice values it returns the choice type and an array -of values: +`struct spa_pod*` with and array of values. For invalid PODs +it returns the POD and no values. For normal PODs it returns +the POD and one value. For choice values it returns the choice +type and an array of values. If the choice doesn't fit even a +single value, the array will have no values. \code{.c} struct spa_pod *value; @@ -1019,8 +1020,9 @@ A choice contains an array of possible values. child2 and child3, in steps of child4 in the value array. - Enum (3) : child1 is a default value, options are any value from the value array, preferred values come first. - - Flags (4) : child1 is a default value, options are any value from - the value array, preferred values come first. + - Flags (4) : only child1 is a flag value. When filtering, the flags + are AND-ed together. + - flags: must be 0 ## Pod (20) diff --git a/doc/dox/config/pipewire-client.conf.5.md b/doc/dox/config/pipewire-client.conf.5.md index 66bf3d2ec..38da058d0 100644 --- a/doc/dox/config/pipewire-client.conf.5.md +++ b/doc/dox/config/pipewire-client.conf.5.md @@ -48,7 +48,7 @@ ALSA client match rules. In addition, the PipeWire context configuration sections may also be specified, see \ref page_man_pipewire_conf_5 "pipewire.conf(5)". -# STREAM PROPERTIES @IDX@ client.conf +# STREAM PROPERTIES @IDX@ client.conf stream.properties The client configuration files contain a stream.properties section that configures the options for client streams: ```css @@ -93,7 +93,7 @@ A list of object properties that can be applied to streams can be found in and \ref props__audio_converter_properties "pipewire-props(7) Audio Adapter Properties" -# STREAM RULES @IDX@ client.conf +# STREAM RULES @IDX@ client.conf stream.rules You can add \ref pipewire_conf__match_rules "match rules, see pipewire(1)" to set properties for certain streams and filters. @@ -127,7 +127,7 @@ stream.rules = [ Will set the node.name of Firefox to "My Name". -# ALSA CLIENT PROPERTIES @IDX@ client.conf +# ALSA CLIENT PROPERTIES @IDX@ client.conf alsa.properties An `alsa.properties` section can be added to configure client applications that connect via the PipeWire ALSA plugin. @@ -169,7 +169,7 @@ The number of bytes in the alsa buffer. The default is 0, which is to allow any This controls the volume curve used on the ALSA mixer. Possible values are `cubic` and `linear`. The default is to use `cubic`. -# ALSA CLIENT RULES @IDX@ client.conf +# ALSA CLIENT RULES @IDX@ client.conf alsa.rules It is possible to set ALSA client specific properties by using \ref pipewire_conf__match_rules "Match rules, see pipewire(1)". You can diff --git a/doc/dox/config/pipewire-jack.conf.5.md b/doc/dox/config/pipewire-jack.conf.5.md index b8a4a5cf0..8636aab21 100644 --- a/doc/dox/config/pipewire-jack.conf.5.md +++ b/doc/dox/config/pipewire-jack.conf.5.md @@ -38,7 +38,7 @@ JACK client match rules. In addition, the PipeWire context configuration sections may also be specified, see \ref page_man_pipewire_conf_5 "pipewire.conf(5)". -# JACK PROPERTIES @IDX@ jack.conf +# JACK PROPERTIES @IDX@ jack.conf jack.properties The configuration file can contain an extra JACK specific section called `jack.properties` like this: ```css @@ -206,7 +206,7 @@ JACK apps don't know about this flag yet and refuse to show the port. Set this to true for applications that know how to handle MIDI2 ports. \endparblock -# MATCH RULES @IDX@ jack.conf +# MATCH RULES @IDX@ jack.conf jack.rules `jack.rules` provides an `update-props` action that takes an object with properties that are updated on the client and node object of the jack client. diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index d7e6af820..964ebec0d 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -36,13 +36,96 @@ type. Other properties control settings of a specific kinds of device or node (ALSA, Bluetooth, ...), and have meaning only for those objects. -Usually, all the properties are configured in the session manager -configuration. For how to configure them, see the session manager -documentation. In minimal PipeWire setups without a session manager, -they can be configured via -\ref pipewire_conf__context_objects "context.objects in pipewire.conf(5)". +# CUSTOMIZING PROPERTIES @IDX@ props -\see [WirePlumber configuration](https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration.html) +Usually, all device properties are configured in the session manager +configuration, see the session manager documentation. +Application properties are configured in +``client.conf`` (for native PipeWire and ALSA applications), +``pipewire-pulse.conf`` (for Pulseaudio applications), and +``jack.conf`` (for JACK applications). + +In minimal PipeWire setups without a session manager, +the device properties can be configured via +\ref pipewire_conf__context_objects "context.objects in pipewire.conf(5)" +when creating the devices. + +## Examples + +Device configuration using WirePlumber (requires WirePlumber restart to apply). +See [WirePlumber configuration](https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration.html) +```css +# ~/.config/wireplumber/wireplumber.conf.d/custom-props.conf + +monitor.alsa.properties = { + alsa.udev.expose-busy = true +} + +monitor.alsa.rules = [ + { + matches = [ { device.name = "~alsa_card.pci-.*" } ], + actions = { update-props = { api.alsa.soft-mixer = true } } + }, + { + matches = [ { node.name = "alsa_output.pci-0000_03_00.1.hdmi-stereo-extra3" } ] + actions = { update-props = { node.description = "Main Audio" } } + } +] + +monitor.alsa-midi.properties = { + api.alsa.seq.ump = true +} + +monitor.bluez.properties = { + bluez5.hfphsp-backend = ofono +} + +monitor.bluez.rules = [ + { + matches = [ { device.name = "~bluez_card.*" } ], + actions = { update-props = { bluez5.dummy-avrcp player = true } } + } +] +``` + +Native client configuration (requires client application restart to apply). +See \ref client_conf__stream_rules "pipewire-client.conf(5)" +```css +# ~/.config/pipewire/client.conf/custom-props.conf + +stream.rules = [ + { + matches = [ { application.name = "pw-play" } ] + actions = { update-props = { node.description = "Some pw-cat stream" } } + } +] +``` + +Pulseaudio client configuration (requires \ref page_man_pipewire-pulse_1 "pipewire-pulse(1)" restart to apply). +See \ref pipewire-pulse_conf__stream_rules "pipewire-pulse.conf(5)" +```css +# ~/.config/pipewire/pipewire-pulse.conf/custom-props.conf + +stream.rules = [ + { + matches = [ { application.name = "paplay" } ] + actions = { update-props = { node.description = "Some paplay stream" } } + } +] +``` + +JACK client configuration (requires client restart to apply). +See \ref jack_conf__match_rules "pipewire-jack.conf(5)" +```css +# ~/.config/pipewire/jack.conf/custom-props.conf + +jack.rules = [ + { + matches = [ { client.name = "jack_delay" } ] + actions = { update-props = { node.description = "Some JACK node" } } + } +] +``` # COMMON DEVICE PROPERTIES @IDX@ props @@ -91,12 +174,12 @@ ie. for example `device.Param.Props = { ... }` to set `Props`. @PAR@ device-prop device.product.id # integer \parblock -\copydoc PW_KEY_DEVICE_PRODUCT_NAME +\copydoc PW_KEY_DEVICE_PRODUCT_ID \endparblock @PAR@ device-prop device.product.name # string \parblock -\copydoc PW_KEY_DEVICE_PRODUCT_ID +\copydoc PW_KEY_DEVICE_PRODUCT_NAME \endparblock @PAR@ device-prop device.class # string @@ -143,11 +226,14 @@ real or virtual devices. These contain properties to identify the node or to display the node in a GUI application. -@PAR@ node-prop node.name +@PAR@ node-prop node.name # string A (unique) name for the node. This is usually set on sink and sources to identify them as targets for linking by the session manager. -@PAR@ node-prop node.description +@PAR@ node-prop node.nick # string +A short name for the node. + +@PAR@ node-prop node.description # string A human readable description of the node or stream. @PAR@ node-prop media.name @@ -338,14 +424,14 @@ a sink or source. @PAR@ node-prop node.exclusive = false If this node wants to be linked exclusively to the sink/source. +@PAR@ node-prop target.object = +Where the node should link to, this can be a node.name or an object.serial. + @PAR@ node-prop node.target = Where this node should be linked to. This can be a node.name or an object.id of a node. This property is deprecated, the target.object property should be used instead, which uses the more unique object.serial as a possible target. -@PAR@ node-prop target.object = -Where the node should link to, this can be a node.name or an object.serial. - @PAR@ node-prop node.dont-reconnect = false \parblock When the node has a target configured and the target is destroyed, destroy the node as well. @@ -355,6 +441,13 @@ Note that if a stream should appear/disappear in sync with the target, a session should be written instead. \endparblock +@PAR@ node-prop node.dont-fallback = false +If linking this node to its specified target does not succeed, session +manager should not fall back to linking it to the default target. + +@PAR@ node-prop node.dont-move = false +Whether the node target may be changed using metadata. + @PAR@ node-prop node.passive = false \parblock This is a passive node and so it should not keep sinks/sources busy. This property makes the session manager create passive links to the sink/sources. If the node is not otherwise linked (via a non-passive link), the node and the sink it is linked to are idle (and eventually suspended). @@ -371,6 +464,13 @@ Instruct the session manager to not remix the channels of a stream. Normally the @PAR@ node-prop priority.session # integer The priority for selecting this node as the default source or sink. +@PAR@ node-prop session.suspend-timeout-seconds = 3 # float +Timeout in seconds, after which an idle node is suspended. +Value ``0`` means the node will not be suspended. + +@PAR@ node-prop state.restore-props = true +Whether session manager should save state for this node. + ## Format Properties Streams and also most device nodes can be configured in a certain format with properties. @@ -640,8 +740,12 @@ See \ref spa_param_port_config for the meaning. ## Monitor properties -@PAR@ monitor-prop alsa.use-acp # boolean +@PAR@ monitor-prop alsa.use-acp = true # boolean Use \ref monitor-prop__alsa_card_profiles "ALSA Card Profiles" (ACP) for device configuration. +This autodetects available ALSA devices and configures port and hardware mixers. + +@PAR@ monitor-prop alsa.use-ucm # boolean +Enable or disable UCM for all devices. Default: unset. @PAR@ monitor-prop alsa.udev.expose-busy # boolean Expose the ALSA card even if it is busy/in use. Default false. This can be useful when some @@ -658,7 +762,7 @@ 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`. +This option does nothing if `alsa.use-acp` is set to `false`. \endparblock @PAR@ device-prop api.alsa.soft-mixer = false # boolean @@ -718,6 +822,13 @@ some devices. \copydoc SPA_KEY_API_ALSA_SPLIT_ENABLE \endparblock +@PAR@ device-prop api.acp.disable-pro-audio = false # boolean +Disable the "Pro Audio" profile for this device. + +@PAR@ device-prop api.acp.use-eld-channels = false # boolean +Use the channel count and mapping the connected HDMI device +provides via ELD information. + ## Node properties @PAR@ node-prop audio.channels # integer @@ -732,6 +843,9 @@ The audio format to open the device in. By default this is "UNKNOWN", which will @PAR@ node-prop audio.position # JSON array of strings The audio position of the channels in the device. This is auto detected based on the profile. You can configure an array of channel positions, like "[ FL, FR ]". +@PAR@ node-prop audio.layout # string +The audio layout of the channels in the device. You can use any of the predefined layouts, like "Stereo", "5.1" etc. + @PAR@ node-prop audio.allowed-rates # JSON array of integers \parblock The allowed audio rates to open the device with. Default is "[ ]", which means the device can be opened in any supported rate. @@ -778,6 +892,12 @@ Setting this to 0 makes htimestamp never get disabled. Disable timer-based scheduling, and use IRQ for scheduling instead. The "Pro Audio" profile will usually enable this setting, if it is expected it works on the hardware. +@PAR@ node-prop api.alsa.dll-bandwidth-max # double +Sets the maximum bandwidth of the DLL (delay-locked loop) filter used to smooth out rate adjustments. +The default value may be too responsive in some scenarios. +For example, with UAC2 pitch control, the host reacts more slowly compared to local resampling, +so using a lower bandwidth helps avoid oscillations or instability. + @PAR@ node-prop api.alsa.auto-link = false # boolean Link follower PCM devices to the driver PCM device when using IRQ-based scheduling. The "Pro Audio" profile will usually enable this setting, if it is expected it works on the hardware. @@ -791,12 +911,18 @@ Static set the device systemic latency, in nanoseconds. @PAR@ node-prop api.alsa.path # string UNDOCUMENTED +@PAR@ node-prop api.alsa.pcm.card # integer +Card index to open. Usually determined from `api.alsa.path`. + @PAR@ node-prop api.alsa.open.ucm # boolean Open device using UCM. @PAR@ node-prop api.alsa.bind-ctls # boolean UNDOCUMENTED +@PAR@ node-prop api.alsa.bind-ctl.NAME # boolean +UNDOCUMENTED + @PAR@ node-prop iec958.codecs # JSON array of string Enable only specific IEC958 codecs. This can be used to disable some codecs the hardware supports. Available values: PCM, AC3, DTS, MPEG, MPEG2-AAC, EAC3, TRUEHD, DTSHD @@ -818,6 +944,34 @@ Informative property. Informative property. \endparblock +@PAR@ node-prop api.alsa.dsd-lsb = false # boolean +Use LSB bit order for DSD audio output. + + +# ALSA MIDI PROPERTIES @IDX@ props + +## Node properties + +For ALSA MIDI in Wireplumber, MIDI bridge node properties are +configured in the monitor properties. + +@PAR@ monitor-prop api.alsa.seq.ump = true # boolean +Use MIDI 2.0 if possible. + +@PAR@ monitor-prop api.alsa.seq.min-pool = 500 # integer +UNDOCUMENTED + +@PAR@ monitor-prop api.alsa.seq.max-pool = 2000 # integer +UNDOCUMENTED + +@PAR@ monitor-prop clock.name = "clock.system.monotonic" # string +Clock to follow. + +@PAR@ monitor-prop api.alsa.path = "default" # string +Sequencer device to use. + +@PAR@ monitor-prop api.alsa.disable-longname = true # boolean +If card long name should not be passed to MIDI port. # BLUETOOTH PROPERTIES @IDX@ props @@ -963,6 +1117,26 @@ Maximum number of octets supported per codec frame for the LC3 codec (default: 4 @PAR@ monitor-prop bluez5.bap-server-capabilities.max_frames # integer Maximum number of codec frames supported per SDU for the LC3 codec (default: 2). +@PAR@ monitor-prop bluez5.bap-server-capabilities.sink.locations # JSON or integer +Sink audio locations of the server, as channel positions or PACS bitmask. +Example: `FL,FR` + +@PAR@ monitor-prop bluez5.bap-server-capabilities.sink.contexts # integer +Available sink contexts PACS bitmask of the the server. + +@PAR@ monitor-prop bluez5.bap-server-capabilities.sink.supported-contexts # integer +Supported sink contexts PACS bitmask of the the server. + +@PAR@ monitor-prop bluez5.bap-server-capabilities.source.locations # JSON or integer +Source audio locations of the server, as channel positions or PACS bitmask. +Example: `FL,FR` + +@PAR@ monitor-prop bluez5.bap-server-capabilities.source.contexts # integer +Available source contexts PACS bitmask of the the server. + +@PAR@ monitor-prop bluez5.bap-server-capabilities.source.supported-contexts # integer +Supported source contexts PACS bitmask of the the server. + ## Device properties @PAR@ device-prop bluez5.auto-connect # boolean @@ -997,6 +1171,31 @@ PipeWire Opus Pro Audio duplex encoding mode: audio, voip, lowdelay @PAR@ device-prop bluez5.bap.cig = "auto" # integer, or 'auto' Set CIG ID for BAP unicast streams of the device. +@PAR@ device-prop bluez5.bap.preset = "auto" # string +BAP QoS preset name that needed to be used with vendor config. +This property is experimental. +Available: "48_2_1", ... as in the BAP specification. + +@PAR@ device-prop bluez5.bap.rtn # integer +BAP QoS preset name that needed to be used with vendor config. +This property is experimental. +Default: as per QoS preset. + +@PAR@ device-prop bluez5.bap.latency # integer +BAP QoS latency that needs to be applied for vendor defined preset +This property is experimental. +Default: as QoS preset. + +@PAR@ device-prop bluez5.bap.delay = 40000 # integer +BAP QoS delay that needs to be applied for vendor defined preset +This property is experimental. +Default: as per QoS preset. + +@PAR@ device-prop bluez5.framing = false # boolean +BAP QoS framing that needs to be applied for vendor defined preset +This property is experimental. +Default: as per QoS preset. + ## Node properties @PAR@ node-prop bluez5.media-source-role # string @@ -1007,6 +1206,17 @@ this instance. Available values: - input: appear as source node. \endparblock +@PAR@ node-prop bluez5.decode-buffer.latency # integer +Applies on media source nodes and defines the target amount +of samples to be buffered on the output of the decoder. +Default: 0, which means it is automatically determined. + +@PAR@ node-prop node.latency-offset-msec # string +Applies only for BLE MIDI nodes. +Latency adjustment to apply on the node. Larger values add a +constant latency, but reduces timing jitter caused by Bluetooth +transport. + # PORT PROPERTIES @IDX@ props Port properties are usually not directly configurable via PipeWire diff --git a/doc/dox/config/pipewire-pulse.conf.5.md b/doc/dox/config/pipewire-pulse.conf.5.md index d1a65cf49..ad1b213c7 100644 --- a/doc/dox/config/pipewire-pulse.conf.5.md +++ b/doc/dox/config/pipewire-pulse.conf.5.md @@ -54,7 +54,7 @@ for the detailed description. In addition, the PipeWire context configuration sections may also be specified, see \ref page_man_pipewire_conf_5 "pipewire.conf(5)". -# STREAM PROPERTIES @IDX@ pipewire-pulse.conf +# STREAM PROPERTIES @IDX@ pipewire-pulse.conf stream.properties The `stream.properties` section contains properties for streams created by the pipewire-pulse server. @@ -100,18 +100,18 @@ stream.properties = { } ``` -# STREAM RULES @IDX@ pipewire-pulse.conf +# STREAM RULES @IDX@ pipewire-pulse.conf stream.rules The `stream.rules` section works the same as \ref client_conf__stream_rules "pipewire-client.conf(5) stream.rules". -# PULSEAUDIO PROPERTIES @IDX@ pipewire-pulse.conf +# PULSEAUDIO PROPERTIES @IDX@ pipewire-pulse.conf pulse.properties For `pulse.properties` section, see \ref page_module_protocol_pulse "libpipewire-module-protocol-pulse(7)" for available options. -# PULSEAUDIO RULES @IDX@ pipewire-pulse.conf +# PULSEAUDIO RULES @IDX@ pipewire-pulse.conf pulse.rules For each client, a set of rules can be written in `pulse.rules` section to configure quirks of the client or to force some pulse diff --git a/doc/dox/config/pipewire.conf.5.md b/doc/dox/config/pipewire.conf.5.md index dcb1a0a04..617f04751 100644 --- a/doc/dox/config/pipewire.conf.5.md +++ b/doc/dox/config/pipewire.conf.5.md @@ -140,7 +140,7 @@ Array of dictionaries. Match rules for modifying device properties on the server. -# CONTEXT PROPERTIES @IDX@ pipewire.conf +# CONTEXT PROPERTIES @IDX@ pipewire.conf context.properties Available PipeWire properties in `context.properties` and possible default values. @@ -275,6 +275,20 @@ Warn about failures to lock memory. @PAR@ pipewire.conf mem.mlock-all = false Try to mlock all current and future memory by the process. +@PAR@ pipewire.conf rlimit.nofile = 4096 +Try to set the max file descriptor number resource limit of the process. +A value of -1 raises the limit to the system defined hard maximum value. +The file resource limit is usually 1024 and should only be raised if the +program does not use the select() system call. PipeWire does normally not +use select(). + +@PAR@ pipewire.conf rlimit.*resource* = *value* +Set resource limits. *resource* can be one of: as, core, cpu, +data, fsize, locks, memlock, msgqueue, nice, nofile, nproc, rss, rtprio, +rttime, sigpending or stack. See the documentation of setrlimit to get the +meaning of these resources. A value of -1 will set the maximum allowed +limit. + @PAR@ pipewire.conf settings.check-quantum = false Check if the quantum in the settings metadata update is compatible with the configured limits. @@ -302,7 +316,7 @@ the `context.modules` and `context.objects` sections can declare additional conditions that control whether a module or object is loaded depending on what properties are present. -# SPA LIBRARIES @IDX@ pipewire.conf +# SPA LIBRARIES @IDX@ pipewire.conf context.spa-libs SPA plugins are loaded based on their factory-name. This is a well known name that uniquely describes the features that the plugin should @@ -331,7 +345,7 @@ context.spa-libs = { } ``` -# MODULES @IDX@ pipewire.conf +# MODULES @IDX@ pipewire.conf context.modules PipeWire modules to be loaded. See \ref page_man_libpipewire-modules_7 "libpipewire-modules(7)". @@ -364,7 +378,7 @@ A \ref pipewire_conf__match_rules "match rule" `matches` condition. The module is loaded only if one of the expressions in the array matches to a context property. -# CONTEXT OBJECTS @IDX@ pipewire.conf +# CONTEXT OBJECTS @IDX@ pipewire.conf context.objects The `context.objects` section allows you to make some objects from factories (usually created by loading modules in `context.modules`). @@ -417,7 +431,7 @@ context.objects = [ ] ``` -# COMMAND EXECUTION @IDX@ pipewire.conf +# COMMAND EXECUTION @IDX@ pipewire.conf context.exec The `context.exec` section can be used to start arbitrary commands as part of the initialization of the PipeWire program. @@ -590,7 +604,7 @@ matches = [ ``` -# CONTEXT PROPERTIES RULES @IDX@ pipewire.conf +# CONTEXT PROPERTIES RULES @IDX@ pipewire.conf context.properties.rules `context.properties.rules` can be used to dynamically update the properties based on other properties. @@ -614,7 +628,7 @@ context.properties.rules = [ } ``` -# NODE RULES @IDX@ pipewire.conf +# NODE RULES @IDX@ pipewire.conf node.rules The node.rules are evaluated every time the properties on a node are set or updated. This can be used on the server side to override client set @@ -647,7 +661,7 @@ node.rules = [ Will set the `node.force-quantum` property of `jack_simple_client` to 512. -# DEVICE RULES @IDX@ pipewire.conf +# DEVICE RULES @IDX@ pipewire.conf device.rules The device.rules are evaluated every time the properties on a device are set or updated. This can be used on the server side to override client set diff --git a/doc/dox/index.dox b/doc/dox/index.dox index 6688ca161..a90e19cd8 100644 --- a/doc/dox/index.dox +++ b/doc/dox/index.dox @@ -48,5 +48,6 @@ See \ref page_api. - [Bluetooth, PipeWire and Whatsapp calls](https://gjhenrique.com/pipewire.html) - [Intoduction to PipeWire](https://bootlin.com/blog/an-introduction-to-pipewire/) - [A custom PipeWire node](https://bootlin.com/blog/a-custom-pipewire-node/) +- [FOSDEM 2025 talk and slides](https://fosdem.org/2025/schedule/event/fosdem-2025-4749-pipewire-state-of-the-union/) */ diff --git a/doc/dox/internals/design.dox b/doc/dox/internals/design.dox index 147336083..9a186954d 100644 --- a/doc/dox/internals/design.dox +++ b/doc/dox/internals/design.dox @@ -44,6 +44,8 @@ The native protocol and object model is similar to serialization/deserialization of messages. This is because the data structures in the messages are more complicated and not easily expressible in XML. See \ref page_module_protocol_native for details. +See also \ref page_native_protocol for the documentation of the protocol +messages. # Extensibility diff --git a/doc/dox/internals/driver.dox b/doc/dox/internals/driver.dox new file mode 100644 index 000000000..4ab0d229b --- /dev/null +++ b/doc/dox/internals/driver.dox @@ -0,0 +1,115 @@ +/** \page page_driver Driver architecture and workflow + +This document explains how drivers are structured and how they operate. +This is useful to know both for debugging and for writing new drivers. + +(For details about how the graph does scheduling, which is tied to the driver, +see \ref page_scheduling ). + +# Clocks + +A driver is a node that starts graph cycles. Typically, this is accomplished +by using a timer that periodically invokes a callback, or by an interrupt. + +Drivers use the monotonic system clock as the reference for timestamping. Note +that "monotonic system clock" does not refer to the \c MONOTONIC_RAW clock in +Linux, but rather, to the regular monotonic clock. + +Drivers may actually be run by a custom internal clock instead of the monotonic +system clock. One example would be a sound card DAC's clock. Another would be +a network adapter with a built in PHC. Or, the driver may be using a system +clock other than the monotonic system clock. The driver then needs to perform +some sort of timestamp translation and drift compensation from that internal +clock to the monotonic clock, since it still needs to generate monotonic clock +timestamps for the beginning cycle. (More on that below.) + +# Updates and graph cycle start + +Every time a driver starts a graph cycle, it must update the contents of the +\ref spa_io_clock instance that is assigned to them through the +\ref spa_node_methods::set_io callback. The fields of the struct must be +updated as follows: + +- \ref spa_io_clock::nsec : Must be set to the time (according to the monotonic + system clock) when the cycle that the driver is about to trigger started. To + minimize jitter, it is usually a good idea to increment this by a fixed amount + except for when the driver starts and when discontinuities occur in its clock. +- \ref spa_io_clock::rate : Set to a value that can translate samples to nanoseconds. +- \ref spa_io_clock::position : Current cycle position, in samples. This is the + ideal position of the graph cycle (this is explained in greater detail further below). + It is incremented by the duration (in samples) at the beginning of each cycle. If + a discontinuity is experienced by the driver that results in a discontinuity in the + position of the old and the current cycle, consider setting the + \ref SPA_IO_CLOCK_FLAG_DISCONT flag to inform other nodes about this. +- \ref spa_io_clock::duration : Duration of this new cycle, in samples. +- \ref spa_io_clock::rate_diff : A decimal value that is set to whatever correction + factor the driver applied to for a drift between an internal driver clock and the + monotonic system clock. A value above 1.0 means that the internal driver clock + is faster than the monotonic system clock, and vice versa. Always set this to + 1.0 if the driver is directly using the monotonic clock. +- \ref spa_io_clock::next_nsec : Must be set to the time (according to the monotonic + system clock) when the cycle that comes after the current one is to be started. In + some cases, this may actually be in the past relative to nsec, for example, when + some internal driver clock experienced a discontinuity. Consider setting the + \ref SPA_IO_CLOCK_FLAG_DISCONT flag in such a case. Just like with nsec, to + minimize jitter, it is usually a good idea to increment this by a fixed amount + except for when the driver starts and when discontinuities occur in its clock. + +The driver node signals the start of the graph cycle by calling \ref spa_node_call_ready +with the \ref SPA_STATUS_HAVE_DATA and \ref SPA_STATUS_NEED_DATA flags passed +to that function call. That call must happen inside the thread that runs the +data loop assigned to the driver node. + +As mentioned above, the \ref spa_io_clock::position field is the _ideal_ position +of the graph cycle, in samples. This contrasts with \ref spa_io_clock::nsec, which +is the moment in monotonic clock time when the cycle _actually_ happens. This is an +important distinction when driver is run by a clock that is different to the monotonic +clock. In that case, the \ref spa_io_clock::nsec timestamps are adjusted to match the +pace of that different clock (explained in the section below). In such a case, +\ref spa_io_clock::position still is incremented by the duration in samples. This +is important, since nodes and modules may use this field as an offset within their own +internal ring buffers or similar structures, using the position field as an offset within +said data structures. This requires the position field to advance in a continuous way. +By incrementing by the duration, this requirement is met. + +# Using clocks other than the monotonic clock + +As mentioned earlier, the driver may be run by an internal clock that is different +to the monotonic clock. If that other clock can be directly used for scheduling +graph cycle initiations, then it is sufficient to compute the offset between that +clock and the monotonic clock (that is, offset = monotonic_clock_time - other_clock_time) +at each cycle and use that offset to translate that other clock's time to the monotonic +clock time. This is accomplished by adding that offset to the \ref spa_io_clock::nsec +and \ref spa_io_clock::next_nsec fields. For example, when the driver uses the realtime +system clock instead of the monotonic system clock, then that realtime clock can still +be used with \c timerfd to schedule callback invocations within the data loop. Then, +computing the (monotonic_clock_time - realtime_clock_time) offset is sufficient, as +mentioned, to be able to translate clock's time to monotonic time for \ref spa_io_clock::nsec +and \ref spa_io_clock::next_nsec (which require monotonic clock timestamps). + +If however that other clock cannot be used for scheduling graph cycle initiations directly +(for example, because the API of that clock has no functionality to trigger callbacks), +then, in addition to the aforementioned offset, the driver has to use the monotonic clock +for triggering callbacks (usually via \c timerfd) and adjust the time when callbacks are +invoked such that they match the pace of that other clock. + +As an example (clock speed difference exaggerated for sake of clarity), suppose the other +clock is twice as fast as the monotonic clock. Then the monotonic clock timestamps have +to be calculated in a manner that halves the durations between said timestamps, and the +\ref spa_io_clock::rate_diff field is set to 2.0. + +The dummy node driver uses a DLL for this purpose. It is fed the difference between the +expected position (in samples) and the actual position (derived from the current time +of the driver's internal clock), passes the delta between these two quantities into the +DLL, and the DLL computes a correction factor (2.0 in the above example) which is used +for scaling durations between \c timerfd timeouts. This forms a control loop, since the +correction factor causes the durations between the timeouts to be adjusted such that the +difference between the expected position and the actual position reaches zero. Keep in +mind the notes above about \ref spa_io_clock::position being the ideal position of the +graph cycle, meaning that even in this case, the duration it is incremented by is +_not_ scaled by the correction factor; the duration in samples remains unchanged. + +(Other popular control loop mechanisms that are suitable alternatives to the DLL are +PID controllers and Kalman filters.) + +*/ diff --git a/doc/dox/internals/index.dox b/doc/dox/internals/index.dox index f7152d61c..89d2e9da3 100644 --- a/doc/dox/internals/index.dox +++ b/doc/dox/internals/index.dox @@ -11,6 +11,9 @@ - \subpage page_library - \subpage page_dma_buf - \subpage page_scheduling +- \subpage page_driver +- \subpage page_latency +- \subpage page_tag - \subpage page_native_protocol diff --git a/doc/dox/internals/latency.dox b/doc/dox/internals/latency.dox new file mode 100644 index 000000000..0ff2cbe95 --- /dev/null +++ b/doc/dox/internals/latency.dox @@ -0,0 +1,282 @@ +/** \page page_latency Latency support + +This document explains how the latency in the PipeWire graph is implemented. + +# Use Cases + +## A node port has a latency + +Applications need to be able to query the latency of a port. + +Linked Nodes need to be informed of the latency of a port. + +## dynamically update port latencies + +It needs to be possible to dynamically update the latency of a port and this +should inform all linked ports/nodes of the updated latency. + +## Linked nodes add latency to the signal + +When two nodes/ports have a latency, the signal is delayed by the sum of +the nodes latencies. + +## Calculate the signal delay upstream and downstream + +A node might need to know how much a signal was delayed since it arrived +in the node. + +A node might need to know how much the signal will be delayed before it +exists the graph. + +## Detect latency mismatch + +When a signal travels through 2 different parts of the graph with different +latencies and then eventually join, there is a latency mismatch. It should +be possible to detect this mismatch. + + +# Concepts + +## Port Latency + +The fundamental object for implementing latency reporting in PipeWire is the +Latency object. + +It consists of a direction (input/output) and min/max latency values. The latency +values can express a latency relative to the graph quantum, the samplerate or in +nanoseconds. There is a mininum and maximum latency value in the Latency object. + +The direction of the latency object determines how the latency object propagates. + +- SPA_DIRECTION_OUTPUT Latency objects move from output ports downstream and contain + the latency from all nodes upstream. An output latency received on an input port + should instruct the node to update the output latency on its output ports related + to this input port. This corresponds to the JackCaptureLatency. + +- SPA_DIRECTION_INPUT Latency objects move from input ports upstream and contain + the latency from all nodes downstream. An input latency received on an output port + should instruct the node to update the input latency on its input ports related + to this output port. This corresponds to the JackPlaybackLatency. + +PipeWire will automatically propagate Latency objects from ports to all linked ports +in the graph. Output Latency objects on output ports are propagated to linked input +ports and input Latency objects on input ports are propagated to linked output ports. + +If a port has links with multiple other ports, the Latency objects are merged by +taking the min of the min values and the max of the max values of all latency objects +on the other ports. + +This way, Output Latency always describes the aggragated total upstream latency of +signal up to the port and Input latency describes the aggregated downstream latency +of the signal from the port. + + +## Node ProcessLatency + +This is a per node property and applies to the latency introduced by the node +logic itself. + +This mostly works if (almost) all data processing ports (input/output) participate in +the same data processing with the same latency, which is a common use case. + +ProcessLatency is mostly used to easily calculate Output and Input Latency on ports. +We can simply add the ProcessLatency to all latency values in the ports Latency objects +to obtain the corresponding Latency object for the other ports. + + +# Latency updates + +Latency params on the ports can be updated as needed. This can happen because some upstream or +downstream latency changed or when the ProcessLatency of a node changes. + +When the ProcessLatency changes, the procedure to notify of latency changes is: + +- Take output latency on input port, add new ProcessLatency, set new latency on output + port. This propagates the new latency downstream. + +- Take input latency on output port, add new ProcessLatency, set new latency on input + port. This propagates the new latency upstream. + +PipeWire will automatically aggregate latency from links and propagate the new latencies +down and upstream. + +# Async nodes + +When a node has the node.async property set to true, it will be considered an async +node and will be scheduled differently, see scheduling.dox. + +A link between a port of an async node and another port (async or not) is called an +async link and will have the link.async=true property. + +An async link will add 1 quantum of latency between the nodes it links. A special +exception is made for the output ports of the driver node, which do not add latency. + +The Latency param will be updated with 1 extra quantum when they travel over an async +link. + +# Examples + +## A source node with a given ProcessLatency + +When we have a source with a ProcessLatency, for example, of 1024 samples: + +``` + +----------+ + Latency: [{ "direction": "output", "min-rate": 1024, "max-rate": 1024 } ] + | FL ---+ Latency: [{ "direction": "input", "min-rate": 0, "max-rate": 0 } ] + | source + + | FR ---+ Latency: [{ "direction": "output", "min-rate": 1024, "max-rate": 1024 } ] + +----------+ + Latency: [{ "direction": "input", "min-rate": 0, "max-rate": 0 } ] + ^ + | + ProcessLatency: [ { "quantum": 0, "rate": 1024, "ns": 0 } ] +``` + +Both output ports have an output latency of 1024 samples and no input latency. + +## A sink node with a given ProcessLatency + +When we have a sink with a ProcessLatency, for example, of 512 samples: + +``` + Latency: [{ "direction": "output", "min-rate": 0, "max-rate": 0 } ] + Latency: [{ "direction": "input", "min-rate": 512, "max-rate": 512 } ] + ^ + | +----------+ + +---- FL | + | sink | <- ProcessLatency: [ { "quantum": 0, "rate": 512, "ns": 0 } ] + +---- FR | + | +----------+ + v + Latency: [{ "direction": "output", "min-rate": 0, "max-rate": 0 } ] + Latency: [{ "direction": "input", "min-rate": 512, "max-rate": 512 } ] +``` + +Both input ports have an input latency of 512 samples and no output latency. + +## A source and sink node linked together + +With the source and sink from above, if we link the FL channels, the input latency +from the input port of the sink is propagated to the output port of the source +and the output latency of the output port is propagated to the input port of the +sink: + + +``` + Latency: [{ "direction": "output", "min-rate": 1024, "max-rate": 1024 } ] + Latency: [{ "direction": "input", "min-rate": 512, "max-rate": 512 } ] + ^ + | Latency: [{ "direction": "output", "min-rate": 1024, "max-rate": 1024 } ] + | Latency: [{ "direction": "input", "min-rate": 512, "max-rate": 512 } ] + | | + +----------+v v+--------+ + | FL ------------ FL | + | source + | sink | + | FR --+ FR | + +----------+ | +--------+ + v + Latency: [{ "direction": "output", "min-rate": 1024, "max-rate": 1024 } ] + Latency: [{ "direction": "input", "min-rate": 0, "max-rate": 0 } ] +``` + + +## Insert a latency node + +If we place a node with a 256 sample latency in the above source-sink graph: + + +``` + Latency: [{ "direction": "output", "min-rate": 1024, "max-rate": 1024 } ] + Latency: [{ "direction": "input", "min-rate": 768, "max-rate": 768 } ] + ^ + | Latency: [{ "direction": "output", "min-rate": 1024, "max-rate": 1024 } ] + | Latency: [{ "direction": "input", "min-rate": 768, "max-rate": 768 } ] + | ^ + | | Latency: [{ "direction": "output", "min-rate": 1280, "max-rate": 1280 } ] + | | Latency: [{ "direction": "input", "min-rate": 512, "max-rate": 512 } ] + | | ^ + | | | + +----------+v v+--------+v +-------+ + | FL ------------ FL FL --------- FL | + | source + | node | ^ | sink | + | . | . | . | + +----------+ +--------+ | +-------+ + v + Latency: [{ "direction": "output", "min-rate": 1280, "max-rate": 1280 } ] + Latency: [{ "direction": "input", "min-rate": 512, "max-rate": 512 } ] +``` + + +See how the output latency propagates and is increased going downstream and the +input latency is increased and traveling upstream. + + +## Link a port to two port with different latencies + +When we introduce a sink2 with different input latency and link this to +the node FL output port, it will aggregate the two input latencies by +taking the min of min and max of max. + + +``` + Latency: [{ "direction": "output", "min-rate": 1024, "max-rate": 1024 } ] + Latency: [{ "direction": "input", "min-rate": 768, "max-rate": 2304 } ] + ^ + | Latency: [{ "direction": "output", "min-rate": 1024, "max-rate": 1024 } ] + | Latency: [{ "direction": "input", "min-rate": 768, "max-rate": 2304 } ] + | ^ + | | Latency: [{ "direction": "output", "min-rate": 1280, "max-rate": 1280 } ] + | | Latency: [{ "direction": "input", "min-rate": 512, "max-rate": 2048 } ] + | | ^ + | | | Latency: [{ "direction": "output", "min-rate": 1280, "max-rate": 1280 } ] + | | | Latency: [{ "direction": "input", "min-rate": 512, "max-rate": 512 } ] + | | | ^ + | | | | + +----------+v v+--------+v v +-------+ + | FL ------------ FL FL --------- FL | + | source + | node | \ | sink | + | . | . \ . | + +----------+ +--------+ \ +-------+ + \ + \ +-------+ + +--- FL | + ^ | sink2 | + | . . + | +-------+ + v + Latency: [{ "direction": "output", "min-rate": 1280, "max-rate": 1280 } ] + Latency: [{ "direction": "input", "min-rate": 2048, "max-rate": 2048 } ] +``` + +The source node now knows that its output signal will be delayed between 768 amd 2304 samples +depending on the path in the graph. + +We also see that node.FL has different min/max-rate input latencies. This information can be +used to insert a delay node to align the latencies again. For example, if we delay the signal +between node.FL and FL.sink with 1536 samples, the latencies will be aligned again. + +## An async output stream and sink node linked together + +The sink has 1 quantum of Input latency. The stream has no output latency. When the Input latency +travels over the async link 1 quantum of latency is added and the Input latency on the stream is +now 2 quanta. Similar for the stream Output latency that receives an additional 1 quantum of +latency when it arrives in the sink over the async link. + +``` + Latency: [{ "direction": "output", "min-quantum": 0, "max-quantum": 0 } ] + Latency: [{ "direction": "input", "min-quantum": 2, "max-quantum": 2 } ] + ^ + | Latency: [{ "direction": "output", "min-quantum": 1, "max-quantum": 1 } ] + | Latency: [{ "direction": "input", "min-quantum": 1, "max-quantum": 1 } ] + | | + +----------+v v+--------+ + | async FL ------------ FL | + | stream + | sink | + | FR --+ FR | + +----------+ | +--------+ + v + Latency: [{ "direction": "output", "min-quantum": 0, "max-quantum": 0 } ] + Latency: [{ "direction": "input", "min-quantum": 0, "max-quantum": 0 } ] +``` + + +*/ diff --git a/doc/dox/internals/midi.dox b/doc/dox/internals/midi.dox index 12283004d..4c86c516b 100644 --- a/doc/dox/internals/midi.dox +++ b/doc/dox/internals/midi.dox @@ -92,6 +92,9 @@ The media session will check the permissions on `/dev/snd/seq` before attempting to create this node. It will also use inotify to wait until the sequencer device node is accessible. +Currently, the session manager does not try to link control messages +automatically. + ## JACK JACK assumes all `"application/control"` ports are MIDI ports. @@ -102,11 +105,12 @@ filtering out the \ref SPA_CONTROL_Midi, \ref SPA_CONTROL_OSC and converted to control messages in a similar way. Normally, all MIDI and UMP messages are converted to MIDI1 jack events unless -the JACK port was created with an explcit "32 bits raw UMP" format, in which -case the raw UMP is passed to the JACK application directly. For output ports, +the JACK port was created with an explcit "32 bit raw UMP" format or with +the JackPortIsMIDI2 flag, in which case the raw UMP is passed to the JACK +application directly. For output ports, the JACK events are assumed to be MIDI1 and converted to UMP unless the port -has the "32 bit raw UMP" format, in which case the UMP messages are simply -passed on. +has the "32 bit raw UMP" format or the JackPortIsMIDI2 flag, in which case +the UMP messages are simply passed on. There is a 1 to 1 mapping between the JACK events and control messages so there is no information loss or need for complicated diff --git a/doc/dox/internals/protocol.dox b/doc/dox/internals/protocol.dox index dadebd749..7a62247a8 100644 --- a/doc/dox/internals/protocol.dox +++ b/doc/dox/internals/protocol.dox @@ -179,7 +179,7 @@ A client requests to bind to the registry object and list the available objects on the server. Like with all bindings, first the client allocates a new proxy id and puts this -as the new_id field. Methods and Events can then be sent and received on the +as the new_id field. Methods and Events can then be sent and received on the new_id (in the message Id field). ``` @@ -237,7 +237,7 @@ Core::Done event. Create a new object from a factory of a certain type. The client allocates a new_id for the proxy. The server will allocate a new -resource with the same new_id and from then on, Methods and Events will be +resource with the same new_id and from then on, Methods and Events will be exchanged between the new object of the given type. ``` @@ -290,7 +290,7 @@ server. String: version String: name Long: change_mask - Struct( + Struct( Int: n_items (String: key String: value)* @@ -434,7 +434,7 @@ registry. Struct( Int: id Int: global_id - Struct( + Struct( Int: n_items (String: key String: value)* @@ -498,7 +498,7 @@ Notify a client about a new global object. Int: permissions String: type Int: version - Struct( + Struct( Int: n_items (String: key String: value)* @@ -556,7 +556,7 @@ Is used to update the properties of a client. ``` Struct( - Struct( + Struct( Int: n_items (String: key String: value)* @@ -610,7 +610,7 @@ when the client info is updated later. Struct( Int: id Long: change_mask - Struct( + Struct( Int: n_items (String: key String: value)* @@ -628,7 +628,7 @@ Emitted as the reply of the GetPermissions method. ``` Struct( Int: index - Struct( + Struct( Int: n_permissions (Int: id Int: permission)* @@ -705,15 +705,15 @@ The info event is emitted when binding or when the device information changed. Struct( Int: id Long: change_mask - Struct( + Struct( Int: n_items ( String: key String: value )* ): props - Struct( + Struct( Int: n_params - ( Int: id - Int: flags )* + ( Id: id + Int: flags )* ): param_info ) ``` @@ -741,7 +741,7 @@ Emitted as a result of EnumParams or SubscribeParams. ``` - seq: the sequence number send by the client EnumParams or server generated in the SubscribeParams case. -- id: the param id that is reported, see enum spa_param_type +- id: the param id that is reported, see enum spa_param_type - index: the index of the parameter - next: the index of the next parameter - param: the parameter. The object type depends on the id @@ -768,7 +768,7 @@ Info is emitted when binding to the factory global or when the information chang String: type Int: version Long: change_mask - Struct( + Struct( Int: n_items ( String: key String: value )* @@ -808,7 +808,7 @@ Info is emitted when binding to the link global or when the information changed. Int: state String: error Pod: format - Struct( + Struct( Int: n_items ( String: key String: value )* @@ -850,7 +850,7 @@ Info is emitted when binding to the module global or when the information change String: filename String: args Long: change_mask - Struct( + Struct( Int: n_items ( String: key String: value )* @@ -926,7 +926,7 @@ Send a Command to the node. Pod: command ) ``` -- command: the command to send. See enum spa_node_command +- command: the command to send. See enum spa_node_command ## Node events @@ -944,15 +944,15 @@ The info event is emitted when binding or when the node information changed. Int: n_output_ports Id: state String: error - Struct( + Struct( Int: n_items ( String: key String: value )* ): props - Struct( + Struct( Int: n_params ( Int: id - Int: flags )* + Int: flags )* ): param_info ) ``` @@ -987,7 +987,7 @@ Emitted as a result of EnumParams or SubscribeParams. ``` - seq: the sequence number send by the client EnumParams or server generated in the SubscribeParams case. -- id: the param id that is reported, see enum spa_param_type +- id: the param id that is reported, see enum spa_param_type - index: the index of the parameter - next: the index of the next parameter - param: the parameter. The object type depends on the id @@ -1040,15 +1040,15 @@ The info event is emitted when binding or when the port information changed. Int: id Int: direction Long: change_mask - Struct( + Struct( Int: n_items ( String: key String: value )* ): props - Struct( + Struct( Int: n_params ( Int: id - Int: flags )* + Int: flags )* ): param_info ) ``` @@ -1077,7 +1077,7 @@ Emitted as a result of EnumParams or SubscribeParams. ``` - seq: the sequence number send by the client EnumParams or server generated in the SubscribeParams case. -- id: the param id that is reported, see enum spa_param_type +- id: the param id that is reported, see enum spa_param_type - index: the index of the parameter - next: the index of the next parameter @@ -1091,6 +1091,77 @@ It creates a server Node that can be controlled from a client. Processing will h in the client. It is used by pw-stream and pw-filter to implement the PipeWire media processing nodes. +To create a client-node, one must first connect to the server and then make a +ClientNode proxy and do Core::CreateObject with the client-node factory. +This will create a server side ClientNode resource that can be controlled with the +proxy. + +After the proxy is set up, the conversation between client and server goes as +follows: + +``` + client server + | | + |---------------------------------------->| + | ClientNode::Update | send initial node configuration + | | + |<----------------------------------------| + | Core::AddMem | memory for node activation + |<----------------------------------------| + | ClientNode::SetActivation | the node activation + |<----------------------------------------| + | ClientNode::Transport | the node transport + |<----------------------------------------| + | ClientNode::SetIO | clock IO + | | + |---------------------------------------->| + | ClientNode::SetActive(true) | set the node active + | | + |<----------------------------------------| + | ClientNode::SetParam | optional volume restore/settings + |<----------------------------------------| + | ClientNode::SetParam | optional PortConfig if needed + |---------------------------------------->| + | ClientNode::Update | Upload changed params + | | + |---------------------------------------->| + | ClientNode::PortUpdate | config for each port + . . + |<----------------------------------------| + | ClientNode::PortSetMixInfo | mixer inputs for each linked port + |<----------------------------------------| + | ClientNode::PortSetParam | Latency of the ports + |<----------------------------------------| + | ClientNode::SetActivation | activation of port peers + |---------------------------------------->| + | ClientNode::PortUpdate | Ack port updates + . . + |<----------------------------------------| + | ClientNode::PortSetParam | formats of the ports + |---------------------------------------->| + | ClientNode::PortUpdate | Ack port format update + . . + |<----------------------------------------| + | Core::AddMem | memory for the port buffers + |<----------------------------------------| + | ClientNode::PortUseBuffers | buffers for a port + . . + |<----------------------------------------| + | Core::AddMem | memory for driver activation + |<----------------------------------------| + | ClientNode::SetActivation | the driver activation + |<----------------------------------------| + | ClientNode::SetIO | driver Position IO + |<----------------------------------------| + | Core::AddMem | memory for port IO + |<----------------------------------------| + | ClientNode::PortSetIO | buffers/async-buffers port IO + . . + |<----------------------------------------| + | ClientNode::Command | Start command + . . +``` + ## ClientNode methods ### ClientNode::GetNode (Opcode 1) @@ -1333,7 +1404,7 @@ Add a new port to the node Struct( Int: direction Int: port_id - Struct( + Struct( Int: n_items ( String: key String: value )* @@ -1486,7 +1557,7 @@ ports of a node. Int: port_id Int: mix_id Int: peer_id - Struct( + Struct( Int: n_items ( String: key String: value )* @@ -1570,7 +1641,7 @@ The profiler has no methods Pod: object ) ``` -- object: a SPA_TYPE_OBJECT_Profiler object. See enum spa_profiler +- object: a SPA_TYPE_OBJECT_Profiler object. See enum spa_profiler # Footer {#native-protocol-footer} diff --git a/doc/dox/internals/scheduling.dox b/doc/dox/internals/scheduling.dox index 989aa0306..38b05596b 100644 --- a/doc/dox/internals/scheduling.dox +++ b/doc/dox/internals/scheduling.dox @@ -2,26 +2,26 @@ This document tries to explain how the PipeWire graph is scheduled. -Graph are constructed from linked nodes together with their ports. This +Graphs are constructed from linked nodes together with their ports. This results in a dependency graph between nodes. Special care is taken for loopback links so that the graph remains a directed graph. # Processing threads -The server (and clients) have two processing threads: +The server (and clients) has two processing threads: -- A main thread that will do all IPC with clients and server and configures the +- A main thread that will do all IPC with clients and server and configure the nodes in the graph for processing. -- A (or more) data processing thread that only does the data processing. +- One (or more) data processing threads that only do the data processing. The data processing threads are given realtime priority and are designed to run with as little overhead as possible. All of the node resources such as -buffers, io areas and metadata will be set up in shared memory before the +buffers, I/O areas and metadata will be set up in shared memory before the node is scheduled to run. This document describes the processing that happens in the data processing -thread after the main-thread has configured it. +thread after the main thread has configured it. # Nodes @@ -41,7 +41,7 @@ Each node also has: +-v---------+ activation { status:OK, // bitmask of NEED_DATA, HAVE_DATA or OK - pending:0, // number of unsatisfied dependencies to be able to run + pending:0, // number of unsatisfied dependencies needed to be able to run required:0 // number of dependencies with other nodes } ``` @@ -49,7 +49,7 @@ Each node also has: The activation record has the following information: - processing state and pending dependencies. As long as there are pending dependencies - the node can not be processed. This is the only relevant information for actually + the node cannot be processed. This is the only relevant information for actually scheduling the graph and is shown in the above illustration. - Current status of the node and profiling info (TRIGGERED, AWAKE, FINISHED, timestamps when the node changed state). @@ -152,14 +152,15 @@ will then: - Check the previous cycle. Did it complete? Mark xrun on unfinished nodes. - Perform reposition requests if any, timebase changes, etc.. - The pending counter of each follower node is set to the required field. + - Update the cycle counter in the driver activation io. - It then loops over all targets of the driver and atomically decrements the required field of the activation record. When the required field is 0, the eventfd is signaled and the node can be scheduled. -In our example above, Node A and B will have their pending state decremented. Node A +In our example above, nodes A and B will have their pending state decremented. Node A will be 0 and will be triggered first (node B has 2 pending dependencies to start with and will not be triggered yet). The driver itself also has 2 dependencies left and will not -be triggered (complete) yet. +be triggered (completed) yet. ## Scheduling node A @@ -171,12 +172,12 @@ After processing, node A goes through the list of targets and decrements each pe field (node A has a reference to B and the driver). In our above example, the driver is decremented (from 2 to 1) but is not yet triggered. -node B is decremented (from 1 to 0) and is triggered by writing to the eventfd. +Node B is decremented (from 1 to 0) and is triggered by writing to the eventfd. ## Scheduling node B Node B is scheduled and processes the input from node A. It then goes through the list of -targets and decrements the pending fields. It decrements the pending field of the +targets and decrements the pending fields. It decrements the pending field of the driver (from 1 to 0) and triggers the driver. ## Scheduling the driver @@ -184,11 +185,131 @@ driver (from 1 to 0) and triggers the driver. The graph always completes after the driver is triggered and scheduled. All required fields from all the nodes in the target list of the driver are now 0. -The driver calculates some stats about cpu time etc. +The driver calculates some stats about CPU time etc. -# Remote nodes. +# Async scheduling -For remote nodes, the eventfd and the activation is transferred from the server +When a node has the node.async property set to true, it will be considered an async +node and will be scheduled differently. + +Async nodes don't increment the pending counter of their peers and the upstream peers +also don't increment the async node pending counters. Only the driver increments the +pending counter to the async node. + +This means that the async nodes do not depend on any other node and also are not a +dependency for other nodes. This also means that the async nodes can be scheduled as +soon as the driver has started the graph. + +The completion of the async node does not influence the completion of the graph in +any way and async nodes are therefore interesting when real-time performance can not +be guaranteed, for example when the processing threads are not running with a real-time +priority. + +A link between a port of an async node and another port (async or not) is called an +async link and will have the link.async=true property. + +Because async nodes then run concurrently with other nodes, a method must be in place +to avoid concurrent access to buffer data. This is done by sending a spa_io_async_buffers +I/O to the (mixer) ports of an async link. The spa_io_async_buffers has 2 spa_io_buffer +slots. + +The driver will increment a cycle counter for each cycle that it starts. Output ports +will write to the spa_io_async_buffers (cycle+1)&1 slot and input ports will read from +(cycle&1) slots. This way the async node will always consume the output of the previous +cycle and will provide data for the next cycle. They will therefore always add 1 cycle +of latency in the graph. + +A special exception is made for the output ports of the driver node. When the driver is +started, the output port buffers are copied to the previous cycle spa_io_buffer slot. +This way, the async nodes will immediately pick up the new data from the driver source. + +Because there are 2 buffers in flight on the spa_io_async_buffers I/O area, the link needs +to negotiate at least 2 buffers for this to work. + + +## Example + +A, B, C are async nodes and have async links between their ports. The async +link has the spa_io_async_buffers with 2 slots (named 0 and 1) below. All the +slots are empty. + +``` + +--------+ +-------+ +-------+ + | A | | B | | C | + | 0 -( )-> 0 0 -( )-> 0 | + | 1 ( ) 1 1 ( ) 1 | + +--------+ +-------+ +-------+ +``` + +cycle 0: A produces a buffer AB0 on the output port in the (cycle+1)&1 slot (1). + B consumes slot cycle&1 (0) with the empty buffer and produces BC0 in slot 1 + C consumes slot cycle&1 (0) with the empty buffer + +``` + +--------+ +-------+ +-------+ + | A | | B | | C | + | (AB0) 0 -( )-> 0 ( ) 0 -( )-> 0 ( ) | + | 1 (AB0) 1 1 (BC0) 1 | + +--------+ +-------+ +-------+ +``` + +cycle 1: A produces a buffer AB1 on the output port in the (cycle+1)&1 slot (0). + B consumes slot cycle&1 (1) with buffer AB0 and produces BC1 in slot 0 + C consumes slot cycle&1 (1) with buffer BC0 + +``` + +--------+ +-------+ +-------+ + | A | | B | | C | + | (AB1) 0 -(AB1)-> 0 (AB0) 0 -(BC1)-> 0 (BC0) | + | 1 (AB0) 1 1 (BC0) 1 | + +--------+ +-------+ +-------+ +``` + +cycle 2: A produces a buffer AB2 on the output port in the (cycle+1)&1 slot (1). + B consumes slot cycle&1 (0) with buffer AB1 and produces BC2 in slot 1 + C consumes slot cycle&1 (0) with buffer BC1 + +``` + +--------+ +-------+ +-------+ + | A | | B | | C | + | (AB2) 0 -(AB1)-> 0 (AB1) 0 -(BC1)-> 0 (BC1) | + | 1 (AB2) 1 1 (BC2) 1 | + +--------+ +-------+ +-------+ +``` + +Each async link adds 1 cycle of latency to the chain. Notice how AB0 from cycle 0, +produces BC1 in cycle 1, which arrives in node C at cycle 2. + +## Latency reporting + +Because the latency is really introduced by the links, the additional cycle of +latency is added when the SPA_PARAM_Latency is copied between the output and +input ports of a link. + +It is possible for a sync node A to be linked to another sync node D and an +async node B: + +``` + +--------+ +-------+ + | A | | B | + | (AB1) 0 -(AB1)-> 0 (AB0) 0 ... + | 1 \(AB0) 1 1 + +--------+ \ +-------+ + \ + \ +-------+ + \ | D | + -(AB1)-> 0 (AB1) | + | | + +-------+ +``` + +The output latency on A's output port is what A reports. When it is copied to the +input port of B, 1 cycle is added and when it is copied to D, nothing is added. + + +# Remote nodes + +For remote nodes, the eventfd and the activation are transferred from the server to the client. This means that writing to the remote client eventfd will wake the client directly @@ -198,7 +319,7 @@ All remote clients also get the activation and eventfd of the peer and driver th are linked to and can directly trigger peers and drivers without going to the server first. -## Remote driver nodes. +## Remote driver nodes Remote drivers start the graph cycle directly without going to the server first. @@ -206,7 +327,8 @@ After they complete (and only when the profiler is active), they will trigger an extra eventfd to signal the server that the graph completed. This is used by the server to generate the profiler info. -## Lazy scheduling + +# Lazy scheduling Normally, a driver will wake up the graph and all the followers need to process the data in sync. There are cases where: @@ -228,7 +350,7 @@ When the graph is started or partially controlled by RequestProcess events and commands we say we have lazy scheduling. The driver is not always scheduling according to its own rhythm but also depending on the follower. -We can't just enable lazy scheduling when no follower will emit RequestProcess events +We cannot just enable lazy scheduling when no follower will emit RequestProcess events or when no driver will listen for RequestProcess commands. Two new node properties are defined: @@ -243,9 +365,9 @@ defined: >1 means request events as a follower are supported with increasing preference We can only enable lazy scheduling when both the driver and (at least one) follower - has the node.supports-lazy and node.supports-request property respectively. + have the node.supports-lazy and node.supports-request properties respectively. - Node can end up as a driver (is_driver()) and lazy scheduling can be enabled (is_lazy()), + Nodes can end up as a driver (is_driver()) and lazy scheduling can be enabled (is_lazy()), which results in the following cases: driver producer @@ -302,7 +424,7 @@ Some use cases: consumer - node.driver = false - -> producer selected as driver, consumer is simple follower. + -> producer selected as driver, consumer is a simple follower. lazy scheduling inactive (no lazy driver or no request follower) diff --git a/doc/dox/internals/tag.dox b/doc/dox/internals/tag.dox new file mode 100644 index 000000000..9374d10c0 --- /dev/null +++ b/doc/dox/internals/tag.dox @@ -0,0 +1,70 @@ +/** \page page_tag Tag support + +This document explains how stream specific metadata is transported in +the PipeWire graph. + +The metadata is a dictionary of string key/value pairs with information +such as the author, track, copyright, album information etc. + +# Use Cases + +## A stream/node/port has some metadata + +Applications need to be able to query the Tag of a port/node/stream. + +Linked Nodes need to be informed of the upstream and downstream tags. + +## dynamically update tags + +It needs to be possible to dynamically update the tags of a port/node/stream +and this should inform all linked ports/nodes of the updated tags. + +## Aggregate tags upstream and downstream + +A node might need to know all the upstream and downstream tags. Each node can +add or remove metadata in the Tag param. + +A mixer node might need to combine the Tags of the two input streams and +generate a combined tag. + +# Concepts + +## Port Tags + +The fundamental object for implementing metadata reporting in PipeWire is the +Tag object. + +It consists of a direction (input/output) and one or more generic dictionaries +with string key/value pairs. + +The direction of the tag object determines how the object propagates in the graph. + +- SPA_DIRECTION_OUTPUT Tag objects move from output ports downstream and contain + the metadata from all nodes upstream. An output tag received on an input port + should instruct the node to update the output tag on its output ports related + to this input port. + +- SPA_DIRECTION_INPUT Tag objects move from input ports upstream and contain + the metadata from all nodes downstream. An input tag received on an output port + should instruct the node to update the input tag on its input ports related + to this output port. + +PipeWire will automatically propagate Tag objects from ports to all linked ports +in the graph. Output Tag objects on output ports are propagated to linked input +ports and input Tag objects on input ports are propagated to linked output ports. + +If a port has links with multiple other ports, the Tag objects are merged by +appending the dictionaties to the Tag param. Intermediate nodes or sinks are allowed +to take the multiple dictionaries in a Tag and combine them into one dictionary if +they would like to do so. + +This way, Output Tag always describes the aggragated total upstream metadata of +signal up to the port and Input tag describes the aggregated downstream metadata +of the signal from the port. + +# Tag updates + +Tag params on the ports can be updated as needed. This can happen because some upstream or +downstream Tag changed or when the metadata of a node/port/stream changes. + +*/ diff --git a/doc/dox/programs/pw-cat.1.md b/doc/dox/programs/pw-cat.1.md index 4860c427f..b6e259a6f 100644 --- a/doc/dox/programs/pw-cat.1.md +++ b/doc/dox/programs/pw-cat.1.md @@ -14,6 +14,10 @@ Play and record media with PipeWire **pw-midirecord** \[*options*\] \[*FILE* \| -\] +**pw-midi2play** \[*options*\] \[*FILE* \| -\] + +**pw-midi2record** \[*options*\] \[*FILE* \| -\] + **pw-dsdplay** \[*options*\] \[*FILE* \| -\] # DESCRIPTION @@ -24,10 +28,10 @@ supported by `libsndfile` for PCM capture and playback. When capturing PCM, the filename extension is used to guess the file format with the WAV file format as the default. -It understands standard MIDI files for playback and recording. This tool -will not render MIDI files, it will simply make the MIDI events -available to the graph. You need a MIDI renderer such as qsynth, -timidity or a hardware MIDI rendered to hear the MIDI. +It understands standard MIDI files and MIDI 2.0 clip files for playback +and recording. This tool will not render MIDI files, it will simply make +the MIDI events available to the graph. You need a MIDI renderer such as +qsynth, timidity or a hardware MIDI renderer to hear the MIDI. DSD playback is supported with the DSF file format. This tool will only work with native DSD capable hardware and will produce an error when no @@ -53,13 +57,13 @@ connection is made to the default PipeWire instance. \par -p | \--playback Playback mode. Read data from the specified file, and play it back. If -the tool is called under the name **pw-play** or **pw-midiplay** this is -the default. +the tool is called under the name **pw-play**, **pw-midiplay** or +**pw-midi2play** this is the default. \par -r | \--record Recording mode. Capture data and write it to the specified file. If the -tool is called under the name **pw-record** or **pw-midirecord** this is -the default. +tool is called under the name **pw-record**, **pw-midirecord** or +**pw-midi2record** this is the default. \par -m | \--midi MIDI mode. *FILE* is a MIDI file. If the tool is called under the name @@ -69,6 +73,14 @@ simply provide the MIDI events in the graph. You need a separate MIDI renderer such as qsynth, timidity or a hardware renderer to hear the MIDI. +\par -c | \--midi-clip +MIDI 2.0 clip mode. *FILE* is a MIDI 2.0 clip file. If the tool is called +under the name **pw-midi2play** or **pw-midi2record** this is the default. +Note that this program will *not* render the MIDI events into audible +samples, it will simply provide the MIDI events in the graph. You need a +separate MIDI renderer such as qsynth, timidity or a hardware renderer to +hear the MIDI. + \par -d | \--dsd DSD mode. *FILE* is a DSF file. If the tool is called under the name **pw-dsdplay** this is the default. Note that this program will *not* diff --git a/doc/dox/programs/pw-cli.1.md b/doc/dox/programs/pw-cli.1.md index 12ee0fdd5..e5b53ecea 100644 --- a/doc/dox/programs/pw-cli.1.md +++ b/doc/dox/programs/pw-cli.1.md @@ -33,6 +33,10 @@ for many commands. \par quit | q Exit from **pw-cli** +\par list-vars +List all currently known variables and their type. Some commands create +objects that are identified with a variable. + # MODULE MANAGEMENT Modules are loaded and unloaded in the local instance, thus the pw-cli @@ -48,12 +52,12 @@ For most modules it is OK to be loaded more than once. This command returns a module variable that can be used to unload the module. -The locally module is *not* visible in the remote instance. It is not +The local module is *not* visible in the remote instance. It is not possible in PipeWire to load modules in a remote instance. \endparblock \par unload-module *module-var* -Unload a module, specified either by its variable. +Unload a module, specified by its variable. # OBJECT INTROSPECTION diff --git a/doc/dox/programs/pw-link.1.md b/doc/dox/programs/pw-link.1.md index aff53d600..44e72f303 100644 --- a/doc/dox/programs/pw-link.1.md +++ b/doc/dox/programs/pw-link.1.md @@ -4,7 +4,7 @@ The PipeWire Link Command # SYNOPSIS -**pw-link** \[*options*\] -o-l \[*out-pattern*\] \[*in-pattern*\] +**pw-link** \[*options*\] -o|-i|-l|-t \[*out-pattern*\] \[*in-pattern*\] **pw-link** \[*options*\] *output* *input* @@ -37,11 +37,14 @@ output ports and their links. List output ports \par -i | \--input -List output ports +List input ports \par -l | \--links List links +\par -t | \--latency +List port latencies + \par -m | \--monitor Monitor links and ports. **pw-link** will not exit but monitor and print new and destroyed ports or links. diff --git a/doc/dox/programs/pw-top.1.md b/doc/dox/programs/pw-top.1.md index c57c33bc4..e1a866b55 100644 --- a/doc/dox/programs/pw-top.1.md +++ b/doc/dox/programs/pw-top.1.md @@ -14,7 +14,7 @@ node and device statistics. A hierarchical view is shown of Driver nodes and follower nodes. The Driver nodes are actively using a timer to schedule dataflow in the followers. The followers of a driver node as shown below their driver -with a + sign in a tree-like representation. +with a + sign (or = for async nodes) in a tree-like representation. The columns presented are as follows: @@ -173,10 +173,20 @@ For Video formats, the layout is \ \parblock Name assigned to the device/node, as found in *pw-dump* node.name -Names are prefixed by *+* when they are linked to a driver (entry -above with no +) +Names are prefixed by *+*/*=* when they are linked to a driver (entry +above with no +/=) \endparblock +# COMMANDS + +The following keys can be used in the interactive mode: + +\par q +Quit + +\par c +Clear the ERR counters. This does *not* clear the counters globally, +it will only reset the counters in this instance of *pw-top*. # OPTIONS diff --git a/doc/dox/tutorial/index.dox b/doc/dox/tutorial/index.dox index 9f178e35d..39cb6b4fb 100644 --- a/doc/dox/tutorial/index.dox +++ b/doc/dox/tutorial/index.dox @@ -9,12 +9,12 @@ PipeWire API step-by-step with simple short examples. - \subpage page_tutorial4 - \subpage page_tutorial5 - \subpage page_tutorial6 +- \subpage page_tutorial7 # More Example Programs - \ref audio-src.c "": \snippet{doc} audio-src.c title -- \ref audio-dsp-filter.c "": \snippet{doc} audio-dsp-filter.c title - \ref video-play.c "": \snippet{doc} video-play.c title - \subpage page_examples diff --git a/doc/dox/tutorial/tutorial6.dox b/doc/dox/tutorial/tutorial6.dox index 0cee850d9..3fee6853d 100644 --- a/doc/dox/tutorial/tutorial6.dox +++ b/doc/dox/tutorial/tutorial6.dox @@ -1,6 +1,6 @@ /** \page page_tutorial6 Tutorial - Part 6: Binding Objects -\ref page_tutorial5 | \ref page_tutorial "Index" +\ref page_tutorial5 | \ref page_tutorial "Index" | \ref page_tutorial7 In this tutorial we show how to bind to an object so that we can receive events and call methods on the object. @@ -64,6 +64,6 @@ you created. Otherwise, they will be leaked: } \endcode -\ref page_tutorial5 | \ref page_tutorial "Index" +\ref page_tutorial5 | \ref page_tutorial "Index" | \ref page_tutorial7 */ diff --git a/doc/dox/tutorial/tutorial7.dox b/doc/dox/tutorial/tutorial7.dox new file mode 100644 index 000000000..50fa50443 --- /dev/null +++ b/doc/dox/tutorial/tutorial7.dox @@ -0,0 +1,242 @@ +/** \page page_tutorial7 Tutorial - Part 7: Creating an Audio DSP Filter + +\ref page_tutorial6 | \ref page_tutorial "Index" + +In this tutorial we show how to use \ref pw_filter "pw_filter" to create +a real-time audio processing filter. This is useful for implementing audio +effects, equalizers, analyzers, and other DSP applications. + +Let's take a look at the code before we break it down: + +\snippet tutorial7.c code + +Save as tutorial7.c and compile with: + + gcc -Wall tutorial7.c -o tutorial7 -lm $(pkg-config --cflags --libs libpipewire-0.3) + +## Overview + +Unlike \ref pw_stream "pw_stream" which is designed for applications that +produce or consume audio data, \ref pw_filter "pw_filter" is designed for +applications that process existing audio streams. Filters have both input +and output ports and operate in the DSP domain using 32-bit floating point +samples. + +## Setting up the Filter + +We start with the usual boilerplate and define our data structure: + +\code{.c} +struct data { + struct pw_main_loop *loop; + struct pw_filter *filter; + struct port *in_port; + struct port *out_port; +}; +\endcode + +The filter object manages both input and output ports. Each port represents +an audio channel that can be connected to other applications. + +## Creating the Filter + +\code{.c} +data.filter = pw_filter_new_simple( + pw_main_loop_get_loop(data.loop), + "audio-filter", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Filter", + PW_KEY_MEDIA_ROLE, "DSP", + NULL), + &filter_events, + &data); +\endcode + +We use `pw_filter_new_simple()` which automatically manages the core connection +for us. The properties are important: + +- `PW_KEY_MEDIA_TYPE`: "Audio" indicates this is an audio filter +- `PW_KEY_MEDIA_CATEGORY`: "Filter" tells the session manager this processes audio +- `PW_KEY_MEDIA_ROLE`: "DSP" indicates this is for audio processing + +## Adding Ports + +Next we add input and output ports: + +\code{.c} +data.in_port = pw_filter_add_port(data.filter, + PW_DIRECTION_INPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(struct port), + pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", + PW_KEY_PORT_NAME, "input", + NULL), + NULL, 0); + +data.out_port = pw_filter_add_port(data.filter, + PW_DIRECTION_OUTPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(struct port), + pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", + PW_KEY_PORT_NAME, "output", + NULL), + NULL, 0); +\endcode + +Key points about filter ports: + +- `PW_DIRECTION_INPUT` and `PW_DIRECTION_OUTPUT` specify the port direction +- `PW_FILTER_PORT_FLAG_MAP_BUFFERS` allows direct memory access to buffers +- `PW_KEY_FORMAT_DSP` indicates this uses 32-bit float DSP format +- DSP ports work with normalized floating-point samples (typically -1.0 to 1.0) + +## Setting Process Latency + +\code{.c} +params[n_params++] = spa_process_latency_build(&b, + SPA_PARAM_ProcessLatency, + &SPA_PROCESS_LATENCY_INFO_INIT( + .ns = 10 * SPA_NSEC_PER_MSEC + )); +\endcode + +This tells PipeWire that our filter adds 10 milliseconds of processing latency. +This information helps the audio system maintain proper timing and latency +compensation throughout the audio graph. + +## Connecting the Filter + +\code{.c} +if (pw_filter_connect(data.filter, + PW_FILTER_FLAG_RT_PROCESS, + params, n_params) < 0) { + fprintf(stderr, "can't connect\n"); + return -1; +} +\endcode + +The `PW_FILTER_FLAG_RT_PROCESS` flag ensures our process callback runs in the +real-time audio thread. This is crucial for low-latency audio processing but +means our process function must be real-time safe (no allocations, file I/O, +or blocking operations). + +## The Process Callback + +The heart of the filter is the process callback: + +\snippet tutorial7.c on_process + +The process function is called for each audio buffer and works as follows: + +1. Get the number of samples to process from `position->clock.duration` +2. Get input and output buffer pointers using `pw_filter_get_dsp_buffer()` +3. Process the audio data (here we just copy input to output) +4. The framework handles queueing the processed buffers + +### Key Points about DSP Processing: + +- **Float Format**: DSP buffers use 32-bit float samples, typically normalized to [-1.0, 1.0] +- **Real-time Safe**: The process function runs in the audio thread and must be real-time safe +- **Buffer Management**: `pw_filter_get_dsp_buffer()` handles the buffer lifecycle automatically +- **Sample-accurate**: Processing happens at the audio sample rate with precise timing + +## Advanced Usage + +This example shows a simple passthrough, but you can implement any audio processing: + +\code{.c} +/* Example: Simple volume control */ +for (uint32_t i = 0; i < n_samples; i++) { + out[i] = in[i] * 0.5f; // Reduce volume by half +} + +/* Example: Simple high-pass filter */ +static float last_sample = 0.0f; +float alpha = 0.99f; +for (uint32_t i = 0; i < n_samples; i++) { + out[i] = alpha * (out[i] + in[i] - last_sample); + last_sample = in[i]; +} +\endcode + +## Comparison with pw_stream + +| Feature | pw_stream | pw_filter | +|---------|-----------|-----------| +| **Use case** | Audio playback/recording | Audio processing/effects | +| **Data format** | Various (S16, S32, etc.) | 32-bit float DSP | +| **Ports** | Single direction | Input and output | +| **Buffer management** | Manual queue/dequeue | Automatic via get_dsp_buffer | +| **Typical apps** | Media players, recorders | Equalizers, effects, analyzers | + +## Connecting and Linking the Filter + +### Manual Linking Options + +Filters require manual connection by design. You can connect them using: + +#### Using pw-link command line: +\code{.sh} +# List output ports (sources) +pw-link -o + +# List input ports (sinks) +pw-link -i + +# List existing connections +pw-link -l + +# Connect a source to filter input +pw-link "source_app:output_FL" "audio-filter:input" + +# Connect filter output to sink +pw-link "audio-filter:output" "sink_app:input_FL" +\endcode + + +### Understanding Filter Auto-Connection Behavior + +**Important**: Unlike audio sources and sinks, filters are **not automatically connected** by WirePlumber. This is by design because filters are meant to be explicitly inserted into audio chains where needed. + +**Why filters don't auto-connect**: +- Filters process existing audio streams rather than generate/consume them +- Auto-connecting filters could create unwanted audio processing +- Filters typically require specific placement in the audio graph +- Manual connection gives users control over when/where effects are applied + +### Testing the Filter + +The filter requires manual connection to test. Here's the recommended workflow: + +1. **Start an audio source** (e.g., `pw-play music.wav`) +2. **Run your filter** (`./tutorial7`) +3. **Check available ports**: + ```sh + # List output ports + pw-link -o | grep -E "(pw-play|audio-filter)" + # List input ports + pw-link -i | grep -E "(audio-filter|playback)" + ``` +4. **Connect the audio chain manually**: + ```sh + # Connect source -> filter -> sink + pw-link "pw-play:output_FL" "audio-filter:input" + pw-link "audio-filter:output" "alsa_output.pci-0000_00_1f.3.analog-stereo:playback_FL" + ``` + +You should hear the audio pass through your filter. Modify the process function +to add effects like volume changes, filtering, or other audio processing. + +**Alternative: Use a patchbay tool** +- **Helvum**: `flatpak install flathub org.pipewire.Helvum` +- **qpwgraph**: Available in most Linux distributions +- **Carla**: Full-featured audio plugin host + +These tools provide graphical interfaces for connecting PipeWire nodes and are ideal for experimenting with filter placement. + +\ref page_tutorial6 | \ref page_tutorial "Index" + +*/ diff --git a/doc/doxygen-awesome.css b/doc/doxygen-awesome.css index 6000d2f56..f992c2329 100644 --- a/doc/doxygen-awesome.css +++ b/doc/doxygen-awesome.css @@ -1,101 +1,92 @@ +/* SPDX-License-Identifier: MIT */ /** Doxygen Awesome https://github.com/jothepro/doxygen-awesome-css -MIT License - -Copyright (c) 2021 jothepro - -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 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. +Copyright (c) 2021 - 2025 jothepro */ -:root { +html { /* primary theme color. This will affect the entire websites color scheme: links, arrows, labels, ... */ - --primary-color: #1982d2; - --primary-dark-color: #00559f; - --primary-light-color: #7aabd6; - --primary-lighter-color: #cae1f1; - --primary-lightest-color: #e9f1f8; + --primary-color: #1779c4; + --primary-dark-color: #335c80; + --primary-light-color: #70b1e9; + --on-primary-color: #ffffff; + + --link-color: var(--primary-color); /* page base colors */ - --page-background-color: white; - --page-foreground-color: #2c3e50; - --page-secondary-foreground-color: #67727e; + --page-background-color: #ffffff; + --page-foreground-color: #2f4153; + --page-secondary-foreground-color: #6f7e8e; /* color for all separators on the website: hr, borders, ... */ --separator-color: #dedede; /* border radius for all rounded components. Will affect many components, like dropdowns, memitems, codeblocks, ... */ - --border-radius-large: 8px; - --border-radius-small: 4px; - --border-radius-medium: 6px; + --border-radius-large: 10px; + --border-radius-small: 5px; + --border-radius-medium: 8px; - /* default spacings. Most compontest reference these values for spacing, to provide uniform spacing on the page. */ + /* default spacings. Most components reference these values for spacing, to provide uniform spacing on the page. */ --spacing-small: 5px; --spacing-medium: 10px; --spacing-large: 16px; + --spacing-xlarge: 20px; - /* default box shadow used for raising an element above the normal content. Used in dropdowns, Searchresult, ... */ - --box-shadow: 0 2px 10px 0 rgba(0,0,0,.1); + /* default box shadow used for raising an element above the normal content. Used in dropdowns, search result, ... */ + --box-shadow: 0 2px 8px 0 rgba(0,0,0,.075); - --odd-color: rgba(0,0,0,.03); + --odd-color: rgba(0,0,0,.028); /* font-families. will affect all text on the website * font-family: the normal font for text, headlines, menus * font-family-monospace: used for preformatted text in memtitle, code, fragments */ --font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif; - --font-family-monospace: source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace; + --font-family-monospace: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; /* font sizes */ --page-font-size: 15.6px; --navigation-font-size: 14.4px; - --code-font-size: 14.4px; /* affects code, fragment */ + --toc-font-size: 13.4px; + --code-font-size: 14px; /* affects code, fragment */ --title-font-size: 22px; /* content text properties. These only affect the page content, not the navigation or any other ui elements */ --content-line-height: 27px; /* The content is centered and constraint in it's width. To make the content fill the whole page, set the variable to auto.*/ - --content-maxwidth: 900px; + --content-maxwidth: 1050px; + --table-line-height: 24px; + --toc-sticky-top: var(--spacing-medium); + --toc-width: 200px; + --toc-max-height: calc(100vh - 2 * var(--spacing-medium) - 85px); /* colors for various content boxes: @warning, @note, @deprecated @bug */ - --warning-color: #fca49b; - --warning-color-dark: #b61825; - --warning-color-darker: #75070f; - --note-color: rgba(255,229,100,.3); - --note-color-dark: #c39900; - --note-color-darker: #8d7400; - --deprecated-color: rgb(214, 216, 224); + --warning-color: #faf3d8; + --warning-color-dark: #f3a600; + --warning-color-darker: #5f4204; + --note-color: #e4f3ff; + --note-color-dark: #1879C4; + --note-color-darker: #274a5c; + --todo-color: #e4dafd; + --todo-color-dark: #5b2bdd; + --todo-color-darker: #2a0d72; + --deprecated-color: #ecf0f3; --deprecated-color-dark: #5b6269; --deprecated-color-darker: #43454a; - --bug-color: rgb(246, 208, 178); - --bug-color-dark: #a53a00; - --bug-color-darker: #5b1d00; - --invariant-color: #b7f8d0; - --invariant-color-dark: #00ba44; - --invariant-color-darker: #008622; + --bug-color: #f8d1cc; + --bug-color-dark: #b61825; + --bug-color-darker: #75070f; + --invariant-color: #d8f1e3; + --invariant-color-dark: #44b86f; + --invariant-color-darker: #265532; /* blockquote colors */ - --blockquote-background: #f5f5f5; - --blockquote-foreground: #727272; + --blockquote-background: #f8f9fa; + --blockquote-foreground: #636568; /* table colors */ --tablehead-background: #f1f1f1; @@ -107,7 +98,7 @@ SOFTWARE. */ --menu-display: block; - --menu-focus-foreground: var(--page-background-color); + --menu-focus-foreground: var(--on-primary-color); --menu-focus-background: var(--primary-color); --menu-selected-background: rgba(0,0,0,.05); @@ -124,14 +115,168 @@ SOFTWARE. * on smaller screens the searchbar will always fill the entire screen width) */ --searchbar-height: 33px; --searchbar-width: 210px; + --searchbar-border-radius: var(--searchbar-height); /* code block colors */ --code-background: #f5f5f5; --code-foreground: var(--page-foreground-color); /* fragment colors */ + --fragment-background: #F8F9FA; + --fragment-foreground: #37474F; + --fragment-keyword: #bb6bb2; + --fragment-keywordtype: #8258b3; + --fragment-keywordflow: #d67c3b; + --fragment-token: #438a59; + --fragment-comment: #969696; + --fragment-link: #5383d6; + --fragment-preprocessor: #46aaa5; + --fragment-linenumber-color: #797979; + --fragment-linenumber-background: #f4f4f5; + --fragment-linenumber-border: #e3e5e7; + --fragment-lineheight: 20px; + + /* sidebar navigation (treeview) colors */ + --side-nav-background: #fbfbfb; + --side-nav-foreground: var(--page-foreground-color); + --side-nav-arrow-opacity: 0; + --side-nav-arrow-hover-opacity: 0.9; + + --toc-background: var(--side-nav-background); + --toc-foreground: var(--side-nav-foreground); + + /* height of an item in any tree / collapsible table */ + --tree-item-height: 30px; + + --memname-font-size: var(--code-font-size); + --memtitle-font-size: 18px; + + --webkit-scrollbar-size: 7px; + --webkit-scrollbar-padding: 4px; + --webkit-scrollbar-color: var(--separator-color); + + --animation-duration: .12s +} + +@media screen and (max-width: 767px) { + html { + --page-font-size: 16px; + --navigation-font-size: 16px; + --toc-font-size: 15px; + --code-font-size: 15px; /* affects code, fragment */ + --title-font-size: 22px; + } +} + +@media (prefers-color-scheme: dark) { + html:not(.light-mode) { + color-scheme: dark; + + --primary-color: #1982d2; + --primary-dark-color: #86a9c4; + --primary-light-color: #4779ac; + + --box-shadow: 0 2px 8px 0 rgba(0,0,0,.35); + + --odd-color: rgba(100,100,100,.06); + + --menu-selected-background: rgba(0,0,0,.4); + + --page-background-color: #1C1D1F; + --page-foreground-color: #d2dbde; + --page-secondary-foreground-color: #859399; + --separator-color: #38393b; + --side-nav-background: #252628; + + --code-background: #2a2c2f; + + --tablehead-background: #2a2c2f; + + --blockquote-background: #222325; + --blockquote-foreground: #7e8c92; + + --warning-color: #3b2e04; + --warning-color-dark: #f1b602; + --warning-color-darker: #ceb670; + --note-color: #163750; + --note-color-dark: #1982D2; + --note-color-darker: #dcf0fa; + --todo-color: #2a2536; + --todo-color-dark: #7661b3; + --todo-color-darker: #ae9ed6; + --deprecated-color: #2e323b; + --deprecated-color-dark: #738396; + --deprecated-color-darker: #abb0bd; + --bug-color: #2e1917; + --bug-color-dark: #ad2617; + --bug-color-darker: #f5b1aa; + --invariant-color: #303a35; + --invariant-color-dark: #76ce96; + --invariant-color-darker: #cceed5; + + --fragment-background: #282c34; + --fragment-foreground: #dbe4eb; + --fragment-keyword: #cc99cd; + --fragment-keywordtype: #ab99cd; + --fragment-keywordflow: #e08000; + --fragment-token: #7ec699; + --fragment-comment: #999999; + --fragment-link: #98c0e3; + --fragment-preprocessor: #65cabe; + --fragment-linenumber-color: #cccccc; + --fragment-linenumber-background: #35393c; + --fragment-linenumber-border: #1f1f1f; + } +} + +/* dark mode variables are defined twice, to support both the dark-mode without and with doxygen-awesome-darkmode-toggle.js */ +html.dark-mode { + color-scheme: dark; + + --primary-color: #1982d2; + --primary-dark-color: #86a9c4; + --primary-light-color: #4779ac; + + --box-shadow: 0 2px 8px 0 rgba(0,0,0,.30); + + --odd-color: rgba(100,100,100,.06); + + --menu-selected-background: rgba(0,0,0,.4); + + --page-background-color: #1C1D1F; + --page-foreground-color: #d2dbde; + --page-secondary-foreground-color: #859399; + --separator-color: #38393b; + --side-nav-background: #252628; + + --code-background: #2a2c2f; + + --tablehead-background: #2a2c2f; + + --blockquote-background: #222325; + --blockquote-foreground: #7e8c92; + + --warning-color: #3b2e04; + --warning-color-dark: #f1b602; + --warning-color-darker: #ceb670; + --note-color: #163750; + --note-color-dark: #1982D2; + --note-color-darker: #dcf0fa; + --todo-color: #2a2536; + --todo-color-dark: #7661b3; + --todo-color-darker: #ae9ed6; + --deprecated-color: #2e323b; + --deprecated-color-dark: #738396; + --deprecated-color-darker: #abb0bd; + --bug-color: #2e1917; + --bug-color-dark: #ad2617; + --bug-color-darker: #f5b1aa; + --invariant-color: #303a35; + --invariant-color-dark: #76ce96; + --invariant-color-darker: #cceed5; + --fragment-background: #282c34; - --fragment-foreground: #ffffff; + --fragment-foreground: #dbe4eb; --fragment-keyword: #cc99cd; --fragment-keywordtype: #ab99cd; --fragment-keywordflow: #e08000; @@ -142,66 +287,6 @@ SOFTWARE. --fragment-linenumber-color: #cccccc; --fragment-linenumber-background: #35393c; --fragment-linenumber-border: #1f1f1f; - --fragment-lineheight: 20px; - - /* sidebar navigation (treeview) colors */ - --side-nav-background: #fbfbfb; - --side-nav-foreground: var(--page-foreground-color); - --side-nav-arrow-color: var(--page-background-color); - - /* height of an item in any tree / collapsible table */ - --tree-item-height: 30px; -} - -@media screen and (max-width: 767px) { - :root { - --page-font-size: 16px; - --navigation-font-size: 16px; - --code-font-size: 15px; /* affects code, fragment */ - --title-font-size: 22px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --primary-color: #00559f; - --primary-dark-color: #1982d2; - --primary-light-color: #4779ac; - --primary-lighter-color: #191e21; - --primary-lightest-color: #191a1c; - - --box-shadow: 0 2px 10px 0 rgba(0,0,0,.35); - - --odd-color: rgba(0,0,0,.1); - - --menu-selected-background: rgba(0,0,0,.4); - - --page-background-color: #1C1D1F; - --page-foreground-color: #d2dbde; - --page-secondary-foreground-color: #859399; - --separator-color: #000000; - --side-nav-background: #252628; - - --code-background: #2a2c2f; - - --tablehead-background: #2a2c2f; - - --blockquote-background: #1f2022; - --blockquote-foreground: #77848a; - - --warning-color: #b61825; - --warning-color-dark: #510a02; - --warning-color-darker: #f5b1aa; - --note-color: rgb(255, 183, 0); - --note-color-dark: #9f7300; - --note-color-darker: #fff6df; - --deprecated-color: rgb(88, 90, 96); - --deprecated-color-dark: #262e37; - --deprecated-color-darker: #a0a5b0; - --bug-color: rgb(248, 113, 0); - --bug-color-dark: #812a00; - --bug-color-darker: #ffd3be; - } } body { @@ -210,22 +295,41 @@ body { font-size: var(--page-font-size); } -body, table, div, p, dl, #nav-tree .label, .title, .sm-dox a, .sm-dox a:hover, .sm-dox a:focus, #projectname, .SelectItem, #MSearchField, .navpath li.navelem a, .navpath li.navelem a:hover { +body, table, div, p, dl, #nav-tree .label, #nav-tree a, .title, +.sm-dox a, .sm-dox a:hover, .sm-dox a:focus, #projectname, +.SelectItem, #MSearchField, .navpath li.navelem a, +.navpath li.navelem a:hover, p.reference, p.definition, div.toc li, div.toc h3, +#page-nav ul.page-outline li a { font-family: var(--font-family); } h1, h2, h3, h4, h5 { - margin-top: .9em; + margin-top: 1em; font-weight: 600; line-height: initial; } -p, div, table, dl { +p, div, table, dl, p.reference, p.definition { font-size: var(--page-font-size); } -a, a.el:visited, a.el:hover, a.el:focus, a.el:active { - color: var(--primary-dark-color); +p.reference, p.definition { + color: var(--page-secondary-foreground-color); +} + +a:link, a:visited, a:hover, a:focus, a:active { + color: var(--link-color) !important; + font-weight: 500; + background: none; +} + +a:hover { + text-decoration: underline; +} + +a.anchor { + scroll-margin-top: var(--spacing-large); + display: block; } /* @@ -235,6 +339,8 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { #top { background: var(--header-background); border-bottom: 1px solid var(--separator-color); + position: relative; + z-index: 99; } @media screen and (min-width: 768px) { @@ -249,6 +355,7 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { #main-nav { flex-grow: 5; padding: var(--spacing-small) var(--spacing-medium); + border-bottom: 0; } #titlearea { @@ -303,10 +410,23 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { margin-bottom: -1px; } +.main-menu-btn-icon, .main-menu-btn-icon:before, .main-menu-btn-icon:after { + background: var(--page-secondary-foreground-color); +} + @media screen and (max-width: 767px) { .sm-dox a span.sub-arrow { background: var(--code-background); } + + #main-menu a.has-submenu span.sub-arrow { + color: var(--page-secondary-foreground-color); + border-radius: var(--border-radius-medium); + } + + #main-menu a.has-submenu:hover span.sub-arrow { + color: var(--page-foreground-color); + } } @media screen and (min-width: 768px) { @@ -315,19 +435,36 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { } .sm-dox a span.sub-arrow { - border-color: var(--header-foreground) transparent transparent transparent; + top: 15px; + right: 10px; + box-sizing: content-box; + padding: 0; + margin: 0; + display: inline-block; + width: 5px; + height: 5px; + transform: rotate(45deg); + border-width: 0; + border-right: 2px solid var(--header-foreground); + border-bottom: 2px solid var(--header-foreground); + background: none; } .sm-dox a:hover span.sub-arrow { - border-color: var(--menu-focus-foreground) transparent transparent transparent; + border-color: var(--menu-focus-foreground); + background: none; } .sm-dox ul a span.sub-arrow { - border-color: transparent transparent transparent var(--header-foreground); + transform: rotate(-45deg); + border-width: 0; + border-right: 2px solid var(--header-foreground); + border-bottom: 2px solid var(--header-foreground); } .sm-dox ul a:hover span.sub-arrow { - border-color: transparent transparent transparent var(--menu-focus-foreground); + border-color: var(--menu-focus-foreground); + background: none; } } @@ -353,8 +490,8 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { } .sm-dox ul a { - color: var(--page-foreground-color); - background: var(--page-background-color); + color: var(--page-foreground-color) !important; + background: none; font-size: var(--navigation-font-size); } @@ -367,8 +504,8 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { } .sm-dox ul a:hover, .sm-dox ul a:active, .sm-dox ul a:focus { - font-size: var(--navigation-font-size); - color: var(--menu-focus-foreground); + font-size: var(--navigation-font-size) !important; + color: var(--menu-focus-foreground) !important; text-shadow: none; background-color: var(--menu-focus-background); border-radius: var(--border-radius-small) !important; @@ -378,9 +515,10 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { text-shadow: none; background: transparent; background-image: none !important; - color: var(--header-foreground); + color: var(--header-foreground) !important; font-weight: normal; font-size: var(--navigation-font-size); + border-radius: var(--border-radius-small) !important; } .sm-dox a:focus { @@ -391,7 +529,7 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { text-shadow: none; font-weight: normal; background: var(--menu-focus-background); - color: var(--menu-focus-foreground); + color: var(--menu-focus-foreground) !important; border-radius: var(--border-radius-small) !important; font-size: var(--navigation-font-size); } @@ -417,7 +555,7 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { #MSearchBox { height: var(--searchbar-height); background: var(--searchbar-background); - border-radius: var(--searchbar-height); + border-radius: var(--searchbar-border-radius); border: 1px solid var(--separator-color); overflow: hidden; width: var(--searchbar-width); @@ -425,10 +563,47 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { box-shadow: none; display: block; margin-top: 0; + margin-right: 0; } -.left #MSearchSelect { +@media (min-width: 768px) { + .sm-dox li { + padding: 0; + } +} + +/* until Doxygen 1.9.4 */ +.left img#MSearchSelect { left: 0; + user-select: none; + padding-left: 8px; +} + +/* Doxygen 1.9.5 */ +.left span#MSearchSelect { + left: 0; + user-select: none; + margin-left: 8px; + padding: 0; +} + +.left #MSearchSelect[src$=".png"] { + padding-left: 0 +} + +/* Doxygen 1.14.0 */ +.search-icon::before { + background: none; + top: 5px; +} + +.search-icon::after { + background: none; + top: 12px; +} + +.SelectionMark { + user-select: none; } .tabs .left #MSearchSelect { @@ -483,16 +658,15 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { top: calc(calc(var(--searchbar-height) / 2) - 11px); } -.left #MSearchSelect { - padding-left: 8px; -} - #MSearchBox span.left, #MSearchBox span.right { background: none; + background-image: none; } #MSearchBox span.right { padding-top: calc(calc(var(--searchbar-height) / 2) - 12px); + position: absolute; + right: var(--spacing-small); } .tabs #MSearchBox span.right { @@ -523,15 +697,39 @@ a, a.el:visited, a.el:hover, a.el:focus, a.el:active { } iframe#MSearchResults { - background: var(--page-background-color); margin: 4px; } +iframe { + color-scheme: normal; +} + +@media (prefers-color-scheme: dark) { + html:not(.light-mode) iframe#MSearchResults { + filter: invert() hue-rotate(180deg); + } +} + +html.dark-mode iframe#MSearchResults { + filter: invert() hue-rotate(180deg); +} + +#MSearchResults .SRPage { + background-color: transparent; +} + +#MSearchResults .SRPage .SREntry { + font-size: 10pt; + padding: var(--spacing-small) var(--spacing-medium); +} + #MSearchSelectWindow { border: 1px solid var(--separator-color); border-radius: var(--border-radius-medium); box-shadow: var(--box-shadow); background: var(--page-background-color); + padding-top: var(--spacing-small); + padding-bottom: var(--spacing-small); } #MSearchSelectWindow a.SelectItem { @@ -539,12 +737,13 @@ iframe#MSearchResults { line-height: var(--content-line-height); margin: 0 var(--spacing-small); border-radius: var(--border-radius-small); - color: var(--page-foreground-color); + color: var(--page-foreground-color) !important; + font-weight: normal; } #MSearchSelectWindow a.SelectItem:hover { background: var(--menu-focus-background); - color: var(--menu-focus-foreground); + color: var(--menu-focus-foreground) !important; } @media screen and (max-width: 767px) { @@ -559,7 +758,7 @@ iframe#MSearchResults { } #MSearchField { - width: calc(100vw - 95px); + width: calc(100vw - 110px); } @keyframes slideInSearchResultsMobile { @@ -580,6 +779,24 @@ iframe#MSearchResults { overflow: auto; transform: translate(0, 20px); animation: ease-out 280ms slideInSearchResultsMobile; + width: auto !important; + } + + /* + * Overwrites for fixing the searchbox on mobile in doxygen 1.9.2 + */ + label.main-menu-btn ~ #searchBoxPos1 { + top: 3px !important; + right: 6px !important; + left: 45px; + display: flex; + } + + label.main-menu-btn ~ #searchBoxPos1 > #MSearchBox { + margin-top: 0; + margin-bottom: 0; + flex-grow: 2; + float: left; } } @@ -588,8 +805,13 @@ iframe#MSearchResults { */ #side-nav { - padding: 0 !important; - background: var(--side-nav-background); + min-width: 8px; + max-width: 50vw; +} + + +#nav-tree, #top { + border-right: 1px solid var(--separator-color); } @media screen and (max-width: 767px) { @@ -599,67 +821,168 @@ iframe#MSearchResults { #doc-content { margin-left: 0 !important; - height: auto !important; - padding-bottom: calc(2 * var(--spacing-large)); + } + + #top { + border-right: none; } } #nav-tree { - background: transparent; + background: var(--side-nav-background); + margin-right: -1px; + padding: 0; } #nav-tree .label { font-size: var(--navigation-font-size); + line-height: var(--tree-item-height); +} + +#nav-tree span.label a:hover { + background: none; } #nav-tree .item { height: var(--tree-item-height); line-height: var(--tree-item-height); + overflow: hidden; + text-overflow: ellipsis; + margin: 0; + padding: 0; +} + +#nav-tree-contents { + margin: 0; +} + +#main-menu > li:last-child { + height: auto; +} + +#nav-tree .item > a:focus { + outline: none; } #nav-sync { - top: 12px !important; - right: 12px; + bottom: var(--spacing-medium); + right: var(--spacing-medium) !important; + top: auto !important; + user-select: none; +} + +div.nav-sync-icon { + border: 1px solid var(--separator-color); + border-radius: var(--border-radius-medium); + background: var(--page-background-color); + width: 30px; + height: 20px; +} + +div.nav-sync-icon:hover { + background: var(--page-background-color); +} + +span.sync-icon-left, div.nav-sync-icon:hover span.sync-icon-left { + border-left: 2px solid var(--primary-color); + border-top: 2px solid var(--primary-color); + top: 5px; + left: 6px; +} +span.sync-icon-right, div.nav-sync-icon:hover span.sync-icon-right { + border-right: 2px solid var(--primary-color); + border-bottom: 2px solid var(--primary-color); + top: 5px; + left: initial; + right: 6px; +} + +div.nav-sync-icon.active::after, div.nav-sync-icon.active:hover::after { + border-top: 2px solid var(--primary-color); + top: 9px; + left: 6px; + width: 19px; } #nav-tree .selected { text-shadow: none; background-image: none; background-color: transparent; - box-shadow: inset 4px 0 0 0 var(--primary-dark-color); + position: relative; + color: var(--primary-color) !important; + font-weight: 500; } +#nav-tree .selected::after { + content: ""; + position: absolute; + top: 1px; + bottom: 1px; + left: 0; + width: 4px; + border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0; + background: var(--primary-color); +} + + #nav-tree a { - color: var(--side-nav-foreground); + color: var(--side-nav-foreground) !important; + font-weight: normal; } #nav-tree a:focus { outline-style: auto; } -.arrow { - color: var(--primary-light-color); - font-family: serif; - height: auto; - text-align: right; +#nav-tree .arrow { + opacity: var(--side-nav-arrow-opacity); + background: none; } -#nav-tree .arrow { +#nav-tree span.arrowhead { + margin: 0 0 1px 2px; +} + +span.arrowhead { + border-color: var(--primary-light-color); +} + +.selected span.arrowhead { + border-color: var(--primary-color); +} + +#nav-tree-contents > ul > li:first-child > div > a { opacity: 0; + pointer-events: none; +} + +.contents .arrow { + color: inherit; + cursor: pointer; + font-size: 45%; + vertical-align: middle; + margin-right: 2px; + font-family: serif; + height: auto; + padding-bottom: 4px; } #nav-tree div.item:hover .arrow, #nav-tree a:focus .arrow { - opacity: 1; + opacity: var(--side-nav-arrow-hover-opacity); } #nav-tree .selected a { - color: var(--primary-dark-color); + color: var(--primary-color) !important; font-weight: bolder; + font-weight: 600; } .ui-resizable-e { + background: none; +} + +.ui-resizable-e:hover { background: var(--separator-color); - width: 1px; } /* @@ -668,10 +991,25 @@ iframe#MSearchResults { div.header { border-bottom: 1px solid var(--separator-color); - background-color: var(--page-background-color); + background: none; background-image: none; } +@media screen and (min-width: 1000px) { + #doc-content > div > div.contents, + .PageDoc > div.contents { + display: flex; + flex-direction: row-reverse; + flex-wrap: nowrap; + align-items: flex-start; + } + + div.contents .textblock { + min-width: 200px; + flex-grow: 1; + } +} + div.contents, div.header .title, div.header .summary { max-width: var(--content-maxwidth); } @@ -691,8 +1029,8 @@ div.headertitle { div.header .title { font-weight: 600; - font-size: 210%; - padding: var(--spacing-medium) var(--spacing-large); + font-size: 225%; + padding: var(--spacing-medium) var(--spacing-xlarge); word-break: break-word; } @@ -707,19 +1045,12 @@ td.memSeparator { border-color: var(--separator-color); } -.mdescLeft, .mdescRight, .memItemLeft, .memItemRight, .memTemplItemLeft, .memTemplItemRight, .memTemplParams { - background: var(--code-background); -} - -.mdescRight { - color: var(--page-secondary-foreground-color); -} - span.mlabel { background: var(--primary-color); + color: var(--on-primary-color); border: none; padding: 4px 9px; - border-radius: 12px; + border-radius: var(--border-radius-large); margin-right: var(--spacing-medium); } @@ -728,7 +1059,7 @@ span.mlabel:last-of-type { } div.contents { - padding: 0 var(--spacing-large); + padding: 0 var(--spacing-xlarge); } div.contents p, div.contents li { @@ -739,88 +1070,345 @@ div.contents div.dyncontent { margin: var(--spacing-medium) 0; } -@media (prefers-color-scheme: dark) { - div.contents div.dyncontent img { - filter: hue-rotate(180deg) invert(); +@media screen and (max-width: 767px) { + div.contents { + padding: 0 var(--spacing-large); + } + + div.header .title { + padding: var(--spacing-medium) var(--spacing-large); } } -h2.groupheader { - border-bottom: 1px solid var(--separator-color); +@media (prefers-color-scheme: dark) { + html:not(.light-mode) div.contents div.dyncontent img, + html:not(.light-mode) div.contents center img, + html:not(.light-mode) div.contents > table img, + html:not(.light-mode) div.contents div.dyncontent iframe, + html:not(.light-mode) div.contents center iframe, + html:not(.light-mode) div.contents table iframe, + html:not(.light-mode) div.contents .dotgraph iframe { + filter: brightness(89%) hue-rotate(180deg) invert(); + } +} + +html.dark-mode div.contents div.dyncontent img, +html.dark-mode div.contents center img, +html.dark-mode div.contents > table img, +html.dark-mode div.contents div.dyncontent iframe, +html.dark-mode div.contents center iframe, +html.dark-mode div.contents table iframe, +html.dark-mode div.contents .dotgraph iframe + { + filter: brightness(89%) hue-rotate(180deg) invert(); +} + +td h2.groupheader, h2.groupheader { + border-bottom: 0px; color: var(--page-foreground-color); + box-shadow: + 100px 0 var(--page-background-color), + -100px 0 var(--page-background-color), + 100px 0.75px var(--separator-color), + -100px 0.75px var(--separator-color), + 500px 0 var(--page-background-color), + -500px 0 var(--page-background-color), + 500px 0.75px var(--separator-color), + -500px 0.75px var(--separator-color), + 900px 0 var(--page-background-color), + -900px 0 var(--page-background-color), + 900px 0.75px var(--separator-color), + -900px 0.75px var(--separator-color), + 1400px 0 var(--page-background-color), + -1400px 0 var(--page-background-color), + 1400px 0.75px var(--separator-color), + -1400px 0.75px var(--separator-color), + 1900px 0 var(--page-background-color), + -1900px 0 var(--page-background-color), + 1900px 0.75px var(--separator-color), + -1900px 0.75px var(--separator-color); } blockquote { - padding: var(--spacing-small) var(--spacing-medium); + margin: 0 var(--spacing-medium) 0 var(--spacing-medium); + padding: var(--spacing-small) var(--spacing-large); background: var(--blockquote-background); color: var(--blockquote-foreground); - border-left: 2px solid var(--blockquote-foreground); - margin: 0; + border-left: 0; + overflow: visible; + border-radius: var(--border-radius-medium); + overflow: visible; + position: relative; +} + +blockquote::before, blockquote::after { + font-weight: bold; + font-family: serif; + font-size: 360%; + opacity: .15; + position: absolute; +} + +blockquote::before { + content: "“"; + left: -10px; + top: 4px; +} + +blockquote::after { + content: "”"; + right: -8px; + bottom: -25px; } blockquote p { margin: var(--spacing-small) 0 var(--spacing-medium) 0; } -.paramname { +.paramname, .paramname em { + font-weight: 600; color: var(--primary-dark-color); } -.glow { - text-shadow: 0 0 15px var(--primary-light-color) !important; +.paramname > code { + border: 0; +} + +table.params .paramname { + font-weight: 600; + font-family: var(--font-family-monospace); + font-size: var(--code-font-size); + padding-right: var(--spacing-small); + line-height: var(--table-line-height); +} + +h1.glow, h2.glow, h3.glow, h4.glow, h5.glow, h6.glow { + text-shadow: 0 0 15px var(--primary-light-color); } .alphachar a { color: var(--page-foreground-color); } +.dotgraph { + max-width: 100%; + overflow-x: scroll; +} + +.dotgraph .caption { + position: sticky; + left: 0; +} + +/* Wrap Graphviz graphs with the `interactive_dotgraph` class if `INTERACTIVE_SVG = YES` */ +.interactive_dotgraph .dotgraph iframe { + max-width: 100%; +} + /* Table of Contents */ -div.toc { - background-color: var(--side-nav-background); - border: 1px solid var(--separator-color); - border-radius: var(--border-radius-medium); - box-shadow: var(--box-shadow); +div.contents .toc { + max-height: var(--toc-max-height); + min-width: var(--toc-width); + border: 0; + border-left: 1px solid var(--separator-color); + border-radius: 0; + background-color: var(--page-background-color); + box-shadow: none; + position: sticky; + top: var(--toc-sticky-top); padding: 0 var(--spacing-large); - margin: 0 0 var(--spacing-medium) var(--spacing-medium); + margin: var(--spacing-small) 0 var(--spacing-large) var(--spacing-large); } div.toc h3 { - color: var(--side-nav-foreground); + color: var(--toc-foreground); font-size: var(--navigation-font-size); - margin: var(--spacing-large) 0; + margin: var(--spacing-large) 0 var(--spacing-medium) 0; } div.toc li { - font-size: var(--navigation-font-size); padding: 0; background: none; + line-height: var(--toc-font-size); + margin: var(--toc-font-size) 0 0 0; } -div.toc li:before { - content: '↓'; - font-weight: 800; - font-family: var(--font-family); - margin-right: var(--spacing-small); - color: var(--side-nav-foreground); - opacity: .4; +div.toc li::before { + display: none; } -div.toc ul li.level1 { - margin: 0; +div.toc ul { + margin-top: 0 } -div.toc ul li.level2, div.toc ul li.level3 { - margin-top: 0; +div.toc li a { + font-size: var(--toc-font-size); + color: var(--page-foreground-color) !important; + text-decoration: none; +} + +div.toc li a:hover, div.toc li a.active { + color: var(--primary-color) !important; +} + +div.toc li a.aboveActive { + color: var(--page-secondary-foreground-color) !important; } -@media screen and (max-width: 767px) { - div.toc { +@media screen and (max-width: 999px) { + div.contents .toc { + max-height: 45vh; float: none; width: auto; margin: 0 0 var(--spacing-medium) 0; + position: relative; + top: 0; + position: relative; + border: 1px solid var(--separator-color); + border-radius: var(--border-radius-medium); + background-color: var(--toc-background); + box-shadow: var(--box-shadow); + } + + div.contents .toc.interactive { + max-height: calc(var(--navigation-font-size) + 2 * var(--spacing-large)); + overflow: hidden; + } + + div.contents .toc > h3 { + -webkit-tap-highlight-color: transparent; + cursor: pointer; + position: sticky; + top: 0; + background-color: var(--toc-background); + margin: 0; + padding: var(--spacing-large) 0; + display: block; + } + + div.contents .toc.interactive > h3::before { + content: ""; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid var(--primary-color); + display: inline-block; + margin-right: var(--spacing-small); + margin-bottom: calc(var(--navigation-font-size) / 4); + transform: rotate(-90deg); + transition: transform var(--animation-duration) ease-out; + } + + div.contents .toc.interactive.open > h3::before { + transform: rotate(0deg); + } + + div.contents .toc.interactive.open { + max-height: 45vh; + overflow: auto; + transition: max-height 0.2s ease-in-out; + } + + div.contents .toc a, div.contents .toc a.active { + color: var(--primary-color) !important; + } + + div.contents .toc a:hover { + text-decoration: underline; + } +} + +/* + Page Outline (Doxygen >= 1.14.0) +*/ + +#page-nav { + background: var(--page-background-color); + border-left: 1px solid var(--separator-color); +} + +#page-nav #page-nav-resize-handle { + background: var(--separator-color); +} + +#page-nav #page-nav-resize-handle::after { + border-left: 1px solid var(--primary-color); + border-right: 1px solid var(--primary-color); +} + +#page-nav #page-nav-tree #page-nav-contents { + top: var(--spacing-large); +} + +#page-nav ul.page-outline { + margin: 0; + padding: 0; +} + +#page-nav ul.page-outline li a { + font-size: var(--toc-font-size) !important; + color: var(--page-secondary-foreground-color) !important; + display: inline-block; + line-height: calc(2 * var(--toc-font-size)); +} + +#page-nav ul.page-outline li a a.anchorlink { + display: none; +} + +#page-nav ul.page-outline li.vis ~ * a { + color: var(--page-foreground-color) !important; +} + +#page-nav ul.page-outline li.vis:not(.vis ~ .vis) a, #page-nav ul.page-outline li a:hover { + color: var(--primary-color) !important; +} + +#page-nav ul.page-outline .vis { + background: var(--page-background-color); + position: relative; +} + +#page-nav ul.page-outline .vis::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 4px; + background: var(--page-secondary-foreground-color); +} + +#page-nav ul.page-outline .vis:not(.vis ~ .vis)::after { + top: 1px; + border-top-right-radius: var(--border-radius-small); +} + +#page-nav ul.page-outline .vis:not(:has(~ .vis))::after { + bottom: 1px; + border-bottom-right-radius: var(--border-radius-small); +} + + +#page-nav ul.page-outline .arrow { + display: inline-block; +} + +#page-nav ul.page-outline .arrow span { + display: none; +} + +@media screen and (max-width: 767px) { + #container { + grid-template-columns: initial !important; + } + + #page-nav { + display: none; } } @@ -828,23 +1416,23 @@ div.toc ul li.level2, div.toc ul li.level3 { Code & Fragments */ -code, div.fragment, pre.fragment { - border-radius: var(--border-radius-small); - border: none; +code, div.fragment, pre.fragment, span.tt { + border: 1px solid var(--separator-color); overflow: hidden; } -code { +code, span.tt { display: inline; background: var(--code-background); color: var(--code-foreground); padding: 2px 6px; - word-break: break-word; + border-radius: var(--border-radius-small); } div.fragment, pre.fragment { + border-radius: var(--border-radius-medium); margin: var(--spacing-medium) 0; - padding: 14px 16px; + padding: calc(var(--spacing-large) - (var(--spacing-large) / 6)) var(--spacing-large); background: var(--fragment-background); color: var(--fragment-foreground); overflow-x: auto; @@ -854,28 +1442,53 @@ div.fragment, pre.fragment { div.fragment, pre.fragment { border-top-right-radius: 0; border-bottom-right-radius: 0; + border-right: 0; } - .contents > div.fragment, .textblock > div.fragment, .textblock > pre.fragment { + .contents > div.fragment, + .textblock > div.fragment, + .textblock > pre.fragment, + .textblock > .tabbed > ul > li > div.fragment, + .textblock > .tabbed > ul > li > pre.fragment, + .contents > .doxygen-awesome-fragment-wrapper > div.fragment, + .textblock > .doxygen-awesome-fragment-wrapper > div.fragment, + .textblock > .doxygen-awesome-fragment-wrapper > pre.fragment, + .textblock > .tabbed > ul > li > .doxygen-awesome-fragment-wrapper > div.fragment, + .textblock > .tabbed > ul > li > .doxygen-awesome-fragment-wrapper > pre.fragment { margin: var(--spacing-medium) calc(0px - var(--spacing-large)); border-radius: 0; + border-left: 0; } - .textblock li > .fragment { + .textblock li > .fragment, + .textblock li > .doxygen-awesome-fragment-wrapper > .fragment { margin: var(--spacing-medium) calc(0px - var(--spacing-large)); } - .memdoc li > .fragment { + .memdoc li > .fragment, + .memdoc li > .doxygen-awesome-fragment-wrapper > .fragment { margin: var(--spacing-medium) calc(0px - var(--spacing-medium)); } - .memdoc > div.fragment, .memdoc > pre.fragment, dl dd > div.fragment, dl dd pre.fragment { + .textblock ul, .memdoc ul { + overflow: initial; + } + + .memdoc > div.fragment, + .memdoc > pre.fragment, + dl dd > div.fragment, + dl dd pre.fragment, + .memdoc > .doxygen-awesome-fragment-wrapper > div.fragment, + .memdoc > .doxygen-awesome-fragment-wrapper > pre.fragment, + dl dd > .doxygen-awesome-fragment-wrapper > div.fragment, + dl dd .doxygen-awesome-fragment-wrapper > pre.fragment { margin: var(--spacing-medium) calc(0px - var(--spacing-medium)); border-radius: 0; + border-left: 0; } } -code, code a, pre.fragment, div.fragment, div.fragment .line, div.fragment span, div.fragment .line a, div.fragment .line span { +code, code a, pre.fragment, div.fragment, div.fragment .line, div.fragment span, div.fragment .line a, div.fragment .line span, span.tt { font-family: var(--font-family-monospace); font-size: var(--code-font-size) !important; } @@ -911,7 +1524,7 @@ div.fragment span.comment { } div.fragment a.code { - color: var(--fragment-link); + color: var(--fragment-link) !important; } div.fragment span.preprocessor { @@ -928,18 +1541,36 @@ div.fragment span.lineno { div.fragment span.lineno a { background: none; - color: var(--fragment-link); + color: var(--fragment-link) !important; } -div.fragment .line:first-child .lineno { +div.fragment > .line:first-child .lineno { box-shadow: -999999px 0px 0 999999px var(--fragment-linenumber-background), -999998px 0px 0 999999px var(--fragment-linenumber-border); + background-color: var(--fragment-linenumber-background) !important; +} + +div.line { + border-radius: var(--border-radius-small); +} + +div.line.glow { + background-color: var(--primary-light-color); + box-shadow: none; } /* dl warning, attention, note, deprecated, bug, ... */ -dl.warning, dl.attention, dl.note, dl.deprecated, dl.bug, dl.invariant, dl.pre { +dl { + line-height: calc(1.65 * var(--page-font-size)); +} + +dl.bug dt a, dl.deprecated dt a, dl.todo dt a { + font-weight: bold !important; +} + +dl.warning, dl.attention, dl.note, dl.deprecated, dl.bug, dl.invariant, dl.pre, dl.post, dl.todo, dl.remark { padding: var(--spacing-medium); margin: var(--spacing-medium) 0; color: var(--page-background-color); @@ -962,16 +1593,30 @@ dl.warning dt, dl.attention dt { color: var(--warning-color-dark); } -dl.note { +dl.note, dl.remark { background: var(--note-color); border-left: 8px solid var(--note-color-dark); color: var(--note-color-darker); } -dl.note dt { +dl.note dt, dl.remark dt { color: var(--note-color-dark); } +dl.todo { + background: var(--todo-color); + border-left: 8px solid var(--todo-color-dark); + color: var(--todo-color-darker); +} + +dl.todo dt a { + color: var(--todo-color-dark) !important; +} + +dl.bug dt a { + color: var(--todo-color-dark) !important; +} + dl.bug { background: var(--bug-color); border-left: 8px solid var(--bug-color-dark); @@ -992,16 +1637,20 @@ dl.deprecated dt a { color: var(--deprecated-color-dark) !important; } -dl.section dd, dl.bug dd, dl.deprecated dd { +dl.section dd, dl.bug dd, dl.deprecated dd, dl.todo dd { margin-inline-start: 0px; } -dl.invariant, dl.pre { +dl.invariant, dl.pre, dl.post { background: var(--invariant-color); border-left: 8px solid var(--invariant-color-dark); color: var(--invariant-color-darker); } +dl.invariant dt, dl.pre dt, dl.post dt { + color: var(--invariant-color-dark); +} + /* memitem */ @@ -1019,38 +1668,70 @@ div.memdoc { h2.memtitle, div.memitem { border: 1px solid var(--separator-color); + box-shadow: var(--box-shadow); +} + +h2.memtitle { + box-shadow: 0px var(--spacing-medium) 0 -1px var(--fragment-background), var(--box-shadow); +} + +div.memitem { + transition: none; } div.memproto, h2.memtitle { - background: var(--code-background); - text-shadow: none; + background: var(--fragment-background); } h2.memtitle { font-weight: 500; - font-family: monospace, fixed; + font-size: var(--memtitle-font-size); + font-family: var(--font-family-monospace); border-bottom: none; border-top-left-radius: var(--border-radius-medium); border-top-right-radius: var(--border-radius-medium); word-break: break-all; + position: relative; +} + +h2.memtitle:after { + content: ""; + display: block; + background: var(--fragment-background); + height: var(--spacing-medium); + bottom: calc(0px - var(--spacing-medium)); + left: 0; + right: -14px; + position: absolute; + border-top-right-radius: var(--border-radius-medium); +} + +h2.memtitle > span.permalink { + font-size: inherit; +} + +h2.memtitle > span.permalink > a { + text-decoration: none; + padding-left: 3px; + margin-right: -4px; + user-select: none; + display: inline-block; + margin-top: -6px; +} + +h2.memtitle > span.permalink > a:hover { + color: var(--primary-dark-color) !important; } a:target + h2.memtitle, a:target + h2.memtitle + div.memitem { border-color: var(--primary-light-color); } -a:target + h2.memtitle { - box-shadow: -3px -3px 3px 0 var(--primary-lightest-color), 3px -3px 3px 0 var(--primary-lightest-color); -} - -a:target + h2.memtitle + div.memitem { - box-shadow: 0 0 10px 0 var(--primary-lighter-color); -} - div.memitem { border-top-right-radius: var(--border-radius-medium); border-bottom-right-radius: var(--border-radius-medium); border-bottom-left-radius: var(--border-radius-medium); + border-top-left-radius: 0; overflow: hidden; display: block !important; } @@ -1073,8 +1754,18 @@ div.memtitle { } div.memproto table.memname { - font-family: monospace, fixed; + font-family: var(--font-family-monospace); color: var(--page-foreground-color); + font-size: var(--memname-font-size); + text-shadow: none; +} + +div.memproto div.memtemplate { + font-family: var(--font-family-monospace); + color: var(--primary-dark-color); + font-size: var(--memname-font-size); + margin-left: 2px; + text-shadow: none; } table.mlabels, table.mlabels > tbody { @@ -1085,6 +1776,12 @@ td.mlabels-left { width: auto; } +td.mlabels-right { + margin-top: 3px; + position: sticky; + left: 0; +} + table.mlabels > tbody > tr:first-child { display: flex; justify-content: space-between; @@ -1100,6 +1797,7 @@ table.mlabels > tbody > tr:first-child { */ dl.reflist { + box-shadow: var(--box-shadow); border-radius: var(--border-radius-medium); border: 1px solid var(--separator-color); overflow: hidden; @@ -1117,6 +1815,7 @@ dl.reflist dt, dl.reflist dd { dl.reflist dt { + font-weight: 500; border-radius: 0; background: var(--code-background); border-bottom: 1px solid var(--separator-color); @@ -1132,55 +1831,426 @@ dl.reflist dd { Table */ -table.markdownTable, table.fieldtable { - width: 100%; - border: 1px solid var(--separator-color); - margin: var(--spacing-medium) 0; +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname), +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) tbody { + display: inline-block; + max-width: 100%; } -table.fieldtable { - box-shadow: none; +.contents > table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname):not(.classindex) { + margin-left: calc(0px - var(--spacing-large)); + margin-right: calc(0px - var(--spacing-large)); + max-width: calc(100% + 2 * var(--spacing-large)); +} + +table.fieldtable, +table.markdownTable tbody, +table.doxtable tbody { + border: none; + margin: var(--spacing-medium) 0; + box-shadow: 0 0 0 1px var(--separator-color); border-radius: var(--border-radius-small); } -th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone { +table.markdownTable, table.doxtable, table.fieldtable { + padding: 1px; +} + +table.doxtable caption { + display: block; +} + +table.fieldtable { + border-collapse: collapse; + width: 100%; +} + +th.markdownTableHeadLeft, +th.markdownTableHeadRight, +th.markdownTableHeadCenter, +th.markdownTableHeadNone, +table.doxtable th { background: var(--tablehead-background); color: var(--tablehead-foreground); font-weight: 600; + font-size: var(--page-font-size); } -table.markdownTable td, table.markdownTable th, table.fieldtable dt { +th.markdownTableHeadLeft:first-child, +th.markdownTableHeadRight:first-child, +th.markdownTableHeadCenter:first-child, +th.markdownTableHeadNone:first-child, +table.doxtable tr th:first-child { + border-top-left-radius: var(--border-radius-small); +} + +th.markdownTableHeadLeft:last-child, +th.markdownTableHeadRight:last-child, +th.markdownTableHeadCenter:last-child, +th.markdownTableHeadNone:last-child, +table.doxtable tr th:last-child { + border-top-right-radius: var(--border-radius-small); +} + +table.markdownTable td, +table.markdownTable th, +table.fieldtable td, +table.fieldtable th, +table.doxtable td, +table.doxtable th { border: 1px solid var(--separator-color); padding: var(--spacing-small) var(--spacing-medium); } +table.markdownTable td:last-child, +table.markdownTable th:last-child, +table.fieldtable td:last-child, +table.fieldtable th:last-child, +table.doxtable td:last-child, +table.doxtable th:last-child { + border-right: none; +} + +table.markdownTable td:first-child, +table.markdownTable th:first-child, +table.fieldtable td:first-child, +table.fieldtable th:first-child, +table.doxtable td:first-child, +table.doxtable th:first-child { + border-left: none; +} + +table.markdownTable tr:first-child td, +table.markdownTable tr:first-child th, +table.fieldtable tr:first-child td, +table.fieldtable tr:first-child th, +table.doxtable tr:first-child td, +table.doxtable tr:first-child th { + border-top: none; +} + +table.markdownTable tr:last-child td, +table.markdownTable tr:last-child th, +table.fieldtable tr:last-child td, +table.fieldtable tr:last-child th, +table.doxtable tr:last-child td, +table.doxtable tr:last-child th { + border-bottom: none; +} + +table.markdownTable tr, table.doxtable tr { + border-bottom: 1px solid var(--separator-color); +} + +table.markdownTable tr:last-child, table.doxtable tr:last-child { + border-bottom: none; +} + +.full_width_table table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) { + display: block; +} + +.full_width_table table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) tbody { + display: table; + width: 100%; +} + table.fieldtable th { font-size: var(--page-font-size); font-weight: 600; background-image: none; background-color: var(--tablehead-background); color: var(--tablehead-foreground); - border-bottom: 1px solid var(--separator-color); } -.fieldtable td.fieldtype, .fieldtable td.fieldname { +table.fieldtable td.fieldtype, .fieldtable td.fieldname, .fieldtable td.fieldinit, .fieldtable td.fielddoc, .fieldtable th { border-bottom: 1px solid var(--separator-color); border-right: 1px solid var(--separator-color); } -.fieldtable td.fielddoc { - border-bottom: 1px solid var(--separator-color); +table.fieldtable tr:last-child td:first-child { + border-bottom-left-radius: var(--border-radius-small); +} + +table.fieldtable tr:last-child td:last-child { + border-bottom-right-radius: var(--border-radius-small); } .memberdecls td.glow, .fieldtable tr.glow { background-color: var(--primary-light-color); - box-shadow: 0 0 15px var(--primary-lighter-color); + box-shadow: none; } table.memberdecls { display: block; - overflow-x: auto; - overflow-y: hidden; + -webkit-tap-highlight-color: transparent; +} + +table.memberdecls tr[class^='memitem'] { + font-family: var(--font-family-monospace); + font-size: var(--code-font-size); +} + +table.memberdecls tr[class^='memitem'] .memTemplParams { + font-family: var(--font-family-monospace); + font-size: var(--code-font-size); + color: var(--primary-dark-color); + white-space: normal; +} + +table.memberdecls tr.heading + tr[class^='memitem'] td.memItemLeft, +table.memberdecls tr.heading + tr[class^='memitem'] td.memItemRight, +table.memberdecls td.memItemLeft, +table.memberdecls td.memItemRight, +table.memberdecls .memTemplItemLeft, +table.memberdecls .memTemplItemRight, +table.memberdecls .memTemplParams { + transition: none; + padding-top: var(--spacing-small); + padding-bottom: var(--spacing-small); + border-top: 1px solid var(--separator-color); + border-bottom: 1px solid var(--separator-color); + background-color: var(--fragment-background); +} + +@media screen and (min-width: 768px) { + + tr.heading + tr[class^='memitem'] td.memItemRight, tr.groupHeader + tr[class^='memitem'] td.memItemRight, tr.inherit_header + tr[class^='memitem'] td.memItemRight { + border-top-right-radius: var(--border-radius-small); + } + + table.memberdecls tr:last-child td.memItemRight, table.memberdecls tr:last-child td.mdescRight, table.memberdecls tr[class^='memitem']:has(+ tr.groupHeader) td.memItemRight, table.memberdecls tr[class^='memitem']:has(+ tr.inherit_header) td.memItemRight, table.memberdecls tr[class^='memdesc']:has(+ tr.groupHeader) td.mdescRight, table.memberdecls tr[class^='memdesc']:has(+ tr.inherit_header) td.mdescRight { + border-bottom-right-radius: var(--border-radius-small); + } + + table.memberdecls tr:last-child td.memItemLeft, table.memberdecls tr:last-child td.mdescLeft, table.memberdecls tr[class^='memitem']:has(+ tr.groupHeader) td.memItemLeft, table.memberdecls tr[class^='memitem']:has(+ tr.inherit_header) td.memItemLeft, table.memberdecls tr[class^='memdesc']:has(+ tr.groupHeader) td.mdescLeft, table.memberdecls tr[class^='memdesc']:has(+ tr.inherit_header) td.mdescLeft { + border-bottom-left-radius: var(--border-radius-small); + } + + tr.heading + tr[class^='memitem'] td.memItemLeft, tr.groupHeader + tr[class^='memitem'] td.memItemLeft, tr.inherit_header + tr[class^='memitem'] td.memItemLeft { + border-top-left-radius: var(--border-radius-small); + } + +} + +table.memname td.memname { + font-size: var(--memname-font-size); +} + +table.memberdecls .memTemplItemLeft, +table.memberdecls .template .memItemLeft, +table.memberdecls .memTemplItemRight, +table.memberdecls .template .memItemRight { + padding-top: 2px; +} + +table.memberdecls .memTemplParams { + border-bottom: 0; + border-left: 1px solid var(--separator-color); + border-right: 1px solid var(--separator-color); + border-radius: var(--border-radius-small) var(--border-radius-small) 0 0; + padding-bottom: var(--spacing-small); +} + +table.memberdecls .memTemplItemLeft, table.memberdecls .template .memItemLeft { + border-radius: 0 0 0 var(--border-radius-small); + border-left: 1px solid var(--separator-color); + border-top: 0; +} + +table.memberdecls .memTemplItemRight, table.memberdecls .template .memItemRight { + border-radius: 0 0 var(--border-radius-small) 0; + border-right: 1px solid var(--separator-color); + padding-left: 0; + border-top: 0; +} + +table.memberdecls .memItemLeft { + border-radius: var(--border-radius-small) 0 0 var(--border-radius-small); + border-left: 1px solid var(--separator-color); + padding-left: var(--spacing-medium); + padding-right: 0; +} + +table.memberdecls .memItemRight { + border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0; + border-right: 1px solid var(--separator-color); + padding-right: var(--spacing-medium); + padding-left: 0; + +} + +table.memberdecls .mdescLeft, table.memberdecls .mdescRight { + background: none; + color: var(--page-foreground-color); + padding: var(--spacing-small) 0; + border: 0; +} + +table.memberdecls [class^="memdesc"] { + box-shadow: none; +} + + +table.memberdecls .memItemLeft, +table.memberdecls .memTemplItemLeft { + padding-right: var(--spacing-medium); +} + +table.memberdecls .memSeparator { + background: var(--page-background-color); + height: var(--spacing-large); + border: 0; + transition: none; +} + +table.memberdecls .groupheader { + margin-bottom: var(--spacing-large); +} + +table.memberdecls .inherit_header td { + padding: 0 0 var(--spacing-medium) 0; + text-indent: -12px; + color: var(--page-secondary-foreground-color); +} + +table.memberdecls span.dynarrow { + left: 10px; +} + +table.memberdecls img[src="closed.png"], +table.memberdecls img[src="open.png"], +div.dynheader img[src="open.png"], +div.dynheader img[src="closed.png"] { + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid var(--primary-color); + margin-top: 8px; + display: block; + float: left; + margin-left: -10px; + transition: transform var(--animation-duration) ease-out; +} + +tr.heading + tr[class^='memitem'] td.memItemLeft, tr.groupHeader + tr[class^='memitem'] td.memItemLeft, tr.inherit_header + tr[class^='memitem'] td.memItemLeft, tr.heading + tr[class^='memitem'] td.memItemRight, tr.groupHeader + tr[class^='memitem'] td.memItemRight, tr.inherit_header + tr[class^='memitem'] td.memItemRight { + border-top: 1px solid var(--separator-color); +} + +table.memberdecls img { + margin-right: 10px; +} + +table.memberdecls img[src="closed.png"], +div.dynheader img[src="closed.png"] { + transform: rotate(-90deg); + +} + +.compoundTemplParams { + font-family: var(--font-family-monospace); + color: var(--primary-dark-color); + font-size: var(--code-font-size); +} + +@media screen and (max-width: 767px) { + + table.memberdecls .memItemLeft, + table.memberdecls .memItemRight, + table.memberdecls .mdescLeft, + table.memberdecls .mdescRight, + table.memberdecls .memTemplItemLeft, + table.memberdecls .memTemplItemRight, + table.memberdecls .memTemplParams, + table.memberdecls .template .memItemLeft, + table.memberdecls .template .memItemRight, + table.memberdecls .template .memParams { + display: block; + text-align: left; + padding-left: var(--spacing-large); + margin: 0 calc(0px - var(--spacing-large)) 0 calc(0px - var(--spacing-large)); + border-right: none; + border-left: none; + border-radius: 0; + white-space: normal; + } + + table.memberdecls .memItemLeft, + table.memberdecls .mdescLeft, + table.memberdecls .memTemplItemLeft, + table.memberdecls .template .memItemLeft { + border-bottom: 0 !important; + padding-bottom: 0 !important; + } + + table.memberdecls .memTemplItemLeft, + table.memberdecls .template .memItemLeft { + padding-top: 0; + } + + table.memberdecls .mdescLeft { + margin-bottom: calc(0px - var(--page-font-size)); + } + + table.memberdecls .memItemRight, + table.memberdecls .mdescRight, + table.memberdecls .memTemplItemRight, + table.memberdecls .template .memItemRight { + border-top: 0 !important; + padding-top: 0 !important; + padding-right: var(--spacing-large); + padding-bottom: var(--spacing-medium); + overflow-x: auto; + } + + table.memberdecls tr[class^='memitem']:not(.inherit) { + display: block; + width: calc(100vw - 2 * var(--spacing-large)); + } + + table.memberdecls .mdescRight { + color: var(--page-foreground-color); + } + + table.memberdecls tr.inherit { + visibility: hidden; + } + + table.memberdecls tr[style="display: table-row;"] { + display: block !important; + visibility: visible; + width: calc(100vw - 2 * var(--spacing-large)); + animation: fade .5s; + } + + @keyframes fade { + 0% { + opacity: 0; + max-height: 0; + } + + 100% { + opacity: 1; + max-height: 200px; + } + } + + tr.heading + tr[class^='memitem'] td.memItemRight, tr.groupHeader + tr[class^='memitem'] td.memItemRight, tr.inherit_header + tr[class^='memitem'] td.memItemRight { + border-top-right-radius: 0; + } + + table.memberdecls tr:last-child td.memItemRight, table.memberdecls tr:last-child td.mdescRight, table.memberdecls tr[class^='memitem']:has(+ tr.groupHeader) td.memItemRight, table.memberdecls tr[class^='memitem']:has(+ tr.inherit_header) td.memItemRight, table.memberdecls tr[class^='memdesc']:has(+ tr.groupHeader) td.mdescRight, table.memberdecls tr[class^='memdesc']:has(+ tr.inherit_header) td.mdescRight { + border-bottom-right-radius: 0; + } + + table.memberdecls tr:last-child td.memItemLeft, table.memberdecls tr:last-child td.mdescLeft, table.memberdecls tr[class^='memitem']:has(+ tr.groupHeader) td.memItemLeft, table.memberdecls tr[class^='memitem']:has(+ tr.inherit_header) td.memItemLeft, table.memberdecls tr[class^='memdesc']:has(+ tr.groupHeader) td.mdescLeft, table.memberdecls tr[class^='memdesc']:has(+ tr.inherit_header) td.mdescLeft { + border-bottom-left-radius: 0; + } + + tr.heading + tr[class^='memitem'] td.memItemLeft, tr.groupHeader + tr[class^='memitem'] td.memItemLeft, tr.inherit_header + tr[class^='memitem'] td.memItemLeft { + border-top-left-radius: 0; + } } @@ -1191,15 +2261,35 @@ table.memberdecls { hr { margin-top: var(--spacing-large); margin-bottom: var(--spacing-large); - border-top:1px solid var(--separator-color); + height: 1px; + background-color: var(--separator-color); + border: 0; } .contents hr { - box-shadow: var(--content-maxwidth) 0 0 0 var(--separator-color), calc(0px - var(--content-maxwidth)) 0 0 0 var(--separator-color); + box-shadow: 100px 0 var(--separator-color), + -100px 0 var(--separator-color), + 500px 0 var(--separator-color), + -500px 0 var(--separator-color), + 900px 0 var(--separator-color), + -900px 0 var(--separator-color), + 1400px 0 var(--separator-color), + -1400px 0 var(--separator-color), + 1900px 0 var(--separator-color), + -1900px 0 var(--separator-color); } -.contents img { +.contents img, .contents .center, .contents center, .contents div.image object { max-width: 100%; + overflow: auto; +} + +@media screen and (max-width: 767px) { + .contents .dyncontent > .center, .contents > center { + margin-left: calc(0px - var(--spacing-large)); + margin-right: calc(0px - var(--spacing-large)); + max-width: calc(100% + 2 * var(--spacing-large)); + } } /* @@ -1215,18 +2305,42 @@ table.directory { font-family: var(--font-family); font-size: var(--page-font-size); font-weight: normal; + width: 100%; } -.directory td.entry { - padding: var(--spacing-small); - display: flex; - align-items: center; +table.directory td.entry, table.directory td.desc { + padding: calc(var(--spacing-small) / 2) var(--spacing-small); + line-height: var(--table-line-height); } -.directory tr.even { +table.directory tr.even td:last-child { + border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0; +} + +table.directory tr.even td:first-child { + border-radius: var(--border-radius-small) 0 0 var(--border-radius-small); +} + +table.directory tr.even:last-child td:last-child { + border-radius: 0 var(--border-radius-small) 0 0; +} + +table.directory tr.even:last-child td:first-child { + border-radius: var(--border-radius-small) 0 0 0; +} + +table.directory td.desc { + min-width: 250px; +} + +table.directory tr.even { background-color: var(--odd-color); } +table.directory tr.odd { + background-color: transparent; +} + .icona { width: auto; height: auto; @@ -1234,15 +2348,21 @@ table.directory { } .icon { - background: var(--primary-dark-color); - width: 18px; - height: 18px; - line-height: 18px; + background: var(--primary-color); + border-radius: var(--border-radius-small); + font-size: var(--page-font-size); + padding: calc(var(--page-font-size) / 5); + line-height: var(--page-font-size); + transform: scale(0.8); + height: auto; + width: var(--page-font-size); + user-select: none; } .iconfopen, .icondoc, .iconfclosed { background-position: center; margin-bottom: 0; + height: var(--table-line-height); } .icondoc { @@ -1251,17 +2371,21 @@ table.directory { @media screen and (max-width: 767px) { div.directory { - margin-left: calc(0px - var(--spacing-medium)); - margin-right: calc(0px - var(--spacing-medium)); + margin-left: calc(0px - var(--spacing-large)); + margin-right: calc(0px - var(--spacing-large)); } } @media (prefers-color-scheme: dark) { - .iconfopen, .iconfclosed { + html:not(.light-mode) .iconfopen, html:not(.light-mode) .iconfclosed { filter: hue-rotate(180deg) invert(); } } +html.dark-mode .iconfopen, html.dark-mode .iconfclosed { + filter: hue-rotate(180deg) invert(); +} + /* Class list */ @@ -1271,10 +2395,35 @@ table.directory { border-radius: var(--border-radius-small); } -@media screen and (max-width: 767px) { - .classindex { - margin: 0 calc(0px - var(--spacing-small)); - } +.classindex dl.even { + background-color: transparent; +} + +/* + Class Index Doxygen 1.8 +*/ + +table.classindex { + margin-left: 0; + margin-right: 0; + width: 100%; +} + +table.classindex table div.ah { + background-image: none; + background-color: initial; + border-color: var(--separator-color); + color: var(--page-foreground-color); + box-shadow: var(--box-shadow); + border-radius: var(--border-radius-large); + padding: var(--spacing-small); +} + +div.qindex { + background-color: var(--odd-color); + border-radius: var(--border-radius-small); + border: 1px solid var(--separator-color); + padding: var(--spacing-small) 0; } /* @@ -1282,7 +2431,6 @@ table.directory { */ #nav-path { - margin-bottom: -1px; width: 100%; } @@ -1291,7 +2439,7 @@ table.directory { background: var(--page-background-color); border: none; border-top: 1px solid var(--separator-color); - border-bottom: 1px solid var(--separator-color); + border-bottom: 0; font-size: var(--navigation-font-size); } @@ -1304,6 +2452,7 @@ img.footer { } address.footer { + color: var(--page-secondary-foreground-color); margin-bottom: var(--spacing-large); } @@ -1316,7 +2465,16 @@ address.footer { .navpath li.navelem a { text-shadow: none; display: inline-block; - color: var(--primary-dark-color) + color: var(--primary-color) !important; +} + +.navpath li.navelem a:hover { + text-shadow: none; +} + +.navpath li.navelem b { + color: var(--primary-dark-color); + font-weight: 500; } li.navelem { @@ -1332,33 +2490,531 @@ li.navelem:first-child:before { display: none; } -#nav-path li.navelem:after { +#nav-path ul { + padding-left: 0; +} + +#nav-path li.navelem:has(.el):after { content: ''; border: 5px solid var(--page-background-color); border-bottom-color: transparent; border-right-color: transparent; border-top-color: transparent; - transform: scaleY(4.2); + transform: translateY(-1px) scaleY(4.2); z-index: 10; margin-left: 6px; } -#nav-path li.navelem:before { +#nav-path li.navelem:not(:has(.el)):after { + background: var(--page-background-color); + box-shadow: 1px -1px 0 1px var(--separator-color); + border-radius: 0 var(--border-radius-medium) 0 50px; +} + +#nav-path li.navelem:not(:has(.el)) { + margin-left: 0; +} + +#nav-path li.navelem:not(:has(.el)):hover, #nav-path li.navelem:not(:has(.el)):hover:after { + background-color: var(--separator-color); +} + +#nav-path li.navelem:has(.el):before { content: ''; border: 5px solid var(--separator-color); border-bottom-color: transparent; border-right-color: transparent; border-top-color: transparent; - transform: scaleY(3.2); + transform: translateY(-1px) scaleY(3.2); margin-right: var(--spacing-small); } -@media (prefers-color-scheme: dark) { - #nav-path li.navelem:after { - text-shadow: 3px 0 0 var(--separator-color), 8px 0 6px rgba(0,0,0,0.4); - } -} - .navpath li.navelem a:hover { color: var(--primary-color); } + +/* + Scrollbars for Webkit +*/ + +#nav-tree::-webkit-scrollbar, +div.fragment::-webkit-scrollbar, +pre.fragment::-webkit-scrollbar, +div.memproto::-webkit-scrollbar, +.contents center::-webkit-scrollbar, +.contents .center::-webkit-scrollbar, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) tbody::-webkit-scrollbar, +div.contents .toc::-webkit-scrollbar, +.contents .dotgraph::-webkit-scrollbar, +.contents .tabs-overview-container::-webkit-scrollbar { + background: transparent; + width: calc(var(--webkit-scrollbar-size) + var(--webkit-scrollbar-padding) + var(--webkit-scrollbar-padding)); + height: calc(var(--webkit-scrollbar-size) + var(--webkit-scrollbar-padding) + var(--webkit-scrollbar-padding)); +} + +#nav-tree::-webkit-scrollbar-thumb, +div.fragment::-webkit-scrollbar-thumb, +pre.fragment::-webkit-scrollbar-thumb, +div.memproto::-webkit-scrollbar-thumb, +.contents center::-webkit-scrollbar-thumb, +.contents .center::-webkit-scrollbar-thumb, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) tbody::-webkit-scrollbar-thumb, +div.contents .toc::-webkit-scrollbar-thumb, +.contents .dotgraph::-webkit-scrollbar-thumb, +.contents .tabs-overview-container::-webkit-scrollbar-thumb { + background-color: transparent; + border: var(--webkit-scrollbar-padding) solid transparent; + border-radius: calc(var(--webkit-scrollbar-padding) + var(--webkit-scrollbar-padding)); + background-clip: padding-box; +} + +#nav-tree:hover::-webkit-scrollbar-thumb, +div.fragment:hover::-webkit-scrollbar-thumb, +pre.fragment:hover::-webkit-scrollbar-thumb, +div.memproto:hover::-webkit-scrollbar-thumb, +.contents center:hover::-webkit-scrollbar-thumb, +.contents .center:hover::-webkit-scrollbar-thumb, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) tbody:hover::-webkit-scrollbar-thumb, +div.contents .toc:hover::-webkit-scrollbar-thumb, +.contents .dotgraph:hover::-webkit-scrollbar-thumb, +.contents .tabs-overview-container:hover::-webkit-scrollbar-thumb { + background-color: var(--webkit-scrollbar-color); +} + +#nav-tree::-webkit-scrollbar-track, +div.fragment::-webkit-scrollbar-track, +pre.fragment::-webkit-scrollbar-track, +div.memproto::-webkit-scrollbar-track, +.contents center::-webkit-scrollbar-track, +.contents .center::-webkit-scrollbar-track, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) tbody::-webkit-scrollbar-track, +div.contents .toc::-webkit-scrollbar-track, +.contents .dotgraph::-webkit-scrollbar-track, +.contents .tabs-overview-container::-webkit-scrollbar-track { + background: transparent; +} + +#nav-tree::-webkit-scrollbar-corner { + background-color: var(--side-nav-background); +} + +#nav-tree, +div.fragment, +pre.fragment, +div.memproto, +.contents center, +.contents .center, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) tbody, +div.contents .toc { + overflow-x: auto; + overflow-x: overlay; +} + +#nav-tree { + overflow-x: auto; + overflow-y: auto; + overflow-y: overlay; +} + +/* + Scrollbars for Firefox +*/ + +#nav-tree, +div.fragment, +pre.fragment, +div.memproto, +.contents center, +.contents .center, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) tbody, +div.contents .toc, +.contents .dotgraph, +.contents .tabs-overview-container { + scrollbar-width: thin; +} + +/* + Optional Dark mode toggle button +*/ + +doxygen-awesome-dark-mode-toggle { + display: inline-block; + margin: 0 0 0 var(--spacing-small); + padding: 0; + width: var(--searchbar-height); + height: var(--searchbar-height); + background: none; + border: none; + border-radius: var(--searchbar-border-radius); + vertical-align: middle; + text-align: center; + line-height: var(--searchbar-height); + font-size: 22px; + display: flex; + align-items: center; + justify-content: center; + user-select: none; + cursor: pointer; +} + +doxygen-awesome-dark-mode-toggle > svg { + transition: transform var(--animation-duration) ease-in-out; +} + +doxygen-awesome-dark-mode-toggle:active > svg { + transform: scale(.5); +} + +doxygen-awesome-dark-mode-toggle:hover { + background-color: rgba(0,0,0,.03); +} + +html.dark-mode doxygen-awesome-dark-mode-toggle:hover { + background-color: rgba(0,0,0,.18); +} + +/* + Optional fragment copy button +*/ +.doxygen-awesome-fragment-wrapper { + position: relative; +} + +doxygen-awesome-fragment-copy-button { + opacity: 0; + background: var(--fragment-background); + width: 28px; + height: 28px; + position: absolute; + right: calc(var(--spacing-large) - (var(--spacing-large) / 2.5)); + top: calc(var(--spacing-large) - (var(--spacing-large) / 2.5)); + border: 1px solid var(--fragment-foreground); + cursor: pointer; + border-radius: var(--border-radius-small); + display: flex; + justify-content: center; + align-items: center; +} + +.doxygen-awesome-fragment-wrapper:hover doxygen-awesome-fragment-copy-button, doxygen-awesome-fragment-copy-button.success { + opacity: .28; +} + +doxygen-awesome-fragment-copy-button:hover, doxygen-awesome-fragment-copy-button.success { + opacity: 1 !important; +} + +doxygen-awesome-fragment-copy-button:active:not([class~=success]) svg { + transform: scale(.91); +} + +doxygen-awesome-fragment-copy-button svg { + fill: var(--fragment-foreground); + width: 18px; + height: 18px; +} + +doxygen-awesome-fragment-copy-button.success svg { + fill: rgb(14, 168, 14); +} + +doxygen-awesome-fragment-copy-button.success { + border-color: rgb(14, 168, 14); +} + +@media screen and (max-width: 767px) { + .textblock > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button, + .textblock li > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button, + .memdoc li > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button, + .memdoc > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button, + dl dd > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button { + right: 0; + } +} + +/* + Optional paragraph link button +*/ + +a.anchorlink { + font-size: 90%; + margin-left: var(--spacing-small); + color: var(--page-foreground-color) !important; + text-decoration: none; + opacity: .15; + display: none; + transition: opacity var(--animation-duration) ease-in-out, color var(--animation-duration) ease-in-out; +} + +a.anchorlink svg { + fill: var(--page-foreground-color); +} + +h3 a.anchorlink svg, h4 a.anchorlink svg { + margin-bottom: -3px; + margin-top: -4px; +} + +a.anchorlink:hover { + opacity: .45; +} + +h2:hover a.anchorlink, h1:hover a.anchorlink, h3:hover a.anchorlink, h4:hover a.anchorlink { + display: inline-block; +} + +/* + Optional tab feature +*/ + +.tabbed > ul { + padding-inline-start: 0px; + margin: 0; + padding: var(--spacing-small) 0; +} + +.tabbed > ul > li { + display: none; +} + +.tabbed > ul > li.selected { + display: block; +} + +.tabs-overview-container { + overflow-x: auto; + display: block; + overflow-y: visible; +} + +.tabs-overview { + border-bottom: 1px solid var(--separator-color); + display: flex; + flex-direction: row; +} + +@media screen and (max-width: 767px) { + .tabs-overview-container { + margin: 0 calc(0px - var(--spacing-large)); + } + .tabs-overview { + padding: 0 var(--spacing-large) + } +} + +.tabs-overview button.tab-button { + color: var(--page-foreground-color); + margin: 0; + border: none; + background: transparent; + padding: calc(var(--spacing-large) / 2) 0; + display: inline-block; + font-size: var(--page-font-size); + cursor: pointer; + box-shadow: 0 1px 0 0 var(--separator-color); + position: relative; + + -webkit-tap-highlight-color: transparent; +} + +.tabs-overview button.tab-button .tab-title::before { + display: block; + content: attr(title); + font-weight: 600; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.tabs-overview button.tab-button .tab-title { + float: left; + white-space: nowrap; + font-weight: normal; + font-family: var(--font-family); + padding: calc(var(--spacing-large) / 2) var(--spacing-large); + border-radius: var(--border-radius-medium); + transition: background-color var(--animation-duration) ease-in-out, font-weight var(--animation-duration) ease-in-out; +} + +.tabs-overview button.tab-button:not(:last-child) .tab-title { + box-shadow: 8px 0 0 -7px var(--separator-color); +} + +.tabs-overview button.tab-button:hover .tab-title { + background: var(--separator-color); + box-shadow: none; +} + +.tabs-overview button.tab-button.active .tab-title { + font-weight: 600; +} + +.tabs-overview button.tab-button::after { + content: ''; + display: block; + position: absolute; + left: 0; + bottom: 0; + right: 0; + height: 0; + width: 0%; + margin: 0 auto; + border-radius: var(--border-radius-small) var(--border-radius-small) 0 0; + background-color: var(--primary-color); + transition: width var(--animation-duration) ease-in-out, height var(--animation-duration) ease-in-out; +} + +.tabs-overview button.tab-button.active::after { + width: 100%; + box-sizing: border-box; + height: 3px; +} + + +/* + Navigation Buttons +*/ + +.section_buttons:not(:empty) { + margin-top: calc(var(--spacing-large) * 3); +} + +.section_buttons table.markdownTable { + display: block; + width: 100%; +} + +.section_buttons table.markdownTable tbody { + display: table !important; + width: 100%; + box-shadow: none; + border-spacing: 10px; +} + +.section_buttons table.markdownTable td { + padding: 0; +} + +.section_buttons table.markdownTable th { + display: none; +} + +.section_buttons table.markdownTable tr.markdownTableHead { + border: none; +} + +.section_buttons tr th, .section_buttons tr td { + background: none; + border: none; + padding: var(--spacing-large) 0 var(--spacing-small); +} + +.section_buttons a { + display: inline-block; + border: 1px solid var(--separator-color); + border-radius: var(--border-radius-medium); + color: var(--page-secondary-foreground-color) !important; + text-decoration: none; + transition: color var(--animation-duration) ease-in-out, background-color var(--animation-duration) ease-in-out; +} + +.section_buttons a:hover { + color: var(--page-foreground-color) !important; + background-color: var(--odd-color); +} + +.section_buttons tr td.markdownTableBodyLeft a { + padding: var(--spacing-medium) var(--spacing-large) var(--spacing-medium) calc(var(--spacing-large) / 2); +} + +.section_buttons tr td.markdownTableBodyRight a { + padding: var(--spacing-medium) calc(var(--spacing-large) / 2) var(--spacing-medium) var(--spacing-large); +} + +.section_buttons tr td.markdownTableBodyLeft a::before, +.section_buttons tr td.markdownTableBodyRight a::after { + color: var(--page-secondary-foreground-color) !important; + display: inline-block; + transition: color .08s ease-in-out, transform .09s ease-in-out; +} + +.section_buttons tr td.markdownTableBodyLeft a::before { + content: '〈'; + padding-right: var(--spacing-large); +} + + +.section_buttons tr td.markdownTableBodyRight a::after { + content: '〉'; + padding-left: var(--spacing-large); +} + + +.section_buttons tr td.markdownTableBodyLeft a:hover::before { + color: var(--page-foreground-color) !important; + transform: translateX(-3px); +} + +.section_buttons tr td.markdownTableBodyRight a:hover::after { + color: var(--page-foreground-color) !important; + transform: translateX(3px); +} + +@media screen and (max-width: 450px) { + .section_buttons a { + width: 100%; + box-sizing: border-box; + } + + .section_buttons tr td:nth-of-type(1).markdownTableBodyLeft a { + border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium); + border-right: none; + } + + .section_buttons tr td:nth-of-type(2).markdownTableBodyRight a { + border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0; + } +} + +/* + Bordered image +*/ + +html.dark-mode .darkmode_inverted_image img, /* < doxygen 1.9.3 */ +html.dark-mode .darkmode_inverted_image object[type="image/svg+xml"] /* doxygen 1.9.3 */ { + filter: brightness(89%) hue-rotate(180deg) invert(); +} + +.bordered_image { + border-radius: var(--border-radius-small); + border: 1px solid var(--separator-color); + display: inline-block; + overflow: hidden; +} + +.bordered_image:empty { + border: none; +} + +html.dark-mode .bordered_image img, /* < doxygen 1.9.3 */ +html.dark-mode .bordered_image object[type="image/svg+xml"] /* doxygen 1.9.3 */ { + border-radius: var(--border-radius-small); +} + +/* + Button +*/ + +.primary-button { + display: inline-block; + cursor: pointer; + background: var(--primary-color); + color: var(--page-background-color) !important; + border-radius: var(--border-radius-medium); + padding: var(--spacing-small) var(--spacing-medium); + text-decoration: none; +} + +.primary-button:hover { + background: var(--primary-dark-color); +} \ No newline at end of file diff --git a/doc/examples/tutorial7.c b/doc/examples/tutorial7.c new file mode 100644 index 000000000..253698ed9 --- /dev/null +++ b/doc/examples/tutorial7.c @@ -0,0 +1,152 @@ +/* + [title] + \ref page_tutorial7 + [title] + */ +/* [code] */ +#include +#include +#include +#include + +#include +#include + +#include +#include + +struct data; + +struct port { + struct data *data; +}; + +struct data { + struct pw_main_loop *loop; + struct pw_filter *filter; + struct port *in_port; + struct port *out_port; +}; + +/* [on_process] */ +static void on_process(void *userdata, struct spa_io_position *position) +{ + struct data *data = userdata; + float *in, *out; + uint32_t n_samples = position->clock.duration; + + pw_log_trace("do process %d", n_samples); + + in = pw_filter_get_dsp_buffer(data->in_port, n_samples); + out = pw_filter_get_dsp_buffer(data->out_port, n_samples); + + if (in == NULL || out == NULL) + return; + + /* Simple passthrough - copy input to output. + * Here you could implement any audio processing: + * - Filters (lowpass, highpass, bandpass) + * - Effects (reverb, delay, distortion) + * - Dynamic processing (compressor, limiter) + * - Equalization + * - etc. + */ + memcpy(out, in, n_samples * sizeof(float)); +} +/* [on_process] */ + +static const struct pw_filter_events filter_events = { + PW_VERSION_FILTER_EVENTS, + .process = on_process, +}; + +static void do_quit(void *userdata, int signal_number) +{ + struct data *data = userdata; + pw_main_loop_quit(data->loop); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + const struct spa_pod *params[1]; + uint32_t n_params = 0; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + pw_init(&argc, &argv); + + /* make a main loop. If you already have another main loop, you can add + * the fd of this pipewire mainloop to it. */ + data.loop = pw_main_loop_new(NULL); + + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data); + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data); + + /* Create a simple filter, the simple filter manages the core and remote + * objects for you if you don't need to deal with them. + * + * Pass your events and a user_data pointer as the last arguments. This + * will inform you about the filter state. The most important event + * you need to listen to is the process event where you need to process + * the data. + */ + data.filter = pw_filter_new_simple( + pw_main_loop_get_loop(data.loop), + "audio-filter", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Filter", + PW_KEY_MEDIA_ROLE, "DSP", + NULL), + &filter_events, + &data); + + /* make an audio DSP input port */ + data.in_port = pw_filter_add_port(data.filter, + PW_DIRECTION_INPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(struct port), + pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", + PW_KEY_PORT_NAME, "input", + NULL), + NULL, 0); + + /* make an audio DSP output port */ + data.out_port = pw_filter_add_port(data.filter, + PW_DIRECTION_OUTPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(struct port), + pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", + PW_KEY_PORT_NAME, "output", + NULL), + NULL, 0); + + /* Set processing latency information */ + params[n_params++] = spa_process_latency_build(&b, + SPA_PARAM_ProcessLatency, + &SPA_PROCESS_LATENCY_INFO_INIT( + .ns = 10 * SPA_NSEC_PER_MSEC + )); + + /* Now connect this filter. We ask that our process function is + * called in a realtime thread. */ + if (pw_filter_connect(data.filter, + PW_FILTER_FLAG_RT_PROCESS, + params, n_params) < 0) { + fprintf(stderr, "can't connect\n"); + return -1; + } + + /* and wait while we let things run */ + pw_main_loop_run(data.loop); + + pw_filter_destroy(data.filter); + pw_main_loop_destroy(data.loop); + pw_deinit(); + + return 0; +} +/* [code] */ \ No newline at end of file diff --git a/doc/input-filter-md.py b/doc/input-filter-md.py index 60aa46279..cd86d23df 100755 --- a/doc/input-filter-md.py +++ b/doc/input-filter-md.py @@ -25,7 +25,7 @@ Assumes BUILD_DIR environment variable is set. containing all index items from the specified section. -# Section title @IDX@
+# Section title @IDX@
[] Adds the section title to the index, and expands to an anchor @@ -51,7 +51,7 @@ def index_key(section, name): BUILD_DIR = os.environ["BUILD_DIR"] PAR_RE = r"^@PAR@\s+([^\s]*)[ \t]+(\S+)(.*)$" -IDX_RE = r"^(#+)(.*)@IDX@[ \t]+(\S+)[ \t]*$" +IDX_RE = r"^(#+)(.*)@IDX@[ \t]+(\S+)([ \t]+\S+)?[ \t]*$" SECREF_RE = r"^@SECREF@[ \t]+([^\n]*)[ \t]*$" @@ -71,10 +71,16 @@ def main(args): level = m.group(1) title = name = m.group(2).strip() section = m.group(3) + alt = m.group(4) if title == title.upper(): name = name.capitalize() key = index_key(section, name) - return f"{level} {title} {{#{key}}}" + text = f"{level} {title} {{#{key}}}" + if alt and alt.strip(): + alt_key = index_key(section, alt.strip()) + if alt_key != key: + text += f"\n\\anchor {alt_key}" + return text def secref(m): import os @@ -148,9 +154,12 @@ def load_index(sections, text): def idx(m): name = m.group(2).strip() section = m.group(3) + alt = m.group(4) if name == name.upper(): name = name.capitalize() sections.setdefault(section, []).append(name) + if alt and alt.strip(): + sections.setdefault(section, []).append(alt.strip()) return "" text = re.sub(PAR_RE, par, text, flags=re.M) diff --git a/doc/meson.build b/doc/meson.build index 5f2e28fb8..f4aa4ba6a 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -58,6 +58,8 @@ extra_docs = [ 'dox/internals/index.dox', 'dox/internals/design.dox', 'dox/internals/access.dox', + 'dox/internals/latency.dox', + 'dox/internals/tag.dox', 'dox/internals/midi.dox', 'dox/internals/portal.dox', 'dox/internals/daemon.dox', @@ -66,6 +68,7 @@ extra_docs = [ 'dox/internals/objects.dox', 'dox/internals/audio.dox', 'dox/internals/scheduling.dox', + 'dox/internals/driver.dox', 'dox/internals/protocol.dox', 'dox/internals/pulseaudio.dox', 'dox/internals/dma-buf.dox', @@ -76,6 +79,7 @@ extra_docs = [ 'dox/tutorial/tutorial4.dox', 'dox/tutorial/tutorial5.dox', 'dox/tutorial/tutorial6.dox', + 'dox/tutorial/tutorial7.dox', 'dox/api/index.dox', 'dox/api/spa-index.dox', 'dox/api/spa-plugins.dox', @@ -170,6 +174,7 @@ example_files = [ 'tutorial4.c', 'tutorial5.c', 'tutorial6.c', + 'tutorial7.c', ] example_dep_files = [] foreach h : example_files diff --git a/doc/tree.dox b/doc/tree.dox index ecc43d604..f62bc62dd 100644 --- a/doc/tree.dox +++ b/doc/tree.dox @@ -43,6 +43,7 @@ This determines the ordering of items in Doxygen sidebar. \addtogroup pw_protocol \addtogroup pw_resource \addtogroup pw_thread_loop +\addtogroup pw_timer_queue \addtogroup pw_work_queue \} diff --git a/meson.build b/meson.build index 3dfdc1842..e82ba03b3 100644 --- a/meson.build +++ b/meson.build @@ -1,10 +1,10 @@ project('pipewire', ['c' ], - version : '1.4.0', + version : '1.5.81', license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], meson_version : '>= 0.61.1', default_options : [ 'warning_level=3', 'c_std=gnu11', - 'cpp_std=c++17', + 'cpp_std=c++20', 'b_pie=true', #'b_sanitize=address,undefined', 'buildtype=debugoptimized' ]) @@ -81,6 +81,7 @@ pkgconfig = import('pkgconfig') common_flags = [ '-fvisibility=hidden', '-fno-strict-aliasing', + '-fno-strict-overflow', '-Werror=suggest-attribute=format', '-Wsign-compare', '-Wpointer-arith', @@ -114,10 +115,11 @@ cc_flags = common_flags + [ '-Werror=old-style-definition', '-Werror=missing-parameter-type', '-Werror=strict-prototypes', + '-DSPA_AUDIO_MAX_CHANNELS=128u', ] add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c') - -cc_flags_native = cc_native.get_supported_arguments(cc_flags) +add_project_arguments(cc_native.get_supported_arguments(cc_flags), + language: 'c', native: true) have_cpp = add_languages('cpp', native: false, required : false) @@ -127,21 +129,30 @@ if have_cpp add_project_arguments(cxx.get_supported_arguments(cxx_flags), language: 'cpp') endif -sse_args = '-msse' -sse2_args = '-msse2' -ssse3_args = '-mssse3' -sse41_args = '-msse4.1' -fma_args = '-mfma' -avx_args = '-mavx' -avx2_args = '-mavx2' +have_sse = false +have_sse2 = false +have_ssse3 = false +have_sse41 = false +have_fma = false +have_avx = false +have_avx2 = false +if host_machine.cpu_family() in ['x86', 'x86_64'] + sse_args = '-msse' + sse2_args = '-msse2' + ssse3_args = '-mssse3' + sse41_args = '-msse4.1' + fma_args = '-mfma' + avx_args = '-mavx' + avx2_args = '-mavx2' -have_sse = cc.has_argument(sse_args) -have_sse2 = cc.has_argument(sse2_args) -have_ssse3 = cc.has_argument(ssse3_args) -have_sse41 = cc.has_argument(sse41_args) -have_fma = cc.has_argument(fma_args) -have_avx = cc.has_argument(avx_args) -have_avx2 = cc.has_argument(avx2_args) + have_sse = cc.has_argument(sse_args) + have_sse2 = cc.has_argument(sse2_args) + have_ssse3 = cc.has_argument(ssse3_args) + have_sse41 = cc.has_argument(sse41_args) + have_fma = cc.has_argument(fma_args) + have_avx = cc.has_argument(avx_args) + have_avx2 = cc.has_argument(avx2_args) +endif have_neon = false if host_machine.cpu_family() == 'aarch64' @@ -268,11 +279,9 @@ endforeach cdata.set('HAVE_PIDFD_OPEN', cc.get_define('SYS_pidfd_open', prefix: '#include ') != '') -systemd = dependency('systemd', required: get_option('systemd')) -systemd_dep = dependency('libsystemd',required: get_option('systemd')) -summary({'systemd conf data': systemd.found()}, bool_yn: true) +systemd_dep = dependency('libsystemd', required: get_option('libsystemd')) summary({'libsystemd': systemd_dep.found()}, bool_yn: true) -cdata.set('HAVE_SYSTEMD', systemd.found() and systemd_dep.found()) +cdata.set('HAVE_SYSTEMD', systemd_dep.found()) logind_dep = dependency(get_option('logind-provider'), required: get_option('logind')) summary({'logind': logind_dep.found()}, bool_yn: true) @@ -312,7 +321,7 @@ cdata.set('HAVE_DBUS', dbus_dep.found()) sdl_dep = dependency('sdl2', required : get_option('sdl2')) summary({'SDL2 (video examples)': sdl_dep.found()}, bool_yn: true, section: 'Misc dependencies') drm_dep = dependency('libdrm', required : false) -fftw_dep = dependency('fftw3f', required : false) +fftw_dep = dependency('fftw3f', required : get_option('fftw')) summary({'fftw3f (filter-chain convolver)': fftw_dep.found()}, bool_yn: true, section: 'Misc dependencies') cdata.set('HAVE_FFTW', fftw_dep.found()) @@ -331,12 +340,23 @@ endif pw_cat_ffmpeg = get_option('pw-cat-ffmpeg') ffmpeg = get_option('ffmpeg') if pw_cat_ffmpeg.allowed() or ffmpeg.allowed() + # libswscale is only used by videoconvert. FFmpeg might however be used for + # compressed audio (both for decoding said compressed audio and for parsing + # it in pw-cat). If users only care about audio, then libswscale would still + # become a requirement if its required flag is defined only by FFmpeg options. + # Make the videoconvert option a factor in swscale_dep as well to avoid this. + videoconvert = get_option('videoconvert') avcodec_dep = dependency('libavcodec', required: pw_cat_ffmpeg.enabled() or ffmpeg.enabled()) avformat_dep = dependency('libavformat', required: pw_cat_ffmpeg.enabled()) + avfilter_dep = dependency('libavfilter', required: ffmpeg.enabled()) avutil_dep = dependency('libavutil', required: pw_cat_ffmpeg.enabled() or ffmpeg.enabled()) - swscale_dep = dependency('libswscale', required: pw_cat_ffmpeg.enabled() or ffmpeg.enabled()) + swscale_dep = dependency('libswscale', required: (pw_cat_ffmpeg.enabled() or ffmpeg.enabled()) and videoconvert.enabled()) else avcodec_dep = dependency('', required: false) + avformat_dep = dependency('', required: false) + avfilter_dep = dependency('', required: false) + avutil_dep = dependency('', required: false) + swscale_dep = dependency('', required: false) endif cdata.set('HAVE_PW_CAT_FFMPEG_INTEGRATION', pw_cat_ffmpeg.allowed()) @@ -371,9 +391,6 @@ libusb_dep = dependency('libusb-1.0', required : get_option('libusb')) summary({'libusb (Bluetooth quirks)': libusb_dep.found()}, bool_yn: true, section: 'Backend') cdata.set('HAVE_LIBUSB', libusb_dep.found()) -cap_lib = dependency('libcap', required : false) -cdata.set('HAVE_LIBCAP', cap_lib.found()) - glib2_dep = dependency('glib-2.0', required : get_option('flatpak')) summary({'GLib-2.0 (Flatpak support)': glib2_dep.found()}, bool_yn: true, section: 'Misc dependencies') flatpak_support = glib2_dep.found() @@ -401,6 +418,7 @@ gst_deps_def = { gst_dep = [] gst_dma_drm_found = false +gst_shm_allocator_found = false foreach depname, kwargs: gst_deps_def dep = dependency(depname, required: gst_option, kwargs: kwargs) summary({depname: dep.found()}, bool_yn: true, section: 'GStreamer modules') @@ -416,9 +434,13 @@ foreach depname, kwargs: gst_deps_def if depname == 'gstreamer-allocators-1.0' and dep.version().version_compare('>= 1.23.1') gst_dma_drm_found = true + gst_shm_allocator_found = true endif endforeach +summary({'gstreamer SHM allocator': gst_shm_allocator_found}, bool_yn: true, section: 'Backend') +cdata.set('HAVE_GSTREAMER_SHM_ALLOCATOR', gst_shm_allocator_found) + # This code relies on the array being empty if any dependency was not found gst_dp_found = gst_dep.length() > 0 summary({'gstreamer-device-provider': gst_dp_found}, bool_yn: true, section: 'Backend') @@ -481,7 +503,7 @@ endif summary({'intl support': libintl_dep.found()}, bool_yn: true) need_alsa = get_option('pipewire-alsa').enabled() or 'media-session' in get_option('session-managers') -alsa_dep = dependency('alsa', version : '>=1.2.10', required: need_alsa) +alsa_dep = dependency('alsa', version : '>=1.2.6', required: need_alsa) summary({'pipewire-alsa': alsa_dep.found()}, bool_yn: true) if host_machine.system() == 'freebsd' or host_machine.system() == 'midnightbsd' @@ -612,6 +634,8 @@ devenv.prepend('GST_PLUGIN_PATH', builddir / 'src'/ 'gst') devenv.prepend('ALSA_PLUGIN_DIR', builddir / 'pipewire-alsa' / 'alsa-plugins') devenv.prepend('LD_LIBRARY_PATH', builddir / 'pipewire-jack' / 'src') +devenv.set('PIPEWIRE_LOG_SYSTEMD', 'false') devenv.set('PW_UNINSTALLED', '1') +devenv.set('PW_BUILDDIR', meson.project_build_root()) meson.add_devenv(devenv) diff --git a/meson_options.txt b/meson_options.txt index dc1b339f2..206d68659 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -30,8 +30,8 @@ option('gstreamer-device-provider', description: 'Build GStreamer device provider plugin', type: 'feature', value: 'auto') -option('systemd', - description: 'Enable systemd integration', +option('libsystemd', + description: 'Enable code that depends on libsystemd', type: 'feature', value: 'auto') option('logind', @@ -48,9 +48,9 @@ option('systemd-system-service', type: 'feature', value: 'disabled') option('systemd-user-service', - description: 'Install systemd user service file (ignored without systemd)', + description: 'Install systemd user service file', type: 'feature', - value: 'enabled') + value: 'auto') option('selinux', description: 'Enable SELinux integration', type: 'feature', @@ -129,6 +129,10 @@ option('bluez5-codec-ldac', description: 'Enable LDAC Sony open source codec implementation', type: 'feature', value: 'auto') +option('bluez5-codec-ldac-dec', + description: 'Enable LDAC Sony open source codec decoding', + type: 'feature', + value: 'auto') option('bluez5-codec-aac', description: 'Enable Fraunhofer FDK AAC open source codec implementation', type: 'feature', @@ -149,6 +153,10 @@ option('bluez5-codec-g722', description: 'Enable G722 open source codec implementation', type: 'feature', value: 'auto') +option('bluez5-plc-spandsp', + description: 'Enable SpanDSP for packet loss concealment', + type: 'feature', + value: 'auto') option('control', description: 'Enable control spa plugin integration', type: 'feature', @@ -379,3 +387,11 @@ option('ebur128', description: 'Enable code that depends on ebur128', type: 'feature', value: 'auto') +option('fftw', + description: 'Enable code that depends on fftw', + type: 'feature', + value: 'auto') +option('onnxruntime', + description: 'Enable code that depends on onnxruntime', + type: 'feature', + value: 'auto') diff --git a/pipewire-alsa/alsa-plugins/ctl_pipewire.c b/pipewire-alsa/alsa-plugins/ctl_pipewire.c index 7fcfd5726..36bc4be5e 100644 --- a/pipewire-alsa/alsa-plugins/ctl_pipewire.c +++ b/pipewire-alsa/alsa-plugins/ctl_pipewire.c @@ -22,9 +22,11 @@ PW_LOG_TOPIC_STATIC(alsa_log_topic, "alsa.ctl"); #define VOLUME_MIN ((uint32_t) 0U) #define VOLUME_MAX ((uint32_t) 0x10000U) +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS + struct volume { uint32_t channels; - long values[SPA_AUDIO_MAX_CHANNELS]; + long values[MAX_CHANNELS]; }; typedef struct { @@ -498,7 +500,7 @@ static struct spa_pod *build_volume_mute(struct spa_pod_builder *b, struct volum spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props); if (volume) { - float volumes[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; uint32_t i, n_volumes = 0; n_volumes = volume->channels; @@ -850,11 +852,11 @@ static void parse_props(struct global *g, const struct spa_pod *param, bool devi break; case SPA_PROP_channelVolumes: { - float volumes[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; uint32_t n_volumes, i; n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - volumes, SPA_AUDIO_MAX_CHANNELS); + volumes, SPA_N_ELEMENTS(volumes)); g->node.channel_volume.channels = n_volumes; for (i = 0; i < n_volumes; i++) diff --git a/pipewire-alsa/alsa-plugins/pcm_pipewire.c b/pipewire-alsa/alsa-plugins/pcm_pipewire.c index bd6836e53..24a66eabc 100644 --- a/pipewire-alsa/alsa-plugins/pcm_pipewire.c +++ b/pipewire-alsa/alsa-plugins/pcm_pipewire.c @@ -30,8 +30,8 @@ PW_LOG_TOPIC_STATIC(alsa_log_topic, "alsa.pcm"); #define MIN_BUFFERS 2u #define MAX_BUFFERS 64u -#define MAX_CHANNELS 64 #define MAX_RATE (48000*8) +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define MIN_PERIOD 64 @@ -617,6 +617,7 @@ static int snd_pcm_pipewire_stop(snd_pcm_ioplug_t *io) if (pw->activated && pw->stream != NULL) { pw_stream_set_active(pw->stream, false); pw->activated = false; + pw_thread_loop_signal(pw->main_loop, false); } pw_thread_loop_unlock(pw->main_loop); return 0; @@ -642,7 +643,7 @@ static int snd_pcm_pipewire_pause(snd_pcm_ioplug_t * io, int enable) #define _FORMAT_BE(p, fmt) p ? SPA_AUDIO_FORMAT_UNKNOWN : SPA_AUDIO_FORMAT_ ## fmt ## _OE #endif -static int set_default_channels(uint32_t channels, uint32_t position[SPA_AUDIO_MAX_CHANNELS]) +static int set_default_channels(uint32_t channels, uint32_t position[MAX_CHANNELS]) { switch (channels) { case 8: @@ -915,12 +916,16 @@ static int snd_pcm_pipewire_set_chmap(snd_pcm_ioplug_t * io, default: return -EINVAL; } + if (map->channels > MAX_CHANNELS) + return -ENOTSUP; + for (i = 0; i < map->channels; i++) { + char buf[8]; position[i] = chmap_to_channel(map->pos[i]); pw_log_debug("map %d: %s / %s", i, snd_pcm_chmap_name(map->pos[i]), - spa_debug_type_find_short_name(spa_type_audio_channel, - position[i])); + spa_type_audio_channel_make_short_name(position[i], + buf, sizeof(buf), "UNK")); } return 1; } diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c index 839a9fc24..f05bf48f3 100644 --- a/pipewire-jack/src/pipewire-jack.c +++ b/pipewire-jack/src/pipewire-jack.c @@ -102,6 +102,7 @@ struct notify { #define NOTIFY_TYPE_SHUTDOWN ((7<<4)|NOTIFY_ACTIVE_FLAG) #define NOTIFY_TYPE_LATENCY ((8<<4)|NOTIFY_ACTIVE_FLAG) #define NOTIFY_TYPE_TOTAL_LATENCY ((9<<4)|NOTIFY_ACTIVE_FLAG) +#define NOTIFY_TYPE_PORT_RENAME ((10<<4)|NOTIFY_ACTIVE_FLAG) int type; struct object *object; int arg1; @@ -171,6 +172,7 @@ struct object { } port_link; struct { unsigned long flags; + char old_name[REAL_JACK_PORT_NAME_SIZE+1]; char name[REAL_JACK_PORT_NAME_SIZE+1]; char alias1[REAL_JACK_PORT_NAME_SIZE+1]; char alias2[REAL_JACK_PORT_NAME_SIZE+1]; @@ -231,6 +233,13 @@ struct buffer { uint32_t n_mem; }; +struct mix_info { + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_control control; + const void *control_body; +}; + struct mix { struct spa_list link; struct spa_list port_link; @@ -245,6 +254,8 @@ struct mix { struct buffer buffers[MAX_BUFFERS]; uint32_t n_buffers; + struct mix_info mix_info; + unsigned int to_free:1; }; @@ -626,8 +637,8 @@ do_mix_set_io(struct spa_loop *loop, bool async, uint32_t seq, static inline void mix_set_io(struct mix *mix, void *data, size_t size) { struct io_info info = { .mix = mix, .data = data, .size = size }; - pw_data_loop_invoke(mix->port->client->loop, - do_mix_set_io, SPA_ID_INVALID, &info, sizeof(info), false, NULL); + pw_loop_locked(mix->port->client->loop->loop, + do_mix_set_io, SPA_ID_INVALID, &info, sizeof(info), NULL); } static void init_mix(struct mix *mix, uint32_t mix_id, struct port *port, uint32_t peer_id) @@ -1083,6 +1094,16 @@ static void on_notify_event(void *data, uint64_t count) notify->arg1, c->portregistration_arg); break; + case NOTIFY_TYPE_PORT_RENAME: + if (o->registered != notify->arg1) + break; + pw_log_debug("%p: port rename %u %s->%s", c, o->serial, + o->port.old_name, o->port.name); + do_callback(c, rename_callback, c->active, + o->serial, + o->port.old_name, o->port.name, + c->rename_arg); + break; case NOTIFY_TYPE_CONNECT: if (o->registered == notify->arg1) break; @@ -1183,6 +1204,9 @@ static int queue_notify(struct client *c, int type, struct object *o, int arg1, emit = c->portregistration_callback != NULL && o != NULL; o->visible = arg1; break; + case NOTIFY_TYPE_PORT_RENAME: + emit = c->rename_callback != NULL && o != NULL; + break; case NOTIFY_TYPE_CONNECT: emit = c->connect_callback != NULL && o != NULL; break; @@ -1477,7 +1501,8 @@ static inline int event_compare(uint8_t s1, uint8_t s2) return priotab[(s2>>4) & 7] - priotab[(s1>>4) & 7]; } -static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b) +static inline int event_sort(struct spa_pod_control *a, const void *abody, + struct spa_pod_control *b, const void *bbody) { if (a->offset < b->offset) return -1; @@ -1488,18 +1513,18 @@ static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control * switch(a->type) { case SPA_CONTROL_Midi: { - uint8_t *sa = SPA_POD_BODY(&a->value), *sb = SPA_POD_BODY(&b->value); + const uint8_t *sa = abody, *sb = bbody; if (SPA_POD_BODY_SIZE(&a->value) < 1 || SPA_POD_BODY_SIZE(&b->value) < 1) return 0; return event_compare(sa[0], sb[0]); } case SPA_CONTROL_UMP: { - uint32_t *sa = SPA_POD_BODY(&a->value), *sb = SPA_POD_BODY(&b->value); + const uint32_t *sa = abody, *sb = bbody; if (SPA_POD_BODY_SIZE(&a->value) < 4 || SPA_POD_BODY_SIZE(&b->value) < 4) return 0; - if ((sa[0] >> 28) != 2 || (sa[0] >> 28) != 4 || - (sb[0] >> 28) != 2 || (sb[0] >> 28) != 4) + if (((sa[0] >> 28) != 2 && (sa[0] >> 28) != 4) || + ((sb[0] >> 28) != 2 && (sb[0] >> 28) != 4)) return 0; return event_compare(sa[0] >> 16, sb[0] >> 16); } @@ -1538,7 +1563,7 @@ static inline jack_midi_data_t* midi_event_reserve(void *port_buffer, res = ev->inline_data; } else { mb->write_pos += data_size; - ev->byte_offset = mb->buffer_size - 1 - mb->write_pos; + ev->byte_offset = mb->buffer_size - mb->write_pos; res = SPA_PTROFF(mb, ev->byte_offset, uint8_t); } mb->event_count += 1; @@ -1546,69 +1571,91 @@ static inline jack_midi_data_t* midi_event_reserve(void *port_buffer, return res; } +static inline int midi_event_append(void *port_buffer, const jack_midi_data_t *data, size_t data_size) +{ + struct midi_buffer *mb = port_buffer; + struct midi_event *events = SPA_PTROFF(mb, sizeof(*mb), struct midi_event); + struct midi_event *ev; + size_t old_size; + uint8_t *old, *buf; + + ev = &events[--mb->event_count]; + mb->write_pos -= ev->size; + old_size = ev->size; + if (old_size <= MIDI_INLINE_MAX) + old = ev->inline_data; + else + old = SPA_PTROFF(mb, ev->byte_offset, uint8_t); + buf = midi_event_reserve(port_buffer, ev->time, old_size + data_size); + if (SPA_UNLIKELY(buf == NULL)) + return -ENOBUFS; + memmove(buf, old, old_size); + memcpy(buf+old_size, data, data_size); + return 0; +} + static inline int midi_event_write(void *port_buffer, jack_nframes_t time, const jack_midi_data_t *data, size_t data_size, bool fix) { jack_midi_data_t *retbuf = midi_event_reserve (port_buffer, time, data_size); - if (SPA_UNLIKELY(retbuf == NULL)) - return -ENOBUFS; + if (SPA_UNLIKELY(retbuf == NULL)) + return -ENOBUFS; memcpy (retbuf, data, data_size); if (fix) fix_midi_event(retbuf, data_size); return 0; } -static void convert_to_event(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi, bool fix, uint32_t type) +static void convert_to_event(struct mix_info **mix, uint32_t n_mix, void *midi, bool fix, uint32_t type) { - struct spa_pod_control *c[n_seq]; - uint64_t state = 0; uint32_t i; int res = 0; - - for (i = 0; i < n_seq; i++) - c[i] = spa_pod_control_first(&seq[i]->body); + bool in_sysex = false; while (true) { - struct spa_pod_control *next = NULL; + struct mix_info *next = NULL; uint32_t next_index = 0; + struct spa_pod_control *control; + size_t size; + uint8_t *data; + uint64_t state = 0; - for (i = 0; i < n_seq; i++) { - if (!spa_pod_control_is_inside(&seq[i]->body, - SPA_POD_BODY_SIZE(seq[i]), c[i])) - continue; - - if (next == NULL || event_sort(c[i], next) <= 0) { - next = c[i]; + for (i = 0; i < n_mix; i++) { + struct mix_info *m = mix[i]; + if (next == NULL || event_sort(&m->control, m->control_body, + &next->control, next->control_body) <= 0) { + next = m; next_index = i; } } if (SPA_UNLIKELY(next == NULL)) break; - switch(next->type) { + control = &next->control; + data = (uint8_t*)next->control_body; + size = SPA_POD_BODY_SIZE(&control->value); + + switch(control->type) { case SPA_CONTROL_OSC: if (!TYPE_ID_CAN_OSC(type)) break; SPA_FALLTHROUGH; case SPA_CONTROL_Midi: { - uint8_t *data = SPA_POD_BODY(&next->value); - size_t size = SPA_POD_BODY_SIZE(&next->value); - if (type == TYPE_ID_UMP) { while (size > 0) { uint32_t ump[4]; int ump_size = spa_ump_from_midi(&data, &size, ump, sizeof(ump), 0, &state); if (ump_size <= 0) break; - if ((res = midi_event_write(midi, next->offset, + if ((res = midi_event_write(midi, control->offset, (uint8_t*)ump, ump_size, false)) < 0) break; } } else { - res = midi_event_write(midi, next->offset, data, size, fix); + res = midi_event_write(midi, control->offset, data, size, fix); } if (res < 0) pw_log_warn("midi %p: can't write event: %s", midi, @@ -1617,25 +1664,43 @@ static void convert_to_event(struct spa_pod_sequence **seq, uint32_t n_seq, void } case SPA_CONTROL_UMP: { - void *data = SPA_POD_BODY(&next->value); - size_t size = SPA_POD_BODY_SIZE(&next->value); - uint8_t ev[32]; - if (type == TYPE_ID_MIDI) { - int ev_size = spa_ump_to_midi(data, size, ev, sizeof(ev)); - if (ev_size <= 0) - break; - size = ev_size; - data = ev; - } else if (type != TYPE_ID_UMP) - break; + uint8_t ev[32]; + const uint32_t *d = (uint32_t*)data; - if ((res = midi_event_write(midi, next->offset, data, size, fix)) < 0) + while (size > 0) { + bool was_sysex = in_sysex; + int ev_size = spa_ump_to_midi(&d, &size, ev, sizeof(ev), &state); + if (ev_size <= 0) + break; + + if (!in_sysex && ev[0] == 0xf0) + in_sysex = true; + if (in_sysex && ev[ev_size-1] == 0xf7) + in_sysex = false; + + if (was_sysex) + res = midi_event_append(midi, ev, ev_size); + else + res = midi_event_write(midi, control->offset, ev, ev_size, fix); + if (res < 0) + break; + + } + } else if (type == TYPE_ID_UMP) { + res = midi_event_write(midi, control->offset, data, size, fix); + } + if (res < 0) pw_log_warn("midi %p: can't write event: %s", midi, spa_strerror(res)); + break; } } - c[next_index] = spa_pod_control_next(c[next_index]); + if (spa_pod_parser_get_control_body(&next->parser, + &next->control, &next->control_body) < 0) { + spa_pod_parser_pop(&next->parser, &next->frame); + mix[next_index] = mix[--n_mix]; + } } } @@ -2459,16 +2524,16 @@ static int client_node_command(void *data, const struct spa_command *command) case SPA_NODE_COMMAND_Suspend: case SPA_NODE_COMMAND_Pause: if (c->started) { - pw_data_loop_invoke(c->loop, - do_unprepare_client, SPA_ID_INVALID, NULL, 0, false, c); + pw_loop_locked(c->loop->loop, + do_unprepare_client, SPA_ID_INVALID, NULL, 0, c); c->started = false; } break; case SPA_NODE_COMMAND_Start: if (!c->started) { - pw_data_loop_invoke(c->loop, - do_prepare_client, SPA_ID_INVALID, NULL, 0, false, c); + pw_loop_locked(c->loop->loop, + do_prepare_client, SPA_ID_INVALID, NULL, 0, c); c->started = true; } break; @@ -3242,8 +3307,8 @@ static int client_node_set_activation(void *data, link->trigger = link->activation->server_version < 1 ? trigger_link_v0 : trigger_link_v1; spa_list_append(&c->links, &link->link); - pw_data_loop_invoke(c->loop, - do_add_link, SPA_ID_INVALID, NULL, 0, false, link); + pw_loop_locked(c->loop->loop, + do_add_link, SPA_ID_INVALID, NULL, 0, link); } else { link = find_activation(&c->links, node_id); @@ -3253,8 +3318,8 @@ static int client_node_set_activation(void *data, } spa_list_remove(&link->link); - pw_data_loop_invoke(c->loop, - do_remove_link, SPA_ID_INVALID, NULL, 0, false, link); + pw_loop_locked(c->loop->loop, + do_remove_link, SPA_ID_INVALID, NULL, 0, link); queue_free_link(c, link); } @@ -3429,24 +3494,6 @@ static const char* type_to_string(jack_port_type_id_t type_id) } } -static const char* type_to_format_dsp(jack_port_type_id_t type_id) -{ - switch(type_id) { - case TYPE_ID_AUDIO: - return JACK_DEFAULT_AUDIO_TYPE; - case TYPE_ID_VIDEO: - return JACK_DEFAULT_VIDEO_TYPE; - case TYPE_ID_OSC: - return JACK_DEFAULT_OSC_TYPE; - case TYPE_ID_MIDI: - return JACK_DEFAULT_MIDI_TYPE; - case TYPE_ID_UMP: - return JACK_DEFAULT_UMP_TYPE; - default: - return NULL; - } -} - static bool type_is_dsp(jack_port_type_id_t type_id) { switch(type_id) { @@ -3638,6 +3685,67 @@ static const struct pw_node_events node_events = { .info = node_info, }; +#define FILTER_NAME " ()[].:*$" +#define FILTER_PORT " ()[].*$" + +static void filter_name(char *str, const char *filter, char filter_char) +{ + char *p; + for (p = str; *p; p++) { + if (strchr(filter, *p) != NULL) + *p = filter_char; + } +} + +static int update_port_name(struct object *o, const char *name) +{ + struct object *ot = o->port.node, *op; + struct client *c = o->client; + char tmp[REAL_JACK_PORT_NAME_SIZE+1]; + char port_name[REAL_JACK_PORT_NAME_SIZE+1]; + + if (o->port.is_monitor && !c->merge_monitor) + snprintf(tmp, sizeof(tmp), "%.*s%s:%s", + (int)(JACK_CLIENT_NAME_SIZE-(sizeof(MONITOR_EXT)-1)), + ot->node.name, MONITOR_EXT, name); + else + snprintf(tmp, sizeof(tmp), "%s:%s", ot->node.name, name); + + if (c->filter_name) + filter_name(tmp, FILTER_PORT, c->filter_char); + + op = find_port_by_name(c, tmp); + if (op != NULL && op != o) + snprintf(port_name, sizeof(port_name), "%.*s-%u", + (int)(sizeof(tmp)-11), tmp, o->serial); + else + snprintf(port_name, sizeof(port_name), "%s", tmp); + + if (spa_streq(port_name, o->port.name)) + return 0; + + strcpy(o->port.old_name, o->port.name); + strcpy(o->port.name, port_name); + return 1; +} + +static void port_info(void *data, const struct pw_port_info *info) +{ + struct object *o = data; + struct client *c = o->client; + + if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS) { + const char *str = spa_dict_lookup(info->props, PW_KEY_PORT_NAME); + if (str != NULL) { + if (update_port_name(o, str) > 0) { + pw_log_info("%p: port rename %u %s->%s", c, o->serial, + o->port.old_name, o->port.name); + queue_notify(c, NOTIFY_TYPE_PORT_RENAME, o, 1, NULL); + } + } + } +} + static void port_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) @@ -3660,27 +3768,16 @@ static void port_param(void *data, int seq, static const struct pw_port_events port_events = { PW_VERSION_PORT_EVENTS, + .info = port_info, .param = port_param, }; -#define FILTER_NAME " ()[].:*$" -#define FILTER_PORT " ()[].*$" - -static void filter_name(char *str, const char *filter, char filter_char) -{ - char *p; - for (p = str; *p; p++) { - if (strchr(filter, *p) != NULL) - *p = filter_char; - } -} - static void registry_event_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { struct client *c = (struct client *) data; - struct object *o, *ot, *op; + struct object *o, *ot; const char *str; bool do_emit = true, do_sync = false; uint32_t serial; @@ -3746,6 +3843,8 @@ static void registry_event_global(void *data, uint32_t id, } if (str == NULL) str = node_name; + if (str == NULL) + str = spa_dict_lookup(props, PW_KEY_OBJECT_PATH); if (str == NULL) str = "node"; @@ -3801,6 +3900,7 @@ static void registry_event_global(void *data, uint32_t id, uint32_t node_id; bool is_monitor = false; char tmp[REAL_JACK_PORT_NAME_SIZE+1]; + const char *name; if ((str = spa_dict_lookup(props, PW_KEY_FORMAT_DSP)) == NULL) str = "other"; @@ -3816,7 +3916,7 @@ static void registry_event_global(void *data, uint32_t id, spa_strstartswith(str, "jack:flags:")) flags = atoi(str+11); - if ((str = spa_dict_lookup(props, PW_KEY_PORT_NAME)) == NULL) + if ((name = spa_dict_lookup(props, PW_KEY_PORT_NAME)) == NULL) goto exit; if (type_id == TYPE_ID_UMP && c->flag_midi2) @@ -3852,7 +3952,7 @@ static void registry_event_global(void *data, uint32_t id, o = NULL; if (node_id == c->node_id) { - snprintf(tmp, sizeof(tmp), "%s:%s", c->name, str); + snprintf(tmp, sizeof(tmp), "%s:%s", c->name, name); o = find_port_by_name(c, tmp); if (o != NULL) pw_log_info("%p: %s found our port %p", c, tmp, o); @@ -3870,6 +3970,7 @@ static void registry_event_global(void *data, uint32_t id, o->port.node = ot; o->port.latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); o->port.latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + o->port.type_id = type_id; do_emit = node_is_active(c, ot); @@ -3891,27 +3992,12 @@ static void registry_event_global(void *data, uint32_t id, pthread_mutex_lock(&c->context.lock); spa_list_append(&c->context.objects, &o->link); pthread_mutex_unlock(&c->context.lock); - - if (is_monitor && !c->merge_monitor) - snprintf(tmp, sizeof(tmp), "%.*s%s:%s", - (int)(JACK_CLIENT_NAME_SIZE-(sizeof(MONITOR_EXT)-1)), - ot->node.name, MONITOR_EXT, str); - else - snprintf(tmp, sizeof(tmp), "%s:%s", ot->node.name, str); - - if (c->filter_name) - filter_name(tmp, FILTER_PORT, c->filter_char); - - op = find_port_by_name(c, tmp); - if (op != NULL) - snprintf(o->port.name, sizeof(o->port.name), "%.*s-%u", - (int)(sizeof(tmp)-11), tmp, serial); - else - snprintf(o->port.name, sizeof(o->port.name), "%s", tmp); - - o->port.type_id = type_id; } + o->port.flags = flags; + o->port.node_id = node_id; + o->port.is_monitor = is_monitor; + if (c->fill_aliases) { if ((str = spa_dict_lookup(props, PW_KEY_OBJECT_PATH)) != NULL) snprintf(o->port.alias1, sizeof(o->port.alias1), "%s", str); @@ -3919,7 +4005,6 @@ static void registry_event_global(void *data, uint32_t id, if ((str = spa_dict_lookup(props, PW_KEY_PORT_ALIAS)) != NULL) snprintf(o->port.alias2, sizeof(o->port.alias2), "%s", str); } - if ((str = spa_dict_lookup(props, PW_KEY_PORT_ID)) != NULL) { o->port.system_id = atoi(str); snprintf(o->port.system, sizeof(o->port.system), "system:%s_%d", @@ -3928,9 +4013,8 @@ static void registry_event_global(void *data, uint32_t id, o->port.system_id+1); } - o->port.flags = flags; - o->port.node_id = node_id; - o->port.is_monitor = is_monitor; + if (node_id != c->node_id) + update_port_name(o, name); pw_log_debug("%p: %p add port %d name:%s %d", c, o, id, o->port.name, type_id); @@ -5507,7 +5591,7 @@ jack_port_t * jack_port_register (jack_client_t *client, spa_list_init(&p->mix); - pw_properties_set(p->props, PW_KEY_FORMAT_DSP, type_to_format_dsp(type_id)); + pw_properties_set(p->props, PW_KEY_FORMAT_DSP, type_to_string(type_id)); pw_properties_set(p->props, PW_KEY_PORT_NAME, port_name); if (flags > 0x1f) { pw_properties_setf(p->props, PW_KEY_PORT_EXTRA, @@ -5719,13 +5803,15 @@ static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames) struct mix *mix; void *ptr = p->emptyptr; struct midi_buffer *mb = (struct midi_buffer*)midi_scratch; - struct spa_pod_sequence *seq[MAX_MIX]; - uint32_t n_seq = 0; + struct mix_info *mix_info[MAX_MIX]; + uint32_t n_mix_info = 0; spa_list_for_each(mix, &p->mix, port_link) { struct spa_data *d; struct buffer *b; - void *pod; + struct mix_info *mi = &mix->mix_info; + struct spa_pod_sequence seq; + const void *seq_body; if (mix->id == SPA_ID_INVALID) continue; @@ -5737,25 +5823,28 @@ static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames) continue; d = &b->datas[0]; + spa_pod_parser_init_from_data(&mi->parser, d->data, d->maxsize, + d->chunk->offset, d->chunk->size); + if (spa_pod_parser_push_sequence_body(&mi->parser, + &mi->frame, &seq, &seq_body) < 0) + continue; + if (spa_pod_parser_get_control_body(&mi->parser, + &mi->control, &mi->control_body) < 0) + continue; - if ((pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size)) == NULL) - continue; - if (!spa_pod_is_sequence(pod)) - continue; - - seq[n_seq++] = pod; - if (n_seq == MAX_MIX) + mix_info[n_mix_info++] = mi; + if (n_mix_info == MAX_MIX) break; } midi_init_buffer(mb, MIDI_SCRATCH_FRAMES, frames); /* first convert to a thread local scratch buffer, then memcpy into * the per port buffer. This makes it possible to call this function concurrently * but also have different pointers per port */ - convert_to_event(seq, n_seq, mb, p->client->fix_midi_events, p->object->port.type_id); + convert_to_event(mix_info, n_mix_info, mb, p->client->fix_midi_events, p->object->port.type_id); memcpy(ptr, mb, sizeof(struct midi_buffer) + (mb->event_count * sizeof(struct midi_event))); - if (mb->write_pos) { - size_t offs = mb->buffer_size - 1 - mb->write_pos; + if (mb->write_pos > 0) { + size_t offs = mb->buffer_size - mb->write_pos; memcpy(SPA_PTROFF(ptr, offs, void), SPA_PTROFF(mb, offs, void), mb->write_pos); } return ptr; @@ -5818,21 +5907,26 @@ void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames) goto done; if (TYPE_ID_IS_EVENT(o->port.type_id)) { - struct spa_pod_sequence *seq[1]; + struct mix_info *mix_info[1], mi; struct spa_data *d; - void *pod; + struct spa_pod_sequence seq; + const void *seq_body; ptr = midi_scratch; midi_init_buffer(ptr, MIDI_SCRATCH_FRAMES, frames); d = &b->datas[0]; - if ((pod = spa_pod_from_data(d->data, d->maxsize, - d->chunk->offset, d->chunk->size)) == NULL) + spa_pod_parser_init_from_data(&mi.parser, d->data, d->maxsize, + d->chunk->offset, d->chunk->size); + if (spa_pod_parser_push_sequence_body(&mi.parser, + &mi.frame, &seq, &seq_body) < 0) goto done; - if (!spa_pod_is_sequence(pod)) + if (spa_pod_parser_get_control_body(&mi.parser, + &mi.control, &mi.control_body) < 0) goto done; - seq[0] = pod; - convert_to_event(seq, 1, ptr, c->fix_midi_events, o->port.type_id); + + mix_info[0] = &mi; + convert_to_event(mix_info, 1, ptr, c->fix_midi_events, o->port.type_id); } else { ptr = get_buffer_data(b, frames); } diff --git a/pipewire-v4l2/src/pipewire-v4l2.c b/pipewire-v4l2/src/pipewire-v4l2.c index 691c8ec37..8fc07151a 100644 --- a/pipewire-v4l2/src/pipewire-v4l2.c +++ b/pipewire-v4l2/src/pipewire-v4l2.c @@ -772,6 +772,19 @@ static int v4l2_dup(int oldfd) return do_dup(oldfd, FD_MAP_DUP); } +/* Deferred PipeWire init (called on first device access) */ +static void pipewire_init_func(void) +{ + pw_init(NULL, NULL); + PW_LOG_TOPIC_INIT(v4l2_log_topic); +} + +static void ensure_pipewire_init(void) +{ + static pthread_once_t pipewire_once = PTHREAD_ONCE_INIT; + pthread_once(&pipewire_once, pipewire_init_func); +} + static int v4l2_openat(int dirfd, const char *path, int oflag, mode_t mode) { int res, flags; @@ -795,6 +808,8 @@ static int v4l2_openat(int dirfd, const char *path, int oflag, mode_t mode) if (passthrough) return globals.old_fops.openat(dirfd, path, oflag, mode); + ensure_pipewire_init(); + pw_log_info("path:%s oflag:%d mode:%d", path, oflag, mode); if ((file = find_file_by_dev(dev_id)) != NULL) { @@ -1385,7 +1400,6 @@ static int vidioc_enum_framesizes(struct file *file, struct v4l2_frmsizeenum *ar spa_list_for_each(p, &g->param_list, link) { const struct format_info *fi; uint32_t media_type, media_subtype, format; - struct spa_rectangle size; if (p->id != SPA_PARAM_EnumFormat || p->param == NULL) continue; @@ -1409,22 +1423,96 @@ static int vidioc_enum_framesizes(struct file *file, struct v4l2_frmsizeenum *ar if (fi->fourcc != arg->pixel_format) continue; - if (spa_pod_parse_object(p->param, - SPA_TYPE_OBJECT_Format, NULL, - SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&size)) < 0) + + const struct spa_pod_prop *size_prop = spa_pod_find_prop(p->param, NULL, SPA_FORMAT_VIDEO_size); + if (!size_prop) continue; - arg->type = V4L2_FRMSIZE_TYPE_DISCRETE; - arg->discrete.width = size.width; - arg->discrete.height = size.height; + uint32_t n_sizes, choice; + const struct spa_pod *size_pods = spa_pod_get_values(&size_prop->value, &n_sizes, &choice); + if (!size_pods || n_sizes <= 0) + continue; + + const struct spa_rectangle *sizes = SPA_POD_BODY_CONST(size_pods); + if (size_pods->type != SPA_TYPE_Rectangle || size_pods->size != sizeof(*sizes)) + continue; + + switch (choice) { + case SPA_CHOICE_Enum: + n_sizes -= 1; + sizes += 1; + SPA_FALLTHROUGH; + case SPA_CHOICE_None: + arg->type = V4L2_FRMSIZE_TYPE_DISCRETE; + + for (size_t i = 0; i < n_sizes; i++, count++) { + arg->discrete.width = sizes[i].width; + arg->discrete.height = sizes[i].height; + + pw_log_debug("count:%u %.4s %ux%u", count, (char*)&fi->fourcc, + arg->discrete.width, arg->discrete.height); + + if (count == arg->index) { + found = true; + break; + } + } - pw_log_debug("count:%d %.4s %dx%d", count, (char*)&fi->fourcc, - size.width, size.height); - if (count == arg->index) { - found = true; break; + case SPA_CHOICE_Range: + if (n_sizes < 3) + continue; + + arg->type = V4L2_FRMSIZE_TYPE_CONTINUOUS; + arg->stepwise.min_width = sizes[1].width; + arg->stepwise.min_height = sizes[1].height; + arg->stepwise.max_width = sizes[2].width; + arg->stepwise.max_height = sizes[2].height; + arg->stepwise.step_width = arg->stepwise.step_height = 1; + + pw_log_debug("count:%u %.4s (%ux%u)-(%ux%u)", count, (char*)&fi->fourcc, + arg->stepwise.min_width, arg->stepwise.min_height, + arg->stepwise.max_width, arg->stepwise.max_height); + + if (count == arg->index) { + found = true; + break; + } + + count++; + + break; + case SPA_CHOICE_Step: + if (n_sizes < 4) + continue; + + arg->type = V4L2_FRMSIZE_TYPE_CONTINUOUS; + arg->stepwise.min_width = sizes[1].width; + arg->stepwise.min_height = sizes[1].height; + arg->stepwise.max_width = sizes[2].width; + arg->stepwise.max_height = sizes[2].height; + arg->stepwise.step_width = sizes[3].width; + arg->stepwise.step_height = sizes[3].height; + + pw_log_debug("count:%u %.4s (%ux%u)-(%ux%u)/(+%u,%u)", count, (char*)&fi->fourcc, + arg->stepwise.min_width, arg->stepwise.min_height, + arg->stepwise.min_width, arg->stepwise.min_height, + arg->stepwise.step_width, arg->stepwise.step_height); + + if (count == arg->index) { + found = true; + break; + } + + count++; + + break; + default: + continue; } - count++; + + if (found) + break; } pw_thread_loop_unlock(file->loop); @@ -2100,7 +2188,7 @@ static struct { { V4L2_CID_SATURATION, SPA_PROP_saturation }, { V4L2_CID_HUE, SPA_PROP_hue }, { V4L2_CID_GAMMA, SPA_PROP_gamma }, - { V4L2_CID_EXPOSURE, SPA_PROP_exposure }, + { V4L2_CID_EXPOSURE_ABSOLUTE, SPA_PROP_exposure }, { V4L2_CID_GAIN, SPA_PROP_gain }, { V4L2_CID_SHARPNESS, SPA_PROP_sharpness }, }; @@ -2311,6 +2399,8 @@ static int vidioc_s_ctrl(struct file *file, struct v4l2_control *arg) struct spa_pod_frame f[1]; struct spa_pod *param; pod = spa_pod_get_values(type, &n_vals, &choice); + if (n_vals < 1) + break; spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Props, SPA_PARAM_Props); @@ -2563,10 +2653,14 @@ static void initialize(void) globals.old_fops.ioctl = dlsym(RTLD_NEXT, "ioctl"); globals.old_fops.mmap = dlsym(RTLD_NEXT, "mmap64"); globals.old_fops.munmap = dlsym(RTLD_NEXT, "munmap"); - - pw_init(NULL, NULL); - PW_LOG_TOPIC_INIT(v4l2_log_topic); - + /* NOTE: + * We avoid calling pw_init() here (constructor/early init path) because + * that can deadlock in certain host processes (e.g. Zoom >= 5.0) when + * the preload causes PipeWire initialisation to run too early. + * + * PipeWire initialisation (pw_init + PW_LOG_TOPIC_INIT) is deferred + * to ensure it runs on-demand in the first actual V4L2 open call. + */ pthread_mutex_init(&globals.lock, NULL); pw_array_init(&globals.file_maps, 1024); pw_array_init(&globals.fd_maps, 256); diff --git a/po/LINGUAS b/po/LINGUAS index a6b999f41..8ebdbea2e 100644 --- a/po/LINGUAS +++ b/po/LINGUAS @@ -1,4 +1,5 @@ af +ar as be bg diff --git a/po/ar.po b/po/ar.po new file mode 100644 index 000000000..348a55f3f --- /dev/null +++ b/po/ar.po @@ -0,0 +1,797 @@ +# Arabic translation of PipeWire +# This file is distributed under the same license as the pipewire package. +# +# SPDX-FileCopyrightText: 2025 r +msgid "" +msgstr "" +"Project-Id-Version: pipewire\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/issues/" +"new\n" +"POT-Creation-Date: 2024-02-25 03:43+0300\n" +"PO-Revision-Date: 2025-06-22 13:09+0200\n" +"Last-Translator: r \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && " +"n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 25.04.0\n" + +#: src/daemon/pipewire.c:26 +#, c-format +msgid "" +"%s [options]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -c, --config Load config (Default %s)\n" +msgstr "" +"%s [خيارات]\n" +" -h, --help إظهار هذه المساعدة\n" +" --version إظهار الإصدار\n" +" -c, --config تحميل التضبيط (الافتراضي %s)\n" + +#: src/daemon/pipewire.desktop.in:4 +msgid "PipeWire Media System" +msgstr "نظام الوسائط بايب‌واير" + +#: src/daemon/pipewire.desktop.in:5 +msgid "Start the PipeWire Media System" +msgstr "ابدأ نظام الوسائط بايب‌واير" + +#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 +#, c-format +msgid "Tunnel to %s%s%s" +msgstr "نفّق إلى %s%s%s" + +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "إخراج وهمي" + +#: src/modules/module-pulse-tunnel.c:774 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "نفّق ل %s@%s" + +#: src/modules/module-zeroconf-discover.c:315 +msgid "Unknown device" +msgstr "جهاز غير معروف" + +#: src/modules/module-zeroconf-discover.c:327 +#, c-format +msgid "%s on %s@%s" +msgstr "%s على %s@%s" + +#: src/modules/module-zeroconf-discover.c:331 +#, c-format +msgid "%s on %s" +msgstr "%s على %s" + +#: src/tools/pw-cat.c:991 +#, c-format +msgid "" +"%s [options] [|-]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -v, --verbose Enable verbose operations\n" +"\n" +msgstr "" +"%s [خيارات] [|-]\n" +" -h, --help إظهار هذه المساعدة\n" +" --version إظهار الإصدار\n" +" -v, --verbose تمكين العمليات المطولة\n" +"\n" + +#: src/tools/pw-cat.c:998 +#, c-format +msgid "" +" -R, --remote Remote daemon name\n" +" --media-type Set media type (default %s)\n" +" --media-category Set media category (default %s)\n" +" --media-role Set media role (default %s)\n" +" --target Set node target serial or name " +"(default %s)\n" +" 0 means don't link\n" +" --latency Set node latency (default %s)\n" +" Xunit (unit = s, ms, us, ns)\n" +" or direct samples (256)\n" +" the rate is the one of the source " +"file\n" +" -P --properties Set node properties\n" +"\n" +msgstr "" +"-R, --remote اسم العملية الخفية البعيدة\n" +" --media-type تعيين نوع الوسائط (الافتراضي %s)\n" +" --media-category تعيين فئة الوسائط (الافتراضي %s)\n" +" --media-role تعيين دور الوسائط (الافتراضي %s)\n" +" --target تعيين المسلسل أو اسم هدف العُقدة " +"\n (الافتراضي %s)" +" 0 يعني عدم الربط\n" +" --latency تعيين زمن انتقال العُقدة (الافتراضي %s" +")\n" +" وحدة X (الوحدة = s, ms, us, ns)\n" +" أو عينات مباشرة (256)\n" +" المعدل هو معدل ملف المصدر\n" +" -P --properties تعيين خصائص العُقدة\n" +"\n" + +#: src/tools/pw-cat.c:1016 +#, c-format +msgid "" +" --rate Sample rate (req. for rec) (default " +"%u)\n" +" --channels Number of channels (req. for rec) " +"(default %u)\n" +" --channel-map Channel map\n" +" one of: \"stereo\", " +"\"surround-51\",... or\n" +" comma separated list of channel " +"names: eg. \"FL,FR\"\n" +" --format Sample format %s (req. for rec) " +"(default %s)\n" +" --volume Stream volume 0-1.0 (default %.3f)\n" +" -q --quality Resampler quality (0 - 15) (default " +"%d)\n" +"\n" +msgstr "" +"--rate معدل العينة (مطلوب للتسجيل) (الافتراضي %u)\n" +" --channels عدد القنوات (مطلوب للتسجيل) (الافتراضي" +" %u)\n" +" --channel-map خريطة القنوات\n" +" أحد الخيارات: \"مِجساميّ\", \"محيط" +"ي-51\",... أو\n" +" قائمة بأسماء القنوات مفصولة بفاصلة" +": مثال \"FL,FR\"\n" +" --format تنسيق العينة %s (مطلوب للتسجيل) (الافت" +"راضي %s)\n" +" --volume حجم البث 0-1.0 (الافتراضي %.3f)\n" +" -q --quality جودة إعادة التشكيل (0 - 15) (الافتراضي" +" %d)\n" +"\n" + +#: src/tools/pw-cat.c:1033 +msgid "" +" -p, --playback Playback mode\n" +" -r, --record Recording mode\n" +" -m, --midi Midi mode\n" +" -d, --dsd DSD mode\n" +" -o, --encoded Encoded mode\n" +"\n" +msgstr "" +"-p, --playback وضع التشغيل\n" +" -r, --record وضع التسجيل\n" +" -m, --midi وضع Midi\n" +" -d, --dsd وضع DSD\n" +" -o, --encoded الوضع المرمّز\n" +"\n" + +#: src/tools/pw-cli.c:2252 +#, c-format +msgid "" +"%s [options] [command]\n" +" -h, --help Show this help\n" +" --version Show version\n" +" -d, --daemon Start as daemon (Default false)\n" +" -r, --remote Remote daemon name\n" +" -m, --monitor Monitor activity\n" +"\n" +msgstr "" +"%s [خيارات] [أمر]\n" +" -h, --help إظهار هذه المساعدة\n" +" --version إظهار الإصدار\n" +" -d, --daemon بدء كخفي (افتراضي خطأ)\n" +" -r, --remote اسم الخفي البعيد\n" +" -m, --monitor مراقبة النشاط\n" +"\n" + +#: spa/plugins/alsa/acp/acp.c:327 +msgid "Pro Audio" +msgstr "صوت احترافي" + +#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/bluez5/bluez5-device.c:1701 +msgid "Off" +msgstr "معطل" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2652 +msgid "Input" +msgstr "الإدخال" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2653 +msgid "Docking Station Input" +msgstr "إدخال محطة الإرساء" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +msgid "Docking Station Microphone" +msgstr "ميكروفون محطة الإرساء" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2655 +msgid "Docking Station Line In" +msgstr "مدخل خطي محط الارساء" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2656 +#: spa/plugins/alsa/acp/alsa-mixer.c:2747 +msgid "Line In" +msgstr "مدخل خطي" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2657 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:1989 +msgid "Microphone" +msgstr "ميكروفون" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2658 +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +msgid "Front Microphone" +msgstr "ميكروفون أمامي" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2659 +#: spa/plugins/alsa/acp/alsa-mixer.c:2743 +msgid "Rear Microphone" +msgstr "ميكروفون خلفي" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +msgid "External Microphone" +msgstr "ميكروفون خارجي" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2661 +#: spa/plugins/alsa/acp/alsa-mixer.c:2745 +msgid "Internal Microphone" +msgstr "ميكروفون داخلي" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2662 +#: spa/plugins/alsa/acp/alsa-mixer.c:2748 +msgid "Radio" +msgstr "راديو" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2663 +#: spa/plugins/alsa/acp/alsa-mixer.c:2749 +msgid "Video" +msgstr "مرئيّ" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2664 +msgid "Automatic Gain Control" +msgstr "التحكم التلقائي في الكسب" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +msgid "No Automatic Gain Control" +msgstr "دون التحكم التلقائي في الكسب" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +msgid "Boost" +msgstr "تعزيز" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +msgid "No Boost" +msgstr "دون تعزيز" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +msgid "Amplifier" +msgstr "" +"" +"مُضَخِّم" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +msgid "No Amplifier" +msgstr "دون مُضَخِّم" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +msgid "Bass Boost" +msgstr "تعزيز القرار" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +msgid "No Bass Boost" +msgstr "دون تعزيز القرار" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2672 +#: spa/plugins/bluez5/bluez5-device.c:1995 +msgid "Speaker" +msgstr "مكبر صوت" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2673 +#: spa/plugins/alsa/acp/alsa-mixer.c:2751 +msgid "Headphones" +msgstr "سماعات الأذن" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 +msgid "Analog Input" +msgstr "إدخال التناظري" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2744 +msgid "Dock Microphone" +msgstr "ميكروفون محطة الارساء" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +msgid "Headset Microphone" +msgstr "ميكروفون سماعة الرأس" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +msgid "Analog Output" +msgstr "إخراج تناظري" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +msgid "Headphones 2" +msgstr "سماعات الأذن 2" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +msgid "Headphones Mono Output" +msgstr "سماعات الأذن أحادية الإخراج" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +msgid "Line Out" +msgstr "مخرج خطي" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +msgid "Analog Mono Output" +msgstr "إخراج أحادي تناظري" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +msgid "Speakers" +msgstr "مكبرات الصوت" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2757 +msgid "HDMI / DisplayPort" +msgstr "HDMI / ديسبلاي بورت" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2758 +msgid "Digital Output (S/PDIF)" +msgstr "إخراج الرقمي (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2759 +msgid "Digital Input (S/PDIF)" +msgstr "إدخال الرقمي (S/PDIF)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2760 +msgid "Multichannel Input" +msgstr "إدخال متعدد القنوات" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2761 +msgid "Multichannel Output" +msgstr "إخراج متعدد القنوات" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +msgid "Game Output" +msgstr "إخراج اللعبة" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2763 +#: spa/plugins/alsa/acp/alsa-mixer.c:2764 +msgid "Chat Output" +msgstr "إخراج المحادثة" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +msgid "Chat Input" +msgstr "إدخال المحادثة" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2766 +msgid "Virtual Surround 7.1" +msgstr "محيطي تخيلي 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4456 +msgid "Analog Mono" +msgstr "تناظري أحادي" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +msgid "Analog Mono (Left)" +msgstr "تناظري أحادي (يسار)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +msgid "Analog Mono (Right)" +msgstr "تناظري أحادي (يمين)" + +#. Note: Not translated to "Analog Stereo Input", because the source +#. * name gets "Input" appended to it automatically, so adding "Input" +#. * here would lead to the source name to become "Analog Stereo Input +#. * Input". The same logic applies to analog-stereo-output, +#. * multichannel-input and multichannel-output. +#: spa/plugins/alsa/acp/alsa-mixer.c:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4467 +#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +msgid "Analog Stereo" +msgstr "مِجْساميّ تناظري" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +msgid "Mono" +msgstr "أحادي" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +msgid "Stereo" +msgstr "مِجْساميّ" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/bluez5/bluez5-device.c:1977 +msgid "Headset" +msgstr "سماعة الرأس" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +msgid "Speakerphone" +msgstr "مكبر صوت الجوّال" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +msgid "Multichannel" +msgstr "متعدد القنوات" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +msgid "Analog Surround 2.1" +msgstr "تناظري محيطي 2.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +msgid "Analog Surround 3.0" +msgstr "تناظري محيطي 3.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +msgid "Analog Surround 3.1" +msgstr "تناظري محيطي 3.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +msgid "Analog Surround 4.0" +msgstr "تناظري محيطي 4.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +msgid "Analog Surround 4.1" +msgstr "تناظري محيطي 4.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +msgid "Analog Surround 5.0" +msgstr "تناظري محيطي 5.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +msgid "Analog Surround 5.1" +msgstr "تناظري محيطي 5.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +msgid "Analog Surround 6.0" +msgstr "تناظري محيطي 6.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +msgid "Analog Surround 6.1" +msgstr "تناظري محيطي 6.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +msgid "Analog Surround 7.0" +msgstr "تناظري محيطي 7.0" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +msgid "Analog Surround 7.1" +msgstr "تناظري محيطي 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +msgid "Digital Stereo (IEC958)" +msgstr "مِجْساميّ رقمي (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +msgid "Digital Surround 4.0 (IEC958/AC3)" +msgstr "محيطي رقمي 4.0 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +msgid "Digital Surround 5.1 (IEC958/AC3)" +msgstr "محيطي رقمي 5.1 (IEC958/AC3)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +msgid "Digital Surround 5.1 (IEC958/DTS)" +msgstr "محيطي رقمي 5.1 (IEC958/DTS)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +msgid "Digital Stereo (HDMI)" +msgstr "مِجْساميّ رقمي (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +msgid "Digital Surround 5.1 (HDMI)" +msgstr "محيطي رقمي 5.1 (HDMI)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +msgid "Chat" +msgstr "محادثة" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +msgid "Game" +msgstr "لعبة" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +msgid "Analog Mono Duplex" +msgstr "تناظري أحادي مزدوج" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +msgid "Analog Stereo Duplex" +msgstr "مِجْساميّ تناظري مزدوج" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +msgid "Digital Stereo Duplex (IEC958)" +msgstr "مِجْساميّ رقمي مزدوج (IEC958)" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +msgid "Multichannel Duplex" +msgstr "مزدوج متعدد القنوات" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +msgid "Stereo Duplex" +msgstr "مِجساميّ مزدوج" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +msgid "Mono Chat + 7.1 Surround" +msgstr "محادثة أحادية + محيطي 7.1" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#, c-format +msgid "%s Output" +msgstr "إخراج %s" + +#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#, c-format +msgid "%s Input" +msgstr "إدخال %s" + +#: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314 +#, c-format +msgid "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " +"ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgid_plural "" +"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu " +"ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr[0] "" +"أرجعت الدالة snd_pcm_avail() قيمة كبيرة بشكل استثنائي: %lu بايت " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[1] "" +"أرجعت الدالة snd_pcm_avail() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[2] "" +"أرجعت الدالة snd_pcm_avail() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[3] "" +"أرجعت الدالة snd_pcm_avail() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[4] "" +"أرجعت الدالة snd_pcm_avail() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[5] "" +"أرجعت الدالة snd_pcm_avail() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." + +#: spa/plugins/alsa/acp/alsa-util.c:1286 +#, c-format +msgid "" +"snd_pcm_delay() returned a value that is exceptionally large: %li byte " +"(%s%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgid_plural "" +"snd_pcm_delay() returned a value that is exceptionally large: %li bytes " +"(%s%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr[0] "" +"أرجعت الدالة snd_pcm_delay() قيمة كبيرة بشكل استثنائي: %li بايت " +"(%s%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[1] "" +"أرجعت الدالة snd_pcm_delay() قيمة كبيرة بشكل استثنائي: %li بايتات " +"(%s%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[2] "" +"أرجعت الدالة snd_pcm_delay() قيمة كبيرة بشكل استثنائي: %li بايتات " +"(%s%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[3] "" +"أرجعت الدالة snd_pcm_delay() قيمة كبيرة بشكل استثنائي: %li بايتات " +"(%s%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[4] "" +"أرجعت الدالة snd_pcm_delay() قيمة كبيرة بشكل استثنائي: %li بايتات " +"(%s%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[5] "" +"أرجعت الدالة snd_pcm_delay() قيمة كبيرة بشكل استثنائي: %li بايتات " +"(%s%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." + +#: spa/plugins/alsa/acp/alsa-util.c:1333 +#, c-format +msgid "" +"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail " +"%lu.\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr "" +"أرجعت الدالة snd_pcm_avail_delay() قيمًا غريبة: التأخير %lu أقل من avail " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." + +#: spa/plugins/alsa/acp/alsa-util.c:1376 +#, c-format +msgid "" +"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte " +"(%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgid_plural "" +"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes " +"(%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " +"to the ALSA developers." +msgstr[0] "" +"أرجعت الدالة snd_pcm_mmap_begin() قيمة كبيرة بشكل استثنائي: %lu بايت " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[1] "" +"أرجعت الدالة snd_pcm_mmap_begin() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[2] "" +"أرجعت الدالة snd_pcm_mmap_begin() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[3] "" +"أرجعت الدالة snd_pcm_mmap_begin() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[4] "" +"أرجعت الدالة snd_pcm_mmap_begin() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." +msgstr[5] "" +"أرجعت الدالة snd_pcm_mmap_begin() قيمة كبيرة بشكل استثنائي: %lu بايتات " +"(%lu مللي ثانية).\n" +"من المرجح أن يكون هذا خطأ في برنامج تشغيل ALSA '%s'. يُرجى الإبلاغ عن هذه المش" +"كلة " +"لمطوري ALSA." + +#: spa/plugins/alsa/acp/channelmap.h:457 +msgid "(invalid)" +msgstr "(غير صالح)" + +#: spa/plugins/alsa/acp/compat.c:193 +msgid "Built-in Audio" +msgstr "صوت مدمج" + +#: spa/plugins/alsa/acp/compat.c:198 +msgid "Modem" +msgstr "مودم" + +#: spa/plugins/bluez5/bluez5-device.c:1712 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "بوابة الصوت (مصدر A2DP و HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:1760 +#, c-format +msgid "High Fidelity Playback (A2DP Sink, codec %s)" +msgstr "تشغيل عالي الدقة (A2DP Sink, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1763 +#, c-format +msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" +msgstr "مزدوج عالي الدقة (مصدر A2DP/Sink, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1771 +msgid "High Fidelity Playback (A2DP Sink)" +msgstr "تشغيل عالي الدقة (A2DP Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1773 +msgid "High Fidelity Duplex (A2DP Source/Sink)" +msgstr "مزدوج عالي الدقة (مصدر A2DP/Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1823 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "تشغيل عالي الدقة (BAP Sink, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1828 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "إدخال عالي الدقة (مصدر BAP, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1832 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "مزدوج عالي الدقة (مصدر BAP/Sink, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1841 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "تشغيل عالي الدقة (BAP Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1845 +msgid "High Fidelity Input (BAP Source)" +msgstr "إدخال عالي الدقة (مصدر BAP)" + +#: spa/plugins/bluez5/bluez5-device.c:1848 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "مزدوج عالي الدقة (مصدر BAP/Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:1897 +#, c-format +msgid "Headset Head Unit (HSP/HFP, codec %s)" +msgstr "وحدة رأس سماعة الرأس (HSP/HFP, codec %s)" + +#: spa/plugins/bluez5/bluez5-device.c:1978 +#: spa/plugins/bluez5/bluez5-device.c:1983 +#: spa/plugins/bluez5/bluez5-device.c:1990 +#: spa/plugins/bluez5/bluez5-device.c:1996 +#: spa/plugins/bluez5/bluez5-device.c:2002 +#: spa/plugins/bluez5/bluez5-device.c:2008 +#: spa/plugins/bluez5/bluez5-device.c:2014 +#: spa/plugins/bluez5/bluez5-device.c:2020 +#: spa/plugins/bluez5/bluez5-device.c:2026 +msgid "Handsfree" +msgstr "دون استخدام اليدين" + +#: spa/plugins/bluez5/bluez5-device.c:1984 +msgid "Handsfree (HFP)" +msgstr "دون استخدام اليدين (HFP)" + +#: spa/plugins/bluez5/bluez5-device.c:2001 +msgid "Headphone" +msgstr "سماعة الأذن" + +#: spa/plugins/bluez5/bluez5-device.c:2007 +msgid "Portable" +msgstr "محمول" + +#: spa/plugins/bluez5/bluez5-device.c:2013 +msgid "Car" +msgstr "سيارة" + +#: spa/plugins/bluez5/bluez5-device.c:2019 +msgid "HiFi" +msgstr "HiFi" + +#: spa/plugins/bluez5/bluez5-device.c:2025 +msgid "Phone" +msgstr "جوّال" + +#: spa/plugins/bluez5/bluez5-device.c:2032 +msgid "Bluetooth" +msgstr "بلوتوث" + +#: spa/plugins/bluez5/bluez5-device.c:2033 +msgid "Bluetooth (HFP)" +msgstr "بلوتوث (HFP)" diff --git a/po/ca.po b/po/ca.po index 1b2423002..50240f3ed 100644 --- a/po/ca.po +++ b/po/ca.po @@ -5,7 +5,7 @@ # Xavier Conde Rueda , 2008. # Agustí Grau , 2009. # Judith Pintó Subirada -# Jordi Mas i Herǹandez, , 2022-2023 +# Jordi Mas i Herǹandez, , 2022-2025 # # 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 @@ -16,7 +16,7 @@ # 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/ +# https://www.softcatala.org/projectes/fedora/ # i contacteu l'anterior traductor/a. # Josep Torné Llavall , 2009, 2012. # Robert Antoni Buj Gelonch , 2016. #zanata @@ -27,10 +27,9 @@ msgid "" msgstr "" "Project-Id-Version: pipewire\n" -"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" -"issues\n" -"POT-Creation-Date: 2023-06-06 15:28+0000\n" -"PO-Revision-Date: 2023-06-06 22:39+0200\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues\n" +"POT-Creation-Date: 2025-07-20 15:32+0000\n" +"PO-Revision-Date: 2025-06-20 21:46+0200\n" "Last-Translator: Jordi Mas i Herǹandez, ,\n" "Language-Team: Catalan \n" "Language: ca\n" @@ -38,60 +37,64 @@ msgstr "" "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.4.2\n" +"X-Generator: Poedit 3.2.2\n" -#: src/daemon/pipewire.c:26 +#: src/daemon/pipewire.c:29 #, c-format msgid "" "%s [options]\n" " -h, --help Show this help\n" +" -v, --verbose Increase verbosity by one level\n" " --version Show version\n" " -c, --config Load config (Default %s)\n" +" -P --properties Set context properties\n" msgstr "" "%s [opcions]\n" " -h, --help Mostra aquesta ajuda\n" +" -v, --verbose Augmenta la verbositat en un nivell\n" " --version Mostra la versió\n" -" -c, --config Carrega la configuració " -"(predeterminada %s)\n" +" -c, --config Carrega la configuració (predeterminada %s)\n" +" -P --properties Estableix les propietats del context\n" +"\n" -#: src/daemon/pipewire.desktop.in:4 +#: src/daemon/pipewire.desktop.in:3 msgid "PipeWire Media System" msgstr "Sistema multimèdia PipeWire" -#: src/daemon/pipewire.desktop.in:5 +#: src/daemon/pipewire.desktop.in:4 msgid "Start the PipeWire Media System" msgstr "Inicia el sistema multimèdia PipeWire" -#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:141 -#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:141 +#: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:159 +#: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:159 #, c-format msgid "Tunnel to %s%s%s" msgstr "Túnel cap a %s%s%s" -#: src/modules/module-fallback-sink.c:31 +#: src/modules/module-fallback-sink.c:40 msgid "Dummy Output" msgstr "Sortida fictícia" -#: src/modules/module-pulse-tunnel.c:844 +#: src/modules/module-pulse-tunnel.c:760 #, c-format msgid "Tunnel for %s@%s" msgstr "Túnel per a %s@%s" -#: src/modules/module-zeroconf-discover.c:315 +#: src/modules/module-zeroconf-discover.c:320 msgid "Unknown device" msgstr "Dispositiu desconegut" -#: src/modules/module-zeroconf-discover.c:327 +#: src/modules/module-zeroconf-discover.c:332 #, c-format msgid "%s on %s@%s" msgstr "%s en %s@%s" -#: src/modules/module-zeroconf-discover.c:331 +#: src/modules/module-zeroconf-discover.c:336 #, c-format msgid "%s on %s" msgstr "%s en %s" -#: src/tools/pw-cat.c:974 +#: src/tools/pw-cat.c:1044 #, c-format msgid "" "%s [options] [|-]\n" @@ -105,86 +108,68 @@ msgstr "" " --version Mostra la versió\n" " -v, --verbose Habilita les operacions detallades\n" -#: src/tools/pw-cat.c:981 +#: src/tools/pw-cat.c:1051 #, c-format msgid "" " -R, --remote Remote daemon name\n" " --media-type Set media type (default %s)\n" " --media-category Set media category (default %s)\n" " --media-role Set media role (default %s)\n" -" --target Set node target serial or name " -"(default %s)\n" +" --target Set node target serial or name (default %s)\n" " 0 means don't link\n" " --latency Set node latency (default %s)\n" " Xunit (unit = s, ms, us, ns)\n" " or direct samples (256)\n" -" the rate is the one of the source " -"file\n" +" the rate is the one of the source file\n" " -P --properties Set node properties\n" "\n" msgstr "" " -R, --remote Nom del dimoni remot\n" -" --media-type Estableix el tipus de mitjà (per " -"defecte %s)\n" -" --media-category Estableix la categoria del " -"mitjà (per defecte %s)\n" -" --media-role Estableix el rol del mitjà (per " -"defecte %s)\n" -" --target Estableix l'objectiu sèrie o el nom " -"del node (per defecte %s)\n" +" --media-type Estableix el tipus de mitjà (per defecte %s)\n" +" --media-category Estableix la categoria del mitjà (per defecte %s)\n" +" --media-role Estableix el rol del mitjà (per defecte %s)\n" +" --target Estableix l'objectiu sèrie o el nom del node (per defecte %s)\n" " 0 vol dir que no enllaça\n" -" --latency Estableix latència del node (per " -"defecte %s)\n" +" --latency Estableix latència del node (per defecte %s)\n" " Xunit (unitat = s, ms, us, ns)\n" " o mostres directes (256)\n" " la taxa és la del fitxer d'origen\n" -" -P --properties Estableix les propietats del " -"node\n" +" -P --properties Estableix les propietats del node\n" "\n" -#: src/tools/pw-cat.c:999 +#: src/tools/pw-cat.c:1069 #, c-format msgid "" -" --rate Sample rate (req. for rec) (default " -"%u)\n" -" --channels Number of channels (req. for rec) " -"(default %u)\n" +" --rate Sample rate (req. for rec) (default %u)\n" +" --channels Number of channels (req. for rec) (default %u)\n" " --channel-map Channel map\n" -" one of: \"stereo\", " -"\"surround-51\",... or\n" -" comma separated list of channel " -"names: eg. \"FL,FR\"\n" -" --format Sample format %s (req. for rec) " -"(default %s)\n" +" one of: \"stereo\", \"surround-51\",... or\n" +" comma separated list of channel names: eg. \"FL,FR\"\n" +" --format Sample format %s (req. for rec) (default %s)\n" " --volume Stream volume 0-1.0 (default %.3f)\n" -" -q --quality Resampler quality (0 - 15) (default " -"%d)\n" +" -q --quality Resampler quality (0 - 15) (default %d)\n" +" -a, --raw RAW mode\n" "\n" msgstr "" -" --rate Freqüència de mostreig (req. per a " -"rec) (predeterminat %u)\n" -" --channels Nombre de canals (req. per a rec) " -"(predeterminat %u)\n" +" --rate Freqüència de mostreig (req. per a rec) (predeterminat %u)\n" +" --channels Nombre de canals (req. per a rec) (predeterminat %u)\n" " --channel-map Mapa de canals\n" -" un dels següents: \"stereo\", " -"\"surround-51\",... o\n" -" llista separada per comes dels " -"noms dels canals: per exemple. «FL,FR»\n" -" --format Format de mostra %s (req. per a rec) " -"(predeterminat %s)\n" -" --volume Volum del flux 0-1.0 (predeterminat " -"%.3f)\n" -" -q --quality Qualitat de remostrador (0 - 15) " -"(predeterminal %d)\n" +" un dels següents: \"stereo\", \"surround-51\",... o\n" +" llista separada per comes dels noms dels canals: per exemple. «FL,FR»\n" +" --format Format de mostra %s (req. per a rec) (predeterminat %s)\n" +" --volume Volum del flux 0-1.0 (predeterminat %.3f)\n" +" -q --quality Qualitat de remostrador (0 - 15) (predeterminat %d)\n" +" -a, --raw Mode RAW\n" "\n" -#: src/tools/pw-cat.c:1016 +#: src/tools/pw-cat.c:1087 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" " -m, --midi Midi mode\n" " -d, --dsd DSD mode\n" " -o, --encoded Encoded mode\n" +" -s, --sysex SysEx mode\n" "\n" msgstr "" " -p, --playback Mode de reproducció\n" @@ -192,9 +177,10 @@ msgstr "" " -m, --midi Mode MIDI\n" " -d, --dsd Mode DSD\n" " -o, --encoded Mode codificat\n" +" -s, --sysex Mode SysEx\n" "\n" -#: src/tools/pw-cli.c:2220 +#: src/tools/pw-cli.c:2386 #, c-format msgid "" "%s [options] [command]\n" @@ -208,21 +194,25 @@ msgstr "" "%s [opcions] [ordre]\n" " -h, --help Mostra aquesta ajuda\n" " --version Mostra la versió\n" -" -d, --daemon Inicia com a dimoni (fals per " -"defecte)\n" +" -d, --daemon Inicia com a dimoni (fals per defecte)\n" " -r, --remote Nom del dimoni remot\n" " -m, --monitor Monitor d'activitat\n" "\n" -#: spa/plugins/alsa/acp/acp.c:303 +#: spa/plugins/alsa/acp/acp.c:351 msgid "Pro Audio" msgstr "Pro Audio" -#: spa/plugins/alsa/acp/acp.c:427 spa/plugins/alsa/acp/alsa-mixer.c:4648 -#: spa/plugins/bluez5/bluez5-device.c:1586 +#: spa/plugins/alsa/acp/acp.c:520 spa/plugins/alsa/acp/alsa-mixer.c:4635 +#: spa/plugins/bluez5/bluez5-device.c:1974 msgid "Off" msgstr "Inactiu" +#: spa/plugins/alsa/acp/acp.c:603 +#, c-format +msgid "%s [ALSA UCM error]" +msgstr "%s [Error d'ALSA UCM]" + #: spa/plugins/alsa/acp/alsa-mixer.c:2652 msgid "Input" msgstr "Entrada" @@ -246,7 +236,7 @@ msgstr "Entrada de línia" #: spa/plugins/alsa/acp/alsa-mixer.c:2657 #: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:1831 +#: spa/plugins/bluez5/bluez5-device.c:2372 msgid "Microphone" msgstr "Micròfon" @@ -312,12 +302,15 @@ msgid "No Bass Boost" msgstr "Sense accentuació dels baixos" #: spa/plugins/alsa/acp/alsa-mixer.c:2672 -#: spa/plugins/bluez5/bluez5-device.c:1837 +#: spa/plugins/bluez5/bluez5-device.c:2378 msgid "Speaker" msgstr "Altaveu" +#. Don't call it "headset", the HF one has the mic #: spa/plugins/alsa/acp/alsa-mixer.c:2673 #: spa/plugins/alsa/acp/alsa-mixer.c:2751 +#: spa/plugins/bluez5/bluez5-device.c:2384 +#: spa/plugins/bluez5/bluez5-device.c:2451 msgid "Headphones" msgstr "Auriculars" @@ -394,15 +387,15 @@ msgstr "Entrada del xat" msgid "Virtual Surround 7.1" msgstr "Envoltant virtual 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 msgid "Analog Mono" msgstr "Mono analògic" -#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4459 msgid "Analog Mono (Left)" msgstr "Mono analògic (esquerra)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 msgid "Analog Mono (Right)" msgstr "Mono analògic (dreta)" @@ -411,338 +404,307 @@ msgstr "Mono analògic (dreta)" #. * here would lead to the source name to become "Analog Stereo Input #. * Input". The same logic applies to analog-stereo-output, #. * multichannel-input and multichannel-output. -#: spa/plugins/alsa/acp/alsa-mixer.c:4474 -#: spa/plugins/alsa/acp/alsa-mixer.c:4482 -#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 msgid "Analog Stereo" msgstr "Estèreo analògic" -#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +#: spa/plugins/alsa/acp/alsa-mixer.c:4462 msgid "Mono" msgstr "Mono" -#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +#: spa/plugins/alsa/acp/alsa-mixer.c:4463 msgid "Stereo" msgstr "Estèreo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4484 -#: spa/plugins/alsa/acp/alsa-mixer.c:4642 -#: spa/plugins/bluez5/bluez5-device.c:1819 +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +#: spa/plugins/bluez5/bluez5-device.c:2360 msgid "Headset" msgstr "Auriculars" -#: spa/plugins/alsa/acp/alsa-mixer.c:4485 -#: spa/plugins/alsa/acp/alsa-mixer.c:4643 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 msgid "Speakerphone" msgstr "Altaveu del telèfon" -#: spa/plugins/alsa/acp/alsa-mixer.c:4486 -#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 msgid "Multichannel" msgstr "Multicanal" -#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 msgid "Analog Surround 2.1" msgstr "Envoltant analògic 2.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 msgid "Analog Surround 3.0" msgstr "Envoltant analògic 3.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 msgid "Analog Surround 3.1" msgstr "Envoltant analògic 3.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 msgid "Analog Surround 4.0" msgstr "Envoltant analògic 4.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4492 +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 msgid "Analog Surround 4.1" msgstr "Envoltant analògic 4.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4493 +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 msgid "Analog Surround 5.0" msgstr "Envoltant analògic 5.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4494 +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 msgid "Analog Surround 5.1" msgstr "Envoltant analògic 5.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4495 +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 msgid "Analog Surround 6.0" msgstr "Envoltant analògic 6.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4496 +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 msgid "Analog Surround 6.1" msgstr "Envoltant analògic 6.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4497 +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 msgid "Analog Surround 7.0" msgstr "Envoltant analògic 7.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4498 +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 msgid "Analog Surround 7.1" msgstr "Envoltant analògic 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4499 +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 msgid "Digital Stereo (IEC958)" msgstr "Estèreo digital (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4500 +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 msgid "Digital Surround 4.0 (IEC958/AC3)" msgstr "Envoltant digital 4.0 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4501 +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 msgid "Digital Surround 5.1 (IEC958/AC3)" msgstr "Envoltant digital 5.1 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4502 +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 msgid "Digital Surround 5.1 (IEC958/DTS)" msgstr "Envoltant digital 5.1 (IEC958/DTS)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4503 +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 msgid "Digital Stereo (HDMI)" msgstr "Estèreo digital (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4504 +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 msgid "Digital Surround 5.1 (HDMI)" msgstr "Envoltant digital 5.1 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4505 +#: spa/plugins/alsa/acp/alsa-mixer.c:4492 msgid "Chat" msgstr "Xat" -#: spa/plugins/alsa/acp/alsa-mixer.c:4506 +#: spa/plugins/alsa/acp/alsa-mixer.c:4493 msgid "Game" msgstr "Joc" -#: spa/plugins/alsa/acp/alsa-mixer.c:4640 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 msgid "Analog Mono Duplex" msgstr "Dúplex mono analògic" -#: spa/plugins/alsa/acp/alsa-mixer.c:4641 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 msgid "Analog Stereo Duplex" msgstr "Dúplex estèreo analògic" -#: spa/plugins/alsa/acp/alsa-mixer.c:4644 +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 msgid "Digital Stereo Duplex (IEC958)" msgstr "Dúplex estèreo digital (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4645 +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 msgid "Multichannel Duplex" msgstr "Dúplex Multicanal" -#: spa/plugins/alsa/acp/alsa-mixer.c:4646 +#: spa/plugins/alsa/acp/alsa-mixer.c:4633 msgid "Stereo Duplex" msgstr "Dúplex estèreo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4647 +#: spa/plugins/alsa/acp/alsa-mixer.c:4634 msgid "Mono Chat + 7.1 Surround" msgstr "Xat mono + 7.1 envoltant" -#: spa/plugins/alsa/acp/alsa-mixer.c:4748 +#: spa/plugins/alsa/acp/alsa-mixer.c:4735 #, c-format msgid "%s Output" msgstr "Sortida %s" -#: spa/plugins/alsa/acp/alsa-mixer.c:4756 +#: spa/plugins/alsa/acp/alsa-mixer.c:4743 #, c-format msgid "%s Input" msgstr "Entrada %s" -#: spa/plugins/alsa/acp/alsa-util.c:1187 spa/plugins/alsa/acp/alsa-util.c:1281 +#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327 #, c-format msgid "" -"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " -"ms).\n" -"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " -"to the ALSA developers." +"snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers." msgid_plural "" -"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu " -"ms).\n" -"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " -"to the ALSA developers." +"snd_pcm_avail() returned a value that is exceptionally large: %lu bytes (%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers." msgstr[0] "" -"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu byte (%lu " -"ms).\n" -"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu " -"d'aquest incident als desenvolupadors de l'ALSA." +"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu byte (%lu ms).\n" +"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu d'aquest incident als desenvolupadors de l'ALSA." msgstr[1] "" -"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu " -"ms).\n" -"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu " -"d'aquest incident als desenvolupadors de l'ALSA." +"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n" +"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu d'aquest incident als desenvolupadors de l'ALSA." -#: spa/plugins/alsa/acp/alsa-util.c:1253 +#: spa/plugins/alsa/acp/alsa-util.c:1299 #, c-format msgid "" -"snd_pcm_delay() returned a value that is exceptionally large: %li byte " -"(%s%lu ms).\n" -"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " -"to the ALSA developers." +"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers." msgid_plural "" -"snd_pcm_delay() returned a value that is exceptionally large: %li bytes " -"(%s%lu ms).\n" -"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " -"to the ALSA developers." +"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers." msgstr[0] "" -"snd_pcm_delay() ha retornat un valor excepcionalment gran: %li byte (%s%lu " -"ms).\n" -"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu " -"d'aquest incident als desenvolupadors de l'ALSA." +"snd_pcm_delay() ha retornat un valor excepcionalment gran: %li byte (%s%lu ms).\n" +"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu d'aquest incident als desenvolupadors de l'ALSA." msgstr[1] "" -"snd_pcm_delay() ha retornat un valor excepcionalment gran: %li bytes (%s%lu " -"ms).\n" -"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu " -"d'aquest incident als desenvolupadors de l'ALSA." +"snd_pcm_delay() ha retornat un valor excepcionalment gran: %li bytes (%s%lu ms).\n" +"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu d'aquest incident als desenvolupadors de l'ALSA." -#: spa/plugins/alsa/acp/alsa-util.c:1300 +#: spa/plugins/alsa/acp/alsa-util.c:1346 #, c-format msgid "" -"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail " -"%lu.\n" -"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " -"to the ALSA developers." +"snd_pcm_avail_delay() returned strange values: delay %lu is less than avail %lu.\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers." msgstr "" -"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu " -"ms).\n" -"Probablement es tracta d'un error del controlador d'ALSA «%s». Informeu " -"d'aquest problema als desenvolupadors d'ALSA." +"snd_pcm_avail() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n" +"Probablement es tracta d'un error del controlador d'ALSA «%s». Informeu d'aquest problema als desenvolupadors d'ALSA." -#: spa/plugins/alsa/acp/alsa-util.c:1343 +#: spa/plugins/alsa/acp/alsa-util.c:1389 #, c-format msgid "" -"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte " -"(%lu ms).\n" -"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " -"to the ALSA developers." +"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte (%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers." msgid_plural "" -"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes " -"(%lu ms).\n" -"Most likely this is a bug in the ALSA driver '%s'. Please report this issue " -"to the ALSA developers." +"snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu bytes (%lu ms).\n" +"Most likely this is a bug in the ALSA driver '%s'. Please report this issue to the ALSA developers." msgstr[0] "" -"snd_pcm_mmap_begin() ha retornat un valor excepcionalment gran: %lu byte " -"(%lu ms).\n" -"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu " -"d'aquest incident als desenvolupadors de l'ALSA." +"snd_pcm_mmap_begin() ha retornat un valor excepcionalment gran: %lu byte (%lu ms).\n" +"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu d'aquest incident als desenvolupadors de l'ALSA." msgstr[1] "" -"snd_pcm_mmap_begin() ha retornat un valor excepcionalment gran: %lu bytes " -"(%lu ms).\n" -"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu " -"d'aquest incident als desenvolupadors de l'ALSA." +"snd_pcm_mmap_begin() ha retornat un valor excepcionalment gran: %lu bytes (%lu ms).\n" +"Probablement es tracta d'un error del controlador de l'ALSA «%s». Informeu d'aquest incident als desenvolupadors de l'ALSA." #: spa/plugins/alsa/acp/channelmap.h:457 msgid "(invalid)" msgstr "(incorrecte)" -#: spa/plugins/alsa/acp/compat.c:189 +#: spa/plugins/alsa/acp/compat.c:194 msgid "Built-in Audio" msgstr "Àudio intern" -#: spa/plugins/alsa/acp/compat.c:194 +#: spa/plugins/alsa/acp/compat.c:199 msgid "Modem" msgstr "Mòdem" -#: spa/plugins/bluez5/bluez5-device.c:1597 +#: spa/plugins/bluez5/bluez5-device.c:1985 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "Passarel·la d'àudio (A2DP Source & HSP/HFP AG)" -#: spa/plugins/bluez5/bluez5-device.c:1622 +#: spa/plugins/bluez5/bluez5-device.c:2014 +msgid "Audio Streaming for Hearing Aids (ASHA Sink)" +msgstr "Retransmissió d'àudio per als audiòfons (ASHA Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:2057 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "Reproducció d'alta fidelitat (Sink A2DP, còdec %s)" -#: spa/plugins/bluez5/bluez5-device.c:1625 +#: spa/plugins/bluez5/bluez5-device.c:2060 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "Dúplex d'alta fidelitat (A2DP Source/Sink, còdec %s)" -#: spa/plugins/bluez5/bluez5-device.c:1633 +#: spa/plugins/bluez5/bluez5-device.c:2068 msgid "High Fidelity Playback (A2DP Sink)" msgstr "Reproducció d'alta fidelitat (A2DP Sink)" -#: spa/plugins/bluez5/bluez5-device.c:1635 +#: spa/plugins/bluez5/bluez5-device.c:2070 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "Dúplex d'alta fidelitat (A2DP Source/Sink)" -#: spa/plugins/bluez5/bluez5-device.c:1677 +#: spa/plugins/bluez5/bluez5-device.c:2144 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "Reproducció d'alta fidelitat (sortida BAP, còdec %s)" -#: spa/plugins/bluez5/bluez5-device.c:1681 +#: spa/plugins/bluez5/bluez5-device.c:2149 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "Entrada d'alta fidelitat (font A2DP, còdec %s)" -#: spa/plugins/bluez5/bluez5-device.c:1685 +#: spa/plugins/bluez5/bluez5-device.c:2153 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "Dúplex d'alta fidelitat (BAP Source/Sink, còdec %s)" -#: spa/plugins/bluez5/bluez5-device.c:1693 +#: spa/plugins/bluez5/bluez5-device.c:2162 msgid "High Fidelity Playback (BAP Sink)" msgstr "Reproducció d'alta fidelitat (Sortida BAP)" -#: spa/plugins/bluez5/bluez5-device.c:1696 +#: spa/plugins/bluez5/bluez5-device.c:2166 msgid "High Fidelity Input (BAP Source)" msgstr "Entrada d'alta fidelitat (Font BAP)" -#: spa/plugins/bluez5/bluez5-device.c:1699 +#: spa/plugins/bluez5/bluez5-device.c:2169 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "Dúplex d'alta fidelitat (BAP Source/Sink)" -#: spa/plugins/bluez5/bluez5-device.c:1735 +#: spa/plugins/bluez5/bluez5-device.c:2209 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" -msgstr "Unitat d'ariculars pel cap (HSP/HFP, còdec %s)" +msgstr "Unitat d'auriculars pel cap (HSP/HFP, còdec %s)" -#: spa/plugins/bluez5/bluez5-device.c:1740 -msgid "Headset Head Unit (HSP/HFP)" -msgstr "Unitat d'ariculars pel cap (HSP/HFP)" - -#: spa/plugins/bluez5/bluez5-device.c:1820 -#: spa/plugins/bluez5/bluez5-device.c:1825 -#: spa/plugins/bluez5/bluez5-device.c:1832 -#: spa/plugins/bluez5/bluez5-device.c:1838 -#: spa/plugins/bluez5/bluez5-device.c:1844 -#: spa/plugins/bluez5/bluez5-device.c:1850 -#: spa/plugins/bluez5/bluez5-device.c:1856 -#: spa/plugins/bluez5/bluez5-device.c:1862 -#: spa/plugins/bluez5/bluez5-device.c:1868 +#: spa/plugins/bluez5/bluez5-device.c:2361 +#: spa/plugins/bluez5/bluez5-device.c:2366 +#: spa/plugins/bluez5/bluez5-device.c:2373 +#: spa/plugins/bluez5/bluez5-device.c:2379 +#: spa/plugins/bluez5/bluez5-device.c:2385 +#: spa/plugins/bluez5/bluez5-device.c:2391 +#: spa/plugins/bluez5/bluez5-device.c:2397 +#: spa/plugins/bluez5/bluez5-device.c:2403 +#: spa/plugins/bluez5/bluez5-device.c:2409 msgid "Handsfree" msgstr "Mans lliures" -#: spa/plugins/bluez5/bluez5-device.c:1826 +#: spa/plugins/bluez5/bluez5-device.c:2367 msgid "Handsfree (HFP)" msgstr "Mans lliures (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:1843 -msgid "Headphone" -msgstr "Auricular" - -#: spa/plugins/bluez5/bluez5-device.c:1849 +#: spa/plugins/bluez5/bluez5-device.c:2390 msgid "Portable" msgstr "Portable" -#: spa/plugins/bluez5/bluez5-device.c:1855 +#: spa/plugins/bluez5/bluez5-device.c:2396 msgid "Car" msgstr "Cotxe" -#: spa/plugins/bluez5/bluez5-device.c:1861 +#: spa/plugins/bluez5/bluez5-device.c:2402 msgid "HiFi" msgstr "HiFi" -#: spa/plugins/bluez5/bluez5-device.c:1867 +#: spa/plugins/bluez5/bluez5-device.c:2408 msgid "Phone" msgstr "Telèfon" -#: spa/plugins/bluez5/bluez5-device.c:1874 +#: spa/plugins/bluez5/bluez5-device.c:2415 msgid "Bluetooth" msgstr "Bluetooth" -#: spa/plugins/bluez5/bluez5-device.c:1875 +#: spa/plugins/bluez5/bluez5-device.c:2416 msgid "Bluetooth (HFP)" msgstr "Bluetooth (HFP)" + diff --git a/po/my.po b/po/my.po index 68a9be7ea..a6c2212a0 100644 --- a/po/my.po +++ b/po/my.po @@ -9,15 +9,15 @@ msgstr "" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" "issues\n" "POT-Creation-Date: 2021-08-26 03:31+0000\n" -"PO-Revision-Date: 2021-08-26 21:52+0630\n" +"PO-Revision-Date: 2025-10-02 11:36+0630\n" +"Last-Translator: zayar lwin \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" -"Last-Translator: zayar lwin \n" -"Language: my\n" +"X-Generator: Poedit 3.7\n" #: src/daemon/pipewire.c:45 #, c-format @@ -27,23 +27,24 @@ msgid "" " --version Show version\n" " -c, --config Load config (Default %s)\n" msgstr "" -"%s [options]\n" -" -h, --help Show this help\n" -" --version Show version\n" -" -c, --config Load config (Default %s)\n" +"%s [options]\r\n" +" -h, --help Show this help\r\n" +" --version Show version\r\n" +" -c, --config Load config (Default %s)\r\n" +"\n" #: src/daemon/pipewire.desktop.in:4 msgid "PipeWire Media System" -msgstr "ပိုက်ဝိုင်ယာ မီဒီယာစစ်စတမ်" +msgstr "ပိုက်ဝိုင်ယာ မီဒီယာစနစ်" #: src/daemon/pipewire.desktop.in:5 msgid "Start the PipeWire Media System" -msgstr "ပိုက်ဝိုင်ယာ မီဒီယာစစ်စတမ် စတင်ရန်" +msgstr "ပိုက်ဝိုင်ယာ မီဒီယာစနစ် စတင်ရန်" #: src/examples/media-session/alsa-monitor.c:588 #: spa/plugins/alsa/acp/compat.c:189 msgid "Built-in Audio" -msgstr "နဂိုတည်းကထည့်သွင်းထားသည်ံ့ အသံကရိယာ" +msgstr "မူလပါရှိပြီးအသံကိရိယာ" #: src/examples/media-session/alsa-monitor.c:592 #: spa/plugins/alsa/acp/compat.c:194 @@ -53,7 +54,7 @@ msgstr "ဆ.သ.ရ-စက်" #: src/examples/media-session/alsa-monitor.c:601 #: src/modules/module-zeroconf-discover.c:296 msgid "Unknown device" -msgstr "မသိသောစက်" +msgstr "မသိ" #: src/modules/module-protocol-pulse/modules/module-tunnel-sink.c:173 #: src/modules/module-protocol-pulse/modules/module-tunnel-source.c:173 @@ -69,12 +70,12 @@ msgstr "%s@%sအတွက် လုံခြုံစွာဒေတာပို #: src/modules/module-zeroconf-discover.c:308 #, c-format msgid "%s on %s@%s" -msgstr "" +msgstr "%s on %s@%s" #: src/modules/module-zeroconf-discover.c:312 #, c-format msgid "%s on %s" -msgstr "" +msgstr "%s on %s" #: src/tools/pw-cat.c:1016 #, c-format @@ -85,6 +86,11 @@ msgid "" " -v, --verbose Enable verbose operations\n" "\n" msgstr "" +"%s [options] \r\n" +" -h, --help Show this help\r\n" +" --version Show version\r\n" +" -v, --verbose Enable verbose operations\r\n" +"\r\n" #: src/tools/pw-cat.c:1023 #, c-format @@ -103,6 +109,20 @@ msgid "" " --list-targets List available targets for --target\n" "\n" msgstr "" +" -R, --remote Remote daemon name\r\n" +" --media-type Set media type (default %s)\r\n" +" --media-category Set media category (default %s)\r\n" +" --media-role Set media role (default %s)\r\n" +" --target Set node target (default %s)\r\n" +" 0 means don't link\r\n" +" --latency Set node latency (default %s)\r\n" +" Xunit (unit = s, ms, us, ns)\r\n" +" or direct samples (256)\r\n" +" the rate is the one of the source " +"file\r\n" +" --list-targets List available targets for --" +"target\r\n" +"\r\n" #: src/tools/pw-cat.c:1041 #, c-format @@ -123,6 +143,22 @@ msgid "" "%d)\n" "\n" msgstr "" +" --rate Sample rate (req. for rec) (default " +"%u)\r\n" +" --channels Number of channels (req. for rec) " +"(default %u)\r\n" +" --channel-map Channel map\r\n" +" one of: \"stereo\", " +"\"surround-51\",... or\r\n" +" comma separated list of channel " +"names: eg. \"FL,FR\"\r\n" +" --format Sample format %s (req. for rec) " +"(default %s)\r\n" +" --volume Stream volume 0-1.0 (default %.3f)" +"\r\n" +" -q --quality Resampler quality (0 - 15) (default " +"%d)\r\n" +"\r\n" #: src/tools/pw-cat.c:1058 msgid "" @@ -131,6 +167,10 @@ msgid "" " -m, --midi Midi mode\n" "\n" msgstr "" +" -p, --playback Playback mode\r\n" +" -r, --record Recording mode\r\n" +" -m, --midi Midi mode\r\n" +"\r\n" #: src/tools/pw-cli.c:2954 #, c-format @@ -142,6 +182,12 @@ msgid "" " -r, --remote Remote daemon name\n" "\n" msgstr "" +"%s [options] [command]\r\n" +" -h, --help Show this help\r\n" +" --version Show version\r\n" +" -d, --daemon Start as daemon (Default false)\r\n" +" -r, --remote Remote daemon name\r\n" +"\r\n" #: spa/plugins/alsa/acp/acp.c:306 msgid "Pro Audio" @@ -473,12 +519,12 @@ msgstr "မိုနို စကားပြောဆိုသံ + ၇.၁ ပ #: spa/plugins/alsa/acp/alsa-mixer.c:4750 #, c-format msgid "%s Output" -msgstr "%s ထုတ်ပို့ကရိယာ" +msgstr "%s အသံထုတ်" #: spa/plugins/alsa/acp/alsa-mixer.c:4757 #, c-format msgid "%s Input" -msgstr "%s လက်ခံကရိယာ" +msgstr "%s အသံသွင်း" #: spa/plugins/alsa/acp/alsa-util.c:1173 spa/plugins/alsa/acp/alsa-util.c:1267 #, c-format @@ -493,20 +539,26 @@ msgid_plural "" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgstr[0] "" +"snd_pcm_avail() မှ ပြန်ပေးသည့်တန်ဖိုးသည် အလွန်ကြီးမားနေသည်- %lu ဘိုက် (%lu ms)။\n" +"ဖြစ်နိုင်ခြေအများဆုံးမှာ ALSA ဒရိုက်ဗာ ‘%s’ တွင် ပရိုဂရမ်အမှားတစ်ခုရှိနေခြင်းဖြစ်သည်။ ဤပြဿနာအား " +"ALSA ရေးသားသူများထံသို့ သတင်းပို့ပေးပါ။" #: spa/plugins/alsa/acp/alsa-util.c:1239 #, c-format msgid "" -"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s" -"%lu ms).\n" +"snd_pcm_delay() returned a value that is exceptionally large: %li byte " +"(%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgid_plural "" -"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s" -"%lu ms).\n" +"snd_pcm_delay() returned a value that is exceptionally large: %li bytes " +"(%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgstr[0] "" +"snd_pcm_delay() မှ ပြန်ပေးသည့်တန်ဖိုးသည် အလွန်ကြီးမားနေသည်- %li ဘိုက် (%s%lu ms)။\n" +"ဖြစ်နိုင်ခြေအများဆုံးမှာ ALSA ဒရိုက်ဗာ ‘%s’ တွင် ပရိုဂရမ်အမှားတစ်ခုရှိနေခြင်းဖြစ်သည်။ ဤပြဿနာအား " +"ALSA ရေးသားသူများထံသို့ သတင်းပို့ပေးပါ။" #: spa/plugins/alsa/acp/alsa-util.c:1286 #, c-format @@ -516,6 +568,10 @@ msgid "" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgstr "" +"snd_pcm_avail_delay() မှ ထူးဆန်းသောတန်ဖိုးများ ပြန်ပေးခဲ့သည်- နှောင့်နှေးမှု %lu သည် ရနိုင်မှု %lu " +"ထက် နည်းနေပါသည်။\n" +"ဖြစ်နိုင်ခြေအများဆုံးမှာ ALSA ဒရိုက်ဘာ '%s’ တွင် ပရိုဂရမ်အမှားတစ်ခုရှိနေခြင်းဖြစ်သည်။ ဤပြဿနာအား " +"ALSA ရေးသားသူများထံသို့ သတင်းပို့ပေးပါ။" #: spa/plugins/alsa/acp/alsa-util.c:1329 #, c-format @@ -530,6 +586,9 @@ msgid_plural "" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgstr[0] "" +"snd_pcm_mmap_begin() မှ ပြန်ပေးသည့်တန်ဖိုးသည် အလွန်ကြီးမားနေသည်- %lu ဘိုက် (%lu ms)။\n" +"ဖြစ်နိုင်ခြေအများဆုံးမှာ ALSA ဒရိုက်ဗာ ‘%s’ တွင် ပရိုဂရမ်အမှားတစ်ခုရှိနေခြင်းဖြစ်သည်။ ဤပြဿနာအား " +"ALSA ရေးသားသူများထံသို့ သတင်းပို့ပေးပါ။" #: spa/plugins/alsa/acp/channelmap.h:466 msgid "(invalid)" diff --git a/po/oc.po b/po/oc.po index f2cfad815..e71993297 100644 --- a/po/oc.po +++ b/po/oc.po @@ -272,7 +272,7 @@ msgstr "Contraròtle automatic del ganh" #: spa/plugins/alsa/acp/alsa-mixer.c:2665 msgid "No Automatic Gain Control" -msgstr "Pas de contraròtle automatic del ganh" +msgstr "Cap de contraròtle automatic del ganh" #: spa/plugins/alsa/acp/alsa-mixer.c:2666 msgid "Boost" @@ -288,7 +288,7 @@ msgstr "Amplificador" #: spa/plugins/alsa/acp/alsa-mixer.c:2669 msgid "No Amplifier" -msgstr "Pas d'amplificador" +msgstr "Cap d'amplificador" #: spa/plugins/alsa/acp/alsa-mixer.c:2670 msgid "Bass Boost" @@ -296,7 +296,7 @@ msgstr "Amplificacion bassas" #: spa/plugins/alsa/acp/alsa-mixer.c:2671 msgid "No Bass Boost" -msgstr "Pas d'amplificacion de las bassas" +msgstr "Cap d'amplificacion de las bassas" #: spa/plugins/alsa/acp/alsa-mixer.c:2672 #: spa/plugins/bluez5/bluez5-device.c:1995 @@ -531,12 +531,12 @@ msgstr "Messatjariá mono + Surround 7.1" #: spa/plugins/alsa/acp/alsa-mixer.c:4733 #, c-format msgid "%s Output" -msgstr "%s Sortida" +msgstr "Sortida %s" #: spa/plugins/alsa/acp/alsa-mixer.c:4741 #, c-format msgid "%s Input" -msgstr "%s Entrada" +msgstr "Entrada %s" #: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314 #, c-format diff --git a/po/pl.po b/po/pl.po index 6d6572a3e..c75ca20bd 100644 --- a/po/pl.po +++ b/po/pl.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" "issues\n" -"POT-Creation-Date: 2025-01-09 15:25+0000\n" -"PO-Revision-Date: 2025-02-09 14:55+0100\n" +"POT-Creation-Date: 2025-09-13 03:33+0000\n" +"PO-Revision-Date: 2025-09-13 11:47+0200\n" "Last-Translator: Piotr Drąg \n" "Language-Team: Polish \n" "Language: pl\n" @@ -38,11 +38,11 @@ msgstr "" "%s)\n" " -P --properties Ustawia właściwości kontekstu\n" -#: src/daemon/pipewire.desktop.in:4 +#: src/daemon/pipewire.desktop.in:3 msgid "PipeWire Media System" msgstr "System multimediów PipeWire" -#: src/daemon/pipewire.desktop.in:5 +#: src/daemon/pipewire.desktop.in:4 msgid "Start the PipeWire Media System" msgstr "Uruchomienie systemu multimediów PipeWire" @@ -75,7 +75,7 @@ msgstr "%s na %s@%s" msgid "%s on %s" msgstr "%s na %s" -#: src/tools/pw-cat.c:973 +#: src/tools/pw-cat.c:1084 #, c-format msgid "" "%s [options] [|-]\n" @@ -90,7 +90,7 @@ msgstr "" " -v, --verbose Wyświetla więcej komunikatów\n" "\n" -#: src/tools/pw-cat.c:980 +#: src/tools/pw-cat.c:1091 #, c-format msgid "" " -R, --remote Remote daemon name\n" @@ -128,7 +128,7 @@ msgstr "" " -P --properties Ustawia właściwości węzła\n" "\n" -#: src/tools/pw-cat.c:998 +#: src/tools/pw-cat.c:1109 #, c-format msgid "" " --rate Sample rate (req. for rec) (default " @@ -146,6 +146,9 @@ msgid "" " -q --quality Resampler quality (0 - 15) (default " "%d)\n" " -a, --raw RAW mode\n" +" -M, --force-midi Force midi format, one of \"midi\" " +"or \"ump\", (default ump)\n" +" -n, --sample-count COUNT Stop after COUNT samples\n" "\n" msgstr "" " --rate Częstotliwość próbki (wymagane do " @@ -164,15 +167,21 @@ msgstr "" " -q --quality Jakość resamplera od 0 do 15 " "(domyślnie %d)\n" " -a, --raw Tryb RAW\n" +" -M, --force-midi Wymusza format MIDI, można użyć " +"„midi”\n" +" albo „ump” (domyślnie ump)\n" +" -n, --sample-count LICZBA Zatrzymuje po LICZBIE próbek\n" "\n" -#: src/tools/pw-cat.c:1016 +#: src/tools/pw-cat.c:1129 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" " -m, --midi Midi mode\n" " -d, --dsd DSD mode\n" " -o, --encoded Encoded mode\n" +" -s, --sysex SysEx mode\n" +" -c, --midi-clip MIDI clip mode\n" "\n" msgstr "" " -p, --playback Tryb odtwarzania\n" @@ -180,9 +189,11 @@ msgstr "" " -m, --midi Tryb MIDI\n" " -d, --dsd Tryb DSD\n" " -o, --encoded Tryb zakodowany\n" +" -s, --sysex Tryb SysEx\n" +" -c, --midi-clip Tryb klipu MIDI\n" "\n" -#: src/tools/pw-cli.c:2306 +#: src/tools/pw-cli.c:2386 #, c-format msgid "" "%s [options] [command]\n" @@ -202,15 +213,20 @@ msgstr "" " -m, --monitor Monitoruje aktywność\n" "\n" -#: spa/plugins/alsa/acp/acp.c:347 +#: spa/plugins/alsa/acp/acp.c:351 msgid "Pro Audio" msgstr "Dźwięk w zastosowaniach profesjonalnych" -#: spa/plugins/alsa/acp/acp.c:507 spa/plugins/alsa/acp/alsa-mixer.c:4635 -#: spa/plugins/bluez5/bluez5-device.c:1795 +#: spa/plugins/alsa/acp/acp.c:527 spa/plugins/alsa/acp/alsa-mixer.c:4635 +#: spa/plugins/bluez5/bluez5-device.c:1974 msgid "Off" msgstr "Wyłączone" +#: spa/plugins/alsa/acp/acp.c:610 +#, c-format +msgid "%s [ALSA UCM error]" +msgstr "%s [błąd UCM biblioteki ALSA]" + #: spa/plugins/alsa/acp/alsa-mixer.c:2652 msgid "Input" msgstr "Wejście" @@ -234,7 +250,7 @@ msgstr "Wejście liniowe" #: spa/plugins/alsa/acp/alsa-mixer.c:2657 #: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:2139 +#: spa/plugins/bluez5/bluez5-device.c:2372 msgid "Microphone" msgstr "Mikrofon" @@ -300,12 +316,15 @@ msgid "No Bass Boost" msgstr "Brak podbicia basów" #: spa/plugins/alsa/acp/alsa-mixer.c:2672 -#: spa/plugins/bluez5/bluez5-device.c:2145 +#: spa/plugins/bluez5/bluez5-device.c:2378 msgid "Speaker" msgstr "Głośnik" +#. Don't call it "headset", the HF one has the mic #: spa/plugins/alsa/acp/alsa-mixer.c:2673 #: spa/plugins/alsa/acp/alsa-mixer.c:2751 +#: spa/plugins/bluez5/bluez5-device.c:2384 +#: spa/plugins/bluez5/bluez5-device.c:2451 msgid "Headphones" msgstr "Słuchawki" @@ -415,7 +434,7 @@ msgstr "Stereo" #: spa/plugins/alsa/acp/alsa-mixer.c:4471 #: spa/plugins/alsa/acp/alsa-mixer.c:4629 -#: spa/plugins/bluez5/bluez5-device.c:2127 +#: spa/plugins/bluez5/bluez5-device.c:2360 msgid "Headset" msgstr "Słuchawki z mikrofonem" @@ -631,112 +650,108 @@ msgstr[2] "" msgid "(invalid)" msgstr "(nieprawidłowe)" -#: spa/plugins/alsa/acp/compat.c:193 +#: spa/plugins/alsa/acp/compat.c:194 msgid "Built-in Audio" msgstr "Wbudowany dźwięk" -#: spa/plugins/alsa/acp/compat.c:198 +#: spa/plugins/alsa/acp/compat.c:199 msgid "Modem" msgstr "Modem" -#: spa/plugins/bluez5/bluez5-device.c:1806 +#: spa/plugins/bluez5/bluez5-device.c:1985 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "Bramka dźwięku (źródło A2DP i AG HSP/HFP)" -#: spa/plugins/bluez5/bluez5-device.c:1834 +#: spa/plugins/bluez5/bluez5-device.c:2014 msgid "Audio Streaming for Hearing Aids (ASHA Sink)" msgstr "Przesyłanie dźwięku do aparatów słuchowych (odpływ ASHA)" -#: spa/plugins/bluez5/bluez5-device.c:1874 +#: spa/plugins/bluez5/bluez5-device.c:2057 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1877 +#: spa/plugins/bluez5/bluez5-device.c:2060 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "Dupleks o wysokiej dokładności (źródło/odpływ A2DP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1885 +#: spa/plugins/bluez5/bluez5-device.c:2068 msgid "High Fidelity Playback (A2DP Sink)" msgstr "Odtwarzanie o wysokiej dokładności (odpływ A2DP)" -#: spa/plugins/bluez5/bluez5-device.c:1887 +#: spa/plugins/bluez5/bluez5-device.c:2070 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "Dupleks o wysokiej dokładności (źródło/odpływ A2DP)" -#: spa/plugins/bluez5/bluez5-device.c:1937 +#: spa/plugins/bluez5/bluez5-device.c:2144 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "Odtwarzanie o wysokiej dokładności (odpływ BAP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1942 +#: spa/plugins/bluez5/bluez5-device.c:2149 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "Wejście o wysokiej dokładności (źródło BAP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1946 +#: spa/plugins/bluez5/bluez5-device.c:2153 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "Dupleks o wysokiej dokładności (źródło/odpływ BAP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1955 +#: spa/plugins/bluez5/bluez5-device.c:2162 msgid "High Fidelity Playback (BAP Sink)" msgstr "Odtwarzanie o wysokiej dokładności (odpływ BAP)" -#: spa/plugins/bluez5/bluez5-device.c:1959 +#: spa/plugins/bluez5/bluez5-device.c:2166 msgid "High Fidelity Input (BAP Source)" msgstr "Wejście o wysokiej dokładności (źródło BAP)" -#: spa/plugins/bluez5/bluez5-device.c:1962 +#: spa/plugins/bluez5/bluez5-device.c:2169 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "Dupleks o wysokiej dokładności (źródło/odpływ BAP)" -#: spa/plugins/bluez5/bluez5-device.c:2008 +#: spa/plugins/bluez5/bluez5-device.c:2209 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "Jednostka główna słuchawek z mikrofonem (HSP/HFP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:2128 -#: spa/plugins/bluez5/bluez5-device.c:2133 -#: spa/plugins/bluez5/bluez5-device.c:2140 -#: spa/plugins/bluez5/bluez5-device.c:2146 -#: spa/plugins/bluez5/bluez5-device.c:2152 -#: spa/plugins/bluez5/bluez5-device.c:2158 -#: spa/plugins/bluez5/bluez5-device.c:2164 -#: spa/plugins/bluez5/bluez5-device.c:2170 -#: spa/plugins/bluez5/bluez5-device.c:2176 +#: spa/plugins/bluez5/bluez5-device.c:2361 +#: spa/plugins/bluez5/bluez5-device.c:2366 +#: spa/plugins/bluez5/bluez5-device.c:2373 +#: spa/plugins/bluez5/bluez5-device.c:2379 +#: spa/plugins/bluez5/bluez5-device.c:2385 +#: spa/plugins/bluez5/bluez5-device.c:2391 +#: spa/plugins/bluez5/bluez5-device.c:2397 +#: spa/plugins/bluez5/bluez5-device.c:2403 +#: spa/plugins/bluez5/bluez5-device.c:2409 msgid "Handsfree" msgstr "Zestaw głośnomówiący" -#: spa/plugins/bluez5/bluez5-device.c:2134 +#: spa/plugins/bluez5/bluez5-device.c:2367 msgid "Handsfree (HFP)" msgstr "Zestaw głośnomówiący (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2151 -msgid "Headphone" -msgstr "Słuchawki" - -#: spa/plugins/bluez5/bluez5-device.c:2157 +#: spa/plugins/bluez5/bluez5-device.c:2390 msgid "Portable" msgstr "Przenośne" -#: spa/plugins/bluez5/bluez5-device.c:2163 +#: spa/plugins/bluez5/bluez5-device.c:2396 msgid "Car" msgstr "Samochód" -#: spa/plugins/bluez5/bluez5-device.c:2169 +#: spa/plugins/bluez5/bluez5-device.c:2402 msgid "HiFi" msgstr "HiFi" -#: spa/plugins/bluez5/bluez5-device.c:2175 +#: spa/plugins/bluez5/bluez5-device.c:2408 msgid "Phone" msgstr "Telefon" -#: spa/plugins/bluez5/bluez5-device.c:2182 +#: spa/plugins/bluez5/bluez5-device.c:2415 msgid "Bluetooth" msgstr "Bluetooth" -#: spa/plugins/bluez5/bluez5-device.c:2183 +#: spa/plugins/bluez5/bluez5-device.c:2416 msgid "Bluetooth (HFP)" msgstr "Bluetooth (HFP)" diff --git a/po/sl.po b/po/sl.po index de3f94528..bb0015c60 100644 --- a/po/sl.po +++ b/po/sl.po @@ -9,16 +9,16 @@ msgstr "" "Project-Id-Version: PipeWire master\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" "issues\n" -"POT-Creation-Date: 2025-01-09 15:25+0000\n" -"PO-Revision-Date: 2025-01-23 09:23+0100\n" +"POT-Creation-Date: 2025-09-11 03:34+0000\n" +"PO-Revision-Date: 2025-09-11 11:47+0200\n" "Last-Translator: Martin Srebotnjak \n" "Language-Team: Slovenian \n" "Language: sl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n" -"%100<=4 ? 2 : 3);\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && " +"n%100<=4 ? 2 : 3);\n" "X-Generator: Poedit 2.2.1\n" #: src/daemon/pipewire.c:29 @@ -37,13 +37,13 @@ msgstr "" " --version Pokaži različico\n" " -c, --config Naloži prilagoditev config (privzeto " "%s)\n" -" -P —properties Določi lastnosti konteksta\n" +" -P --properties Določi lastnosti konteksta\n" -#: src/daemon/pipewire.desktop.in:4 +#: src/daemon/pipewire.desktop.in:3 msgid "PipeWire Media System" msgstr "Medijski sistem PipeWire" -#: src/daemon/pipewire.desktop.in:5 +#: src/daemon/pipewire.desktop.in:4 msgid "Start the PipeWire Media System" msgstr "Zaženite medijski sistem PipeWire" @@ -76,7 +76,7 @@ msgstr "%s na %s@%s" msgid "%s on %s" msgstr "%s na %s" -#: src/tools/pw-cat.c:973 +#: src/tools/pw-cat.c:1084 #, c-format msgid "" "%s [options] [|-]\n" @@ -92,7 +92,7 @@ msgstr "" "\n" "\n" -#: src/tools/pw-cat.c:980 +#: src/tools/pw-cat.c:1091 #, c-format msgid "" " -R, --remote Remote daemon name\n" @@ -129,7 +129,7 @@ msgstr "" " -P --properties Nastavi lastnosti vozlišča\n" "\n" -#: src/tools/pw-cat.c:998 +#: src/tools/pw-cat.c:1109 #, c-format msgid "" " --rate Sample rate (req. for rec) (default " @@ -147,14 +147,17 @@ msgid "" " -q --quality Resampler quality (0 - 15) (default " "%d)\n" " -a, --raw RAW mode\n" +" -M, --force-midi Force midi format, one of \"midi\" " +"or \"ump\", (default ump)\n" +" -n, --sample-count COUNT Stop after COUNT samples\n" "\n" msgstr "" -" --rate Mera vzorčenja (zahtevano za rec) " +" --rate Mera vzorčenja (zaht. za rec) " +"(privzeto %u)\n" +" --channels Število kanalov (zaht. za snemanje) " "(privzeto %u)\n" -" --channels Število kanalov (zahteva za " -"snemanje) (privzeto %u)\n" " --channel-map Preslikava kanalov\n" -" Ena izmed: \"Stereo\", " +" Ena izmed: \"stereo\", " "\"surround-51\",... ali\n" " seznam imen kanalov, ločen z " "vejico: npr. \"FL,FR\"\n" @@ -164,16 +167,20 @@ msgstr "" " -q --quality Kakovost prevzorčenja (0 - 15) " "(privzeto %d)\n" " -a, --raw neobdelan način (RAW)\n" -"\n" +" -M, --force-midi Vsili zapis midi, eden izmed " +"\"midi\" ali \"ump\" (privzeto ump)\n" +" -n, --sample-count ŠTEVEC Ustavi po ŠTEVEC vzorcih\n" "\n" -#: src/tools/pw-cat.c:1016 +#: src/tools/pw-cat.c:1129 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" " -m, --midi Midi mode\n" " -d, --dsd DSD mode\n" " -o, --encoded Encoded mode\n" +" -s, --sysex SysEx mode\n" +" -c, --midi-clip MIDI clip mode\n" "\n" msgstr "" " -p, --playback Način predvajanja\n" @@ -181,9 +188,11 @@ msgstr "" " -m, --midi Midi način\n" " -d, --dsd Način DSD\n" " -o, --encoded Kodiran način\n" +" -s, --sysex Način SysEx\n" +" -c, --midi-clip Način posnetka MIDI\n" "\n" -#: src/tools/pw-cli.c:2306 +#: src/tools/pw-cli.c:2386 #, c-format msgid "" "%s [options] [command]\n" @@ -203,15 +212,20 @@ msgstr "" " -m, --monitor Spremljaj dejavnosti\n" "\n" -#: spa/plugins/alsa/acp/acp.c:347 +#: spa/plugins/alsa/acp/acp.c:351 msgid "Pro Audio" msgstr "Profesionalni zvok" -#: spa/plugins/alsa/acp/acp.c:507 spa/plugins/alsa/acp/alsa-mixer.c:4635 -#: spa/plugins/bluez5/bluez5-device.c:1795 +#: spa/plugins/alsa/acp/acp.c:527 spa/plugins/alsa/acp/alsa-mixer.c:4635 +#: spa/plugins/bluez5/bluez5-device.c:1974 msgid "Off" msgstr "Izklopljeno" +#: spa/plugins/alsa/acp/acp.c:610 +#, c-format +msgid "%s [ALSA UCM error]" +msgstr "%s [napaka ALSA UCM]" + #: spa/plugins/alsa/acp/alsa-mixer.c:2652 msgid "Input" msgstr "Vhod" @@ -235,7 +249,7 @@ msgstr "Linijski vhod" #: spa/plugins/alsa/acp/alsa-mixer.c:2657 #: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:2139 +#: spa/plugins/bluez5/bluez5-device.c:2372 msgid "Microphone" msgstr "Mikrofon" @@ -301,12 +315,15 @@ msgid "No Bass Boost" msgstr "Brez ojačitve nizkih tonov" #: spa/plugins/alsa/acp/alsa-mixer.c:2672 -#: spa/plugins/bluez5/bluez5-device.c:2145 +#: spa/plugins/bluez5/bluez5-device.c:2378 msgid "Speaker" msgstr "Zvočnik" +#. Don't call it "headset", the HF one has the mic #: spa/plugins/alsa/acp/alsa-mixer.c:2673 #: spa/plugins/alsa/acp/alsa-mixer.c:2751 +#: spa/plugins/bluez5/bluez5-device.c:2384 +#: spa/plugins/bluez5/bluez5-device.c:2451 msgid "Headphones" msgstr "Slušalke" @@ -416,7 +433,7 @@ msgstr "Stereo" #: spa/plugins/alsa/acp/alsa-mixer.c:4471 #: spa/plugins/alsa/acp/alsa-mixer.c:4629 -#: spa/plugins/bluez5/bluez5-device.c:2127 +#: spa/plugins/bluez5/bluez5-device.c:2360 msgid "Headset" msgstr "Slušalka" @@ -575,13 +592,13 @@ msgstr[3] "" #: spa/plugins/alsa/acp/alsa-util.c:1299 #, c-format msgid "" -"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s" -"%lu ms).\n" +"snd_pcm_delay() returned a value that is exceptionally large: %li byte " +"(%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgid_plural "" -"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s" -"%lu ms).\n" +"snd_pcm_delay() returned a value that is exceptionally large: %li bytes " +"(%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgstr[0] "" @@ -655,112 +672,108 @@ msgstr[3] "" msgid "(invalid)" msgstr "(neveljavno)" -#: spa/plugins/alsa/acp/compat.c:193 +#: spa/plugins/alsa/acp/compat.c:194 msgid "Built-in Audio" msgstr "Vgrajen zvok" -#: spa/plugins/alsa/acp/compat.c:198 +#: spa/plugins/alsa/acp/compat.c:199 msgid "Modem" msgstr "Modem" -#: spa/plugins/bluez5/bluez5-device.c:1806 +#: spa/plugins/bluez5/bluez5-device.c:1985 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "Zvožni prehod (vir A2DP in HSP/HFP AG)" -#: spa/plugins/bluez5/bluez5-device.c:1834 +#: spa/plugins/bluez5/bluez5-device.c:2014 msgid "Audio Streaming for Hearing Aids (ASHA Sink)" msgstr "Pretakanje zvoka za slušne aparate (ponor ASHA)" -#: spa/plugins/bluez5/bluez5-device.c:1874 +#: spa/plugins/bluez5/bluez5-device.c:2057 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "Predvajanje visoke ločljivosti (ponor A2DP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1877 +#: spa/plugins/bluez5/bluez5-device.c:2060 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "Dupleks visoke ločljivosti (vir/ponor A2DP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1885 +#: spa/plugins/bluez5/bluez5-device.c:2068 msgid "High Fidelity Playback (A2DP Sink)" msgstr "Predvajanje visoke ločljivosti (ponor A2DP)" -#: spa/plugins/bluez5/bluez5-device.c:1887 +#: spa/plugins/bluez5/bluez5-device.c:2070 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "Dupleks visoke ločljivosti (vir/ponor A2DP)" -#: spa/plugins/bluez5/bluez5-device.c:1937 +#: spa/plugins/bluez5/bluez5-device.c:2144 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "Predvajanje visoke ločljivosti (ponor BAP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1942 +#: spa/plugins/bluez5/bluez5-device.c:2149 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "Vhod visoke ločljivosti (vir BAP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1946 +#: spa/plugins/bluez5/bluez5-device.c:2153 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "Dupleks visoke ločljivosti (vir/ponor BAP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1955 +#: spa/plugins/bluez5/bluez5-device.c:2162 msgid "High Fidelity Playback (BAP Sink)" msgstr "Predvajanje visoke ločljivosti (ponor BAP)" -#: spa/plugins/bluez5/bluez5-device.c:1959 +#: spa/plugins/bluez5/bluez5-device.c:2166 msgid "High Fidelity Input (BAP Source)" msgstr "Vhod visoke ločljivosti (vir BAP)" -#: spa/plugins/bluez5/bluez5-device.c:1962 +#: spa/plugins/bluez5/bluez5-device.c:2169 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "Dupleks visoke ločljivosti (vir/ponor BAP)" -#: spa/plugins/bluez5/bluez5-device.c:2008 +#: spa/plugins/bluez5/bluez5-device.c:2209 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "Naglavna enota slušalk (HSP/HFP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:2128 -#: spa/plugins/bluez5/bluez5-device.c:2133 -#: spa/plugins/bluez5/bluez5-device.c:2140 -#: spa/plugins/bluez5/bluez5-device.c:2146 -#: spa/plugins/bluez5/bluez5-device.c:2152 -#: spa/plugins/bluez5/bluez5-device.c:2158 -#: spa/plugins/bluez5/bluez5-device.c:2164 -#: spa/plugins/bluez5/bluez5-device.c:2170 -#: spa/plugins/bluez5/bluez5-device.c:2176 +#: spa/plugins/bluez5/bluez5-device.c:2361 +#: spa/plugins/bluez5/bluez5-device.c:2366 +#: spa/plugins/bluez5/bluez5-device.c:2373 +#: spa/plugins/bluez5/bluez5-device.c:2379 +#: spa/plugins/bluez5/bluez5-device.c:2385 +#: spa/plugins/bluez5/bluez5-device.c:2391 +#: spa/plugins/bluez5/bluez5-device.c:2397 +#: spa/plugins/bluez5/bluez5-device.c:2403 +#: spa/plugins/bluez5/bluez5-device.c:2409 msgid "Handsfree" msgstr "Prostoročno telefoniranje" -#: spa/plugins/bluez5/bluez5-device.c:2134 +#: spa/plugins/bluez5/bluez5-device.c:2367 msgid "Handsfree (HFP)" msgstr "Prostoročno telefoniranje (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2151 -msgid "Headphone" -msgstr "Slušalke" - -#: spa/plugins/bluez5/bluez5-device.c:2157 +#: spa/plugins/bluez5/bluez5-device.c:2390 msgid "Portable" msgstr "Prenosna naprava" -#: spa/plugins/bluez5/bluez5-device.c:2163 +#: spa/plugins/bluez5/bluez5-device.c:2396 msgid "Car" msgstr "Avtomobil" -#: spa/plugins/bluez5/bluez5-device.c:2169 +#: spa/plugins/bluez5/bluez5-device.c:2402 msgid "HiFi" msgstr "HiFi" -#: spa/plugins/bluez5/bluez5-device.c:2175 +#: spa/plugins/bluez5/bluez5-device.c:2408 msgid "Phone" msgstr "Telefon" -#: spa/plugins/bluez5/bluez5-device.c:2182 +#: spa/plugins/bluez5/bluez5-device.c:2415 msgid "Bluetooth" msgstr "Bluetooth" -#: spa/plugins/bluez5/bluez5-device.c:2183 +#: spa/plugins/bluez5/bluez5-device.c:2416 msgid "Bluetooth (HFP)" msgstr "Bluetooth (HFP)" diff --git a/po/sv.po b/po/sv.po index b867795ca..0bc2796a4 100644 --- a/po/sv.po +++ b/po/sv.po @@ -1,9 +1,9 @@ # Swedish translation for pipewire. -# Copyright © 2008-2024 Free Software Foundation, Inc. +# Copyright © 2008-2025 Free Software Foundation, Inc. # This file is distributed under the same license as the pipewire package. # Daniel Nylander , 2008, 2012. # Josef Andersson , 2014, 2017. -# Anders Jonsson , 2021, 2022, 2023, 2024. +# Anders Jonsson , 2021, 2022, 2023, 2024, 2025. # # Termer: # input/output: ingång/utgång (det handlar om ljud) @@ -19,8 +19,8 @@ msgstr "" "Project-Id-Version: pipewire\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" "issues\n" -"POT-Creation-Date: 2024-11-05 03:27+0000\n" -"PO-Revision-Date: 2024-11-07 21:52+0100\n" +"POT-Creation-Date: 2025-04-16 15:31+0000\n" +"PO-Revision-Date: 2025-04-22 10:43+0200\n" "Last-Translator: Anders Jonsson \n" "Language-Team: Swedish \n" "Language: sv\n" @@ -65,7 +65,7 @@ msgstr "Tunnel till %s%s%s" msgid "Dummy Output" msgstr "Attrapputgång" -#: src/modules/module-pulse-tunnel.c:777 +#: src/modules/module-pulse-tunnel.c:760 #, c-format msgid "Tunnel for %s@%s" msgstr "Tunnel för %s@%s" @@ -184,7 +184,7 @@ msgstr "" " -o, --encoded Kodat läge\n" "\n" -#: src/tools/pw-cli.c:2318 +#: src/tools/pw-cli.c:2306 #, c-format msgid "" "%s [options] [command]\n" @@ -198,20 +198,25 @@ msgstr "" "%s [flaggor] [kommando]\n" " -h, --help Visa denna hjälp\n" " --version Show version\n" -" -d, --daemon Starta som demon (Standard false)\n" +" -d, --daemon Starta som demon (standard false)\n" " -r, --remote Fjärrdemonnamn\n" " -m, --monitor Övervaka aktivitet\n" "\n" -#: spa/plugins/alsa/acp/acp.c:327 +#: spa/plugins/alsa/acp/acp.c:350 msgid "Pro Audio" msgstr "Professionellt ljud" -#: spa/plugins/alsa/acp/acp.c:487 spa/plugins/alsa/acp/alsa-mixer.c:4633 -#: spa/plugins/bluez5/bluez5-device.c:1705 +#: spa/plugins/alsa/acp/acp.c:511 spa/plugins/alsa/acp/alsa-mixer.c:4635 +#: spa/plugins/bluez5/bluez5-device.c:1802 msgid "Off" msgstr "Av" +#: spa/plugins/alsa/acp/acp.c:593 +#, c-format +msgid "%s [ALSA UCM error]" +msgstr "%s [ALSA UCM-fel]" + #: spa/plugins/alsa/acp/alsa-mixer.c:2652 msgid "Input" msgstr "Ingång" @@ -235,7 +240,7 @@ msgstr "Linje in" #: spa/plugins/alsa/acp/alsa-mixer.c:2657 #: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:2026 +#: spa/plugins/bluez5/bluez5-device.c:2146 msgid "Microphone" msgstr "Mikrofon" @@ -301,7 +306,7 @@ msgid "No Bass Boost" msgstr "Ingen basökning" #: spa/plugins/alsa/acp/alsa-mixer.c:2672 -#: spa/plugins/bluez5/bluez5-device.c:2032 +#: spa/plugins/bluez5/bluez5-device.c:2152 msgid "Speaker" msgstr "Högtalare" @@ -383,15 +388,15 @@ msgstr "Chatt-ingång" msgid "Virtual Surround 7.1" msgstr "Virtual surround 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4456 +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 msgid "Analog Mono" msgstr "Analog mono" -#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +#: spa/plugins/alsa/acp/alsa-mixer.c:4459 msgid "Analog Mono (Left)" msgstr "Analog mono (vänster)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 msgid "Analog Mono (Right)" msgstr "Analog mono (höger)" @@ -400,147 +405,147 @@ msgstr "Analog mono (höger)" #. * here would lead to the source name to become "Analog Stereo Input #. * Input". The same logic applies to analog-stereo-output, #. * multichannel-input and multichannel-output. -#: spa/plugins/alsa/acp/alsa-mixer.c:4459 -#: spa/plugins/alsa/acp/alsa-mixer.c:4467 -#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 msgid "Analog Stereo" msgstr "Analog stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +#: spa/plugins/alsa/acp/alsa-mixer.c:4462 msgid "Mono" msgstr "Mono" -#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +#: spa/plugins/alsa/acp/alsa-mixer.c:4463 msgid "Stereo" msgstr "Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4469 -#: spa/plugins/alsa/acp/alsa-mixer.c:4627 -#: spa/plugins/bluez5/bluez5-device.c:2014 +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +#: spa/plugins/bluez5/bluez5-device.c:2134 msgid "Headset" msgstr "Headset" -#: spa/plugins/alsa/acp/alsa-mixer.c:4470 -#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 msgid "Speakerphone" msgstr "Högtalartelefon" -#: spa/plugins/alsa/acp/alsa-mixer.c:4471 -#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 msgid "Multichannel" msgstr "Multikanal" -#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 msgid "Analog Surround 2.1" msgstr "Analog surround 2.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 msgid "Analog Surround 3.0" msgstr "Analog surround 3.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 msgid "Analog Surround 3.1" msgstr "Analog surround 3.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 msgid "Analog Surround 4.0" msgstr "Analog surround 4.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 msgid "Analog Surround 4.1" msgstr "Analog surround 4.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 msgid "Analog Surround 5.0" msgstr "Analog surround 5.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 msgid "Analog Surround 5.1" msgstr "Analog surround 5.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 msgid "Analog Surround 6.0" msgstr "Analog surround 6.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 msgid "Analog Surround 6.1" msgstr "Analog surround 6.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 msgid "Analog Surround 7.0" msgstr "Analog surround 7.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 msgid "Analog Surround 7.1" msgstr "Analog surround 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 msgid "Digital Stereo (IEC958)" msgstr "Digital stereo (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 msgid "Digital Surround 4.0 (IEC958/AC3)" msgstr "Digital surround 4.0 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 msgid "Digital Surround 5.1 (IEC958/AC3)" msgstr "Digital surround 5.1 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 msgid "Digital Surround 5.1 (IEC958/DTS)" msgstr "Digital surround 5.1 (IEC958/DTS)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 msgid "Digital Stereo (HDMI)" msgstr "Digital stereo (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 msgid "Digital Surround 5.1 (HDMI)" msgstr "Digital surround 5.1 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +#: spa/plugins/alsa/acp/alsa-mixer.c:4492 msgid "Chat" msgstr "Chatt" -#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +#: spa/plugins/alsa/acp/alsa-mixer.c:4493 msgid "Game" msgstr "Spel" -#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 msgid "Analog Mono Duplex" msgstr "Analog mono duplex" -#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 msgid "Analog Stereo Duplex" msgstr "Analog stereo duplex" -#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 msgid "Digital Stereo Duplex (IEC958)" msgstr "Digital stereo duplex (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 msgid "Multichannel Duplex" msgstr "Multikanalduplex" -#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +#: spa/plugins/alsa/acp/alsa-mixer.c:4633 msgid "Stereo Duplex" msgstr "Stereo duplex" -#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +#: spa/plugins/alsa/acp/alsa-mixer.c:4634 msgid "Mono Chat + 7.1 Surround" msgstr "Mono Chatt + 7.1 Surround" -#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#: spa/plugins/alsa/acp/alsa-mixer.c:4735 #, c-format msgid "%s Output" msgstr "%s-utgång" -#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#: spa/plugins/alsa/acp/alsa-mixer.c:4743 #, c-format msgid "%s Input" msgstr "%s-ingång" -#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325 +#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327 #, c-format msgid "" "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " @@ -563,7 +568,7 @@ msgstr[1] "" "Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera " "problemet till ALSA-utvecklarna." -#: spa/plugins/alsa/acp/alsa-util.c:1297 +#: spa/plugins/alsa/acp/alsa-util.c:1299 #, c-format msgid "" "snd_pcm_delay() returned a value that is exceptionally large: %li byte " @@ -586,7 +591,7 @@ msgstr[1] "" "Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera " "problemet till ALSA-utvecklarna." -#: spa/plugins/alsa/acp/alsa-util.c:1344 +#: spa/plugins/alsa/acp/alsa-util.c:1346 #, c-format msgid "" "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail " @@ -599,7 +604,7 @@ msgstr "" "Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera " "problemet till ALSA-utvecklarna." -#: spa/plugins/alsa/acp/alsa-util.c:1387 +#: spa/plugins/alsa/acp/alsa-util.c:1389 #, c-format msgid "" "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte " @@ -634,100 +639,104 @@ msgstr "Inbyggt ljud" msgid "Modem" msgstr "Modem" -#: spa/plugins/bluez5/bluez5-device.c:1716 +#: spa/plugins/bluez5/bluez5-device.c:1813 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "Audio gateway (A2DP-källa & HSP/HFP AG)" -#: spa/plugins/bluez5/bluez5-device.c:1764 +#: spa/plugins/bluez5/bluez5-device.c:1841 +msgid "Audio Streaming for Hearing Aids (ASHA Sink)" +msgstr "Ljudströmning för hörhjälpmedel (ASHA-utgång)" + +#: spa/plugins/bluez5/bluez5-device.c:1881 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "High fidelity-uppspelning (A2DP-utgång, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1767 +#: spa/plugins/bluez5/bluez5-device.c:1884 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "High fidelity duplex (A2DP-källa/utgång, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1775 +#: spa/plugins/bluez5/bluez5-device.c:1892 msgid "High Fidelity Playback (A2DP Sink)" msgstr "High fidelity-uppspelning (A2DP-utgång)" -#: spa/plugins/bluez5/bluez5-device.c:1777 +#: spa/plugins/bluez5/bluez5-device.c:1894 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "High fidelity duplex (A2DP-källa/utgång)" -#: spa/plugins/bluez5/bluez5-device.c:1827 +#: spa/plugins/bluez5/bluez5-device.c:1944 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "High fidelity-uppspelning (BAP-utgång, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1832 +#: spa/plugins/bluez5/bluez5-device.c:1949 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "High fidelity-ingång (BAP-källa, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1836 +#: spa/plugins/bluez5/bluez5-device.c:1953 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "High fidelity duplex (BAP-källa/utgång, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:1845 +#: spa/plugins/bluez5/bluez5-device.c:1962 msgid "High Fidelity Playback (BAP Sink)" msgstr "High fidelity-uppspelning (BAP-utgång)" -#: spa/plugins/bluez5/bluez5-device.c:1849 +#: spa/plugins/bluez5/bluez5-device.c:1966 msgid "High Fidelity Input (BAP Source)" msgstr "High fidelity-ingång (BAP-källa)" -#: spa/plugins/bluez5/bluez5-device.c:1852 +#: spa/plugins/bluez5/bluez5-device.c:1969 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "High fidelity duplex (BAP-källa/utgång)" -#: spa/plugins/bluez5/bluez5-device.c:1901 +#: spa/plugins/bluez5/bluez5-device.c:2015 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "Headset-huvudenhet (HSP/HFP, kodek %s)" -#: spa/plugins/bluez5/bluez5-device.c:2015 -#: spa/plugins/bluez5/bluez5-device.c:2020 -#: spa/plugins/bluez5/bluez5-device.c:2027 -#: spa/plugins/bluez5/bluez5-device.c:2033 -#: spa/plugins/bluez5/bluez5-device.c:2039 -#: spa/plugins/bluez5/bluez5-device.c:2045 -#: spa/plugins/bluez5/bluez5-device.c:2051 -#: spa/plugins/bluez5/bluez5-device.c:2057 -#: spa/plugins/bluez5/bluez5-device.c:2063 +#: spa/plugins/bluez5/bluez5-device.c:2135 +#: spa/plugins/bluez5/bluez5-device.c:2140 +#: spa/plugins/bluez5/bluez5-device.c:2147 +#: spa/plugins/bluez5/bluez5-device.c:2153 +#: spa/plugins/bluez5/bluez5-device.c:2159 +#: spa/plugins/bluez5/bluez5-device.c:2165 +#: spa/plugins/bluez5/bluez5-device.c:2171 +#: spa/plugins/bluez5/bluez5-device.c:2177 +#: spa/plugins/bluez5/bluez5-device.c:2183 msgid "Handsfree" msgstr "Handsfree" -#: spa/plugins/bluez5/bluez5-device.c:2021 +#: spa/plugins/bluez5/bluez5-device.c:2141 msgid "Handsfree (HFP)" msgstr "Handsfree (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2038 +#: spa/plugins/bluez5/bluez5-device.c:2158 msgid "Headphone" msgstr "Hörlurar" -#: spa/plugins/bluez5/bluez5-device.c:2044 +#: spa/plugins/bluez5/bluez5-device.c:2164 msgid "Portable" msgstr "Bärbar" -#: spa/plugins/bluez5/bluez5-device.c:2050 +#: spa/plugins/bluez5/bluez5-device.c:2170 msgid "Car" msgstr "Bil" -#: spa/plugins/bluez5/bluez5-device.c:2056 +#: spa/plugins/bluez5/bluez5-device.c:2176 msgid "HiFi" msgstr "HiFi" -#: spa/plugins/bluez5/bluez5-device.c:2062 +#: spa/plugins/bluez5/bluez5-device.c:2182 msgid "Phone" msgstr "Telefon" -#: spa/plugins/bluez5/bluez5-device.c:2069 +#: spa/plugins/bluez5/bluez5-device.c:2189 msgid "Bluetooth" msgstr "Bluetooth" -#: spa/plugins/bluez5/bluez5-device.c:2070 +#: spa/plugins/bluez5/bluez5-device.c:2190 msgid "Bluetooth (HFP)" msgstr "Bluetooth (HFP)" diff --git a/po/tr.po b/po/tr.po index 49eb6eaba..345963c70 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,46 +1,51 @@ # Turkish translation for PipeWire. -# Copyright (C) 2014-2024 PipeWire's COPYRIGHT HOLDER +# Copyright (C) 2014-2025 PipeWire's COPYRIGHT HOLDER # This file is distributed under the same license as the PipeWire package. # # Necdet Yücel , 2014. # Kaan Özdinçer , 2014. # Muhammet Kara , 2015, 2016, 2017. # Oğuz Ersen , 2021-2022. -# Sabri Ünal , 2024. +# Sabri Ünal , 2024, 2025. # msgid "" msgstr "" "Project-Id-Version: PipeWire master\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-25 03:43+0300\n" -"PO-Revision-Date: 2024-02-25 03:49+0300\n" -"Last-Translator: Sabri Ünal \n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" +"issues\n" +"POT-Creation-Date: 2025-10-24 15:37+0000\n" +"PO-Revision-Date: 2025-10-24 20:15+0300\n" +"Last-Translator: Sabri Ünal \n" "Language-Team: Türkçe \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: Poedit 3.4.2\n" +"X-Generator: Poedit 3.8\n" -#: src/daemon/pipewire.c:26 +#: src/daemon/pipewire.c:29 #, c-format msgid "" "%s [options]\n" " -h, --help Show this help\n" +" -v, --verbose Increase verbosity by one level\n" " --version Show version\n" " -c, --config Load config (Default %s)\n" +" -P --properties Set context properties\n" msgstr "" "%s [seçenekler]\n" " -h, --help Bu yardımı göster\n" +" -v, --verbose Ayrıntı düzeyini bir düzey artır\n" " --version Sürümü göster\n" " -c, --config Yapılandırmayı yükle (Öntanımlı %s)\n" +" -P --properties Bağlam özelliklerini ayarla\n" -#: src/daemon/pipewire.desktop.in:4 +#: src/daemon/pipewire.desktop.in:3 msgid "PipeWire Media System" msgstr "PipeWire Ortam Sistemi" -#: src/daemon/pipewire.desktop.in:5 +#: src/daemon/pipewire.desktop.in:4 msgid "Start the PipeWire Media System" msgstr "PipeWire Ortam Sistemini Başlat" @@ -54,26 +59,26 @@ msgstr "%s%s%s tüneli" msgid "Dummy Output" msgstr "Temsili Çıkış" -#: src/modules/module-pulse-tunnel.c:774 +#: src/modules/module-pulse-tunnel.c:760 #, c-format msgid "Tunnel for %s@%s" msgstr "%s@%s için tünel" -#: src/modules/module-zeroconf-discover.c:315 +#: src/modules/module-zeroconf-discover.c:320 msgid "Unknown device" msgstr "Bilinmeyen aygıt" -#: src/modules/module-zeroconf-discover.c:327 +#: src/modules/module-zeroconf-discover.c:332 #, c-format msgid "%s on %s@%s" msgstr "%s, %s@%s" -#: src/modules/module-zeroconf-discover.c:331 +#: src/modules/module-zeroconf-discover.c:336 #, c-format msgid "%s on %s" msgstr "%s, %s" -#: src/tools/pw-cat.c:991 +#: src/tools/pw-cat.c:1096 #, c-format msgid "" "%s [options] [|-]\n" @@ -88,7 +93,7 @@ msgstr "" " -v, --verbose Ayrıntılı işlemleri etkinleştir\n" "\n" -#: src/tools/pw-cat.c:998 +#: src/tools/pw-cat.c:1103 #, c-format msgid "" " -R, --remote Remote daemon name\n" @@ -122,7 +127,7 @@ msgstr "" " -P --properties Düğüm özelliklerini ayarla\n" "\n" -#: src/tools/pw-cat.c:1016 +#: src/tools/pw-cat.c:1121 #, c-format msgid "" " --rate Sample rate (req. for rec) (default " @@ -139,6 +144,10 @@ msgid "" " --volume Stream volume 0-1.0 (default %.3f)\n" " -q --quality Resampler quality (0 - 15) (default " "%d)\n" +" -a, --raw RAW mode\n" +" -M, --force-midi Force midi format, one of \"midi\" " +"or \"ump\", (default ump)\n" +" -n, --sample-count COUNT Stop after COUNT samples\n" "\n" msgstr "" " --rate Örnekleme oranı (kayıt için gerekli) " @@ -156,15 +165,21 @@ msgstr "" "%.3f)\n" " -q --quality Yeniden örnekleyici kalitesi (0 - " "15) (öntanımlı %d)\n" +" -a, --raw HAM kipi\n" +" -M, --force-midi Midi biçimini zorla, ikisinden " +"birisi \"midi\" ya da\"ump\", (öntanımlı ump)\n" +" -n, --sample-count COUNT COUNT örnekleme sonrası dur\n" "\n" -#: src/tools/pw-cat.c:1033 +#: src/tools/pw-cat.c:1141 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" " -m, --midi Midi mode\n" " -d, --dsd DSD mode\n" " -o, --encoded Encoded mode\n" +" -s, --sysex SysEx mode\n" +" -c, --midi-clip MIDI clip mode\n" "\n" msgstr "" " -p, --playback Çalma kipi\n" @@ -172,9 +187,11 @@ msgstr "" " -m, --midi Midi kipi\n" " -d, --dsd DSD kipi\n" " -o, --encoded Kodlanmış kip\n" +" -s, --sysex SysEx kipi\n" +" -c, --midi-clip MIDI klip kipi\n" "\n" -#: src/tools/pw-cli.c:2252 +#: src/tools/pw-cli.c:2386 #, c-format msgid "" "%s [options] [command]\n" @@ -193,195 +210,203 @@ msgstr "" " -r, --remote Uzak arka plan programı adı\n" " -m, --monitor Etkinliği izle\n" -#: spa/plugins/alsa/acp/acp.c:327 +#: spa/plugins/alsa/acp/acp.c:361 msgid "Pro Audio" msgstr "Profesyonel Ses" -#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633 -#: spa/plugins/bluez5/bluez5-device.c:1701 +#: spa/plugins/alsa/acp/acp.c:537 spa/plugins/alsa/acp/alsa-mixer.c:4699 +#: spa/plugins/bluez5/bluez5-device.c:1976 msgid "Off" msgstr "Kapalı" -#: spa/plugins/alsa/acp/alsa-mixer.c:2652 +#: spa/plugins/alsa/acp/acp.c:620 +#, c-format +msgid "%s [ALSA UCM error]" +msgstr "%s [ALSA UCM hatası]" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2721 msgid "Input" msgstr "Giriş" -#: spa/plugins/alsa/acp/alsa-mixer.c:2653 +#: spa/plugins/alsa/acp/alsa-mixer.c:2722 msgid "Docking Station Input" msgstr "Yerleştirme İstasyonu Girişi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +#: spa/plugins/alsa/acp/alsa-mixer.c:2723 msgid "Docking Station Microphone" msgstr "Yerleştirme İstasyonu Mikrofonu" -#: spa/plugins/alsa/acp/alsa-mixer.c:2655 +#: spa/plugins/alsa/acp/alsa-mixer.c:2724 msgid "Docking Station Line In" msgstr "Yerleştirme İstasyonu Hat Girişi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2656 -#: spa/plugins/alsa/acp/alsa-mixer.c:2747 +#: spa/plugins/alsa/acp/alsa-mixer.c:2725 +#: spa/plugins/alsa/acp/alsa-mixer.c:2816 msgid "Line In" msgstr "Hat Girişi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2657 -#: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:1989 +#: spa/plugins/alsa/acp/alsa-mixer.c:2726 +#: spa/plugins/alsa/acp/alsa-mixer.c:2810 +#: spa/plugins/bluez5/bluez5-device.c:2374 msgid "Microphone" msgstr "Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2658 -#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +#: spa/plugins/alsa/acp/alsa-mixer.c:2727 +#: spa/plugins/alsa/acp/alsa-mixer.c:2811 msgid "Front Microphone" msgstr "Ön Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2659 -#: spa/plugins/alsa/acp/alsa-mixer.c:2743 +#: spa/plugins/alsa/acp/alsa-mixer.c:2728 +#: spa/plugins/alsa/acp/alsa-mixer.c:2812 msgid "Rear Microphone" msgstr "Arka Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +#: spa/plugins/alsa/acp/alsa-mixer.c:2729 msgid "External Microphone" msgstr "Harici Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2661 -#: spa/plugins/alsa/acp/alsa-mixer.c:2745 +#: spa/plugins/alsa/acp/alsa-mixer.c:2730 +#: spa/plugins/alsa/acp/alsa-mixer.c:2814 msgid "Internal Microphone" msgstr "Dahili Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2662 -#: spa/plugins/alsa/acp/alsa-mixer.c:2748 +#: spa/plugins/alsa/acp/alsa-mixer.c:2731 +#: spa/plugins/alsa/acp/alsa-mixer.c:2817 msgid "Radio" msgstr "Radyo" -#: spa/plugins/alsa/acp/alsa-mixer.c:2663 -#: spa/plugins/alsa/acp/alsa-mixer.c:2749 +#: spa/plugins/alsa/acp/alsa-mixer.c:2732 +#: spa/plugins/alsa/acp/alsa-mixer.c:2818 msgid "Video" msgstr "Video" -#: spa/plugins/alsa/acp/alsa-mixer.c:2664 +#: spa/plugins/alsa/acp/alsa-mixer.c:2733 msgid "Automatic Gain Control" msgstr "Otomatik Kazanç Denetimi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +#: spa/plugins/alsa/acp/alsa-mixer.c:2734 msgid "No Automatic Gain Control" msgstr "Otomatik Kazanç Denetimi Yok" -#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +#: spa/plugins/alsa/acp/alsa-mixer.c:2735 msgid "Boost" msgstr "Artır" -#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +#: spa/plugins/alsa/acp/alsa-mixer.c:2736 msgid "No Boost" msgstr "Artırma Yok" -#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +#: spa/plugins/alsa/acp/alsa-mixer.c:2737 msgid "Amplifier" msgstr "Yükseltici" -#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +#: spa/plugins/alsa/acp/alsa-mixer.c:2738 msgid "No Amplifier" msgstr "Yükseltici Yok" -#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +#: spa/plugins/alsa/acp/alsa-mixer.c:2739 msgid "Bass Boost" msgstr "Bas Artır" -#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 msgid "No Bass Boost" msgstr "Bas Artırma Yok" -#: spa/plugins/alsa/acp/alsa-mixer.c:2672 -#: spa/plugins/bluez5/bluez5-device.c:1995 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:2380 msgid "Speaker" msgstr "Hoparlör" -#: spa/plugins/alsa/acp/alsa-mixer.c:2673 -#: spa/plugins/alsa/acp/alsa-mixer.c:2751 +#. Don't call it "headset", the HF one has the mic +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +#: spa/plugins/alsa/acp/alsa-mixer.c:2820 +#: spa/plugins/bluez5/bluez5-device.c:2386 +#: spa/plugins/bluez5/bluez5-device.c:2453 msgid "Headphones" msgstr "Kulaklık" -#: spa/plugins/alsa/acp/alsa-mixer.c:2740 +#: spa/plugins/alsa/acp/alsa-mixer.c:2809 msgid "Analog Input" msgstr "Analog Giriş" -#: spa/plugins/alsa/acp/alsa-mixer.c:2744 +#: spa/plugins/alsa/acp/alsa-mixer.c:2813 msgid "Dock Microphone" msgstr "Yapışık Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +#: spa/plugins/alsa/acp/alsa-mixer.c:2815 msgid "Headset Microphone" msgstr "Mikrofonlu Kulaklık" -#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +#: spa/plugins/alsa/acp/alsa-mixer.c:2819 msgid "Analog Output" msgstr "Analog Çıkış" -#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +#: spa/plugins/alsa/acp/alsa-mixer.c:2821 msgid "Headphones 2" msgstr "Kulaklık 2" -#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +#: spa/plugins/alsa/acp/alsa-mixer.c:2822 msgid "Headphones Mono Output" msgstr "Kulaklık Tek Kanallı Çıkış" -#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +#: spa/plugins/alsa/acp/alsa-mixer.c:2823 msgid "Line Out" msgstr "Hat Çıkışı" -#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +#: spa/plugins/alsa/acp/alsa-mixer.c:2824 msgid "Analog Mono Output" msgstr "Analog Tek Kanallı Çıkış" -#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +#: spa/plugins/alsa/acp/alsa-mixer.c:2825 msgid "Speakers" msgstr "Hoparlörler" -#: spa/plugins/alsa/acp/alsa-mixer.c:2757 +#: spa/plugins/alsa/acp/alsa-mixer.c:2826 msgid "HDMI / DisplayPort" msgstr "HDMI / DisplayPort" -#: spa/plugins/alsa/acp/alsa-mixer.c:2758 +#: spa/plugins/alsa/acp/alsa-mixer.c:2827 msgid "Digital Output (S/PDIF)" msgstr "Sayısal Çıkış (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2759 +#: spa/plugins/alsa/acp/alsa-mixer.c:2828 msgid "Digital Input (S/PDIF)" msgstr "Sayısal Giriş (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2760 +#: spa/plugins/alsa/acp/alsa-mixer.c:2829 msgid "Multichannel Input" msgstr "Çok Kanallı Giriş" -#: spa/plugins/alsa/acp/alsa-mixer.c:2761 +#: spa/plugins/alsa/acp/alsa-mixer.c:2830 msgid "Multichannel Output" msgstr "Çok Kanallı Çıkış" -#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +#: spa/plugins/alsa/acp/alsa-mixer.c:2831 msgid "Game Output" msgstr "Oyun Çıkışı" -#: spa/plugins/alsa/acp/alsa-mixer.c:2763 -#: spa/plugins/alsa/acp/alsa-mixer.c:2764 +#: spa/plugins/alsa/acp/alsa-mixer.c:2832 +#: spa/plugins/alsa/acp/alsa-mixer.c:2833 msgid "Chat Output" msgstr "Sohbet Çıkışı" -#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +#: spa/plugins/alsa/acp/alsa-mixer.c:2834 msgid "Chat Input" msgstr "Sohbet Girişi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2766 +#: spa/plugins/alsa/acp/alsa-mixer.c:2835 msgid "Virtual Surround 7.1" msgstr "Sanal Çevresel Ses 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4456 +#: spa/plugins/alsa/acp/alsa-mixer.c:4522 msgid "Analog Mono" msgstr "Analog Tek Kanallı" -#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +#: spa/plugins/alsa/acp/alsa-mixer.c:4523 msgid "Analog Mono (Left)" msgstr "Analog Tek Kanallı (Sol)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +#: spa/plugins/alsa/acp/alsa-mixer.c:4524 msgid "Analog Mono (Right)" msgstr "Analog Tek Kanallı (Sağ)" @@ -390,147 +415,147 @@ msgstr "Analog Tek Kanallı (Sağ)" #. * here would lead to the source name to become "Analog Stereo Input #. * Input". The same logic applies to analog-stereo-output, #. * multichannel-input and multichannel-output. -#: spa/plugins/alsa/acp/alsa-mixer.c:4459 -#: spa/plugins/alsa/acp/alsa-mixer.c:4467 -#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +#: spa/plugins/alsa/acp/alsa-mixer.c:4525 +#: spa/plugins/alsa/acp/alsa-mixer.c:4533 +#: spa/plugins/alsa/acp/alsa-mixer.c:4534 msgid "Analog Stereo" msgstr "Analog Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +#: spa/plugins/alsa/acp/alsa-mixer.c:4526 msgid "Mono" msgstr "Tek Kanallı" -#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +#: spa/plugins/alsa/acp/alsa-mixer.c:4527 msgid "Stereo" msgstr "Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4469 -#: spa/plugins/alsa/acp/alsa-mixer.c:4627 -#: spa/plugins/bluez5/bluez5-device.c:1977 +#: spa/plugins/alsa/acp/alsa-mixer.c:4535 +#: spa/plugins/alsa/acp/alsa-mixer.c:4693 +#: spa/plugins/bluez5/bluez5-device.c:2362 msgid "Headset" msgstr "Kulaklık" -#: spa/plugins/alsa/acp/alsa-mixer.c:4470 -#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +#: spa/plugins/alsa/acp/alsa-mixer.c:4536 +#: spa/plugins/alsa/acp/alsa-mixer.c:4694 msgid "Speakerphone" msgstr "Hoparlör" -#: spa/plugins/alsa/acp/alsa-mixer.c:4471 -#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4537 +#: spa/plugins/alsa/acp/alsa-mixer.c:4538 msgid "Multichannel" msgstr "Çok kanallı" -#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4539 msgid "Analog Surround 2.1" msgstr "Analog Çevresel Ses 2.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +#: spa/plugins/alsa/acp/alsa-mixer.c:4540 msgid "Analog Surround 3.0" msgstr "Analog Çevresel Ses 3.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +#: spa/plugins/alsa/acp/alsa-mixer.c:4541 msgid "Analog Surround 3.1" msgstr "Analog Çevresel Ses 3.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +#: spa/plugins/alsa/acp/alsa-mixer.c:4542 msgid "Analog Surround 4.0" msgstr "Analog Çevresel Ses 4.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +#: spa/plugins/alsa/acp/alsa-mixer.c:4543 msgid "Analog Surround 4.1" msgstr "Analog Çevresel Ses 4.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +#: spa/plugins/alsa/acp/alsa-mixer.c:4544 msgid "Analog Surround 5.0" msgstr "Analog Çevresel Ses 5.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +#: spa/plugins/alsa/acp/alsa-mixer.c:4545 msgid "Analog Surround 5.1" msgstr "Analog Çevresel Ses 5.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +#: spa/plugins/alsa/acp/alsa-mixer.c:4546 msgid "Analog Surround 6.0" msgstr "Analog Çevresel Ses 6.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +#: spa/plugins/alsa/acp/alsa-mixer.c:4547 msgid "Analog Surround 6.1" msgstr "Analog Çevresel Ses 6.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +#: spa/plugins/alsa/acp/alsa-mixer.c:4548 msgid "Analog Surround 7.0" msgstr "Analog Çevresel Ses 7.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +#: spa/plugins/alsa/acp/alsa-mixer.c:4549 msgid "Analog Surround 7.1" msgstr "Analog Çevresel Ses 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +#: spa/plugins/alsa/acp/alsa-mixer.c:4550 msgid "Digital Stereo (IEC958)" msgstr "Sayısal Stereo (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +#: spa/plugins/alsa/acp/alsa-mixer.c:4551 msgid "Digital Surround 4.0 (IEC958/AC3)" msgstr "Sayısal Çevresel Ses 4.0 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +#: spa/plugins/alsa/acp/alsa-mixer.c:4552 msgid "Digital Surround 5.1 (IEC958/AC3)" msgstr "Sayısal Çevresel Ses 5.1 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +#: spa/plugins/alsa/acp/alsa-mixer.c:4553 msgid "Digital Surround 5.1 (IEC958/DTS)" msgstr "Sayısal Çevresel Ses 5.1 (IEC958/DTS)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +#: spa/plugins/alsa/acp/alsa-mixer.c:4554 msgid "Digital Stereo (HDMI)" msgstr "Sayısal Stereo (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +#: spa/plugins/alsa/acp/alsa-mixer.c:4555 msgid "Digital Surround 5.1 (HDMI)" msgstr "Sayısal Çevresel Ses 5.1 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +#: spa/plugins/alsa/acp/alsa-mixer.c:4556 msgid "Chat" msgstr "Sohbet" -#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +#: spa/plugins/alsa/acp/alsa-mixer.c:4557 msgid "Game" msgstr "Oyun" -#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +#: spa/plugins/alsa/acp/alsa-mixer.c:4691 msgid "Analog Mono Duplex" msgstr "Analog Tek Kanallı İkili" -#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +#: spa/plugins/alsa/acp/alsa-mixer.c:4692 msgid "Analog Stereo Duplex" msgstr "Analog İkili Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +#: spa/plugins/alsa/acp/alsa-mixer.c:4695 msgid "Digital Stereo Duplex (IEC958)" msgstr "Sayısal İkili Stereo (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +#: spa/plugins/alsa/acp/alsa-mixer.c:4696 msgid "Multichannel Duplex" msgstr "Çok Kanallı İkili" -#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +#: spa/plugins/alsa/acp/alsa-mixer.c:4697 msgid "Stereo Duplex" msgstr "İkili Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +#: spa/plugins/alsa/acp/alsa-mixer.c:4698 msgid "Mono Chat + 7.1 Surround" msgstr "Tek Kanallı Sohbet + 7.1 Çevresel Ses" -#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#: spa/plugins/alsa/acp/alsa-mixer.c:4799 #, c-format msgid "%s Output" msgstr "%s Çıkışı" -#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#: spa/plugins/alsa/acp/alsa-mixer.c:4807 #, c-format msgid "%s Input" msgstr "%s Girişi" -#: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314 +#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327 #, c-format msgid "" "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " @@ -547,16 +572,16 @@ msgstr[0] "" "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA " "geliştiricilerine bildirin." -#: spa/plugins/alsa/acp/alsa-util.c:1286 +#: spa/plugins/alsa/acp/alsa-util.c:1299 #, c-format msgid "" -"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s" -"%lu ms).\n" +"snd_pcm_delay() returned a value that is exceptionally large: %li byte " +"(%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgid_plural "" -"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s" -"%lu ms).\n" +"snd_pcm_delay() returned a value that is exceptionally large: %li bytes " +"(%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgstr[0] "" @@ -564,7 +589,7 @@ msgstr[0] "" "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA " "geliştiricilerine bildirin." -#: spa/plugins/alsa/acp/alsa-util.c:1333 +#: spa/plugins/alsa/acp/alsa-util.c:1346 #, c-format msgid "" "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail " @@ -577,7 +602,7 @@ msgstr "" "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA " "geliştiricilerine bildirin." -#: spa/plugins/alsa/acp/alsa-util.c:1376 +#: spa/plugins/alsa/acp/alsa-util.c:1389 #, c-format msgid "" "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte " @@ -595,112 +620,112 @@ msgstr[0] "" "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA " "geliştiricilerine bildirin." -#: spa/plugins/alsa/acp/channelmap.h:457 +#: spa/plugins/alsa/acp/channelmap.h:460 msgid "(invalid)" msgstr "(geçersiz)" -#: spa/plugins/alsa/acp/compat.c:193 +#: spa/plugins/alsa/acp/compat.c:194 msgid "Built-in Audio" msgstr "Dahili Ses" -#: spa/plugins/alsa/acp/compat.c:198 +#: spa/plugins/alsa/acp/compat.c:199 msgid "Modem" msgstr "Modem" -#: spa/plugins/bluez5/bluez5-device.c:1712 +#: spa/plugins/bluez5/bluez5-device.c:1987 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "Ses Geçidi (A2DP Kaynak & HSP/HFP AG)" -#: spa/plugins/bluez5/bluez5-device.c:1760 +#: spa/plugins/bluez5/bluez5-device.c:2016 +msgid "Audio Streaming for Hearing Aids (ASHA Sink)" +msgstr "İşitme Aygıtları İçin Ses Akışı (ASHA Alıcı)" + +#: spa/plugins/bluez5/bluez5-device.c:2059 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1763 +#: spa/plugins/bluez5/bluez5-device.c:2062 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1771 +#: spa/plugins/bluez5/bluez5-device.c:2070 msgid "High Fidelity Playback (A2DP Sink)" msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı)" -#: spa/plugins/bluez5/bluez5-device.c:1773 +#: spa/plugins/bluez5/bluez5-device.c:2072 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı)" -#: spa/plugins/bluez5/bluez5-device.c:1823 +#: spa/plugins/bluez5/bluez5-device.c:2146 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "Yüksek Kaliteli Çalma (BAP Alıcı, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1828 +#: spa/plugins/bluez5/bluez5-device.c:2151 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "Yüksek Kaliteli Giriş (BAP Kaynak, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1832 +#: spa/plugins/bluez5/bluez5-device.c:2155 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1841 +#: spa/plugins/bluez5/bluez5-device.c:2164 msgid "High Fidelity Playback (BAP Sink)" msgstr "Yüksek Kaliteli Çalma (BAP Alıcı)" -#: spa/plugins/bluez5/bluez5-device.c:1845 +#: spa/plugins/bluez5/bluez5-device.c:2168 msgid "High Fidelity Input (BAP Source)" msgstr "Yüksek Kaliteli Giriş (BAP Kaynak)" -#: spa/plugins/bluez5/bluez5-device.c:1848 +#: spa/plugins/bluez5/bluez5-device.c:2171 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı)" -#: spa/plugins/bluez5/bluez5-device.c:1897 +#: spa/plugins/bluez5/bluez5-device.c:2211 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "Kulaklık Ana Birimi (HSP/HFP, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1978 -#: spa/plugins/bluez5/bluez5-device.c:1983 -#: spa/plugins/bluez5/bluez5-device.c:1990 -#: spa/plugins/bluez5/bluez5-device.c:1996 -#: spa/plugins/bluez5/bluez5-device.c:2002 -#: spa/plugins/bluez5/bluez5-device.c:2008 -#: spa/plugins/bluez5/bluez5-device.c:2014 -#: spa/plugins/bluez5/bluez5-device.c:2020 -#: spa/plugins/bluez5/bluez5-device.c:2026 +#: spa/plugins/bluez5/bluez5-device.c:2363 +#: spa/plugins/bluez5/bluez5-device.c:2368 +#: spa/plugins/bluez5/bluez5-device.c:2375 +#: spa/plugins/bluez5/bluez5-device.c:2381 +#: spa/plugins/bluez5/bluez5-device.c:2387 +#: spa/plugins/bluez5/bluez5-device.c:2393 +#: spa/plugins/bluez5/bluez5-device.c:2399 +#: spa/plugins/bluez5/bluez5-device.c:2405 +#: spa/plugins/bluez5/bluez5-device.c:2411 msgid "Handsfree" msgstr "Ahizesiz" -#: spa/plugins/bluez5/bluez5-device.c:1984 +#: spa/plugins/bluez5/bluez5-device.c:2369 msgid "Handsfree (HFP)" msgstr "Ahizesiz (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2001 -msgid "Headphone" -msgstr "Kulaklık" - -#: spa/plugins/bluez5/bluez5-device.c:2007 +#: spa/plugins/bluez5/bluez5-device.c:2392 msgid "Portable" msgstr "Taşınabilir" -#: spa/plugins/bluez5/bluez5-device.c:2013 +#: spa/plugins/bluez5/bluez5-device.c:2398 msgid "Car" msgstr "Araba" -#: spa/plugins/bluez5/bluez5-device.c:2019 +#: spa/plugins/bluez5/bluez5-device.c:2404 msgid "HiFi" msgstr "Yüksek Kalite" -#: spa/plugins/bluez5/bluez5-device.c:2025 +#: spa/plugins/bluez5/bluez5-device.c:2410 msgid "Phone" msgstr "Telefon" -#: spa/plugins/bluez5/bluez5-device.c:2032 +#: spa/plugins/bluez5/bluez5-device.c:2417 msgid "Bluetooth" msgstr "Bluetooth" -#: spa/plugins/bluez5/bluez5-device.c:2033 +#: spa/plugins/bluez5/bluez5-device.c:2418 msgid "Bluetooth (HFP)" msgstr "Bluetooth (HFP)" diff --git a/po/zh_CN.po b/po/zh_CN.po index 1022f5d65..697be961c 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -6,15 +6,15 @@ # Cheng-Chia Tseng , 2010, 2012. # Frank Hill , 2015. # Mingye Wang (Arthur2e5) , 2015. -# lumingzh , 2024. +# lumingzh , 2024-2025. # msgid "" msgstr "" "Project-Id-Version: pipewire.master-tx\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" "issues\n" -"POT-Creation-Date: 2024-09-09 16:36+0000\n" -"PO-Revision-Date: 2024-10-08 09:41+0800\n" +"POT-Creation-Date: 2025-09-21 15:33+0000\n" +"PO-Revision-Date: 2025-09-22 08:53+0800\n" "Last-Translator: lumingzh \n" "Language-Team: Chinese (China) \n" "Language: zh_CN\n" @@ -22,7 +22,7 @@ msgstr "" "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: Gtranslator 47.0\n" +"X-Generator: Gtranslator 49.0\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/daemon/pipewire.c:29 @@ -42,11 +42,11 @@ msgstr "" " -c, --config 加载配置 (默认 %s)\n" " -P --properties 设置上下文属性\n" -#: src/daemon/pipewire.desktop.in:4 +#: src/daemon/pipewire.desktop.in:3 msgid "PipeWire Media System" msgstr "PipeWire 多媒体系统" -#: src/daemon/pipewire.desktop.in:5 +#: src/daemon/pipewire.desktop.in:4 msgid "Start the PipeWire Media System" msgstr "启动 PipeWire 多媒体系统" @@ -60,26 +60,26 @@ msgstr "至 %s%s%s 的隧道" msgid "Dummy Output" msgstr "虚拟输出" -#: src/modules/module-pulse-tunnel.c:774 +#: src/modules/module-pulse-tunnel.c:760 #, c-format msgid "Tunnel for %s@%s" msgstr "用于 %s@%s 的隧道" -#: src/modules/module-zeroconf-discover.c:318 +#: src/modules/module-zeroconf-discover.c:320 msgid "Unknown device" msgstr "未知设备" -#: src/modules/module-zeroconf-discover.c:330 +#: src/modules/module-zeroconf-discover.c:332 #, c-format msgid "%s on %s@%s" msgstr "%2$s@%3$s 上的 %1$s" -#: src/modules/module-zeroconf-discover.c:334 +#: src/modules/module-zeroconf-discover.c:336 #, c-format msgid "%s on %s" msgstr "%2$s 上的 %1$s" -#: src/tools/pw-cat.c:996 +#: src/tools/pw-cat.c:1084 #, c-format msgid "" "%s [options] [|-]\n" @@ -94,7 +94,7 @@ msgstr "" " -v, --verbose 输出详细操作\n" "\n" -#: src/tools/pw-cat.c:1003 +#: src/tools/pw-cat.c:1091 #, c-format msgid "" " -R, --remote Remote daemon name\n" @@ -126,7 +126,7 @@ msgstr "" " -P --properties 设置节点属性\n" "\n" -#: src/tools/pw-cat.c:1021 +#: src/tools/pw-cat.c:1109 #, c-format msgid "" " --rate Sample rate (req. for rec) (default " @@ -144,6 +144,9 @@ msgid "" " -q --quality Resampler quality (0 - 15) (default " "%d)\n" " -a, --raw RAW mode\n" +" -M, --force-midi Force midi format, one of \"midi\" " +"or \"ump\", (default ump)\n" +" -n, --sample-count COUNT Stop after COUNT samples\n" "\n" msgstr "" " --rate 采样率 (录制模式需要) (默认 %u)\n" @@ -151,22 +154,27 @@ msgstr "" " --channel-map 通道映射\n" " \"stereo\", \"surround-51\",... " "中的其一或\n" -" 以\",\"分隔的通道名列表: 如 \"FL," -"FR\"\n" +" 以\",\"分隔的通道名列表: 如 " +"\"FL,FR\"\n" " --format 采样格式 %s (录制模式需要) (默认 " "%s)\n" " --volume 媒体流音量 0-1.0 (默认 %.3f)\n" " -q --quality 重采样质量 (0 - 15) (默认 %d)\n" " -a, --raw 原生模式\n" +" -M, --force-midi 强制 midi 格式,\"midi\" 或 \"ump\" " +"其一 (默认 ump)\n" +" -n, --sample-count COUNT 计数采样后停止\n" "\n" -#: src/tools/pw-cat.c:1039 +#: src/tools/pw-cat.c:1129 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" " -m, --midi Midi mode\n" " -d, --dsd DSD mode\n" " -o, --encoded Encoded mode\n" +" -s, --sysex SysEx mode\n" +" -c, --midi-clip MIDI clip mode\n" "\n" msgstr "" " -p, --playback 回放模式\n" @@ -174,9 +182,11 @@ msgstr "" " -m, --midi Midi 模式\n" " -d, --dsd DSD 模式\n" " -o, --encoded 编码模式\n" +" -s, --sysex SysEx 模式\n" +" -c, --midi-clip MIDI 剪辑模式\n" "\n" -#: src/tools/pw-cli.c:2285 +#: src/tools/pw-cli.c:2386 #, c-format msgid "" "%s [options] [command]\n" @@ -194,15 +204,20 @@ msgstr "" " -m, --monitor 监视器活动\n" "\n" -#: spa/plugins/alsa/acp/acp.c:327 +#: spa/plugins/alsa/acp/acp.c:351 msgid "Pro Audio" msgstr "专业音频" -#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633 -#: spa/plugins/bluez5/bluez5-device.c:1701 +#: spa/plugins/alsa/acp/acp.c:527 spa/plugins/alsa/acp/alsa-mixer.c:4635 +#: spa/plugins/bluez5/bluez5-device.c:1974 msgid "Off" msgstr "关" +#: spa/plugins/alsa/acp/acp.c:610 +#, c-format +msgid "%s [ALSA UCM error]" +msgstr "%s [ALSA UCM 错误]" + #: spa/plugins/alsa/acp/alsa-mixer.c:2652 msgid "Input" msgstr "输入" @@ -226,7 +241,7 @@ msgstr "输入插孔" #: spa/plugins/alsa/acp/alsa-mixer.c:2657 #: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:1989 +#: spa/plugins/bluez5/bluez5-device.c:2372 msgid "Microphone" msgstr "话筒" @@ -292,12 +307,15 @@ msgid "No Bass Boost" msgstr "无重低音增强" #: spa/plugins/alsa/acp/alsa-mixer.c:2672 -#: spa/plugins/bluez5/bluez5-device.c:1995 +#: spa/plugins/bluez5/bluez5-device.c:2378 msgid "Speaker" msgstr "扬声器" +#. Don't call it "headset", the HF one has the mic #: spa/plugins/alsa/acp/alsa-mixer.c:2673 #: spa/plugins/alsa/acp/alsa-mixer.c:2751 +#: spa/plugins/bluez5/bluez5-device.c:2384 +#: spa/plugins/bluez5/bluez5-device.c:2451 msgid "Headphones" msgstr "模拟耳机" @@ -374,15 +392,15 @@ msgstr "语音输入" msgid "Virtual Surround 7.1" msgstr "虚拟环绕 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4456 +#: spa/plugins/alsa/acp/alsa-mixer.c:4458 msgid "Analog Mono" msgstr "模拟单声道" -#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +#: spa/plugins/alsa/acp/alsa-mixer.c:4459 msgid "Analog Mono (Left)" msgstr "模拟单声道 (左声道)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +#: spa/plugins/alsa/acp/alsa-mixer.c:4460 msgid "Analog Mono (Right)" msgstr "模拟单声道 (右声道)" @@ -391,147 +409,147 @@ msgstr "模拟单声道 (右声道)" #. * here would lead to the source name to become "Analog Stereo Input #. * Input". The same logic applies to analog-stereo-output, #. * multichannel-input and multichannel-output. -#: spa/plugins/alsa/acp/alsa-mixer.c:4459 -#: spa/plugins/alsa/acp/alsa-mixer.c:4467 -#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +#: spa/plugins/alsa/acp/alsa-mixer.c:4469 +#: spa/plugins/alsa/acp/alsa-mixer.c:4470 msgid "Analog Stereo" msgstr "模拟立体声" -#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +#: spa/plugins/alsa/acp/alsa-mixer.c:4462 msgid "Mono" msgstr "单声道" -#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +#: spa/plugins/alsa/acp/alsa-mixer.c:4463 msgid "Stereo" msgstr "立体声" -#: spa/plugins/alsa/acp/alsa-mixer.c:4469 -#: spa/plugins/alsa/acp/alsa-mixer.c:4627 -#: spa/plugins/bluez5/bluez5-device.c:1977 +#: spa/plugins/alsa/acp/alsa-mixer.c:4471 +#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +#: spa/plugins/bluez5/bluez5-device.c:2360 msgid "Headset" msgstr "耳机" -#: spa/plugins/alsa/acp/alsa-mixer.c:4470 -#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4630 msgid "Speakerphone" msgstr "扬声麦克风" -#: spa/plugins/alsa/acp/alsa-mixer.c:4471 -#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4474 msgid "Multichannel" msgstr "多声道" -#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4475 msgid "Analog Surround 2.1" msgstr "模拟环绕 2.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +#: spa/plugins/alsa/acp/alsa-mixer.c:4476 msgid "Analog Surround 3.0" msgstr "模拟环绕 3.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +#: spa/plugins/alsa/acp/alsa-mixer.c:4477 msgid "Analog Surround 3.1" msgstr "模拟环绕 3.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +#: spa/plugins/alsa/acp/alsa-mixer.c:4478 msgid "Analog Surround 4.0" msgstr "模拟环绕 4.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +#: spa/plugins/alsa/acp/alsa-mixer.c:4479 msgid "Analog Surround 4.1" msgstr "模拟环绕 4.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +#: spa/plugins/alsa/acp/alsa-mixer.c:4480 msgid "Analog Surround 5.0" msgstr "模拟环绕 5.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +#: spa/plugins/alsa/acp/alsa-mixer.c:4481 msgid "Analog Surround 5.1" msgstr "模拟环绕 5.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +#: spa/plugins/alsa/acp/alsa-mixer.c:4482 msgid "Analog Surround 6.0" msgstr "模拟环绕 6.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +#: spa/plugins/alsa/acp/alsa-mixer.c:4483 msgid "Analog Surround 6.1" msgstr "模拟环绕 6.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +#: spa/plugins/alsa/acp/alsa-mixer.c:4484 msgid "Analog Surround 7.0" msgstr "模拟环绕 7.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +#: spa/plugins/alsa/acp/alsa-mixer.c:4485 msgid "Analog Surround 7.1" msgstr "模拟环绕 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +#: spa/plugins/alsa/acp/alsa-mixer.c:4486 msgid "Digital Stereo (IEC958)" msgstr "数字立体声 (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +#: spa/plugins/alsa/acp/alsa-mixer.c:4487 msgid "Digital Surround 4.0 (IEC958/AC3)" msgstr "数字环绕 4.0 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +#: spa/plugins/alsa/acp/alsa-mixer.c:4488 msgid "Digital Surround 5.1 (IEC958/AC3)" msgstr "数字环绕 5.1 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +#: spa/plugins/alsa/acp/alsa-mixer.c:4489 msgid "Digital Surround 5.1 (IEC958/DTS)" msgstr "数字环绕 5.1 (IEC958/DTS)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +#: spa/plugins/alsa/acp/alsa-mixer.c:4490 msgid "Digital Stereo (HDMI)" msgstr "数字立体声 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +#: spa/plugins/alsa/acp/alsa-mixer.c:4491 msgid "Digital Surround 5.1 (HDMI)" msgstr "数字环绕 5.1 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +#: spa/plugins/alsa/acp/alsa-mixer.c:4492 msgid "Chat" msgstr "语音" -#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +#: spa/plugins/alsa/acp/alsa-mixer.c:4493 msgid "Game" msgstr "游戏" -#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +#: spa/plugins/alsa/acp/alsa-mixer.c:4627 msgid "Analog Mono Duplex" msgstr "模拟单声道双工" -#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +#: spa/plugins/alsa/acp/alsa-mixer.c:4628 msgid "Analog Stereo Duplex" msgstr "模拟立体声双工" -#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +#: spa/plugins/alsa/acp/alsa-mixer.c:4631 msgid "Digital Stereo Duplex (IEC958)" msgstr "数字立体声双工 (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +#: spa/plugins/alsa/acp/alsa-mixer.c:4632 msgid "Multichannel Duplex" msgstr "多声道双工" -#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +#: spa/plugins/alsa/acp/alsa-mixer.c:4633 msgid "Stereo Duplex" msgstr "模拟立体声双工" -#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +#: spa/plugins/alsa/acp/alsa-mixer.c:4634 msgid "Mono Chat + 7.1 Surround" msgstr "单声道语音 + 7.1 环绕声" -#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#: spa/plugins/alsa/acp/alsa-mixer.c:4735 #, c-format msgid "%s Output" msgstr "%s 输出" -#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#: spa/plugins/alsa/acp/alsa-mixer.c:4743 #, c-format msgid "%s Input" msgstr "%s 输入" -#: spa/plugins/alsa/acp/alsa-util.c:1231 spa/plugins/alsa/acp/alsa-util.c:1325 +#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327 #, c-format msgid "" "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " @@ -547,7 +565,7 @@ msgstr[0] "" "snd_pcm_avail() 返回的值非常大:%lu 字节(%lu 毫秒)。\n" "这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。" -#: spa/plugins/alsa/acp/alsa-util.c:1297 +#: spa/plugins/alsa/acp/alsa-util.c:1299 #, c-format msgid "" "snd_pcm_delay() returned a value that is exceptionally large: %li byte " @@ -563,7 +581,7 @@ msgstr[0] "" "snd_pcm_delay() 返回的值非常大:%li 字节(%s%lu 毫秒)。\n" "这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。" -#: spa/plugins/alsa/acp/alsa-util.c:1344 +#: spa/plugins/alsa/acp/alsa-util.c:1346 #, c-format msgid "" "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail " @@ -574,7 +592,7 @@ msgstr "" "snd_pcm_avail_delay() 返回的值非常很奇怪:延迟 %lu 小于可用 (avail) %lu。\n" "这很可能是由 ALSA 驱动程序 %s 的缺陷导致的。请向 ALSA 开发者报告这个问题。" -#: spa/plugins/alsa/acp/alsa-util.c:1387 +#: spa/plugins/alsa/acp/alsa-util.c:1389 #, c-format msgid "" "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte " @@ -594,111 +612,114 @@ msgstr[0] "" msgid "(invalid)" msgstr "(无效)" -#: spa/plugins/alsa/acp/compat.c:193 +#: spa/plugins/alsa/acp/compat.c:194 msgid "Built-in Audio" msgstr "内置音频" -#: spa/plugins/alsa/acp/compat.c:198 +#: spa/plugins/alsa/acp/compat.c:199 msgid "Modem" msgstr "调制解调器" -#: spa/plugins/bluez5/bluez5-device.c:1712 +#: spa/plugins/bluez5/bluez5-device.c:1985 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "音频网关 (A2DP 信源 或 HSP/HFP 网关)" -#: spa/plugins/bluez5/bluez5-device.c:1760 +#: spa/plugins/bluez5/bluez5-device.c:2014 +msgid "Audio Streaming for Hearing Aids (ASHA Sink)" +msgstr "助听器音频流 (ASHA 信宿)" + +#: spa/plugins/bluez5/bluez5-device.c:2057 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "高保真回放 (A2DP 信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:1763 +#: spa/plugins/bluez5/bluez5-device.c:2060 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "高保真双工 (A2DP 信源/信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:1771 +#: spa/plugins/bluez5/bluez5-device.c:2068 msgid "High Fidelity Playback (A2DP Sink)" msgstr "高保真回放 (A2DP 信宿)" -#: spa/plugins/bluez5/bluez5-device.c:1773 +#: spa/plugins/bluez5/bluez5-device.c:2070 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "高保真双工 (A2DP 信源/信宿)" -#: spa/plugins/bluez5/bluez5-device.c:1823 +#: spa/plugins/bluez5/bluez5-device.c:2144 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "高保真回放 (BAP 信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:1828 +#: spa/plugins/bluez5/bluez5-device.c:2149 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "高保真输入 (BAP 信源, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:1832 +#: spa/plugins/bluez5/bluez5-device.c:2153 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "高保真双工 (BAP 信源/信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:1841 +#: spa/plugins/bluez5/bluez5-device.c:2162 msgid "High Fidelity Playback (BAP Sink)" msgstr "高保真回放 (BAP 信宿)" -#: spa/plugins/bluez5/bluez5-device.c:1845 +#: spa/plugins/bluez5/bluez5-device.c:2166 msgid "High Fidelity Input (BAP Source)" msgstr "高保真输入 (BAP 信源)" -#: spa/plugins/bluez5/bluez5-device.c:1848 +#: spa/plugins/bluez5/bluez5-device.c:2169 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "高保真双工 (BAP 信源/信宿)" -#: spa/plugins/bluez5/bluez5-device.c:1897 +#: spa/plugins/bluez5/bluez5-device.c:2209 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "头戴式耳机单元 (HSP/HFP, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:1978 -#: spa/plugins/bluez5/bluez5-device.c:1983 -#: spa/plugins/bluez5/bluez5-device.c:1990 -#: spa/plugins/bluez5/bluez5-device.c:1996 -#: spa/plugins/bluez5/bluez5-device.c:2002 -#: spa/plugins/bluez5/bluez5-device.c:2008 -#: spa/plugins/bluez5/bluez5-device.c:2014 -#: spa/plugins/bluez5/bluez5-device.c:2020 -#: spa/plugins/bluez5/bluez5-device.c:2026 +#: spa/plugins/bluez5/bluez5-device.c:2361 +#: spa/plugins/bluez5/bluez5-device.c:2366 +#: spa/plugins/bluez5/bluez5-device.c:2373 +#: spa/plugins/bluez5/bluez5-device.c:2379 +#: spa/plugins/bluez5/bluez5-device.c:2385 +#: spa/plugins/bluez5/bluez5-device.c:2391 +#: spa/plugins/bluez5/bluez5-device.c:2397 +#: spa/plugins/bluez5/bluez5-device.c:2403 +#: spa/plugins/bluez5/bluez5-device.c:2409 msgid "Handsfree" msgstr "免手操作" -#: spa/plugins/bluez5/bluez5-device.c:1984 +#: spa/plugins/bluez5/bluez5-device.c:2367 msgid "Handsfree (HFP)" msgstr "免手操作 (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2001 -msgid "Headphone" -msgstr "头戴耳机" - -#: spa/plugins/bluez5/bluez5-device.c:2007 +#: spa/plugins/bluez5/bluez5-device.c:2390 msgid "Portable" msgstr "便携式" -#: spa/plugins/bluez5/bluez5-device.c:2013 +#: spa/plugins/bluez5/bluez5-device.c:2396 msgid "Car" msgstr "车内" -#: spa/plugins/bluez5/bluez5-device.c:2019 +#: spa/plugins/bluez5/bluez5-device.c:2402 msgid "HiFi" msgstr "高保真" -#: spa/plugins/bluez5/bluez5-device.c:2025 +#: spa/plugins/bluez5/bluez5-device.c:2408 msgid "Phone" msgstr "电话" -#: spa/plugins/bluez5/bluez5-device.c:2032 +#: spa/plugins/bluez5/bluez5-device.c:2415 msgid "Bluetooth" msgstr "蓝牙" -#: spa/plugins/bluez5/bluez5-device.c:2033 +#: spa/plugins/bluez5/bluez5-device.c:2416 msgid "Bluetooth (HFP)" msgstr "蓝牙 (HFP)" +#~ msgid "Headphone" +#~ msgstr "头戴耳机" + #~ msgid "Headset Head Unit (HSP/HFP)" #~ msgstr "头戴式耳机单元 (HSP/HFP)" diff --git a/pw-uninstalled.sh b/pw-uninstalled.sh index 1bb6c55c2..af6aa4d87 100755 --- a/pw-uninstalled.sh +++ b/pw-uninstalled.sh @@ -53,6 +53,7 @@ export ALSA_PLUGIN_DIR="${BUILDDIR}/pipewire-alsa/alsa-plugins" export PW_BUILDDIR=$BUILDDIR export PW_UNINSTALLED=1 export PKG_CONFIG_PATH="${BUILDDIR}/meson-uninstalled/:${PKG_CONFIG_PATH}" +export PIPEWIRE_LOG_SYSTEMD=false if [ -d "${BUILDDIR}/subprojects/wireplumber" ]; then # FIXME: find a nice, shell-neutral way to specify a prompt diff --git a/spa/examples/adapter-control.c b/spa/examples/adapter-control.c index 6aa36dfaa..0a48f2fd4 100644 --- a/spa/examples/adapter-control.c +++ b/spa/examples/adapter-control.c @@ -578,7 +578,7 @@ static int make_nodes(struct data *data) SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT), SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); - if ((res = spa_node_set_param(data->source_node, SPA_PARAM_PortConfig, 0, param) < 0)) { + if ((res = spa_node_set_param(data->source_node, SPA_PARAM_PortConfig, 0, param)) < 0) { printf("can't setup source node %d\n", res); return res; } @@ -647,7 +647,7 @@ static int make_nodes(struct data *data) SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); - if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_PortConfig, 0, param) < 0)) { + if ((res = spa_node_set_param(data->sink_node, SPA_PARAM_PortConfig, 0, param)) < 0) { printf("can't setup sink node %d\n", res); return res; } @@ -987,7 +987,7 @@ int main(int argc, char *argv[]) setlocale(LC_ALL, ""); - while ((c = getopt_long(argc, argv, "hdmstiac:", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hd:m:s:t:i:a:c:", long_options, NULL)) != -1) { switch (c) { case 'h': show_help(&data, argv[0], false); diff --git a/spa/examples/local-libcamera.c b/spa/examples/local-libcamera.c index d6e9f80d9..8e8a279aa 100644 --- a/spa/examples/local-libcamera.c +++ b/spa/examples/local-libcamera.c @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -83,7 +84,8 @@ struct data { unsigned int n_buffers; }; -static int load_handle(struct data *data, struct spa_handle **handle, const char *lib, const char *name) +static int load_handle(struct data *data, struct spa_handle **handle, const char *lib, const char *name, + const struct spa_dict *params) { int res; void *hnd; @@ -117,9 +119,9 @@ static int load_handle(struct data *data, struct spa_handle **handle, const char if (!spa_streq(factory->name, name)) continue; - *handle = calloc(1, spa_handle_factory_get_size(factory, NULL)); + *handle = calloc(1, spa_handle_factory_get_size(factory, params)); if ((res = spa_handle_factory_init(factory, *handle, - NULL, data->support, + params, data->support, data->n_support)) < 0) { printf("can't make factory instance: %d\n", res); return res; @@ -129,13 +131,14 @@ static int load_handle(struct data *data, struct spa_handle **handle, const char return -EBADF; } -static int make_node(struct data *data, struct spa_node **node, const char *lib, const char *name) +static int make_node(struct data *data, struct spa_node **node, const char *lib, const char *name, + const struct spa_dict *params) { struct spa_handle *handle = NULL; void *iface; int res; - if ((res = load_handle(data, &handle, lib, name)) < 0) + if ((res = load_handle(data, &handle, lib, name, params)) < 0) return res; if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Node, &iface)) < 0) { @@ -237,10 +240,13 @@ static int make_nodes(struct data *data, const char *device) uint8_t buffer[256]; uint32_t index; - if ((res = - make_node(data, &data->source, - "libcamera/libspa-libcamera.so", - SPA_NAME_API_LIBCAMERA_SOURCE)) < 0) { + const struct spa_dict_item items[] = { + { SPA_KEY_API_LIBCAMERA_PATH, device }, + }; + + if ((res = make_node(data, &data->source, + "libcamera/libspa-libcamera.so", SPA_NAME_API_LIBCAMERA_SOURCE, + &SPA_DICT_INIT_ARRAY(items))) < 0) { printf("can't create libcamera-source: %d\n", res); return res; } @@ -254,14 +260,6 @@ static int make_nodes(struct data *data, const char *device) spa_debug_pod(0, NULL, props); } - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - props = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_Props, 0, - SPA_PROP_device, SPA_POD_String(device ? device : "/dev/media0")); - - if ((res = spa_node_set_param(data->source, SPA_PARAM_Props, 0, props)) < 0) - printf("got set_props error %d\n", res); - return res; } @@ -442,13 +440,18 @@ int main(int argc, char *argv[]) struct spa_handle *handle = NULL; void *iface; + if (argc < 2) { + printf("usage: %s \n", argv[0]); + return EXIT_FAILURE; + } + if ((str = getenv("SPA_PLUGIN_DIR")) == NULL) str = PLUGINDIR; data.plugin_dir = str; if ((res = load_handle(&data, &handle, "support/libspa-support.so", - SPA_NAME_SUPPORT_SYSTEM)) < 0) + SPA_NAME_SUPPORT_SYSTEM, NULL)) < 0) return res; if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) { @@ -460,7 +463,7 @@ int main(int argc, char *argv[]) if ((res = load_handle(&data, &handle, "support/libspa-support.so", - SPA_NAME_SUPPORT_LOOP)) < 0) + SPA_NAME_SUPPORT_LOOP, NULL)) < 0) return res; if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) { diff --git a/spa/include/spa/buffer/alloc.h b/spa/include/spa/buffer/alloc.h index dc8f4cc64..fdda40bfe 100644 --- a/spa/include/spa/buffer/alloc.h +++ b/spa/include/spa/buffer/alloc.h @@ -5,12 +5,12 @@ #ifndef SPA_BUFFER_ALLOC_H #define SPA_BUFFER_ALLOC_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - #ifndef SPA_API_BUFFER_ALLOC #ifdef SPA_API_IMPL #define SPA_API_BUFFER_ALLOC SPA_API_IMPL @@ -122,7 +122,7 @@ SPA_API_BUFFER_ALLOC int spa_buffer_alloc_fill_info(struct spa_buffer_alloc_info * | | uint32_t offset | * | | uint32_t size | * | | int32_t stride | - * | | int32_t dummy | + * | | int32_t flags | * | | ... chunks | * | +------------------------------+ * +>| data | memory for n_datas data, aligned diff --git a/spa/include/spa/buffer/buffer.h b/spa/include/spa/buffer/buffer.h index 03fd13b2a..29ef9f534 100644 --- a/spa/include/spa/buffer/buffer.h +++ b/spa/include/spa/buffer/buffer.h @@ -5,13 +5,13 @@ #ifndef SPA_BUFFER_H #define SPA_BUFFER_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #ifndef SPA_API_BUFFER #ifdef SPA_API_IMPL #define SPA_API_BUFFER SPA_API_IMPL @@ -118,6 +118,17 @@ SPA_API_BUFFER void *spa_buffer_find_meta_data(const struct spa_buffer *b, uint3 return NULL; } +SPA_API_BUFFER bool spa_buffer_has_meta_features(const struct spa_buffer *b, uint32_t type, uint32_t features) +{ + uint32_t i; + for (i = 0; i < b->n_metas; i++) { + uint32_t t = b->metas[i].type; + if ((t >> 16) == type && (t & features) == features) + return true; + } + return false; +} + /** * \} */ diff --git a/spa/include/spa/buffer/meta.h b/spa/include/spa/buffer/meta.h index b484cfb01..287410f02 100644 --- a/spa/include/spa/buffer/meta.h +++ b/spa/include/spa/buffer/meta.h @@ -5,13 +5,13 @@ #ifndef SPA_META_H #define SPA_META_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #ifndef SPA_API_META #ifdef SPA_API_IMPL #define SPA_API_META SPA_API_IMPL @@ -37,10 +37,17 @@ enum spa_meta_type { SPA_META_Busy, /**< don't write to buffer when count > 0 */ SPA_META_VideoTransform, /**< struct spa_meta_transform */ SPA_META_SyncTimeline, /**< struct spa_meta_sync_timeline */ - _SPA_META_LAST, /**< not part of ABI/API */ + + SPA_META_START_custom = 0x200, + + SPA_META_START_features = 0x10000, /* features start, these have 0 size, the + * type in the upper 16 bits and a bitmask in + * the lower 16 bits with type specific features. */ }; +#define SPA_META_TYPE_FEATURES(type,features) (((type)<<16)|(features)) + /** * A metadata element. * @@ -183,7 +190,12 @@ struct spa_meta_videotransform { * this metadata as SPA_PARAM_BUFFERS_metaType when negotiating a buffer * layout with 2 extra fds. */ +#define SPA_META_FEATURE_SYNC_TIMELINE_RELEASE (1<<0) /**< metadata supports RELEASE */ + struct spa_meta_sync_timeline { +#define SPA_META_SYNC_TIMELINE_UNSCHEDULED_RELEASE (1<<0) /**< this flag is set by the producer and cleared + * by the consumer when it promises to signal + * the release point */ uint32_t flags; uint32_t padding; uint64_t acquire_point; /**< the timeline acquire point, this is when the data diff --git a/spa/include/spa/buffer/type-info.h b/spa/include/spa/buffer/type-info.h index cdfe52770..47577dccb 100644 --- a/spa/include/spa/buffer/type-info.h +++ b/spa/include/spa/buffer/type-info.h @@ -5,6 +5,10 @@ #ifndef SPA_BUFFER_TYPES_H #define SPA_BUFFER_TYPES_H +#include +#include +#include + /** * \addtogroup spa_buffer * \{ @@ -14,10 +18,6 @@ extern "C" { #endif -#include -#include -#include - #define SPA_TYPE_INFO_Buffer SPA_TYPE_INFO_POINTER_BASE "Buffer" #define SPA_TYPE_INFO_BUFFER_BASE SPA_TYPE_INFO_Buffer ":" diff --git a/spa/include/spa/control/control.h b/spa/include/spa/control/control.h index 1c1ec81fb..69b0d106e 100644 --- a/spa/include/spa/control/control.h +++ b/spa/include/spa/control/control.h @@ -5,13 +5,13 @@ #ifndef SPA_CONTROL_H #define SPA_CONTROL_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** \defgroup spa_control Control * Control type declarations */ diff --git a/spa/include/spa/control/type-info.h b/spa/include/spa/control/type-info.h index 6c7bd6a96..eae237b01 100644 --- a/spa/include/spa/control/type-info.h +++ b/spa/include/spa/control/type-info.h @@ -5,6 +5,10 @@ #ifndef SPA_CONTROL_TYPES_H #define SPA_CONTROL_TYPES_H +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include -#include -#include - /* base for parameter object enumerations */ #define SPA_TYPE_INFO_Control SPA_TYPE_INFO_ENUM_BASE "Control" #define SPA_TYPE_INFO_CONTROL_BASE SPA_TYPE_INFO_Control ":" diff --git a/spa/include/spa/control/ump-utils.h b/spa/include/spa/control/ump-utils.h index 9f57efb93..582c44e2d 100644 --- a/spa/include/spa/control/ump-utils.h +++ b/spa/include/spa/control/ump-utils.h @@ -6,13 +6,13 @@ #ifndef SPA_CONTROL_UMP_UTILS_H #define SPA_CONTROL_UMP_UTILS_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #ifndef SPA_API_CONTROL_UMP_UTILS #ifdef SPA_API_IMPL #define SPA_API_CONTROL_UMP_UTILS SPA_API_IMPL @@ -48,64 +48,98 @@ SPA_API_CONTROL_UMP_UTILS size_t spa_ump_message_size(uint8_t message_type) return ump_sizes[message_type & 0xf]; } -SPA_API_CONTROL_UMP_UTILS int spa_ump_to_midi(uint32_t *ump, size_t ump_size, - uint8_t *midi, size_t midi_maxsize) +SPA_API_CONTROL_UMP_UTILS int spa_ump_to_midi(const uint32_t **ump, size_t *ump_size, + uint8_t *midi, size_t midi_maxsize, uint64_t *state) { int size = 0; + uint32_t to_consume = 0; + const uint32_t *u = *ump; - if (ump_size < 4) - return 0; + if (*ump_size < 4 || + (to_consume = (spa_ump_message_size(u[0]>>28) * 4)) > *ump_size) { + to_consume = *ump_size; + goto done; + } if (midi_maxsize < 8) return -ENOSPC; - switch (ump[0] >> 28) { + switch (u[0] >> 28) { case 0x1: /* System Real Time and System Common Messages (except System Exclusive) */ - midi[size++] = (ump[0] >> 16) & 0xff; + midi[size++] = (u[0] >> 16) & 0xff; if (midi[0] >= 0xf1 && midi[0] <= 0xf3) { - midi[size++] = (ump[0] >> 8) & 0x7f; + midi[size++] = (u[0] >> 8) & 0x7f; if (midi[0] == 0xf2) - midi[size++] = ump[0] & 0x7f; + midi[size++] = u[0] & 0x7f; } break; case 0x2: /* MIDI 1.0 Channel Voice Messages */ - midi[size++] = (ump[0] >> 16); - midi[size++] = (ump[0] >> 8); + midi[size++] = (u[0] >> 16); + midi[size++] = (u[0] >> 8); if (midi[0] < 0xc0 || midi[0] > 0xdf) - midi[size++] = (ump[0]); + midi[size++] = (u[0]); break; case 0x3: /* Data Messages (including System Exclusive) */ { uint8_t status, i, bytes; - if (ump_size < 8) - return 0; - - status = (ump[0] >> 20) & 0xf; - bytes = SPA_CLAMP((ump[0] >> 16) & 0xf, 0u, 6u); + status = (u[0] >> 20) & 0xf; + bytes = SPA_CLAMP((u[0] >> 16) & 0xf, 0u, 6u); if (status == 0 || status == 1) midi[size++] = 0xf0; for (i = 0 ; i < bytes; i++) - /* ump[0] >> 8 | ump[0] | ump[1] >> 24 | ump[1] >>16 ... */ - midi[size++] = ump[(i+2)/4] >> ((5-i)%4 * 8); + /* u[0] >> 8 | u[0] | u[1] >> 24 | u[1] >>16 ... */ + midi[size++] = u[(i+2)/4] >> ((5-i)%4 * 8); if (status == 0 || status == 3) midi[size++] = 0xf7; break; } case 0x4: /* MIDI 2.0 Channel Voice Messages */ - if (ump_size < 8) - return 0; - midi[size++] = (ump[0] >> 16) | 0x80; - if (midi[0] < 0xc0 || midi[0] > 0xdf) - midi[size++] = (ump[0] >> 8) & 0x7f; - midi[size++] = (ump[1] >> 25); + { + uint8_t status = (u[0] >> 16) | 0x80; + switch (status & 0xf0) { + case 0xc0: + /* program/bank change */ + if (!(u[0] & 1)) + *state = 2; + if (*state == 0) { + midi[size++] = (status & 0xf) | 0xb0; + midi[size++] = 0; + midi[size++] = (u[1] >> 8); + to_consume = 0; + *state = 1; + } + else if (*state == 1) { + midi[size++] = (status & 0xf) | 0xb0; + midi[size++] = 32; + midi[size++] = u[1]; + to_consume = 0; + *state = 2; + } + else if (*state == 2) { + midi[size++] = status; + midi[size++] = (u[1] >> 24); + *state = 0; + } + break; + default: + midi[size++] = status; + midi[size++] = (u[0] >> 8) & 0x7f; + SPA_FALLTHROUGH; + case 0xd0: + midi[size++] = (u[1] >> 25); + break; + } break; - + } case 0x0: /* Utility Messages */ case 0x5: /* Data Messages */ default: - return 0; + break; } +done: + (*ump_size) -= to_consume; + (*ump) = SPA_PTROFF(*ump, to_consume, uint32_t); return size; } @@ -189,15 +223,15 @@ SPA_API_CONTROL_UMP_UTILS int spa_ump_from_midi(uint8_t **midi, size_t *midi_siz break; case 0xf2: to_consume = 3; - prefix = 0x10000000; + prefix |= 0x10000000; break; case 0xf1: case 0xf3: to_consume = 2; - prefix = 0x10000000; + prefix |= 0x10000000; break; case 0xf4 ... 0xff: to_consume = 1; - prefix = 0x10000000; + prefix |= 0x10000000; break; default: return -EIO; diff --git a/spa/include/spa/debug/buffer.h b/spa/include/spa/debug/buffer.h index eea48ae4c..c0b533564 100644 --- a/spa/include/spa/debug/buffer.h +++ b/spa/include/spa/debug/buffer.h @@ -5,6 +5,11 @@ #ifndef SPA_DEBUG_BUFFER_H #define SPA_DEBUG_BUFFER_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -18,11 +23,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_DEBUG_BUFFER #ifdef SPA_API_IMPL #define SPA_API_DEBUG_BUFFER SPA_API_IMPL diff --git a/spa/include/spa/debug/context.h b/spa/include/spa/debug/context.h index 13002f666..73ae96535 100644 --- a/spa/include/spa/debug/context.h +++ b/spa/include/spa/debug/context.h @@ -5,15 +5,16 @@ #ifndef SPA_DEBUG_CONTEXT_H #define SPA_DEBUG_CONTEXT_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include + +#ifdef __cplusplus +extern "C" { +#endif + /** * \addtogroup spa_debug * \{ diff --git a/spa/include/spa/debug/dict.h b/spa/include/spa/debug/dict.h index 5657b2d98..5d9e22e6f 100644 --- a/spa/include/spa/debug/dict.h +++ b/spa/include/spa/debug/dict.h @@ -5,6 +5,9 @@ #ifndef SPA_DEBUG_DICT_H #define SPA_DEBUG_DICT_H +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,9 +17,6 @@ extern "C" { * \{ */ -#include -#include - #ifndef SPA_API_DEBUG_DICT #ifdef SPA_API_IMPL #define SPA_API_DEBUG_DICT SPA_API_IMPL diff --git a/spa/include/spa/debug/file.h b/spa/include/spa/debug/file.h index 17ce46b7e..9d1a2e2be 100644 --- a/spa/include/spa/debug/file.h +++ b/spa/include/spa/debug/file.h @@ -5,10 +5,6 @@ #ifndef SPA_DEBUG_FILE_H #define SPA_DEBUG_FILE_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include @@ -21,6 +17,10 @@ extern "C" { #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * \addtogroup spa_debug * \{ diff --git a/spa/include/spa/debug/format.h b/spa/include/spa/debug/format.h index ad7f20480..1448ae223 100644 --- a/spa/include/spa/debug/format.h +++ b/spa/include/spa/debug/format.h @@ -5,6 +5,15 @@ #ifndef SPA_DEBUG_FORMAT_H #define SPA_DEBUG_FORMAT_H +#include + +#include +#include +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,13 +23,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include -#include -#include - #ifndef SPA_API_DEBUG_FORMAT #ifdef SPA_API_IMPL #define SPA_API_DEBUG_FORMAT SPA_API_IMPL @@ -41,10 +43,11 @@ spa_debug_strbuf_format_value(struct spa_strbuf *buffer, const struct spa_type_i break; case SPA_TYPE_Id: { - const char *str = spa_debug_type_find_short_name(info, *(int32_t *) body); + uint32_t value = *(uint32_t *) body; + const char *str = spa_debug_type_find_short_name(info, value); char tmp[64]; if (str == NULL) { - snprintf(tmp, sizeof(tmp), "%d", *(int32_t*)body); + snprintf(tmp, sizeof(tmp), "%" PRIu32, value); str = tmp; } spa_strbuf_append(buffer, "%s", str); @@ -63,7 +66,7 @@ spa_debug_strbuf_format_value(struct spa_strbuf *buffer, const struct spa_type_i spa_strbuf_append(buffer, "%f", *(double *) body); break; case SPA_TYPE_String: - spa_strbuf_append(buffer, "%s", (char *) body); + spa_strbuf_append(buffer, "%-*s", size, (char *) body); break; case SPA_TYPE_Rectangle: { @@ -90,10 +93,12 @@ spa_debug_strbuf_format_value(struct spa_strbuf *buffer, const struct spa_type_i int i = 0; info = info && info->values ? info->values : info; spa_strbuf_append(buffer, "< "); - SPA_POD_ARRAY_BODY_FOREACH(b, size, p) { - if (i++ > 0) - spa_strbuf_append(buffer, ", "); - spa_debug_strbuf_format_value(buffer, info, b->child.type, p, b->child.size); + if (b->child.size >= spa_pod_type_size(b->child.type)) { + SPA_POD_ARRAY_BODY_FOREACH(b, size, p) { + if (i++ > 0) + spa_strbuf_append(buffer, ", "); + spa_debug_strbuf_format_value(buffer, info, b->child.type, p, b->child.size); + } } spa_strbuf_append(buffer, " >"); break; @@ -128,7 +133,7 @@ SPA_API_DEBUG_FORMAT int spa_debugc_format(struct spa_debug_context *ctx, int in if (info == NULL) info = spa_type_format; - if (format == NULL || SPA_POD_TYPE(format) != SPA_TYPE_Object) + if (format == NULL || format->type != SPA_TYPE_Object) return -EINVAL; if (spa_format_parse(format, &mtype, &mstype) < 0) @@ -158,11 +163,11 @@ SPA_API_DEBUG_FORMAT int spa_debugc_format(struct spa_debug_context *ctx, int in type = val->type; size = val->size; - vals = SPA_POD_BODY(val); - if (type < SPA_TYPE_None || type >= _SPA_TYPE_LAST) + if (type < SPA_TYPE_None || type >= _SPA_TYPE_LAST || n_vals < 1) continue; + vals = SPA_POD_BODY(val); ti = spa_debug_type_find(info, prop->key); key = ti ? ti->name : NULL; diff --git a/spa/include/spa/debug/log.h b/spa/include/spa/debug/log.h index 05c3bd50f..4ede23905 100644 --- a/spa/include/spa/debug/log.h +++ b/spa/include/spa/debug/log.h @@ -5,10 +5,6 @@ #ifndef SPA_DEBUG_LOG_H #define SPA_DEBUG_LOG_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -20,6 +16,10 @@ extern "C" { #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_DEBUG_LOG #ifdef SPA_API_IMPL #define SPA_API_DEBUG_LOG SPA_API_IMPL diff --git a/spa/include/spa/debug/mem.h b/spa/include/spa/debug/mem.h index 966629482..98f1761a1 100644 --- a/spa/include/spa/debug/mem.h +++ b/spa/include/spa/debug/mem.h @@ -5,19 +5,19 @@ #ifndef SPA_DEBUG_MEM_H #define SPA_DEBUG_MEM_H +#include + +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_debug * \{ */ -#include - #ifndef SPA_API_DEBUG_MEM #ifdef SPA_API_IMPL #define SPA_API_DEBUG_MEM SPA_API_IMPL diff --git a/spa/include/spa/debug/node.h b/spa/include/spa/debug/node.h index baa273ffe..c25112f4d 100644 --- a/spa/include/spa/debug/node.h +++ b/spa/include/spa/debug/node.h @@ -5,6 +5,10 @@ #ifndef SPA_DEBUG_NODE_H #define SPA_DEBUG_NODE_H +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include -#include -#include - #ifndef SPA_API_DEBUG_NODE #ifdef SPA_API_IMPL #define SPA_API_DEBUG_NODE SPA_API_IMPL diff --git a/spa/include/spa/debug/pod.h b/spa/include/spa/debug/pod.h index 9db6f4b05..a6d3b91db 100644 --- a/spa/include/spa/debug/pod.h +++ b/spa/include/spa/debug/pod.h @@ -5,6 +5,14 @@ #ifndef SPA_DEBUG_POD_H #define SPA_DEBUG_POD_H +#include + +#include +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,12 +22,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include -#include - #ifndef SPA_API_DEBUG_POD #ifdef SPA_API_IMPL #define SPA_API_DEBUG_POD SPA_API_IMPL @@ -37,11 +39,11 @@ spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa spa_debugc(ctx, "%*s" "Bool %s", indent, "", (*(int32_t *) body) ? "true" : "false"); break; case SPA_TYPE_Id: - spa_debugc(ctx, "%*s" "Id %-8d (%s)", indent, "", *(int32_t *) body, - spa_debug_type_find_name(info, *(int32_t *) body)); + spa_debugc(ctx, "%*s" "Id %-8" PRIu32 " (%s)", indent, "", *(uint32_t *) body, + spa_debug_type_find_name(info, *(uint32_t *) body)); break; case SPA_TYPE_Int: - spa_debugc(ctx, "%*s" "Int %d", indent, "", *(int32_t *) body); + spa_debugc(ctx, "%*s" "Int %" PRId32, indent, "", *(int32_t *) body); break; case SPA_TYPE_Long: spa_debugc(ctx, "%*s" "Long %" PRIi64 "", indent, "", *(int64_t *) body); @@ -85,13 +87,18 @@ spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa struct spa_pod_array_body *b = (struct spa_pod_array_body *)body; void *p; const struct spa_type_info *ti = spa_debug_type_find(SPA_TYPE_ROOT, b->child.type); + uint32_t min_size = spa_pod_type_size(b->child.type); - spa_debugc(ctx, "%*s" "Array: child.size %d, child.type %s", indent, "", + spa_debugc(ctx, "%*s" "Array: child.size %" PRIu32 ", child.type %s", indent, "", b->child.size, ti ? ti->name : "unknown"); - info = info && info->values ? info->values : info; - SPA_POD_ARRAY_BODY_FOREACH(b, size, p) - spa_debugc_pod_value(ctx, indent + 2, info, b->child.type, p, b->child.size); + if (b->child.size < min_size) { + spa_debugc(ctx, "%*s" " INVALID child.size < %" PRIu32, indent, "", min_size); + } else { + info = info && info->values ? info->values : info; + SPA_POD_ARRAY_BODY_FOREACH(b, size, p) + spa_debugc_pod_value(ctx, indent + 2, info, b->child.type, p, b->child.size); + } break; } case SPA_TYPE_Choice: @@ -99,20 +106,31 @@ spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa struct spa_pod_choice_body *b = (struct spa_pod_choice_body *)body; void *p; const struct spa_type_info *ti = spa_debug_type_find(spa_type_choice, b->type); + uint32_t min_size = spa_pod_type_size(b->child.type); - spa_debugc(ctx, "%*s" "Choice: type %s, flags %08x %d %d", indent, "", + spa_debugc(ctx, "%*s" "Choice: type %s, flags %08" PRIx32 " %" PRIu32 " %" PRIu32, indent, "", ti ? ti->name : "unknown", b->flags, size, b->child.size); - SPA_POD_CHOICE_BODY_FOREACH(b, size, p) - spa_debugc_pod_value(ctx, indent + 2, info, b->child.type, p, b->child.size); + if (b->child.size < min_size) { + spa_debugc(ctx, "%*s" "INVALID child.size < %" PRIu32, indent, "", min_size); + } else { + SPA_POD_CHOICE_BODY_FOREACH(b, size, p) + spa_debugc_pod_value(ctx, indent + 2, info, b->child.type, p, b->child.size); + } break; } case SPA_TYPE_Struct: { struct spa_pod *b = (struct spa_pod *)body, *p; - spa_debugc(ctx, "%*s" "Struct: size %d", indent, "", size); - SPA_POD_FOREACH(b, size, p) - spa_debugc_pod_value(ctx, indent + 2, info, p->type, SPA_POD_BODY(p), p->size); + spa_debugc(ctx, "%*s" "Struct: size %" PRIu32, indent, "", size); + SPA_POD_FOREACH(b, size, p) { + uint32_t min_size = spa_pod_type_size(p->type); + if (p->size < min_size) { + spa_debugc(ctx, "%*s" "INVALID child.size < %" PRIu32, indent, "", min_size); + } else { + spa_debugc_pod_value(ctx, indent + 2, info, p->type, SPA_POD_BODY(p), p->size); + } + } break; } case SPA_TYPE_Object: @@ -125,21 +143,37 @@ spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa ii = ti ? spa_debug_type_find(ti->values, 0) : NULL; ii = ii ? spa_debug_type_find(ii->values, b->id) : NULL; - spa_debugc(ctx, "%*s" "Object: size %d, type %s (%d), id %s (%d)", indent, "", size, - ti ? ti->name : "unknown", b->type, ii ? ii->name : "unknown", b->id); + spa_debugc(ctx, "%*s" "Object: size %" PRIu32 ", type %s (%" PRIu32 "), id %s (%" PRIu32 ")", + indent, "", size, ti ? ti->name : "unknown", b->type, ii ? ii->name : "unknown", b->id); info = ti ? ti->values : info; SPA_POD_OBJECT_BODY_FOREACH(b, size, p) { + static const char custom_prefix[] = SPA_TYPE_INFO_PROPS_BASE "Custom:"; + char custom_name[sizeof(custom_prefix) + 16]; + const char *name = "unknown"; + uint32_t min_size = spa_pod_type_size(p->value.type); + ii = spa_debug_type_find(info, p->key); + if (ii) { + name = ii->name; + } else if (p->key >= SPA_PROP_START_CUSTOM) { + snprintf(custom_name, sizeof(custom_name), + "%s%" PRIu32, custom_prefix, p->key - SPA_PROP_START_CUSTOM); + name = custom_name; + } - spa_debugc(ctx, "%*s" "Prop: key %s (%d), flags %08x", indent+2, "", - ii ? ii->name : "unknown", p->key, p->flags); + spa_debugc(ctx, "%*s" "Prop: key %s (%" PRIu32 "), flags %08" PRIx32, + indent+2, "", name, p->key, p->flags); - spa_debugc_pod_value(ctx, indent + 4, ii ? ii->values : NULL, - p->value.type, - SPA_POD_CONTENTS(struct spa_pod_prop, p), - p->value.size); + if (p->value.size < min_size) { + spa_debugc(ctx, "%*s" "INVALID value.size < %" PRIu32, indent, "", min_size); + } else { + spa_debugc_pod_value(ctx, indent + 4, ii ? ii->values : NULL, + p->value.type, + SPA_POD_CONTENTS(struct spa_pod_prop, p), + p->value.size); + } } break; } @@ -151,19 +185,25 @@ spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa ti = spa_debug_type_find(info, b->unit); - spa_debugc(ctx, "%*s" "Sequence: size %d, unit %s", indent, "", size, + spa_debugc(ctx, "%*s" "Sequence: size %" PRIu32 ", unit %s", indent, "", size, ti ? ti->name : "unknown"); SPA_POD_SEQUENCE_BODY_FOREACH(b, size, c) { + uint32_t min_size = spa_pod_type_size(c->value.type); + ii = spa_debug_type_find(spa_type_control, c->type); - spa_debugc(ctx, "%*s" "Control: offset %d, type %s", indent+2, "", + spa_debugc(ctx, "%*s" "Control: offset %" PRIu32 ", type %s", indent+2, "", c->offset, ii ? ii->name : "unknown"); - spa_debugc_pod_value(ctx, indent + 4, ii ? ii->values : NULL, - c->value.type, - SPA_POD_CONTENTS(struct spa_pod_control, c), - c->value.size); + if (c->value.size < min_size) { + spa_debugc(ctx, "%*s" "INVALID value.size < %" PRIu32, indent, "", min_size); + } else { + spa_debugc_pod_value(ctx, indent + 4, ii ? ii->values : NULL, + c->value.type, + SPA_POD_CONTENTS(struct spa_pod_control, c), + c->value.size); + } } break; } @@ -176,7 +216,7 @@ spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa spa_debugc_mem(ctx, indent + 2, body, size); break; default: - spa_debugc(ctx, "%*s" "unhandled POD type %d", indent, "", type); + spa_debugc(ctx, "%*s" "unhandled POD type %" PRIu32, indent, "", type); break; } return 0; @@ -185,10 +225,10 @@ spa_debugc_pod_value(struct spa_debug_context *ctx, int indent, const struct spa SPA_API_DEBUG_POD int spa_debugc_pod(struct spa_debug_context *ctx, int indent, const struct spa_type_info *info, const struct spa_pod *pod) { + if (pod->size < spa_pod_type_size(pod->type)) + return -EINVAL; return spa_debugc_pod_value(ctx, indent, info ? info : SPA_TYPE_ROOT, - SPA_POD_TYPE(pod), - SPA_POD_BODY(pod), - SPA_POD_BODY_SIZE(pod)); + pod->type, SPA_POD_BODY(pod), pod->size); } SPA_API_DEBUG_POD int diff --git a/spa/include/spa/debug/types.h b/spa/include/spa/debug/types.h index d7ca83666..d52dbc89f 100644 --- a/spa/include/spa/debug/types.h +++ b/spa/include/spa/debug/types.h @@ -5,6 +5,10 @@ #ifndef SPA_DEBUG_TYPES_H #define SPA_DEBUG_TYPES_H +#include + +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include - -#include - #ifndef SPA_API_DEBUG_TYPES #ifdef SPA_API_IMPL #define SPA_API_DEBUG_TYPES SPA_API_IMPL diff --git a/spa/include/spa/filter-graph/filter-graph.h b/spa/include/spa/filter-graph/filter-graph.h index 05904c7f3..481085c5f 100644 --- a/spa/include/spa/filter-graph/filter-graph.h +++ b/spa/include/spa/filter-graph/filter-graph.h @@ -5,16 +5,16 @@ #ifndef SPA_FILTER_GRAPH_H #define SPA_FILTER_GRAPH_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_FILTER_GRAPH #ifdef SPA_API_IMPL #define SPA_API_FILTER_GRAPH SPA_API_IMPL diff --git a/spa/include/spa/graph/graph.h b/spa/include/spa/graph/graph.h index 537e6e75f..0db28ad3f 100644 --- a/spa/include/spa/graph/graph.h +++ b/spa/include/spa/graph/graph.h @@ -5,6 +5,13 @@ #ifndef SPA_GRAPH_H #define SPA_GRAPH_H +#include +#include +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -18,13 +25,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include -#include -#include - #ifndef SPA_API_GRAPH #ifdef SPA_API_IMPL #define SPA_API_GRAPH SPA_API_IMPL diff --git a/spa/include/spa/monitor/device.h b/spa/include/spa/monitor/device.h index 73b4a94ff..cc51f9ef1 100644 --- a/spa/include/spa/monitor/device.h +++ b/spa/include/spa/monitor/device.h @@ -5,15 +5,15 @@ #ifndef SPA_DEVICE_H #define SPA_DEVICE_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_DEVICE #ifdef SPA_API_IMPL #define SPA_API_DEVICE SPA_API_IMPL diff --git a/spa/include/spa/monitor/event.h b/spa/include/spa/monitor/event.h index 8955f81a5..accf8382f 100644 --- a/spa/include/spa/monitor/event.h +++ b/spa/include/spa/monitor/event.h @@ -5,12 +5,12 @@ #ifndef SPA_EVENT_DEVICE_H #define SPA_EVENT_DEVICE_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_device * \{ diff --git a/spa/include/spa/monitor/type-info.h b/spa/include/spa/monitor/type-info.h index e43a17b89..1658594b4 100644 --- a/spa/include/spa/monitor/type-info.h +++ b/spa/include/spa/monitor/type-info.h @@ -5,14 +5,14 @@ #ifndef SPA_DEVICE_TYPE_INFO_H #define SPA_DEVICE_TYPE_INFO_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * \addtogroup spa_device * \{ diff --git a/spa/include/spa/monitor/utils.h b/spa/include/spa/monitor/utils.h index 4337ecebc..c4b74935b 100644 --- a/spa/include/spa/monitor/utils.h +++ b/spa/include/spa/monitor/utils.h @@ -5,13 +5,13 @@ #ifndef SPA_DEVICE_UTILS_H #define SPA_DEVICE_UTILS_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #ifndef SPA_API_DEVICE_UTILS #ifdef SPA_API_IMPL #define SPA_API_DEVICE_UTILS SPA_API_IMPL diff --git a/spa/include/spa/node/command.h b/spa/include/spa/node/command.h index 24c81a53e..49a1cc09d 100644 --- a/spa/include/spa/node/command.h +++ b/spa/include/spa/node/command.h @@ -5,6 +5,8 @@ #ifndef SPA_COMMAND_NODE_H #define SPA_COMMAND_NODE_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /* object id of SPA_TYPE_COMMAND_Node */ enum spa_node_command { SPA_NODE_COMMAND_Suspend, /**< suspend a node, this removes all configured @@ -36,12 +36,24 @@ enum spa_node_command { SPA_NODE_COMMAND_ParamEnd, /**< end a transaction */ SPA_NODE_COMMAND_RequestProcess,/**< Sent to a driver when some other node emitted * the RequestProcess event. */ + SPA_NODE_COMMAND_User, /**< User defined command */ }; #define SPA_NODE_COMMAND_ID(cmd) SPA_COMMAND_ID(cmd, SPA_TYPE_COMMAND_Node) #define SPA_NODE_COMMAND_INIT(id) SPA_COMMAND_INIT(SPA_TYPE_COMMAND_Node, id) +/* properties for SPA_TYPE_COMMAND_Node */ +enum spa_command_node { + SPA_COMMAND_NODE_START, + + SPA_COMMAND_NODE_START_User = 0x1000, + SPA_COMMAND_NODE_extra, /** extra info (String) */ + + SPA_COMMAND_NODE_START_CUSTOM = 0x1000000, +}; + + /** * \} */ diff --git a/spa/include/spa/node/event.h b/spa/include/spa/node/event.h index b975a7bfc..d01f5f2a8 100644 --- a/spa/include/spa/node/event.h +++ b/spa/include/spa/node/event.h @@ -5,6 +5,8 @@ #ifndef SPA_EVENT_NODE_H #define SPA_EVENT_NODE_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /* object id of SPA_TYPE_EVENT_Node */ enum spa_node_event { SPA_NODE_EVENT_Error, @@ -23,6 +23,7 @@ enum spa_node_event { SPA_NODE_EVENT_RequestRefresh, SPA_NODE_EVENT_RequestProcess, /*< Ask the driver to start processing * the graph */ + SPA_NODE_EVENT_User, /* User defined event */ }; #define SPA_NODE_EVENT_ID(ev) SPA_EVENT_ID(ev, SPA_TYPE_EVENT_Node) @@ -31,6 +32,11 @@ enum spa_node_event { /* properties for SPA_TYPE_EVENT_Node */ enum spa_event_node { SPA_EVENT_NODE_START, + + SPA_EVENT_NODE_START_User = 0x1000, + SPA_EVENT_NODE_extra, /** extra info (String) */ + + SPA_EVENT_NODE_START_CUSTOM = 0x1000000, }; /** diff --git a/spa/include/spa/node/io.h b/spa/include/spa/node/io.h index c1c725ebf..c03ef3e02 100644 --- a/spa/include/spa/node/io.h +++ b/spa/include/spa/node/io.h @@ -5,6 +5,9 @@ #ifndef SPA_IO_H #define SPA_IO_H +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,9 +17,6 @@ extern "C" { * \{ */ -#include -#include - /** IO areas * * IO information for a port on a node. This is allocated @@ -114,23 +114,54 @@ struct spa_io_range { * Driver nodes are supposed to update the contents of \ref SPA_IO_Clock before * signaling the start of a graph cycle. These updated clock values become * visible to other nodes in \ref SPA_IO_Position. Non-driver nodes do - * not need to update the contents of their \ref SPA_IO_Clock. + * not need to update the contents of their \ref SPA_IO_Clock. Also + * see \ref page_driver for further details. * * The host generally gives each node a separate \ref spa_io_clock in \ref * SPA_IO_Clock, so that updates made by the driver are not visible in the * contents of \ref SPA_IO_Clock of other nodes. Instead, \ref SPA_IO_Position * is used to look up the current graph time. * - * A node is a driver when \ref spa_io_clock.id in \ref SPA_IO_Clock and - * \ref spa_io_position.clock.id in \ref SPA_IO_Position are the same. + * A node is a driver when \ref spa_io_clock::id and the ID in + * \ref spa_io_position.clock in \ref SPA_IO_Position are the same. + * + * The flags are set by the graph driver at the start of each cycle. */ struct spa_io_clock { -#define SPA_IO_CLOCK_FLAG_FREEWHEEL (1u<<0) /* graph is freewheeling */ -#define SPA_IO_CLOCK_FLAG_XRUN_RECOVER (1u<<1) /* recovering from xrun */ -#define SPA_IO_CLOCK_FLAG_LAZY (1u<<2) /* lazy scheduling */ -#define SPA_IO_CLOCK_FLAG_NO_RATE (1u<<3) /* the rate of the clock is only approximately. - * it is recommended to use the nsec as a clock source. - * The rate_diff contains the measured inaccuracy. */ +#define SPA_IO_CLOCK_FLAG_FREEWHEEL (1u<<0) /**< Graph is freewheeling. It runs at the maximum + * possible rate, only constrained by the processing + * power of the machine it runs on. This can be useful + * for offline processing, where processing in real + * time is not desired. */ +#define SPA_IO_CLOCK_FLAG_XRUN_RECOVER (1u<<1) /**< A node's process callback did not complete within + * the last cycle's deadline, resulting in an xrun. + * This flag is not set for the entire graph. Instead, + * it is set at the start of the current cycle before + * a node that experienced an xrun has its process + * callback invoked. After said callback finished, the + * flag is cleared again. That way, the node knows that + * during the last cycle it experienced an xrun. They + * can use this information for example to resynchronize + * or clear custom stale states. */ +#define SPA_IO_CLOCK_FLAG_LAZY (1u<<2) /**< The driver uses lazy scheduling. For details, see + * \ref PW_KEY_NODE_SUPPORTS_LAZY . */ +#define SPA_IO_CLOCK_FLAG_NO_RATE (1u<<3) /**< The rate of the clock is only approximately. + * It is recommended to use the nsec as a clock source. + * The rate_diff contains the measured inaccuracy. */ +#define SPA_IO_CLOCK_FLAG_DISCONT (1u<<4) /**< The clock experienced a discontinuity in its + * timestamps since the last cycle. If this is set, + * nodes know that timestamps between the last and + * the current cycle cannot be assumed to be + * continuous. Nodes that synchronize playback against + * clock timestamps should resynchronize (for example + * by flushing buffers to avoid incorrect delays). + * This differs from an xrun in that it is not necessariy + * an error and that it is not caused by missed process + * deadlines. If for example a custom network time + * based driver starts to follow a different time + * server, and the offset between that server and its + * local clock consequently suddenly changes, then that + * driver should set this flag. */ uint32_t flags; /**< Clock flags */ uint32_t id; /**< Unique clock id, set by host application */ char name[64]; /**< Clock name prefixed with API, set by node when it receives @@ -138,13 +169,16 @@ struct spa_io_clock { * can be used to check if nodes share the same clock. */ uint64_t nsec; /**< Time in nanoseconds against monotonic clock * (CLOCK_MONOTONIC). This fields reflects a real time instant - * in the past. The value may have jitter. */ + * in the past, when the current cycle started. The value may + * have jitter. */ struct spa_fraction rate; /**< Rate for position/duration/delay/xrun */ uint64_t position; /**< Current position, in samples @ \ref rate */ uint64_t duration; /**< Duration of current cycle, in samples @ \ref rate */ int64_t delay; /**< Delay between position and hardware, in samples @ \ref rate */ double rate_diff; /**< Rate difference between clock and monotonic time, as a ratio of - * clock speeds. */ + * clock speeds. A value higher than 1.0 means that the driver's + * internal clock is faster than the monotonic clock (by that + * factor), and vice versa. */ uint64_t next_nsec; /**< Estimated next wakeup time in nanoseconds. * This time is a logical start time of the next cycle, and * is not necessarily in the future. @@ -278,8 +312,8 @@ enum spa_io_position_state { * * It is set on all nodes in \ref SPA_IO_Position, and the contents of \ref * spa_io_position.clock contain the clock updates made by the driving node in - * the graph in its \ref SPA_IO_Clock. Also, \ref spa_io_position.clock.id - * will contain the clock id of the driving node in the graph. + * the graph in its \ref SPA_IO_Clock. Also, the ID in \ref spa_io_position.clock + * will be the clock id of the driving node in the graph. * * The position clock indicates the logical start time of the current graph * cycle. @@ -313,7 +347,7 @@ struct spa_io_position { * and node rates. The \a flags and \a rate fields may be modified by the node. * * The node can request a correction to the resampling rate in its process(), by setting - * \ref SPA_IO_RATE_MATCH_ACTIVE on \a flags, and setting \a rate to the desired rate + * \ref SPA_IO_RATE_MATCH_FLAG_ACTIVE on \a flags, and setting \a rate to the desired rate * correction. Usually the rate is obtained from DLL or other adaptive mechanism that * e.g. drives the node buffer fill level toward a specific value. * diff --git a/spa/include/spa/node/node.h b/spa/include/spa/node/node.h index ae9f63549..85813a064 100644 --- a/spa/include/spa/node/node.h +++ b/spa/include/spa/node/node.h @@ -5,6 +5,14 @@ #ifndef SPA_NODE_H #define SPA_NODE_H +#include +#include +#include +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -19,14 +27,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include -#include -#include -#include - #ifndef SPA_API_NODE #ifdef SPA_API_IMPL #define SPA_API_NODE SPA_API_IMPL diff --git a/spa/include/spa/node/type-info.h b/spa/include/spa/node/type-info.h index 5b956348b..41cc5a73d 100644 --- a/spa/include/spa/node/type-info.h +++ b/spa/include/spa/node/type-info.h @@ -5,6 +5,12 @@ #ifndef SPA_NODE_TYPES_H #define SPA_NODE_TYPES_H +#include + +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,12 +20,6 @@ extern "C" { * \{ */ -#include - -#include -#include -#include - #define SPA_TYPE_INFO_IO SPA_TYPE_INFO_ENUM_BASE "IO" #define SPA_TYPE_INFO_IO_BASE SPA_TYPE_INFO_IO ":" @@ -46,16 +46,20 @@ static const struct spa_type_info spa_type_node_event_id[] = { { SPA_NODE_EVENT_Buffering, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "Buffering", NULL }, { SPA_NODE_EVENT_RequestRefresh, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "RequestRefresh", NULL }, { SPA_NODE_EVENT_RequestProcess, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "RequestProcess", NULL }, + { SPA_NODE_EVENT_User, SPA_TYPE_EVENT_Node, SPA_TYPE_INFO_NODE_EVENT_BASE "User", NULL }, { 0, 0, NULL, NULL }, }; static const struct spa_type_info spa_type_node_event[] = { { SPA_EVENT_NODE_START, SPA_TYPE_Id, SPA_TYPE_INFO_NODE_EVENT_BASE, spa_type_node_event_id }, + + { SPA_EVENT_NODE_extra, SPA_TYPE_String, SPA_TYPE_INFO_NODE_EVENT_BASE "extra", NULL }, + { 0, 0, NULL, NULL }, }; #define SPA_TYPE_INFO_NodeCommand SPA_TYPE_INFO_COMMAND_BASE "Node" -#define SPA_TYPE_INFO_NODE_COMMAND_BASE SPA_TYPE_INFO_NodeCommand ":" +#define SPA_TYPE_INFO_NODE_COMMAND_BASE SPA_TYPE_INFO_NodeCommand ":" static const struct spa_type_info spa_type_node_command_id[] = { { SPA_NODE_COMMAND_Suspend, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "Suspend", NULL }, @@ -69,11 +73,15 @@ static const struct spa_type_info spa_type_node_command_id[] = { { SPA_NODE_COMMAND_ParamBegin, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "ParamBegin", NULL }, { SPA_NODE_COMMAND_ParamEnd, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "ParamEnd", NULL }, { SPA_NODE_COMMAND_RequestProcess, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "RequestProcess", NULL }, + { SPA_NODE_COMMAND_User, SPA_TYPE_COMMAND_Node, SPA_TYPE_INFO_NODE_COMMAND_BASE "User", NULL }, { 0, 0, NULL, NULL }, }; static const struct spa_type_info spa_type_node_command[] = { - { 0, SPA_TYPE_Id, SPA_TYPE_INFO_NODE_COMMAND_BASE, spa_type_node_command_id }, + { SPA_COMMAND_NODE_START, SPA_TYPE_Id, SPA_TYPE_INFO_NODE_COMMAND_BASE, spa_type_node_command_id }, + + { SPA_COMMAND_NODE_extra, SPA_TYPE_String, SPA_TYPE_INFO_NODE_COMMAND_BASE "extra", NULL }, + { 0, 0, NULL, NULL }, }; diff --git a/spa/include/spa/node/utils.h b/spa/include/spa/node/utils.h index b7724e922..a46cbe44b 100644 --- a/spa/include/spa/node/utils.h +++ b/spa/include/spa/node/utils.h @@ -5,6 +5,10 @@ #ifndef SPA_NODE_UTILS_H #define SPA_NODE_UTILS_H +#include + +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include - -#include - #ifndef SPA_API_NODE_UTILS #ifdef SPA_API_IMPL #define SPA_API_NODE_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/aac-types.h b/spa/include/spa/param/audio/aac-types.h index 6e5047394..176185bc0 100644 --- a/spa/include/spa/param/audio/aac-types.h +++ b/spa/include/spa/param/audio/aac-types.h @@ -5,13 +5,13 @@ #ifndef SPA_AUDIO_AAC_TYPES_H #define SPA_AUDIO_AAC_TYPES_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/aac-utils.h b/spa/include/spa/param/audio/aac-utils.h index 01f226bb8..3ec362d19 100644 --- a/spa/include/spa/param/audio/aac-utils.h +++ b/spa/include/spa/param/audio/aac-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_AAC_UTILS_H #define SPA_AUDIO_AAC_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_AAC_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_AAC_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/aac.h b/spa/include/spa/param/audio/aac.h index 0bc1dea89..1a7fb1612 100644 --- a/spa/include/spa/param/audio/aac.h +++ b/spa/include/spa/param/audio/aac.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_AAC_H #define SPA_AUDIO_AAC_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ @@ -18,19 +18,19 @@ extern "C" { enum spa_audio_aac_stream_format { SPA_AUDIO_AAC_STREAM_FORMAT_UNKNOWN, - /* Raw AAC frames */ + /** Raw AAC frames */ SPA_AUDIO_AAC_STREAM_FORMAT_RAW, - /* ISO/IEC 13818-7 MPEG-2 Audio Data Transport Stream (ADTS) */ + /** ISO/IEC 13818-7 MPEG-2 Audio Data Transport Stream (ADTS) */ SPA_AUDIO_AAC_STREAM_FORMAT_MP2ADTS, - /* ISO/IEC 14496-3 MPEG-4 Audio Data Transport Stream (ADTS) */ + /** ISO/IEC 14496-3 MPEG-4 Audio Data Transport Stream (ADTS) */ SPA_AUDIO_AAC_STREAM_FORMAT_MP4ADTS, - /* ISO/IEC 14496-3 Low Overhead Audio Stream (LOAS) */ + /** ISO/IEC 14496-3 Low Overhead Audio Stream (LOAS) */ SPA_AUDIO_AAC_STREAM_FORMAT_MP4LOAS, - /* ISO/IEC 14496-3 Low Overhead Audio Transport Multiplex (LATM) */ + /** ISO/IEC 14496-3 Low Overhead Audio Transport Multiplex (LATM) */ SPA_AUDIO_AAC_STREAM_FORMAT_MP4LATM, - /* ISO/IEC 14496-3 Audio Data Interchange Format (ADIF) */ + /** ISO/IEC 14496-3 Audio Data Interchange Format (ADIF) */ SPA_AUDIO_AAC_STREAM_FORMAT_ADIF, - /* ISO/IEC 14496-12 MPEG-4 file format */ + /** ISO/IEC 14496-12 MPEG-4 file format */ SPA_AUDIO_AAC_STREAM_FORMAT_MP4FF, SPA_AUDIO_AAC_STREAM_FORMAT_CUSTOM = 0x10000, diff --git a/spa/include/spa/param/audio/ac3-utils.h b/spa/include/spa/param/audio/ac3-utils.h new file mode 100644 index 000000000..9ee48d4ea --- /dev/null +++ b/spa/include/spa/param/audio/ac3-utils.h @@ -0,0 +1,69 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_AC3_UTILS_H +#define SPA_AUDIO_AC3_UTILS_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#ifndef SPA_API_AUDIO_AC3_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_AC3_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_AC3_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_AC3_UTILS int +spa_format_audio_ac3_parse(const struct spa_pod *format, struct spa_audio_info_ac3 *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + return res; +} + +SPA_API_AUDIO_AC3_UTILS struct spa_pod * +spa_format_audio_ac3_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_ac3 *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_ac3), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_AC3_UTILS_H */ diff --git a/spa/include/spa/param/audio/ac3.h b/spa/include/spa/param/audio/ac3.h new file mode 100644 index 000000000..1a02c665a --- /dev/null +++ b/spa/include/spa/param/audio/ac3.h @@ -0,0 +1,35 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_AC3_H +#define SPA_AUDIO_AC3_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +/** Dolby AC-3 audio info. */ +struct spa_audio_info_ac3 { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_AC3_INIT(...) ((struct spa_audio_info_ac3) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_AC3_H */ diff --git a/spa/include/spa/param/audio/alac-utils.h b/spa/include/spa/param/audio/alac-utils.h index 898a84e5f..11a76c581 100644 --- a/spa/include/spa/param/audio/alac-utils.h +++ b/spa/include/spa/param/audio/alac-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_ALAC_UTILS_H #define SPA_AUDIO_ALAC_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_ALAC_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_ALAC_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/alac.h b/spa/include/spa/param/audio/alac.h index 6e6f19fb2..cf682da4c 100644 --- a/spa/include/spa/param/audio/alac.h +++ b/spa/include/spa/param/audio/alac.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_ALAC_H #define SPA_AUDIO_ALAC_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/amr-types.h b/spa/include/spa/param/audio/amr-types.h index 9ab4c9e41..73bfa051b 100644 --- a/spa/include/spa/param/audio/amr-types.h +++ b/spa/include/spa/param/audio/amr-types.h @@ -5,13 +5,13 @@ #ifndef SPA_AUDIO_AMR_TYPES_H #define SPA_AUDIO_AMR_TYPES_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/amr-utils.h b/spa/include/spa/param/audio/amr-utils.h index cfe6aa5dc..6a782735d 100644 --- a/spa/include/spa/param/audio/amr-utils.h +++ b/spa/include/spa/param/audio/amr-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_AMR_UTILS_H #define SPA_AUDIO_AMR_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_AMR_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_AMR_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/amr.h b/spa/include/spa/param/audio/amr.h index d00125a45..2d970e388 100644 --- a/spa/include/spa/param/audio/amr.h +++ b/spa/include/spa/param/audio/amr.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_AMR_H #define SPA_AUDIO_AMR_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/ape-utils.h b/spa/include/spa/param/audio/ape-utils.h index d05c596c0..3068e0d2e 100644 --- a/spa/include/spa/param/audio/ape-utils.h +++ b/spa/include/spa/param/audio/ape-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_APE_UTILS_H #define SPA_AUDIO_APE_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_APE_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_APE_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/ape.h b/spa/include/spa/param/audio/ape.h index 03f787f53..27c2aa5e2 100644 --- a/spa/include/spa/param/audio/ape.h +++ b/spa/include/spa/param/audio/ape.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_APE_H #define SPA_AUDIO_APE_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/compressed.h b/spa/include/spa/param/audio/compressed.h index 1105415b9..d61ba321e 100644 --- a/spa/include/spa/param/audio/compressed.h +++ b/spa/include/spa/param/audio/compressed.h @@ -7,12 +7,17 @@ #define SPA_AUDIO_COMPRESSED_H #include +#include #include #include #include +#include #include #include +#include +#include #include +#include #include #include diff --git a/spa/include/spa/param/audio/dsd-utils.h b/spa/include/spa/param/audio/dsd-utils.h index 3f7065b26..2d99e79f2 100644 --- a/spa/include/spa/param/audio/dsd-utils.h +++ b/spa/include/spa/param/audio/dsd-utils.h @@ -5,6 +5,12 @@ #ifndef SPA_AUDIO_DSD_UTILS_H #define SPA_AUDIO_DSD_UTILS_H +#include +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +20,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_DSD_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_DSD_UTILS SPA_API_IMPL @@ -41,7 +42,7 @@ spa_format_audio_dsd_parse(const struct spa_pod *format, struct spa_audio_info_d SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels), SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position)); if (position == NULL || - !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS)) + !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_N_ELEMENTS(info->position))) SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); return res; diff --git a/spa/include/spa/param/audio/dsd.h b/spa/include/spa/param/audio/dsd.h index 53678d4c5..73f3d4e8b 100644 --- a/spa/include/spa/param/audio/dsd.h +++ b/spa/include/spa/param/audio/dsd.h @@ -7,13 +7,13 @@ #include +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/dsp-utils.h b/spa/include/spa/param/audio/dsp-utils.h index af107f1eb..ec183a914 100644 --- a/spa/include/spa/param/audio/dsp-utils.h +++ b/spa/include/spa/param/audio/dsp-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_DSP_UTILS_H #define SPA_AUDIO_DSP_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_DSP_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_DSP_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/dsp.h b/spa/include/spa/param/audio/dsp.h index 592f25c18..a285fdf2d 100644 --- a/spa/include/spa/param/audio/dsp.h +++ b/spa/include/spa/param/audio/dsp.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_DSP_H #define SPA_AUDIO_DSP_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/dts-types.h b/spa/include/spa/param/audio/dts-types.h new file mode 100644 index 000000000..306859648 --- /dev/null +++ b/spa/include/spa/param/audio/dts-types.h @@ -0,0 +1,39 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_DTS_TYPES_H +#define SPA_AUDIO_DTS_TYPES_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#define SPA_TYPE_INFO_AudioDTSExtType SPA_TYPE_INFO_ENUM_BASE "AudioDTSExtType" +#define SPA_TYPE_INFO_AUDIO_DTS_EXT_TYPE_BASE SPA_TYPE_INFO_AudioDTSExtType ":" + +static const struct spa_type_info spa_type_audio_dts_ext_type[] = { + { SPA_AUDIO_DTS_EXT_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_DTS_EXT_TYPE_BASE "UNKNOWN", NULL }, + { SPA_AUDIO_DTS_EXT_NONE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_DTS_EXT_TYPE_BASE "NONE", NULL }, + { SPA_AUDIO_DTS_EXT_HD_HRA, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_DTS_EXT_TYPE_BASE "HRA", NULL }, + { SPA_AUDIO_DTS_EXT_HD_MA, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_DTS_EXT_TYPE_BASE "MA", NULL }, + { 0, 0, NULL, NULL }, +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_DTS_TYPES_H */ diff --git a/spa/include/spa/param/audio/dts-utils.h b/spa/include/spa/param/audio/dts-utils.h new file mode 100644 index 000000000..182f1ebbc --- /dev/null +++ b/spa/include/spa/param/audio/dts-utils.h @@ -0,0 +1,73 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_DTS_UTILS_H +#define SPA_AUDIO_DTS_UTILS_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#ifndef SPA_API_AUDIO_DTS_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_DTS_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_DTS_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_DTS_UTILS int +spa_format_audio_dts_parse(const struct spa_pod *format, struct spa_audio_info_dts *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels), + SPA_FORMAT_AUDIO_DTS_extType, SPA_POD_OPT_Id(&info->ext_type)); + return res; +} + +SPA_API_AUDIO_DTS_UTILS struct spa_pod * +spa_format_audio_dts_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_dts *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dts), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + if (info->ext_type != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_DTS_extType, SPA_POD_Id(info->ext_type), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_DTS_UTILS_H */ diff --git a/spa/include/spa/param/audio/dts.h b/spa/include/spa/param/audio/dts.h new file mode 100644 index 000000000..285b8d96a --- /dev/null +++ b/spa/include/spa/param/audio/dts.h @@ -0,0 +1,51 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_DTS_H +#define SPA_AUDIO_DTS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +/** + * Possible high-definition DTS extensions on top of core DTS. + */ +enum spa_audio_dts_ext_type { + SPA_AUDIO_DTS_EXT_UNKNOWN, + SPA_AUDIO_DTS_EXT_NONE, /**< No extension present; this is just regular DTS data */ + SPA_AUDIO_DTS_EXT_HD_HRA, /**< DTS-HD High Resolution Audio (lossy HD audio extension) */ + SPA_AUDIO_DTS_EXT_HD_MA, /**< DTS-HD Master Audio (lossless HD audio extension) */ +}; + +/** + * DTS Coherent Acoustics audio info. Optional extensions on top + * of the DTS content can be present, resulting in what is known + * as DTS-HD. \a ext_type specifies which extension is used in + * combination with the core DTS content (if any). + */ +struct spa_audio_info_dts { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ + enum spa_audio_dts_ext_type ext_type; /*< DTS-HD extension type */ +}; + +#define SPA_AUDIO_INFO_DTS_INIT(...) ((struct spa_audio_info_dts) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_DTS_H */ diff --git a/spa/include/spa/param/audio/eac3-utils.h b/spa/include/spa/param/audio/eac3-utils.h new file mode 100644 index 000000000..926d0fd80 --- /dev/null +++ b/spa/include/spa/param/audio/eac3-utils.h @@ -0,0 +1,69 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_EAC3_UTILS_H +#define SPA_AUDIO_EAC3_UTILS_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#ifndef SPA_API_AUDIO_EAC3_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_EAC3_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_EAC3_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_EAC3_UTILS int +spa_format_audio_eac3_parse(const struct spa_pod *format, struct spa_audio_info_eac3 *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + return res; +} + +SPA_API_AUDIO_EAC3_UTILS struct spa_pod * +spa_format_audio_eac3_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_eac3 *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_eac3), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_EAC3_UTILS_H */ diff --git a/spa/include/spa/param/audio/eac3.h b/spa/include/spa/param/audio/eac3.h new file mode 100644 index 000000000..dc95bdd09 --- /dev/null +++ b/spa/include/spa/param/audio/eac3.h @@ -0,0 +1,35 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_EAC3_H +#define SPA_AUDIO_EAC3_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +/** Dolby E-AC-3 audio info. */ +struct spa_audio_info_eac3 { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_EAC3_INIT(...) ((struct spa_audio_info_eac3) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_EAC3_H */ diff --git a/spa/include/spa/param/audio/flac-utils.h b/spa/include/spa/param/audio/flac-utils.h index bc3d8afc2..2472abce0 100644 --- a/spa/include/spa/param/audio/flac-utils.h +++ b/spa/include/spa/param/audio/flac-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_FLAC_UTILS_H #define SPA_AUDIO_FLAC_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_FLAC_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_FLAC_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/flac.h b/spa/include/spa/param/audio/flac.h index f213e3f83..5b229ef16 100644 --- a/spa/include/spa/param/audio/flac.h +++ b/spa/include/spa/param/audio/flac.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_FLAC_H #define SPA_AUDIO_FLAC_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/format-utils.h b/spa/include/spa/param/audio/format-utils.h index 4ff40faa1..24d06dd1e 100644 --- a/spa/include/spa/param/audio/format-utils.h +++ b/spa/include/spa/param/audio/format-utils.h @@ -5,10 +5,6 @@ #ifndef SPA_PARAM_AUDIO_FORMAT_UTILS_H #define SPA_PARAM_AUDIO_FORMAT_UTILS_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include @@ -27,7 +23,15 @@ extern "C" { #include #include #include +#include +#include +#include +#include +#include +#ifdef __cplusplus +extern "C" { +#endif /** * \addtogroup spa_param @@ -42,20 +46,61 @@ extern "C" { #endif #endif +SPA_API_AUDIO_FORMAT_UTILS bool +spa_format_audio_ext_valid_size(uint32_t media_subtype, size_t size) +{ + switch (media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + return size >= offsetof(struct spa_audio_info, info.raw) && + SPA_AUDIO_INFO_RAW_VALID_SIZE(size - offsetof(struct spa_audio_info, info.raw)); + +#define _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(format) \ + case SPA_MEDIA_SUBTYPE_ ## format: \ + return size >= offsetof(struct spa_audio_info, info.format) + sizeof(struct spa_audio_info_ ## format); + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(dsp) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(iec958) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(dsd) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(mp3) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(aac) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(vorbis) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(wma) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(ra) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(amr) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(alac) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(flac) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(ape) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(ac3) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(eac3) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(truehd) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(dts) + _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE(mpegh) +#undef _SPA_FORMAT_AUDIO_EXT_VALID_SIZE_CASE + } + return false; +} + SPA_API_AUDIO_FORMAT_UTILS int -spa_format_audio_parse(const struct spa_pod *format, struct spa_audio_info *info) +spa_format_audio_ext_parse(const struct spa_pod *format, struct spa_audio_info *info, size_t size) { int res; + uint32_t media_type, media_subtype; - if ((res = spa_format_parse(format, &info->media_type, &info->media_subtype)) < 0) + if ((res = spa_format_parse(format, &media_type, &media_subtype)) < 0) return res; - if (info->media_type != SPA_MEDIA_TYPE_audio) + if (media_type != SPA_MEDIA_TYPE_audio) return -EINVAL; - switch (info->media_subtype) { + if (!spa_format_audio_ext_valid_size(media_subtype, size)) + return -EINVAL; + + info->media_type = media_type; + info->media_subtype = media_subtype; + + switch (media_subtype) { case SPA_MEDIA_SUBTYPE_raw: - return spa_format_audio_raw_parse(format, &info->info.raw); + return spa_format_audio_raw_ext_parse(format, &info->info.raw, + size - offsetof(struct spa_audio_info, info.raw)); case SPA_MEDIA_SUBTYPE_dsp: return spa_format_audio_dsp_parse(format, &info->info.dsp); case SPA_MEDIA_SUBTYPE_iec958: @@ -80,17 +125,39 @@ spa_format_audio_parse(const struct spa_pod *format, struct spa_audio_info *info return spa_format_audio_flac_parse(format, &info->info.flac); case SPA_MEDIA_SUBTYPE_ape: return spa_format_audio_ape_parse(format, &info->info.ape); + case SPA_MEDIA_SUBTYPE_ac3: + return spa_format_audio_ac3_parse(format, &info->info.ac3); + case SPA_MEDIA_SUBTYPE_eac3: + return spa_format_audio_eac3_parse(format, &info->info.eac3); + case SPA_MEDIA_SUBTYPE_truehd: + return spa_format_audio_truehd_parse(format, &info->info.truehd); + case SPA_MEDIA_SUBTYPE_dts: + return spa_format_audio_dts_parse(format, &info->info.dts); + case SPA_MEDIA_SUBTYPE_mpegh: + return spa_format_audio_mpegh_parse(format, &info->info.mpegh); } return -ENOTSUP; } -SPA_API_AUDIO_FORMAT_UTILS struct spa_pod * -spa_format_audio_build(struct spa_pod_builder *builder, uint32_t id, - const struct spa_audio_info *info) +SPA_API_AUDIO_FORMAT_UTILS int +spa_format_audio_parse(const struct spa_pod *format, struct spa_audio_info *info) { + return spa_format_audio_ext_parse(format, info, sizeof(*info)); +} + +SPA_API_AUDIO_FORMAT_UTILS struct spa_pod * +spa_format_audio_ext_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info *info, size_t size) +{ + if (!spa_format_audio_ext_valid_size(info->media_subtype, size)) { + errno = EINVAL; + return NULL; + } + switch (info->media_subtype) { case SPA_MEDIA_SUBTYPE_raw: - return spa_format_audio_raw_build(builder, id, &info->info.raw); + return spa_format_audio_raw_ext_build(builder, id, &info->info.raw, + size - offsetof(struct spa_audio_info, info.raw)); case SPA_MEDIA_SUBTYPE_dsp: return spa_format_audio_dsp_build(builder, id, &info->info.dsp); case SPA_MEDIA_SUBTYPE_iec958: @@ -115,10 +182,27 @@ spa_format_audio_build(struct spa_pod_builder *builder, uint32_t id, return spa_format_audio_flac_build(builder, id, &info->info.flac); case SPA_MEDIA_SUBTYPE_ape: return spa_format_audio_ape_build(builder, id, &info->info.ape); + case SPA_MEDIA_SUBTYPE_ac3: + return spa_format_audio_ac3_build(builder, id, &info->info.ac3); + case SPA_MEDIA_SUBTYPE_eac3: + return spa_format_audio_eac3_build(builder, id, &info->info.eac3); + case SPA_MEDIA_SUBTYPE_truehd: + return spa_format_audio_truehd_build(builder, id, &info->info.truehd); + case SPA_MEDIA_SUBTYPE_dts: + return spa_format_audio_dts_build(builder, id, &info->info.dts); + case SPA_MEDIA_SUBTYPE_mpegh: + return spa_format_audio_mpegh_build(builder, id, &info->info.mpegh); } errno = ENOTSUP; return NULL; } + +SPA_API_AUDIO_FORMAT_UTILS struct spa_pod * +spa_format_audio_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info *info) +{ + return spa_format_audio_ext_build(builder, id, info, sizeof(*info)); +} /** * \} */ diff --git a/spa/include/spa/param/audio/format.h b/spa/include/spa/param/audio/format.h index 0619de394..6e7a71f6c 100644 --- a/spa/include/spa/param/audio/format.h +++ b/spa/include/spa/param/audio/format.h @@ -5,15 +5,6 @@ #ifndef SPA_PARAM_AUDIO_FORMAT_H #define SPA_PARAM_AUDIO_FORMAT_H -#ifdef __cplusplus -extern "C" { -#endif - -/** - * \addtogroup spa_param - * \{ - */ - #include #include #include @@ -29,6 +20,20 @@ extern "C" { #include #include #include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ struct spa_audio_info { uint32_t media_type; @@ -48,7 +53,14 @@ struct spa_audio_info { struct spa_audio_info_flac flac; struct spa_audio_info_ape ape; struct spa_audio_info_ape opus; + struct spa_audio_info_ac3 ac3; + struct spa_audio_info_eac3 eac3; + struct spa_audio_info_truehd truehd; + struct spa_audio_info_dts dts; + struct spa_audio_info_mpegh mpegh; } info; + + /* padding follows here when info has flexible size */ }; /** diff --git a/spa/include/spa/param/audio/iec958-types.h b/spa/include/spa/param/audio/iec958-types.h index adcffdc96..5fe876f1b 100644 --- a/spa/include/spa/param/audio/iec958-types.h +++ b/spa/include/spa/param/audio/iec958-types.h @@ -5,13 +5,13 @@ #ifndef SPA_AUDIO_IEC958_TYPES_H #define SPA_AUDIO_IEC958_TYPES_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/iec958-utils.h b/spa/include/spa/param/audio/iec958-utils.h index 1c4ec105b..4c0342f4b 100644 --- a/spa/include/spa/param/audio/iec958-utils.h +++ b/spa/include/spa/param/audio/iec958-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_IEC958_UTILS_H #define SPA_AUDIO_IEC958_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_IEC958_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_IEC958_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/iec958.h b/spa/include/spa/param/audio/iec958.h index 05475188e..57b081856 100644 --- a/spa/include/spa/param/audio/iec958.h +++ b/spa/include/spa/param/audio/iec958.h @@ -31,7 +31,7 @@ enum spa_audio_iec958_codec { }; struct spa_audio_info_iec958 { - enum spa_audio_iec958_codec codec; /*< format, one of the DSP formats in enum spa_audio_format_dsp */ + enum spa_audio_iec958_codec codec; /*< codec, one of the values in enum spa_audio_iec958_codec */ uint32_t flags; /*< extra flags */ uint32_t rate; /*< sample rate */ }; diff --git a/spa/include/spa/param/audio/layout-types.h b/spa/include/spa/param/audio/layout-types.h new file mode 100644 index 000000000..c8c78af39 --- /dev/null +++ b/spa/include/spa/param/audio/layout-types.h @@ -0,0 +1,118 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_LAYOUT_TYPES_H +#define SPA_AUDIO_LAYOUT_TYPES_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#ifndef SPA_API_AUDIO_LAYOUT_TYPES + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_LAYOUT_TYPES SPA_API_IMPL + #else + #define SPA_API_AUDIO_LAYOUT_TYPES static inline + #endif +#endif + +static const struct spa_type_audio_layout_info { + const char *name; + struct spa_audio_layout_info layout; +} spa_type_audio_layout_info[] = { + { "Mono", { SPA_AUDIO_LAYOUT_Mono } }, + { "Stereo", { SPA_AUDIO_LAYOUT_Stereo } }, + { "Quad", { SPA_AUDIO_LAYOUT_Quad } }, + { "Pentagonal", { SPA_AUDIO_LAYOUT_Pentagonal } }, + { "Hexagonal", { SPA_AUDIO_LAYOUT_Hexagonal } }, + { "Octagonal", { SPA_AUDIO_LAYOUT_Octagonal } }, + { "Cube", { SPA_AUDIO_LAYOUT_Cube } }, + { "MPEG-1.0", { SPA_AUDIO_LAYOUT_MPEG_1_0 } }, + { "MPEG-2.0", { SPA_AUDIO_LAYOUT_MPEG_2_0 } }, + { "MPEG-3.0A", { SPA_AUDIO_LAYOUT_MPEG_3_0A } }, + { "MPEG-3.0B", { SPA_AUDIO_LAYOUT_MPEG_3_0B } }, + { "MPEG-4.0A", { SPA_AUDIO_LAYOUT_MPEG_4_0A } }, + { "MPEG-4.0B", { SPA_AUDIO_LAYOUT_MPEG_4_0B } }, + { "MPEG-5.0A", { SPA_AUDIO_LAYOUT_MPEG_5_0A } }, + { "MPEG-5.0B", { SPA_AUDIO_LAYOUT_MPEG_5_0B } }, + { "MPEG-5.0C", { SPA_AUDIO_LAYOUT_MPEG_5_0C } }, + { "MPEG-5.0D", { SPA_AUDIO_LAYOUT_MPEG_5_0D } }, + { "MPEG-5.1A", { SPA_AUDIO_LAYOUT_MPEG_5_1A } }, + { "MPEG-5.1B", { SPA_AUDIO_LAYOUT_MPEG_5_1B } }, + { "MPEG-5.1C", { SPA_AUDIO_LAYOUT_MPEG_5_1C } }, + { "MPEG-5.1D", { SPA_AUDIO_LAYOUT_MPEG_5_1D } }, + { "MPEG-6.1A", { SPA_AUDIO_LAYOUT_MPEG_6_1A } }, + { "MPEG-7.1A", { SPA_AUDIO_LAYOUT_MPEG_7_1A } }, + { "MPEG-7.1B", { SPA_AUDIO_LAYOUT_MPEG_7_1B } }, + { "MPEG-7.1C", { SPA_AUDIO_LAYOUT_MPEG_7_1C } }, + { "2.1", { SPA_AUDIO_LAYOUT_2_1 } }, + { "2RC", { SPA_AUDIO_LAYOUT_2RC } }, + { "2FC", { SPA_AUDIO_LAYOUT_2FC } }, + { "3.1", { SPA_AUDIO_LAYOUT_3_1 } }, + { "4.0", { SPA_AUDIO_LAYOUT_4_0 } }, + { "2.2", { SPA_AUDIO_LAYOUT_2_2 } }, + { "4.1", { SPA_AUDIO_LAYOUT_4_1 } }, + { "5.0", { SPA_AUDIO_LAYOUT_5_0 } }, + { "5.0R", { SPA_AUDIO_LAYOUT_5_0R } }, + { "5.1", { SPA_AUDIO_LAYOUT_5_1 } }, + { "5.1R", { SPA_AUDIO_LAYOUT_5_1R } }, + { "6.0", { SPA_AUDIO_LAYOUT_6_0 } }, + { "6.0F", { SPA_AUDIO_LAYOUT_6_0F } }, + { "6.1", { SPA_AUDIO_LAYOUT_6_1 } }, + { "6.1F", { SPA_AUDIO_LAYOUT_6_1F } }, + { "7.0", { SPA_AUDIO_LAYOUT_7_0 } }, + { "7.0F", { SPA_AUDIO_LAYOUT_7_0F } }, + { "7.1", { SPA_AUDIO_LAYOUT_7_1 } }, + { "7.1W", { SPA_AUDIO_LAYOUT_7_1W } }, + { "7.1WR", { SPA_AUDIO_LAYOUT_7_1WR } }, + { NULL, }, +}; + +SPA_API_AUDIO_LAYOUT_TYPES int +spa_audio_layout_info_parse_name(struct spa_audio_layout_info *layout, size_t size, + const char *name) +{ + uint32_t max_position = SPA_AUDIO_LAYOUT_INFO_MAX_POSITION(size); + if (spa_strstartswith(name, "AUX")) { + uint32_t i, n_pos; + if (spa_atou32(name+3, &n_pos, 10)) { + if (n_pos > max_position) + return -ECHRNG; + for (i = 0; i < 0x1000 && i < n_pos; i++) + layout->position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + for (; i < n_pos; i++) + layout->position[i] = SPA_AUDIO_CHANNEL_UNKNOWN; + layout->n_channels = n_pos; + return n_pos; + } + } + SPA_FOR_EACH_ELEMENT_VAR(spa_type_audio_layout_info, i) { + if (spa_streq(name, i->name)) { + if (i->layout.n_channels > max_position) + return -ECHRNG; + *layout = i->layout; + return i->layout.n_channels; + } + } + return -ENOTSUP; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_LAYOUT_TYPES_H */ diff --git a/spa/include/spa/param/audio/layout.h b/spa/include/spa/param/audio/layout.h index 545ceae32..2293d41f1 100644 --- a/spa/include/spa/param/audio/layout.h +++ b/spa/include/spa/param/audio/layout.h @@ -5,23 +5,27 @@ #ifndef SPA_AUDIO_LAYOUT_H #define SPA_AUDIO_LAYOUT_H +#include + +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ */ -#include struct spa_audio_layout_info { uint32_t n_channels; uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + /* padding may follow to allow more channels */ }; +#define SPA_AUDIO_LAYOUT_INFO_MAX_POSITION(size) (((size)-offsetof(struct spa_audio_layout_info,position))/sizeof(uint32_t)) + #define SPA_AUDIO_LAYOUT_Mono 1, { SPA_AUDIO_CHANNEL_MONO, } #define SPA_AUDIO_LAYOUT_Stereo 2, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, } #define SPA_AUDIO_LAYOUT_Quad 4, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \ @@ -36,7 +40,7 @@ struct spa_audio_layout_info { SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \ SPA_AUDIO_CHANNEL_FC, SPA_AUDIO_CHANNEL_RC, \ SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR, } -#define SPA_AUDIO_LAYOUT_Cube 8, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR }, \ +#define SPA_AUDIO_LAYOUT_Cube 8, { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, \ SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, \ SPA_AUDIO_CHANNEL_TFL, SPA_AUDIO_CHANNEL_TFR, \ SPA_AUDIO_CHANNEL_TRL, SPA_AUDIO_CHANNEL_TRR, } diff --git a/spa/include/spa/param/audio/mp3-types.h b/spa/include/spa/param/audio/mp3-types.h index a7ba22ace..826398d84 100644 --- a/spa/include/spa/param/audio/mp3-types.h +++ b/spa/include/spa/param/audio/mp3-types.h @@ -5,13 +5,13 @@ #ifndef SPA_AUDIO_MP3_TYPES_H #define SPA_AUDIO_MP3_TYPES_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/mp3-utils.h b/spa/include/spa/param/audio/mp3-utils.h index 481000e25..44b7310ec 100644 --- a/spa/include/spa/param/audio/mp3-utils.h +++ b/spa/include/spa/param/audio/mp3-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_MP3_UTILS_H #define SPA_AUDIO_MP3_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_MP3_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_MP3_UTILS SPA_API_IMPL @@ -33,8 +33,9 @@ spa_format_audio_mp3_parse(const struct spa_pod *format, struct spa_audio_info_m int res; res = spa_pod_parse_object(format, SPA_TYPE_OBJECT_Format, NULL, - SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), - SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels), + SPA_FORMAT_AUDIO_MP3_channelMode, SPA_POD_OPT_Id(&info->channel_mode)); return res; } @@ -55,6 +56,9 @@ spa_format_audio_mp3_build(struct spa_pod_builder *builder, uint32_t id, if (info->channels != 0) spa_pod_builder_add(builder, SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + if (info->channel_mode != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_MP3_channelMode, SPA_POD_Id(info->channel_mode), 0); return (struct spa_pod*)spa_pod_builder_pop(builder, &f); } diff --git a/spa/include/spa/param/audio/mp3.h b/spa/include/spa/param/audio/mp3.h index 86be21a1e..83a978f19 100644 --- a/spa/include/spa/param/audio/mp3.h +++ b/spa/include/spa/param/audio/mp3.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_MP3_H #define SPA_AUDIO_MP3_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ @@ -18,15 +18,26 @@ extern "C" { enum spa_audio_mp3_channel_mode { SPA_AUDIO_MP3_CHANNEL_MODE_UNKNOWN, + /** Mono mode, only used if channel count is 1 */ SPA_AUDIO_MP3_CHANNEL_MODE_MONO, + /** Regular stereo mode with two independent channels */ SPA_AUDIO_MP3_CHANNEL_MODE_STEREO, + /** + * Joint stereo mode, exploiting the similarities between channels + * using techniques like mid-side coding + */ SPA_AUDIO_MP3_CHANNEL_MODE_JOINTSTEREO, + /** + * Two mono tracks, different from stereo in that each channel + * contains entirely different content (like two different mono songs) + */ SPA_AUDIO_MP3_CHANNEL_MODE_DUAL, }; struct spa_audio_info_mp3 { - uint32_t rate; /*< sample rate */ - uint32_t channels; /*< number of channels */ + uint32_t rate; /*< sample rate in Hz */ + uint32_t channels; /*< number of channels */ + enum spa_audio_mp3_channel_mode channel_mode; /*< MP3 channel mode */ }; #define SPA_AUDIO_INFO_MP3_INIT(...) ((struct spa_audio_info_mp3) { __VA_ARGS__ }) diff --git a/spa/include/spa/param/audio/mpegh-utils.h b/spa/include/spa/param/audio/mpegh-utils.h new file mode 100644 index 000000000..67e18cf4c --- /dev/null +++ b/spa/include/spa/param/audio/mpegh-utils.h @@ -0,0 +1,65 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_MPEGH_UTILS_H +#define SPA_AUDIO_MPEGH_UTILS_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#ifndef SPA_API_AUDIO_MPEGH_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_MPEGH_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_MPEGH_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_MPEGH_UTILS int +spa_format_audio_mpegh_parse(const struct spa_pod *format, struct spa_audio_info_mpegh *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate)); + return res; +} + +SPA_API_AUDIO_MPEGH_UTILS struct spa_pod * +spa_format_audio_mpegh_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_mpegh *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_mpegh), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_MPEGH_UTILS_H */ diff --git a/spa/include/spa/param/audio/mpegh.h b/spa/include/spa/param/audio/mpegh.h new file mode 100644 index 000000000..1c9549338 --- /dev/null +++ b/spa/include/spa/param/audio/mpegh.h @@ -0,0 +1,52 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_MPEGH_H +#define SPA_AUDIO_MPEGH_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +/** + * MPEG-H 3D audio info. + * + * MPEG-H content is assumed to be provided in the form of an MPEG-H + * 3D Audio Stream (MHAS). MHAS is a lightweight bitstream format that + * encapsulates MPEG-H 3D Audio frames along with associated metadata. + * It serves a similar role to the Annex B byte stream format used for + * H.264, providing framing and synchronization for MPEG-H frames. + * + * MPEG-H is documented in the ISO/IEC 23008-3 specification. + * MHAS is specified in ISO/IEC 23008-3, Clause 14. + * + * Note that unlike other formats, this one does not specify a channel + * count. This is because MPEG-H is entity-based; it contains multiple + * entities of different types (channel beds, audio objects etc.) which + * do not map 1:1 to channels. The channel amount is determined by + * decoders instead, based on the audio scene content and the target + * playback system. + */ +struct spa_audio_info_mpegh { + uint32_t rate; /*< sample rate */ +}; + +#define SPA_AUDIO_INFO_MPEGH_INIT(...) ((struct spa_audio_info_mpegh) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_MPEGH_H */ diff --git a/spa/include/spa/param/audio/opus.h b/spa/include/spa/param/audio/opus.h index cdd0e6c5b..ebdf678f7 100644 --- a/spa/include/spa/param/audio/opus.h +++ b/spa/include/spa/param/audio/opus.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_OPUS_H #define SPA_AUDIO_OPUS_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/ra-utils.h b/spa/include/spa/param/audio/ra-utils.h index 79e96514a..4fd6a87e3 100644 --- a/spa/include/spa/param/audio/ra-utils.h +++ b/spa/include/spa/param/audio/ra-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_RA_UTILS_H #define SPA_AUDIO_RA_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_RA_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_RA_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/ra.h b/spa/include/spa/param/audio/ra.h index b784ab588..5fb0dbc09 100644 --- a/spa/include/spa/param/audio/ra.h +++ b/spa/include/spa/param/audio/ra.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_RA_H #define SPA_AUDIO_RA_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/raw-json.h b/spa/include/spa/param/audio/raw-json.h index 07f0e0c45..6b1b25164 100644 --- a/spa/include/spa/param/audio/raw-json.h +++ b/spa/include/spa/param/audio/raw-json.h @@ -5,6 +5,12 @@ #ifndef SPA_AUDIO_RAW_JSON_H #define SPA_AUDIO_RAW_JSON_H +#include +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +20,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_RAW_JSON #ifdef SPA_API_IMPL #define SPA_API_AUDIO_RAW_JSON SPA_API_IMPL @@ -28,8 +29,8 @@ extern "C" { #endif SPA_API_AUDIO_RAW_JSON int -spa_audio_parse_position(const char *str, size_t len, - uint32_t *position, uint32_t *n_channels) +spa_audio_parse_position_n(const char *str, size_t len, + uint32_t *position, uint32_t max_position, uint32_t *n_channels) { struct spa_json iter; char v[256]; @@ -38,18 +39,46 @@ spa_audio_parse_position(const char *str, size_t len, if (spa_json_begin_array_relax(&iter, str, len) <= 0) return 0; - while (spa_json_get_string(&iter, v, sizeof(v)) > 0 && - channels < SPA_AUDIO_MAX_CHANNELS) { - position[channels++] = spa_type_audio_channel_from_short_name(v); + while (spa_json_get_string(&iter, v, sizeof(v)) > 0) { + if (channels < max_position) + position[channels] = spa_type_audio_channel_from_short_name(v); + channels++; } *n_channels = channels; return channels; } SPA_API_AUDIO_RAW_JSON int -spa_audio_info_raw_update(struct spa_audio_info_raw *info, const char *key, const char *val, bool force) +spa_audio_parse_position(const char *str, size_t len, + uint32_t *position, uint32_t *n_channels) +{ + return spa_audio_parse_position_n(str, len, position, SPA_AUDIO_MAX_CHANNELS, n_channels); +} + +SPA_API_AUDIO_RAW_JSON int +spa_audio_parse_layout(const char *str, uint32_t *position, uint32_t max_position, + uint32_t *n_channels) +{ + struct spa_audio_layout_info l; + uint32_t i; + if (spa_audio_layout_info_parse_name(&l, sizeof(l), str) <= 0) + return 0; + for (i = 0; i < l.n_channels && i < max_position; i++) + position[i] = l.position[i]; + *n_channels = l.n_channels; + return l.n_channels; +} + +SPA_API_AUDIO_RAW_JSON int +spa_audio_info_raw_ext_update(struct spa_audio_info_raw *info, size_t size, + const char *key, const char *val, bool force) { uint32_t v; + uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size); + + if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size)) + return -EINVAL; + if (spa_streq(key, SPA_KEY_AUDIO_FORMAT)) { if (force || info->format == 0) info->format = (enum spa_audio_format)spa_type_audio_format_from_short_name(val); @@ -57,41 +86,97 @@ spa_audio_info_raw_update(struct spa_audio_info_raw *info, const char *key, cons if (spa_atou32(val, &v, 0) && (force || info->rate == 0)) info->rate = v; } else if (spa_streq(key, SPA_KEY_AUDIO_CHANNELS)) { - if (spa_atou32(val, &v, 0) && (force || info->channels == 0)) - info->channels = SPA_MIN(v, SPA_AUDIO_MAX_CHANNELS); + if (spa_atou32(val, &v, 0) && (force || info->channels == 0)) { + if (v > max_position) + return -ECHRNG; + info->channels = v; + } + } else if (spa_streq(key, SPA_KEY_AUDIO_LAYOUT)) { + if (force || info->channels == 0) { + if (spa_audio_parse_layout(val, info->position, max_position, &v) > 0) { + if (v > max_position) + return -ECHRNG; + info->channels = v; + SPA_FLAG_CLEAR(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); + } + } } else if (spa_streq(key, SPA_KEY_AUDIO_POSITION)) { if (force || info->channels == 0) { - if (spa_audio_parse_position(val, strlen(val), info->position, &info->channels) > 0) + if (spa_audio_parse_position_n(val, strlen(val), info->position, + max_position, &v) > 0) { + if (v > max_position) + return -ECHRNG; + info->channels = v; SPA_FLAG_CLEAR(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); + } } } return 0; } +SPA_API_AUDIO_RAW_JSON int +spa_audio_info_raw_update(struct spa_audio_info_raw *info, + const char *key, const char *val, bool force) +{ + return spa_audio_info_raw_ext_update(info, sizeof(*info), key, val, force); +} + +SPA_API_AUDIO_RAW_JSON int +spa_audio_info_raw_ext_init_dict_keys_va(struct spa_audio_info_raw *info, size_t size, + const struct spa_dict *defaults, + const struct spa_dict *dict, va_list args) +{ + int res; + + if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size)) + return -EINVAL; + + memset(info, 0, size); + SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); + if (dict) { + const char *val, *key; + while ((key = va_arg(args, const char *))) { + if ((val = spa_dict_lookup(dict, key)) == NULL) + continue; + if ((res = spa_audio_info_raw_ext_update(info, size, + key, val, true)) < 0) + return res; + } + } + if (defaults) { + const struct spa_dict_item *it; + spa_dict_for_each(it, defaults) + if ((res = spa_audio_info_raw_ext_update(info, size, + it->key, it->value, false)) < 0) + return res; + } + return 0; +} + +SPA_API_AUDIO_RAW_JSON int SPA_SENTINEL +spa_audio_info_raw_ext_init_dict_keys(struct spa_audio_info_raw *info, size_t size, + const struct spa_dict *defaults, + const struct spa_dict *dict, ...) +{ + va_list args; + int res; + va_start(args, dict); + res = spa_audio_info_raw_ext_init_dict_keys_va(info, size, defaults, dict, args); + va_end(args); + return res; +} + SPA_API_AUDIO_RAW_JSON int SPA_SENTINEL spa_audio_info_raw_init_dict_keys(struct spa_audio_info_raw *info, const struct spa_dict *defaults, const struct spa_dict *dict, ...) { - spa_zero(*info); - SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); - if (dict) { - const char *val, *key; - va_list args; - va_start(args, dict); - while ((key = va_arg(args, const char *))) { - if ((val = spa_dict_lookup(dict, key)) == NULL) - continue; - spa_audio_info_raw_update(info, key, val, true); - } - va_end(args); - } - if (defaults) { - const struct spa_dict_item *it; - spa_dict_for_each(it, defaults) - spa_audio_info_raw_update(info, it->key, it->value, false); - } - return 0; + va_list args; + int res; + va_start(args, dict); + res = spa_audio_info_raw_ext_init_dict_keys_va(info, sizeof(*info), defaults, dict, args); + va_end(args); + return res; } /** diff --git a/spa/include/spa/param/audio/raw-types.h b/spa/include/spa/param/audio/raw-types.h index 9aa9591c0..d01ad4fd5 100644 --- a/spa/include/spa/param/audio/raw-types.h +++ b/spa/include/spa/param/audio/raw-types.h @@ -5,6 +5,10 @@ #ifndef SPA_AUDIO_RAW_TYPES_H #define SPA_AUDIO_RAW_TYPES_H +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include -#include -#include - #ifndef SPA_API_AUDIO_RAW_TYPES #ifdef SPA_API_IMPL #define SPA_API_AUDIO_RAW_TYPES SPA_API_IMPL @@ -192,7 +192,7 @@ static const struct spa_type_info spa_type_audio_channel[] = { { SPA_AUDIO_CHANNEL_TFRC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TFRC", NULL }, { SPA_AUDIO_CHANNEL_TSL, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TSL", NULL }, { SPA_AUDIO_CHANNEL_TSR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "TSR", NULL }, - { SPA_AUDIO_CHANNEL_LLFE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "LLFR", NULL }, + { SPA_AUDIO_CHANNEL_LLFE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "LLFE", NULL }, { SPA_AUDIO_CHANNEL_RLFE, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "RLFE", NULL }, { SPA_AUDIO_CHANNEL_BC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "BC", NULL }, { SPA_AUDIO_CHANNEL_BLC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_CHANNEL_BASE "BLC", NULL }, @@ -267,13 +267,44 @@ static const struct spa_type_info spa_type_audio_channel[] = { SPA_API_AUDIO_RAW_TYPES uint32_t spa_type_audio_channel_from_short_name(const char *name) { - return spa_type_from_short_name(name, spa_type_audio_channel, SPA_AUDIO_CHANNEL_UNKNOWN); + uint32_t res; + if (spa_strstartswith(name, "AUX")) { + if (spa_atou32(name+3, &res, 10) && res < 0x1000) + res = SPA_AUDIO_CHANNEL_AUX0 + res; + else + res = SPA_AUDIO_CHANNEL_UNKNOWN; + } else { + res = spa_type_from_short_name(name, spa_type_audio_channel, SPA_AUDIO_CHANNEL_UNKNOWN); + } + return res; } SPA_API_AUDIO_RAW_TYPES const char * spa_type_audio_channel_to_short_name(uint32_t type) { return spa_type_to_short_name(type, spa_type_audio_channel, "UNK"); } +SPA_API_AUDIO_RAW_TYPES const char * spa_type_audio_channel_make_short_name(uint32_t type, + char *buf, size_t size, const char *unknown) +{ + if (SPA_AUDIO_CHANNEL_IS_AUX(type)) { + snprintf(buf, size, "AUX%u", type - SPA_AUDIO_CHANNEL_AUX0); + } else { + const char *str = spa_type_to_short_name(type, spa_type_audio_channel, NULL); + if (str == NULL) + return unknown; + snprintf(buf, size, "%.7s", str); + } + return buf; +} +#define SPA_TYPE_INFO_AudioVolumeRampScale SPA_TYPE_INFO_ENUM_BASE "AudioVolumeRampScale" +#define SPA_TYPE_INFO_AUDIO_VOLUME_RAMP_SCALE_BASE SPA_TYPE_INFO_AudioVolumeRampScale ":" + +static const struct spa_type_info spa_type_audio_volume_ramp_scale[] = { + { SPA_AUDIO_VOLUME_RAMP_INVALID, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_VOLUME_RAMP_SCALE_BASE "INVALID", NULL }, + { SPA_AUDIO_VOLUME_RAMP_LINEAR, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_VOLUME_RAMP_SCALE_BASE "LINEAR", NULL }, + { SPA_AUDIO_VOLUME_RAMP_CUBIC, SPA_TYPE_Int, SPA_TYPE_INFO_AUDIO_VOLUME_RAMP_SCALE_BASE "CUBIC", NULL }, + { 0, 0, NULL, NULL }, +}; /** * \} @@ -283,4 +314,4 @@ SPA_API_AUDIO_RAW_TYPES const char * spa_type_audio_channel_to_short_name(uint32 } /* extern "C" */ #endif -#endif /* SPA_AUDIO_RAW_RAW_TYPES_H */ +#endif /* SPA_AUDIO_RAW_TYPES_H */ diff --git a/spa/include/spa/param/audio/raw-utils.h b/spa/include/spa/param/audio/raw-utils.h index 178e3dd11..da1ec7317 100644 --- a/spa/include/spa/param/audio/raw-utils.h +++ b/spa/include/spa/param/audio/raw-utils.h @@ -5,6 +5,12 @@ #ifndef SPA_AUDIO_RAW_UTILS_H #define SPA_AUDIO_RAW_UTILS_H +#include +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +20,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_RAW_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_RAW_UTILS SPA_API_IMPL @@ -28,10 +29,15 @@ extern "C" { #endif SPA_API_AUDIO_RAW_UTILS int -spa_format_audio_raw_parse(const struct spa_pod *format, struct spa_audio_info_raw *info) +spa_format_audio_raw_ext_parse(const struct spa_pod *format, struct spa_audio_info_raw *info, size_t size) { struct spa_pod *position = NULL; int res; + uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size); + + if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size)) + return -EINVAL; + info->flags = 0; res = spa_pod_parse_object(format, SPA_TYPE_OBJECT_Format, NULL, @@ -39,18 +45,33 @@ spa_format_audio_raw_parse(const struct spa_pod *format, struct spa_audio_info_r SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels), SPA_FORMAT_AUDIO_position, SPA_POD_OPT_Pod(&position)); + if (info->channels > max_position) + return -ECHRNG; if (position == NULL || - !spa_pod_copy_array(position, SPA_TYPE_Id, info->position, SPA_AUDIO_MAX_CHANNELS)) + spa_pod_copy_array(position, SPA_TYPE_Id, info->position, max_position) != info->channels) SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); return res; } +SPA_API_AUDIO_RAW_UTILS int +spa_format_audio_raw_parse(const struct spa_pod *format, struct spa_audio_info_raw *info) +{ + return spa_format_audio_raw_ext_parse(format, info, sizeof(*info)); +} + SPA_API_AUDIO_RAW_UTILS struct spa_pod * -spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, - const struct spa_audio_info_raw *info) +spa_format_audio_raw_ext_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_raw *info, size_t size) { struct spa_pod_frame f; + uint32_t max_position = SPA_AUDIO_INFO_RAW_MAX_POSITION(size); + + if (!SPA_AUDIO_INFO_RAW_VALID_SIZE(size)) { + errno = EINVAL; + return NULL; + } + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); spa_pod_builder_add(builder, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), @@ -65,7 +86,10 @@ spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, if (info->channels != 0) { spa_pod_builder_add(builder, SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); - if (!SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED)) { + /* we drop the positions here when we can't read all of them. This is + * really a malformed spa_audio_info structure. */ + if (!SPA_FLAG_IS_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED) && + info->channels <= max_position) { spa_pod_builder_add(builder, SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, info->channels, info->position), 0); @@ -74,6 +98,13 @@ spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, return (struct spa_pod*)spa_pod_builder_pop(builder, &f); } +SPA_API_AUDIO_RAW_UTILS struct spa_pod * +spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_raw *info) +{ + return spa_format_audio_raw_ext_build(builder, id, info, sizeof(*info)); +} + /** * \} */ diff --git a/spa/include/spa/param/audio/raw.h b/spa/include/spa/param/audio/raw.h index 8bed3f8a4..8500c4f17 100644 --- a/spa/include/spa/param/audio/raw.h +++ b/spa/include/spa/param/audio/raw.h @@ -5,20 +5,24 @@ #ifndef SPA_AUDIO_RAW_H #define SPA_AUDIO_RAW_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * \addtogroup spa_param * \{ */ +/* This is the max number of channels, changing this will change the + * size of some helper structures. This value should be at least 64 */ +#ifndef SPA_AUDIO_MAX_CHANNELS #define SPA_AUDIO_MAX_CHANNELS 64u +#endif enum spa_audio_format { SPA_AUDIO_FORMAT_UNKNOWN, @@ -259,6 +263,8 @@ enum spa_audio_channel { SPA_AUDIO_CHANNEL_START_Custom = 0x10000, }; +#define SPA_AUDIO_CHANNEL_IS_AUX(ch) ((ch)>=SPA_AUDIO_CHANNEL_START_Aux && (ch)<=SPA_AUDIO_CHANNEL_LAST_Aux) + enum spa_audio_volume_ramp_scale { SPA_AUDIO_VOLUME_RAMP_INVALID, SPA_AUDIO_VOLUME_RAMP_LINEAR, @@ -269,23 +275,34 @@ enum spa_audio_volume_ramp_scale { #define SPA_AUDIO_FLAG_NONE (0) /*< no valid flag */ #define SPA_AUDIO_FLAG_UNPOSITIONED (1 << 0) /*< the position array explicitly * contains unpositioned channels. */ -/** Audio information description */ +/** Audio information description. You can assume when you receive this structure + * that there is enought padding to accomodate all channel positions in case the + * channel count is more than SPA_AUDIO_MAX_CHANNELS. */ struct spa_audio_info_raw { enum spa_audio_format format; /*< format, one of enum spa_audio_format */ uint32_t flags; /*< extra flags */ uint32_t rate; /*< sample rate */ - uint32_t channels; /*< number of channels */ + uint32_t channels; /*< number of channels. This can be more than SPA_AUDIO_MAX_CHANNELS + * and you may assume there is enough padding for the extra + * channel positions. */ uint32_t position[SPA_AUDIO_MAX_CHANNELS]; /*< channel position from enum spa_audio_channel */ + /* padding follows here when channels > SPA_AUDIO_MAX_CHANNELS */ }; #define SPA_AUDIO_INFO_RAW_INIT(...) ((struct spa_audio_info_raw) { __VA_ARGS__ }) +#define SPA_AUDIO_INFO_RAW_MAX_POSITION(size) (((size)-offsetof(struct spa_audio_info_raw,position))/sizeof(uint32_t)) + +#define SPA_AUDIO_INFO_RAW_VALID_SIZE(size) ((size) >= offsetof(struct spa_audio_info_raw, position)) + + #define SPA_KEY_AUDIO_FORMAT "audio.format" /**< an audio format as string, * Ex. "S16LE" */ #define SPA_KEY_AUDIO_CHANNEL "audio.channel" /**< an audio channel as string, * Ex. "FL" */ #define SPA_KEY_AUDIO_CHANNELS "audio.channels" /**< an audio channel count as int */ #define SPA_KEY_AUDIO_RATE "audio.rate" /**< an audio sample rate as int */ +#define SPA_KEY_AUDIO_LAYOUT "audio.layout" /**< channel positions as predefined layout */ #define SPA_KEY_AUDIO_POSITION "audio.position" /**< channel positions as comma separated list * of channels ex. "FL,FR" */ #define SPA_KEY_AUDIO_ALLOWED_RATES "audio.allowed-rates" /**< a list of allowed samplerates diff --git a/spa/include/spa/param/audio/truehd-utils.h b/spa/include/spa/param/audio/truehd-utils.h new file mode 100644 index 000000000..7d386ee54 --- /dev/null +++ b/spa/include/spa/param/audio/truehd-utils.h @@ -0,0 +1,69 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_TRUEHD_UTILS_H +#define SPA_AUDIO_TRUEHD_UTILS_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#ifndef SPA_API_AUDIO_TRUEHD_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_AUDIO_TRUEHD_UTILS SPA_API_IMPL + #else + #define SPA_API_AUDIO_TRUEHD_UTILS static inline + #endif +#endif + +SPA_API_AUDIO_TRUEHD_UTILS int +spa_format_audio_truehd_parse(const struct spa_pod *format, struct spa_audio_info_truehd *info) +{ + int res; + res = spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&info->rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&info->channels)); + return res; +} + +SPA_API_AUDIO_TRUEHD_UTILS struct spa_pod * +spa_format_audio_truehd_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_audio_info_truehd *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_truehd), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_ENCODED), + 0); + if (info->rate != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info->rate), 0); + if (info->channels != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info->channels), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_TRUEHD_UTILS_H */ diff --git a/spa/include/spa/param/audio/truehd.h b/spa/include/spa/param/audio/truehd.h new file mode 100644 index 000000000..fee222b87 --- /dev/null +++ b/spa/include/spa/param/audio/truehd.h @@ -0,0 +1,35 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Carlos Rafael Giani */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_AUDIO_TRUEHD_H +#define SPA_AUDIO_TRUEHD_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +/** Dolby TrueHD audio info. */ +struct spa_audio_info_truehd { + uint32_t rate; /*< sample rate */ + uint32_t channels; /*< number of channels */ +}; + +#define SPA_AUDIO_INFO_TRUEHD_INIT(...) ((struct spa_audio_info_truehd) { __VA_ARGS__ }) + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_AUDIO_TRUEHD_H */ diff --git a/spa/include/spa/param/audio/type-info.h b/spa/include/spa/param/audio/type-info.h index 8a3aa49aa..3c5d6f4c7 100644 --- a/spa/include/spa/param/audio/type-info.h +++ b/spa/include/spa/param/audio/type-info.h @@ -6,10 +6,12 @@ #define SPA_AUDIO_TYPES_H #include +#include #include #include #include #include #include +#include #endif /* SPA_AUDIO_TYPES_H */ diff --git a/spa/include/spa/param/audio/vorbis-utils.h b/spa/include/spa/param/audio/vorbis-utils.h index bc901e616..13114e89b 100644 --- a/spa/include/spa/param/audio/vorbis-utils.h +++ b/spa/include/spa/param/audio/vorbis-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_VORBIS_UTILS_H #define SPA_AUDIO_VORBIS_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_VORBIS_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_VORBIS_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/vorbis.h b/spa/include/spa/param/audio/vorbis.h index e3ab490ef..e73895ba7 100644 --- a/spa/include/spa/param/audio/vorbis.h +++ b/spa/include/spa/param/audio/vorbis.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_VORBIS_H #define SPA_AUDIO_VORBIS_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/wma-types.h b/spa/include/spa/param/audio/wma-types.h index 40f9d6682..663e54f24 100644 --- a/spa/include/spa/param/audio/wma-types.h +++ b/spa/include/spa/param/audio/wma-types.h @@ -5,13 +5,13 @@ #ifndef SPA_AUDIO_WMA_TYPES_H #define SPA_AUDIO_WMA_TYPES_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/audio/wma-utils.h b/spa/include/spa/param/audio/wma-utils.h index ca15f7d0c..8dadb3992 100644 --- a/spa/include/spa/param/audio/wma-utils.h +++ b/spa/include/spa/param/audio/wma-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_AUDIO_WMA_UTILS_H #define SPA_AUDIO_WMA_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #ifndef SPA_API_AUDIO_WMA_UTILS #ifdef SPA_API_IMPL #define SPA_API_AUDIO_WMA_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/audio/wma.h b/spa/include/spa/param/audio/wma.h index dd1e53a86..1cd3ff465 100644 --- a/spa/include/spa/param/audio/wma.h +++ b/spa/include/spa/param/audio/wma.h @@ -5,12 +5,12 @@ #ifndef SPA_AUDIO_WMA_H #define SPA_AUDIO_WMA_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_param * \{ diff --git a/spa/include/spa/param/bluetooth/audio.h b/spa/include/spa/param/bluetooth/audio.h index c95e22d6d..34b45e297 100644 --- a/spa/include/spa/param/bluetooth/audio.h +++ b/spa/include/spa/param/bluetooth/audio.h @@ -41,6 +41,7 @@ enum spa_bluetooth_audio_codec { SPA_BLUETOOTH_AUDIO_CODEC_CVSD = 0x100, SPA_BLUETOOTH_AUDIO_CODEC_MSBC, SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB, + SPA_BLUETOOTH_AUDIO_CODEC_LC3_A127, /* BAP */ SPA_BLUETOOTH_AUDIO_CODEC_LC3 = 0x200, diff --git a/spa/include/spa/param/bluetooth/type-info.h b/spa/include/spa/param/bluetooth/type-info.h index 7d9cd3653..7bf1da52c 100644 --- a/spa/include/spa/param/bluetooth/type-info.h +++ b/spa/include/spa/param/bluetooth/type-info.h @@ -5,6 +5,8 @@ #ifndef SPA_BLUETOOTH_TYPES_H #define SPA_BLUETOOTH_TYPES_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - #define SPA_TYPE_INFO_BluetoothAudioCodec SPA_TYPE_INFO_ENUM_BASE "BluetoothAudioCodec" #define SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE SPA_TYPE_INFO_BluetoothAudioCodec ":" @@ -44,6 +44,7 @@ static const struct spa_type_info spa_type_bluetooth_audio_codec[] = { { SPA_BLUETOOTH_AUDIO_CODEC_CVSD, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "cvsd", NULL }, { SPA_BLUETOOTH_AUDIO_CODEC_MSBC, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "msbc", NULL }, { SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3_swb", NULL }, + { SPA_BLUETOOTH_AUDIO_CODEC_LC3_A127, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3_a127", NULL }, { SPA_BLUETOOTH_AUDIO_CODEC_LC3, SPA_TYPE_Int, SPA_TYPE_INFO_BLUETOOTH_AUDIO_CODEC_BASE "lc3", NULL }, diff --git a/spa/include/spa/param/buffers-types.h b/spa/include/spa/param/buffers-types.h index 4ca45cb02..723aa1b3a 100644 --- a/spa/include/spa/param/buffers-types.h +++ b/spa/include/spa/param/buffers-types.h @@ -5,6 +5,11 @@ #ifndef SPA_PARAM_BUFFERS_TYPES_H #define SPA_PARAM_BUFFERS_TYPES_H +#include +#include + +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include - -#include - #define SPA_TYPE_INFO_PARAM_Meta SPA_TYPE_INFO_PARAM_BASE "Meta" #define SPA_TYPE_INFO_PARAM_META_BASE SPA_TYPE_INFO_PARAM_Meta ":" diff --git a/spa/include/spa/param/buffers.h b/spa/include/spa/param/buffers.h index 9c157ae2a..bb07a3415 100644 --- a/spa/include/spa/param/buffers.h +++ b/spa/include/spa/param/buffers.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_BUFFERS_H #define SPA_PARAM_BUFFERS_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /** properties for SPA_TYPE_OBJECT_ParamBuffers */ enum spa_param_buffers { SPA_PARAM_BUFFERS_START, @@ -33,6 +33,7 @@ enum spa_param_meta { SPA_PARAM_META_START, SPA_PARAM_META_type, /**< the metadata, one of enum spa_meta_type (Id enum spa_meta_type) */ SPA_PARAM_META_size, /**< the expected maximum size the meta (Int) */ + SPA_PARAM_META_features, /**< meta data features (Features Int) */ }; /** properties for SPA_TYPE_OBJECT_ParamIO */ diff --git a/spa/include/spa/param/format-types.h b/spa/include/spa/param/format-types.h index 8daaa103e..8b9de29c3 100644 --- a/spa/include/spa/param/format-types.h +++ b/spa/include/spa/param/format-types.h @@ -5,6 +5,13 @@ #ifndef SPA_PARAM_FORMAT_TYPES_H #define SPA_PARAM_FORMAT_TYPES_H +#include +#include + +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,13 +21,6 @@ extern "C" { * \{ */ -#include -#include - -#include -#include -#include - #define SPA_TYPE_INFO_Format SPA_TYPE_INFO_PARAM_BASE "Format" #define SPA_TYPE_INFO_FORMAT_BASE SPA_TYPE_INFO_Format ":" @@ -65,6 +65,11 @@ static const struct spa_type_info spa_type_media_subtype[] = { { SPA_MEDIA_SUBTYPE_flac, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "flac", NULL }, { SPA_MEDIA_SUBTYPE_ape, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "ape", NULL }, { SPA_MEDIA_SUBTYPE_opus, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "opus", NULL }, + { SPA_MEDIA_SUBTYPE_ac3, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "ac3", NULL }, + { SPA_MEDIA_SUBTYPE_eac3, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "eac3", NULL }, + { SPA_MEDIA_SUBTYPE_truehd, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "truehd", NULL }, + { SPA_MEDIA_SUBTYPE_dts, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dts", NULL }, + { SPA_MEDIA_SUBTYPE_mpegh, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpegh", NULL }, /* video subtypes */ { SPA_MEDIA_SUBTYPE_h264, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h264", NULL }, { SPA_MEDIA_SUBTYPE_mjpg, SPA_TYPE_Int, SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mjpg", NULL }, @@ -97,6 +102,10 @@ static const struct spa_type_info spa_type_media_subtype[] = { #define SPA_TYPE_INFO_FORMAT_AUDIO_WMA_BASE SPA_TYPE_INFO_FORMAT_AUDIO_WMA ":" #define SPA_TYPE_INFO_FORMAT_AUDIO_AMR SPA_TYPE_INFO_FORMAT_AUDIO_BASE "AMR" #define SPA_TYPE_INFO_FORMAT_AUDIO_AMR_BASE SPA_TYPE_INFO_FORMAT_AUDIO_AMR ":" +#define SPA_TYPE_INFO_FORMAT_AUDIO_MP3 SPA_TYPE_INFO_FORMAT_AUDIO_BASE "MP3" +#define SPA_TYPE_INFO_FORMAT_AUDIO_MP3_BASE SPA_TYPE_INFO_FORMAT_AUDIO_MP3 ":" +#define SPA_TYPE_INFO_FORMAT_AUDIO_DTS SPA_TYPE_INFO_FORMAT_AUDIO_BASE "DTS" +#define SPA_TYPE_INFO_FORMAT_AUDIO_DTS_BASE SPA_TYPE_INFO_FORMAT_AUDIO_DTS ":" #define SPA_TYPE_INFO_FormatVideo SPA_TYPE_INFO_FORMAT_BASE "Video" #define SPA_TYPE_INFO_FORMAT_VIDEO_BASE SPA_TYPE_INFO_FormatVideo ":" @@ -139,6 +148,10 @@ static const struct spa_type_info spa_type_format[] = { spa_type_audio_wma_profile }, { SPA_FORMAT_AUDIO_AMR_bandMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_AMR_BASE "bandMode", spa_type_audio_amr_band_mode }, + { SPA_FORMAT_AUDIO_MP3_channelMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_MP3_BASE "channelMode", + spa_type_audio_mp3_channel_mode }, + { SPA_FORMAT_AUDIO_DTS_extType, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_AUDIO_DTS_BASE "extType", + spa_type_audio_dts_ext_type }, { SPA_FORMAT_VIDEO_format, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "format", spa_type_video_format, }, @@ -153,10 +166,14 @@ static const struct spa_type_info spa_type_format[] = { { SPA_FORMAT_VIDEO_multiviewMode, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "multiviewMode", NULL }, { SPA_FORMAT_VIDEO_multiviewFlags, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "multiviewFlags", NULL }, { SPA_FORMAT_VIDEO_chromaSite, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "chromaSite", NULL }, - { SPA_FORMAT_VIDEO_colorRange, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorRange", NULL }, - { SPA_FORMAT_VIDEO_colorMatrix, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorMatrix", NULL }, - { SPA_FORMAT_VIDEO_transferFunction, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "transferFunction", NULL }, - { SPA_FORMAT_VIDEO_colorPrimaries, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorPrimaries", NULL }, + { SPA_FORMAT_VIDEO_colorRange, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorRange", + spa_type_video_color_range, }, + { SPA_FORMAT_VIDEO_colorMatrix, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorMatrix", + spa_type_video_color_matrix, }, + { SPA_FORMAT_VIDEO_transferFunction, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "transferFunction", + spa_type_video_transfer_function, }, + { SPA_FORMAT_VIDEO_colorPrimaries, SPA_TYPE_Id, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "colorPrimaries", + spa_type_video_color_primaries, }, { SPA_FORMAT_VIDEO_profile, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "profile", NULL }, { SPA_FORMAT_VIDEO_level, SPA_TYPE_Int, SPA_TYPE_INFO_FORMAT_VIDEO_BASE "level", NULL }, diff --git a/spa/include/spa/param/format-utils.h b/spa/include/spa/param/format-utils.h index 27fc5f58c..e4c96f391 100644 --- a/spa/include/spa/param/format-utils.h +++ b/spa/include/spa/param/format-utils.h @@ -5,6 +5,9 @@ #ifndef SPA_PARAM_FORMAT_UTILS_H #define SPA_PARAM_FORMAT_UTILS_H +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -15,9 +18,6 @@ extern "C" { * \{ */ -#include -#include - #ifndef SPA_API_FORMAT_UTILS #ifdef SPA_API_IMPL #define SPA_API_FORMAT_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/format.h b/spa/include/spa/param/format.h index eb3b851be..6da11819f 100644 --- a/spa/include/spa/param/format.h +++ b/spa/include/spa/param/format.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_FORMAT_H #define SPA_PARAM_FORMAT_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /** media type for SPA_TYPE_OBJECT_Format */ enum spa_media_type { SPA_MEDIA_TYPE_unknown, @@ -52,6 +52,11 @@ enum spa_media_subtype { SPA_MEDIA_SUBTYPE_flac, /** since 0.3.65 */ SPA_MEDIA_SUBTYPE_ape, /** since 0.3.65 */ SPA_MEDIA_SUBTYPE_opus, /** since 0.3.68 */ + SPA_MEDIA_SUBTYPE_ac3, /** since 1.5.1 */ + SPA_MEDIA_SUBTYPE_eac3, /** since 1.5.1 */ + SPA_MEDIA_SUBTYPE_truehd, /** since 1.5.1 */ + SPA_MEDIA_SUBTYPE_dts, /** since 1.5.1 */ + SPA_MEDIA_SUBTYPE_mpegh, /** since 1.5.1 */ SPA_MEDIA_SUBTYPE_START_Video = 0x20000, SPA_MEDIA_SUBTYPE_h264, @@ -67,6 +72,7 @@ enum spa_media_subtype { SPA_MEDIA_SUBTYPE_vp8, SPA_MEDIA_SUBTYPE_vp9, SPA_MEDIA_SUBTYPE_bayer, + SPA_MEDIA_SUBTYPE_h265, SPA_MEDIA_SUBTYPE_START_Image = 0x30000, SPA_MEDIA_SUBTYPE_jpeg, @@ -109,6 +115,10 @@ enum spa_format { SPA_FORMAT_AUDIO_AMR_bandMode, /**< AMR band mode (Id enum spa_audio_amr_band_mode) */ + SPA_FORMAT_AUDIO_MP3_channelMode, /**< MP3 channel mode, (Id enum spa_audio_mp3_channel_mode) */ + + SPA_FORMAT_AUDIO_DTS_extType, /**< DTS extension type (Id enum spa_audio_dts_ext_type) */ + /* Video Format keys */ SPA_FORMAT_START_Video = 0x20000, @@ -132,6 +142,8 @@ enum spa_format { SPA_FORMAT_VIDEO_level, /**< (Int) */ SPA_FORMAT_VIDEO_H264_streamFormat, /**< (Id enum spa_h264_stream_format) */ SPA_FORMAT_VIDEO_H264_alignment, /**< (Id enum spa_h264_alignment) */ + SPA_FORMAT_VIDEO_H265_streamFormat, /**< (Id enum spa_h265_stream_format) */ + SPA_FORMAT_VIDEO_H265_alignment, /**< (Id enum spa_h265_alignment) */ /* Image Format keys */ SPA_FORMAT_START_Image = 0x30000, diff --git a/spa/include/spa/param/latency-types.h b/spa/include/spa/param/latency-types.h index 883375f3f..4ab1f3625 100644 --- a/spa/include/spa/param/latency-types.h +++ b/spa/include/spa/param/latency-types.h @@ -5,6 +5,10 @@ #ifndef SPA_PARAM_LATENCY_TYPES_H #define SPA_PARAM_LATENCY_TYPES_H +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include -#include -#include - #define SPA_TYPE_INFO_PARAM_Latency SPA_TYPE_INFO_PARAM_BASE "Latency" #define SPA_TYPE_INFO_PARAM_LATENCY_BASE SPA_TYPE_INFO_PARAM_Latency ":" diff --git a/spa/include/spa/param/latency-utils.h b/spa/include/spa/param/latency-utils.h index 45f817ebb..bd3c274dd 100644 --- a/spa/include/spa/param/latency-utils.h +++ b/spa/include/spa/param/latency-utils.h @@ -5,6 +5,12 @@ #ifndef SPA_PARAM_LATENCY_UTILS_H #define SPA_PARAM_LATENCY_UTILS_H +#include + +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,12 +20,6 @@ extern "C" { * \{ */ -#include - -#include -#include -#include - #ifndef SPA_API_LATENCY_UTILS #ifdef SPA_API_IMPL #define SPA_API_LATENCY_UTILS SPA_API_IMPL @@ -44,28 +44,24 @@ spa_latency_info_compare(const struct spa_latency_info *a, const struct spa_late SPA_API_LATENCY_UTILS void spa_latency_info_combine_start(struct spa_latency_info *info, enum spa_direction direction) { - *info = SPA_LATENCY_INFO(direction, - .min_quantum = FLT_MAX, - .max_quantum = FLT_MIN, - .min_rate = INT32_MAX, - .max_rate = INT32_MIN, - .min_ns = INT64_MAX, - .max_ns = INT64_MIN); + *info = SPA_LATENCY_INFO_UNSET(direction); } + SPA_API_LATENCY_UTILS void spa_latency_info_combine_finish(struct spa_latency_info *info) { - if (info->min_quantum == FLT_MAX) + struct spa_latency_info unset = SPA_LATENCY_INFO_UNSET(info->direction); + if (info->min_quantum == unset.min_quantum) info->min_quantum = 0; - if (info->max_quantum == FLT_MIN) + if (info->max_quantum == unset.max_quantum) info->max_quantum = 0; - if (info->min_rate == INT32_MAX) + if (info->min_rate == unset.min_rate) info->min_rate = 0; - if (info->max_rate == INT32_MIN) + if (info->max_rate == unset.max_rate) info->max_rate = 0; - if (info->min_ns == INT64_MAX) + if (info->min_ns == unset.min_ns) info->min_ns = 0; - if (info->max_ns == INT64_MIN) + if (info->max_ns == unset.max_ns) info->max_ns = 0; } diff --git a/spa/include/spa/param/latency.h b/spa/include/spa/param/latency.h index 4087941ca..fdc479858 100644 --- a/spa/include/spa/param/latency.h +++ b/spa/include/spa/param/latency.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_LATENY_H #define SPA_PARAM_LATENY_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /** * Properties for SPA_TYPE_OBJECT_ParamLatency * @@ -57,6 +57,10 @@ struct spa_latency_info { }; #define SPA_LATENCY_INFO(dir,...) ((struct spa_latency_info) { .direction = (dir), ## __VA_ARGS__ }) +#define SPA_LATENCY_INFO_UNSET(dir) SPA_LATENCY_INFO(dir, \ + .min_quantum = FLT_MAX, .max_quantum = FLT_MIN, \ + .min_rate = INT32_MAX, .max_rate = INT32_MIN, \ + .min_ns = INT64_MAX, .max_ns = INT64_MIN) /** * Properties for SPA_TYPE_OBJECT_ParamProcessLatency diff --git a/spa/include/spa/param/param-types.h b/spa/include/spa/param/param-types.h index ebb8d988b..cb4bcd666 100644 --- a/spa/include/spa/param/param-types.h +++ b/spa/include/spa/param/param-types.h @@ -5,6 +5,10 @@ #ifndef SPA_PARAM_TYPES_H #define SPA_PARAM_TYPES_H +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include -#include -#include - /* base for parameter object enumerations */ #define SPA_TYPE_INFO_ParamId SPA_TYPE_INFO_ENUM_BASE "ParamId" #define SPA_TYPE_INFO_PARAM_ID_BASE SPA_TYPE_INFO_ParamId ":" @@ -41,6 +41,7 @@ static const struct spa_type_info spa_type_param[] = { { SPA_PARAM_Latency, SPA_TYPE_OBJECT_ParamLatency, SPA_TYPE_INFO_PARAM_ID_BASE "Latency", NULL }, { SPA_PARAM_ProcessLatency, SPA_TYPE_OBJECT_ParamProcessLatency, SPA_TYPE_INFO_PARAM_ID_BASE "ProcessLatency", NULL }, { SPA_PARAM_Tag, SPA_TYPE_OBJECT_ParamTag, SPA_TYPE_INFO_PARAM_ID_BASE "Tag", NULL }, + { SPA_PARAM_PeerFormats, SPA_TYPE_Struct, SPA_TYPE_INFO_PARAM_ID_BASE "PeerFormats", NULL }, { 0, 0, NULL, NULL }, }; diff --git a/spa/include/spa/param/param.h b/spa/include/spa/param/param.h index 51c442c35..cf72ac43e 100644 --- a/spa/include/spa/param/param.h +++ b/spa/include/spa/param/param.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_H #define SPA_PARAM_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -18,8 +20,6 @@ extern "C" { * \{ */ -#include - /** different parameter types that can be queried */ enum spa_param_type { SPA_PARAM_Invalid, /**< invalid */ @@ -40,6 +40,7 @@ enum spa_param_type { SPA_PARAM_Latency, /**< latency reporting, a SPA_TYPE_OBJECT_ParamLatency */ SPA_PARAM_ProcessLatency, /**< processing latency, a SPA_TYPE_OBJECT_ParamProcessLatency */ SPA_PARAM_Tag, /**< tag reporting, a SPA_TYPE_OBJECT_ParamTag. Since 0.3.79 */ + SPA_PARAM_PeerFormats, /**< peer formats, a SPA_TYPE_Struct of SPA_TYPE_OBJECT_Format. Since 1.5.0 */ }; /** information about a parameter */ diff --git a/spa/include/spa/param/port-config-types.h b/spa/include/spa/param/port-config-types.h index 2eabb785c..9801b1117 100644 --- a/spa/include/spa/param/port-config-types.h +++ b/spa/include/spa/param/port-config-types.h @@ -5,6 +5,10 @@ #ifndef SPA_PARAM_PORT_CONFIG_TYPES_H #define SPA_PARAM_PORT_CONFIG_TYPES_H +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include -#include -#include - #define SPA_TYPE_INFO_ParamPortConfigMode SPA_TYPE_INFO_ENUM_BASE "ParamPortConfigMode" #define SPA_TYPE_INFO_PARAM_PORT_CONFIG_MODE_BASE SPA_TYPE_INFO_ParamPortConfigMode ":" diff --git a/spa/include/spa/param/port-config.h b/spa/include/spa/param/port-config.h index 7e05eda27..fcb39c162 100644 --- a/spa/include/spa/param/port-config.h +++ b/spa/include/spa/param/port-config.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_PORT_CONFIG_H #define SPA_PARAM_PORT_CONFIG_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - enum spa_param_port_config_mode { SPA_PARAM_PORT_CONFIG_MODE_none, /**< no configuration */ SPA_PARAM_PORT_CONFIG_MODE_passthrough, /**< passthrough configuration */ diff --git a/spa/include/spa/param/profile-types.h b/spa/include/spa/param/profile-types.h index 3373d64c5..9edc9fc30 100644 --- a/spa/include/spa/param/profile-types.h +++ b/spa/include/spa/param/profile-types.h @@ -5,6 +5,10 @@ #ifndef SPA_PARAM_PROFILE_TYPES_H #define SPA_PARAM_PROFILE_TYPES_H +#include + +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include - -#include - #define SPA_TYPE_INFO_PARAM_Profile SPA_TYPE_INFO_PARAM_BASE "Profile" #define SPA_TYPE_INFO_PARAM_PROFILE_BASE SPA_TYPE_INFO_PARAM_Profile ":" diff --git a/spa/include/spa/param/profile.h b/spa/include/spa/param/profile.h index 00468f36d..11c195195 100644 --- a/spa/include/spa/param/profile.h +++ b/spa/include/spa/param/profile.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_PROFILE_H #define SPA_PARAM_PROFILE_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /** properties for SPA_TYPE_OBJECT_ParamProfile */ enum spa_param_profile { SPA_PARAM_PROFILE_START, diff --git a/spa/include/spa/param/profiler-types.h b/spa/include/spa/param/profiler-types.h index 57f3f3692..d378bae94 100644 --- a/spa/include/spa/param/profiler-types.h +++ b/spa/include/spa/param/profiler-types.h @@ -5,6 +5,9 @@ #ifndef SPA_PARAM_PROFILER_TYPES_H #define SPA_PARAM_PROFILER_TYPES_H +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,9 +17,6 @@ extern "C" { * \{ */ -#include -#include - #define SPA_TYPE_INFO_Profiler SPA_TYPE_INFO_OBJECT_BASE "Profiler" #define SPA_TYPE_INFO_PROFILER_BASE SPA_TYPE_INFO_Profiler ":" diff --git a/spa/include/spa/param/profiler.h b/spa/include/spa/param/profiler.h index 36af0fc24..8c87662d7 100644 --- a/spa/include/spa/param/profiler.h +++ b/spa/include/spa/param/profiler.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_PROFILER_H #define SPA_PARAM_PROFILER_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /** properties for SPA_TYPE_OBJECT_Profiler */ enum spa_profiler { SPA_PROFILER_START, @@ -66,7 +66,8 @@ enum spa_profiler { * Long : finish, * Int : status, * Fraction : latency, - * Int : xrun_count)) */ + * Int : xrun_count)) + * Bool : async)) */ SPA_PROFILER_followerClock, /**< follower clock information * (Struct( * Int : clock id, diff --git a/spa/include/spa/param/props-types.h b/spa/include/spa/param/props-types.h index e66125992..0a7c916b8 100644 --- a/spa/include/spa/param/props-types.h +++ b/spa/include/spa/param/props-types.h @@ -5,6 +5,10 @@ #ifndef SPA_PARAM_PROPS_TYPES_H #define SPA_PARAM_PROPS_TYPES_H +#include + +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include - -#include - /** Props Param */ #define SPA_TYPE_INFO_Props SPA_TYPE_INFO_PARAM_BASE "Props" #define SPA_TYPE_INFO_PROPS_BASE SPA_TYPE_INFO_Props ":" @@ -40,13 +40,16 @@ static const struct spa_type_info spa_type_props[] = { { SPA_PROP_quality, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "quality", NULL }, { SPA_PROP_bluetoothAudioCodec, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "bluetoothAudioCodec", spa_type_bluetooth_audio_codec }, { SPA_PROP_bluetoothOffloadActive, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "bluetoothOffloadActive", NULL }, + { SPA_PROP_clockId, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "clockId", NULL }, + { SPA_PROP_clockDevice, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "clockDevice", NULL }, + { SPA_PROP_clockInterface, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "clockInterface", NULL }, - { SPA_PROP_waveType, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "waveType", NULL }, + { SPA_PROP_waveType, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "waveType", NULL }, { SPA_PROP_frequency, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "frequency", NULL }, { SPA_PROP_volume, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "volume", NULL }, { SPA_PROP_mute, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "mute", NULL }, - { SPA_PROP_patternType, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "patternType", NULL }, - { SPA_PROP_ditherType, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "ditherType", NULL }, + { SPA_PROP_patternType, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "patternType", NULL }, + { SPA_PROP_ditherType, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "ditherType", NULL }, { SPA_PROP_truncate, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "truncate", NULL }, { SPA_PROP_channelVolumes, SPA_TYPE_Array, SPA_TYPE_INFO_PROPS_BASE "channelVolumes", spa_type_prop_float_array }, { SPA_PROP_volumeBase, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "volumeBase", NULL }, @@ -62,7 +65,7 @@ static const struct spa_type_info spa_type_props[] = { { SPA_PROP_volumeRampStepSamples, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampStepSamples", NULL }, { SPA_PROP_volumeRampTime, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampTime", NULL }, { SPA_PROP_volumeRampStepTime, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampStepTime", NULL }, - { SPA_PROP_volumeRampScale, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "volumeRampScale", NULL }, + { SPA_PROP_volumeRampScale, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "volumeRampScale", spa_type_audio_volume_ramp_scale }, { SPA_PROP_brightness, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "brightness", NULL }, { SPA_PROP_contrast, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "contrast", NULL }, diff --git a/spa/include/spa/param/props.h b/spa/include/spa/param/props.h index a7a2e4c25..48338b870 100644 --- a/spa/include/spa/param/props.h +++ b/spa/include/spa/param/props.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_PROPS_H #define SPA_PARAM_PROPS_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /** properties of SPA_TYPE_OBJECT_PropInfo */ enum spa_prop_info { SPA_PROP_INFO_START, @@ -55,6 +55,9 @@ enum spa_prop { SPA_PROP_quality, SPA_PROP_bluetoothAudioCodec, SPA_PROP_bluetoothOffloadActive, + SPA_PROP_clockId, + SPA_PROP_clockDevice, + SPA_PROP_clockInterface, SPA_PROP_START_Audio = 0x10000, /**< audio related properties */ SPA_PROP_waveType, diff --git a/spa/include/spa/param/route-types.h b/spa/include/spa/param/route-types.h index 78ced495e..311491aaf 100644 --- a/spa/include/spa/param/route-types.h +++ b/spa/include/spa/param/route-types.h @@ -5,6 +5,11 @@ #ifndef SPA_PARAM_ROUTE_TYPES_H #define SPA_PARAM_ROUTE_TYPES_H +#include +#include + +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include - -#include - #define SPA_TYPE_INFO_PARAM_Route SPA_TYPE_INFO_PARAM_BASE "Route" #define SPA_TYPE_INFO_PARAM_ROUTE_BASE SPA_TYPE_INFO_PARAM_Route ":" diff --git a/spa/include/spa/param/route.h b/spa/include/spa/param/route.h index d73880c5a..b74267029 100644 --- a/spa/include/spa/param/route.h +++ b/spa/include/spa/param/route.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_ROUTE_H #define SPA_PARAM_ROUTE_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /** properties for SPA_TYPE_OBJECT_ParamRoute */ enum spa_param_route { SPA_PARAM_ROUTE_START, diff --git a/spa/include/spa/param/tag-types.h b/spa/include/spa/param/tag-types.h index 573fb4aff..0284684f2 100644 --- a/spa/include/spa/param/tag-types.h +++ b/spa/include/spa/param/tag-types.h @@ -5,6 +5,10 @@ #ifndef SPA_PARAM_TAG_TYPES_H #define SPA_PARAM_TAG_TYPES_H +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include -#include -#include - #define SPA_TYPE_INFO_PARAM_Tag SPA_TYPE_INFO_PARAM_BASE "Tag" #define SPA_TYPE_INFO_PARAM_TAG_BASE SPA_TYPE_INFO_PARAM_Tag ":" diff --git a/spa/include/spa/param/tag-utils.h b/spa/include/spa/param/tag-utils.h index ba8a952c1..7d7fbb092 100644 --- a/spa/include/spa/param/tag-utils.h +++ b/spa/include/spa/param/tag-utils.h @@ -5,6 +5,14 @@ #ifndef SPA_PARAM_TAG_UTILS_H #define SPA_PARAM_TAG_UTILS_H +#include + +#include +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,13 +22,6 @@ extern "C" { * \{ */ -#include - -#include -#include -#include -#include - #ifndef SPA_API_TAG_UTILS #ifdef SPA_API_IMPL #define SPA_API_TAG_UTILS SPA_API_IMPL @@ -90,6 +91,8 @@ spa_tag_info_parse(const struct spa_tag_info *info, struct spa_dict *dict, struc SPA_POD_String(&value), NULL) < 0) break; + if (key == NULL || value == NULL) + return -EINVAL; items[n].key = key; items[n].value = value; } diff --git a/spa/include/spa/param/tag.h b/spa/include/spa/param/tag.h index 8e36ce5c3..5bfff552f 100644 --- a/spa/include/spa/param/tag.h +++ b/spa/include/spa/param/tag.h @@ -5,6 +5,8 @@ #ifndef SPA_PARAM_TAG_H #define SPA_PARAM_TAG_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - /** properties for SPA_TYPE_OBJECT_ParamTag */ enum spa_param_tag { SPA_PARAM_TAG_START, diff --git a/spa/include/spa/param/video/color-types.h b/spa/include/spa/param/video/color-types.h new file mode 100644 index 000000000..cc8cc25be --- /dev/null +++ b/spa/include/spa/param/video/color-types.h @@ -0,0 +1,79 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 PipeWire authors */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_COLOR_TYPES_H +#define SPA_VIDEO_COLOR_TYPES_H + +#include +#include + +#define SPA_TYPE_INFO_VideColorRange SPA_TYPE_INFO_ENUM_BASE "VideoColorRange" +#define SPA_TYPE_INFO_VIDEO_COLOR_RANGE_BASE SPA_TYPE_INFO_VideColorRange ":" + +static const struct spa_type_info spa_type_video_color_range[] = { + { SPA_VIDEO_COLOR_RANGE_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_RANGE_BASE "unknown", NULL }, + { SPA_VIDEO_COLOR_RANGE_0_255, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_RANGE_BASE "0-255", NULL }, + { SPA_VIDEO_COLOR_RANGE_16_235, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_RANGE_BASE "16-235", NULL }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_VideoColorMatrix SPA_TYPE_INFO_ENUM_BASE "VideoColorMatrix" +#define SPA_TYPE_INFO_VIDEO_COLOR_MATRIX_BASE SPA_TYPE_INFO_VideoColorMatrix ":" + +static const struct spa_type_info spa_type_video_color_matrix[] = { + { SPA_VIDEO_COLOR_MATRIX_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_MATRIX_BASE "unknown", NULL }, + { SPA_VIDEO_COLOR_MATRIX_RGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_MATRIX_BASE "rgb", NULL }, + { SPA_VIDEO_COLOR_MATRIX_FCC, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_MATRIX_BASE "fcc", NULL }, + { SPA_VIDEO_COLOR_MATRIX_BT709, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_MATRIX_BASE "bt709", NULL }, + { SPA_VIDEO_COLOR_MATRIX_BT601, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_MATRIX_BASE "bt601", NULL }, + { SPA_VIDEO_COLOR_MATRIX_SMPTE240M, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_MATRIX_BASE "smpte240m", NULL }, + { SPA_VIDEO_COLOR_MATRIX_BT2020, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_MATRIX_BASE "bt2020", NULL }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_VideoTransferFunction SPA_TYPE_INFO_ENUM_BASE "VideoTransferFunction" +#define SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE SPA_TYPE_INFO_VideoTransferFunction ":" + +static const struct spa_type_info spa_type_video_transfer_function[] = { + { SPA_VIDEO_TRANSFER_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "unknown", NULL }, + { SPA_VIDEO_TRANSFER_GAMMA10, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "gamma10", NULL }, + { SPA_VIDEO_TRANSFER_GAMMA18, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "gamma18", NULL }, + { SPA_VIDEO_TRANSFER_GAMMA20, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "gamma20", NULL }, + { SPA_VIDEO_TRANSFER_GAMMA22, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "gamma22", NULL }, + { SPA_VIDEO_TRANSFER_BT709, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "bt709", NULL }, + { SPA_VIDEO_TRANSFER_SMPTE240M, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "smpte240m", NULL }, + { SPA_VIDEO_TRANSFER_SRGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "srgb", NULL }, + { SPA_VIDEO_TRANSFER_GAMMA28, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "gamma28", NULL }, + { SPA_VIDEO_TRANSFER_LOG100, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "log100", NULL }, + { SPA_VIDEO_TRANSFER_LOG316, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "log316", NULL }, + { SPA_VIDEO_TRANSFER_BT2020_12, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "bt2020-12", NULL }, + { SPA_VIDEO_TRANSFER_ADOBERGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "adobergb", NULL }, + { SPA_VIDEO_TRANSFER_BT2020_10, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "bt2020-10", NULL }, + { SPA_VIDEO_TRANSFER_SMPTE2084, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "smpte2084", NULL }, + { SPA_VIDEO_TRANSFER_ARIB_STD_B67, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "arib-std-b67", NULL }, + { SPA_VIDEO_TRANSFER_BT601, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_TRANSFER_FUNCTION_BASE "bt601", NULL }, + { 0, 0, NULL, NULL }, +}; + +#define SPA_TYPE_INFO_VideoColorPrimaries SPA_TYPE_INFO_ENUM_BASE "VideoColorPrimaries" +#define SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE SPA_TYPE_INFO_VideoColorPrimaries ":" + +static const struct spa_type_info spa_type_video_color_primaries[] = { + { SPA_VIDEO_COLOR_PRIMARIES_UNKNOWN, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "unknown", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_BT709, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "bt709", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_BT470M, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "bt470m", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_BT470BG, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "bt470bg", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_SMPTE170M, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "smpte170m", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_SMPTE240M, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "smpte240m", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_FILM, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "film", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_BT2020, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "bt2020", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_ADOBERGB, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "adobergb", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_SMPTEST428, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "smptest428", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_SMPTERP431, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "smpterp431", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_SMPTEEG432, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "smpteeg432", NULL }, + { SPA_VIDEO_COLOR_PRIMARIES_EBU3213, SPA_TYPE_Int, SPA_TYPE_INFO_VIDEO_COLOR_PRIMARIES_BASE "ebu3213", NULL }, + { 0, 0, NULL, NULL }, +}; + +#endif /* SPA_VIDEO_COLOR_TYPES_H */ diff --git a/spa/include/spa/param/video/color.h b/spa/include/spa/param/video/color.h index 9a65bf04a..68b69a5b3 100644 --- a/spa/include/spa/param/video/color.h +++ b/spa/include/spa/param/video/color.h @@ -36,7 +36,7 @@ enum spa_video_color_matrix { SPA_VIDEO_COLOR_MATRIX_BT709, /**< ITU BT.709 color matrix */ SPA_VIDEO_COLOR_MATRIX_BT601, /**< ITU BT.601 color matrix */ SPA_VIDEO_COLOR_MATRIX_SMPTE240M, /**< SMTPE 240M color matrix */ - SPA_VIDEO_COLOR_MATRIX_BT2020, /**< ITU-R BT.2020 color matrix. since 1.6. */ + SPA_VIDEO_COLOR_MATRIX_BT2020, /**< ITU-R BT.2020 color matrix */ }; /** @@ -57,8 +57,18 @@ enum spa_video_transfer_function { SPA_VIDEO_TRANSFER_LOG316, /**< Logarithmic transfer characteristic 316.22777:1 range */ SPA_VIDEO_TRANSFER_BT2020_12, /**< Gamma 2.2 curve with a linear segment in the lower * range. Used for BT.2020 with 12 bits per - * component. \since 1.6. */ - SPA_VIDEO_TRANSFER_ADOBERGB, /**< Gamma 2.19921875. \since 1.8 */ + * component */ + SPA_VIDEO_TRANSFER_ADOBERGB, /**< Gamma 2.19921875 */ + SPA_VIDEO_TRANSFER_BT2020_10, /**< Rec. ITU-R BT.2020-2 with 10 bits per component. + * (functionally the same as the values + * SPA_VIDEO_TRANSFER_BT709 and SPA_VIDEO_TRANSFER_BT601) */ + SPA_VIDEO_TRANSFER_SMPTE2084, /**< SMPTE ST 2084 for 10, 12, 14, and 16-bit systems. + * Known as perceptual quantization (PQ) */ + SPA_VIDEO_TRANSFER_ARIB_STD_B67,/**< Association of Radio Industries and Businesses (ARIB) + * STD-B67 and Rec. ITU-R BT.2100-1 hybrid loggamma (HLG) system */ + SPA_VIDEO_TRANSFER_BT601, /**< also known as SMPTE170M / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC + * Functionally the same as the values + * SPA_VIDEO_TRANSFER_BT709, and SPA_VIDEO_TRANSFER_BT2020_10 */ }; /** @@ -73,8 +83,12 @@ enum spa_video_color_primaries { SPA_VIDEO_COLOR_PRIMARIES_SMPTE170M, /**< SMPTE170M primaries */ SPA_VIDEO_COLOR_PRIMARIES_SMPTE240M, /**< SMPTE240M primaries */ SPA_VIDEO_COLOR_PRIMARIES_FILM, /**< Generic film */ - SPA_VIDEO_COLOR_PRIMARIES_BT2020, /**< BT2020 primaries. \since 1.6. */ - SPA_VIDEO_COLOR_PRIMARIES_ADOBERGB, /**< Adobe RGB primaries. \since 1.8 */ + SPA_VIDEO_COLOR_PRIMARIES_BT2020, /**< BT2020 primaries */ + SPA_VIDEO_COLOR_PRIMARIES_ADOBERGB, /**< Adobe RGB primaries */ + SPA_VIDEO_COLOR_PRIMARIES_SMPTEST428, /**< SMPTE ST 428 primaries (CIE 1931 XYZ) */ + SPA_VIDEO_COLOR_PRIMARIES_SMPTERP431, /**< SMPTE RP 431 primaries (ST 431-2 (2011) / DCI P3) */ + SPA_VIDEO_COLOR_PRIMARIES_SMPTEEG432, /**< SMPTE EG 432 primaries (ST 432-1 (2010) / P3 D65) */ + SPA_VIDEO_COLOR_PRIMARIES_EBU3213, /**< EBU 3213 primaries (JEDEC P22 phosphors) */ }; /** diff --git a/spa/include/spa/param/video/dsp-utils.h b/spa/include/spa/param/video/dsp-utils.h index 6e76309b3..26018cab9 100644 --- a/spa/include/spa/param/video/dsp-utils.h +++ b/spa/include/spa/param/video/dsp-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_VIDEO_DSP_UTILS_H #define SPA_VIDEO_DSP_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include - #ifndef SPA_API_VIDEO_DSP_UTILS #ifdef SPA_API_IMPL #define SPA_API_VIDEO_DSP_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/video/format-utils.h b/spa/include/spa/param/video/format-utils.h index 9efbef612..f7d3864e5 100644 --- a/spa/include/spa/param/video/format-utils.h +++ b/spa/include/spa/param/video/format-utils.h @@ -5,17 +5,18 @@ #ifndef SPA_PARAM_VIDEO_FORMAT_UTILS_H #define SPA_PARAM_VIDEO_FORMAT_UTILS_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include #include +#include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_VIDEO_FORMAT_UTILS #ifdef SPA_API_IMPL #define SPA_API_VIDEO_FORMAT_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/video/format.h b/spa/include/spa/param/video/format.h index 7fa992c96..d49196b31 100644 --- a/spa/include/spa/param/video/format.h +++ b/spa/include/spa/param/video/format.h @@ -5,6 +5,11 @@ #ifndef SPA_PARAM_VIDEO_FORMAT_H #define SPA_PARAM_VIDEO_FORMAT_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - struct spa_video_info { uint32_t media_type; uint32_t media_subtype; diff --git a/spa/include/spa/param/video/h264-utils.h b/spa/include/spa/param/video/h264-utils.h index fa6933291..966315d1a 100644 --- a/spa/include/spa/param/video/h264-utils.h +++ b/spa/include/spa/param/video/h264-utils.h @@ -5,6 +5,10 @@ #ifndef SPA_VIDEO_H264_UTILS_H #define SPA_VIDEO_H264_UTILS_H +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include -#include -#include - #ifndef SPA_API_VIDEO_H264_UTILS #ifdef SPA_API_IMPL #define SPA_API_VIDEO_H264_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/video/h264.h b/spa/include/spa/param/video/h264.h index 33ddffc4e..3a1d62306 100644 --- a/spa/include/spa/param/video/h264.h +++ b/spa/include/spa/param/video/h264.h @@ -5,6 +5,8 @@ #ifndef SPA_VIDEO_H264_H #define SPA_VIDEO_H264_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - enum spa_h264_stream_format { SPA_H264_STREAM_FORMAT_UNKNOWN = 0, SPA_H264_STREAM_FORMAT_AVC, diff --git a/spa/include/spa/param/video/h265-utils.h b/spa/include/spa/param/video/h265-utils.h new file mode 100644 index 000000000..62a797811 --- /dev/null +++ b/spa/include/spa/param/video/h265-utils.h @@ -0,0 +1,79 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2025 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_H265_UTILS_H +#define SPA_VIDEO_H265_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include +#include +#include + +#ifndef SPA_API_VIDEO_H265_UTILS + #ifdef SPA_API_IMPL + #define SPA_API_VIDEO_H265_UTILS SPA_API_IMPL + #else + #define SPA_API_VIDEO_H265_UTILS static inline + #endif +#endif + +SPA_API_VIDEO_H265_UTILS int +spa_format_video_h265_parse(const struct spa_pod *format, + struct spa_video_info_h265 *info) +{ + return spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_VIDEO_size, SPA_POD_OPT_Rectangle(&info->size), + SPA_FORMAT_VIDEO_framerate, SPA_POD_OPT_Fraction(&info->framerate), + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_OPT_Fraction(&info->max_framerate), + SPA_FORMAT_VIDEO_H265_streamFormat, SPA_POD_OPT_Id(&info->stream_format), + SPA_FORMAT_VIDEO_H265_alignment, SPA_POD_OPT_Id(&info->alignment)); +} + +SPA_API_VIDEO_H265_UTILS struct spa_pod * +spa_format_video_h265_build(struct spa_pod_builder *builder, uint32_t id, + const struct spa_video_info_h265 *info) +{ + struct spa_pod_frame f; + spa_pod_builder_push_object(builder, &f, SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(builder, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_h265), + 0); + if (info->size.width != 0 && info->size.height != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&info->size), 0); + if (info->framerate.denom != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&info->framerate), 0); + if (info->max_framerate.denom != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_Fraction(&info->max_framerate), 0); + if (info->stream_format != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_H265_streamFormat, SPA_POD_Id(info->stream_format), 0); + if (info->alignment != 0) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_H265_alignment, SPA_POD_Id(info->alignment), 0); + return (struct spa_pod*)spa_pod_builder_pop(builder, &f); +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_H265_UTILS_H */ diff --git a/spa/include/spa/param/video/h265.h b/spa/include/spa/param/video/h265.h new file mode 100644 index 000000000..41b111147 --- /dev/null +++ b/spa/include/spa/param/video/h265.h @@ -0,0 +1,49 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-FileCopyrightText: Copyright © 2025 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_VIDEO_H265_H +#define SPA_VIDEO_H265_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \addtogroup spa_param + * \{ + */ + +#include + +enum spa_h265_stream_format { + SPA_H265_STREAM_FORMAT_UNKNOWN = 0, + SPA_H265_STREAM_FORMAT_HVC1, + SPA_H265_STREAM_FORMAT_HEV1, + SPA_H265_STREAM_FORMAT_BYTESTREAM +}; + +enum spa_h265_alignment { + SPA_H265_ALIGNMENT_UNKNOWN = 0, + SPA_H265_ALIGNMENT_AU, + SPA_H265_ALIGNMENT_NAL +}; + +struct spa_video_info_h265 { + struct spa_rectangle size; + struct spa_fraction framerate; + struct spa_fraction max_framerate; + enum spa_h265_stream_format stream_format; + enum spa_h265_alignment alignment; +}; + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_VIDEO_H265_H */ diff --git a/spa/include/spa/param/video/mjpg-utils.h b/spa/include/spa/param/video/mjpg-utils.h index f1aa27af5..3b15d3d68 100644 --- a/spa/include/spa/param/video/mjpg-utils.h +++ b/spa/include/spa/param/video/mjpg-utils.h @@ -5,6 +5,10 @@ #ifndef SPA_VIDEO_MJPG_UTILS_H #define SPA_VIDEO_MJPG_UTILS_H +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +18,6 @@ extern "C" { * \{ */ -#include -#include -#include - #ifndef SPA_API_VIDEO_MJPG_UTILS #ifdef SPA_API_IMPL #define SPA_API_VIDEO_MJPG_UTILS SPA_API_IMPL diff --git a/spa/include/spa/param/video/mjpg.h b/spa/include/spa/param/video/mjpg.h index fb85daade..eedd93f5e 100644 --- a/spa/include/spa/param/video/mjpg.h +++ b/spa/include/spa/param/video/mjpg.h @@ -5,6 +5,8 @@ #ifndef SPA_VIDEO_MJPG_H #define SPA_VIDEO_MJPG_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,8 +16,6 @@ extern "C" { * \{ */ -#include - struct spa_video_info_mjpg { struct spa_rectangle size; struct spa_fraction framerate; diff --git a/spa/include/spa/param/video/raw-types.h b/spa/include/spa/param/video/raw-types.h index bca0c8d4e..2b19d73a8 100644 --- a/spa/include/spa/param/video/raw-types.h +++ b/spa/include/spa/param/video/raw-types.h @@ -5,6 +5,9 @@ #ifndef SPA_VIDEO_RAW_TYPES_H #define SPA_VIDEO_RAW_TYPES_H +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -13,8 +16,6 @@ extern "C" { * \addtogroup spa_param * \{ */ -#include -#include #ifndef SPA_API_VIDEO_RAW_TYPES #ifdef SPA_API_IMPL diff --git a/spa/include/spa/param/video/raw-utils.h b/spa/include/spa/param/video/raw-utils.h index 8a5a27784..738f64cf4 100644 --- a/spa/include/spa/param/video/raw-utils.h +++ b/spa/include/spa/param/video/raw-utils.h @@ -5,6 +5,11 @@ #ifndef SPA_VIDEO_RAW_UTILS_H #define SPA_VIDEO_RAW_UTILS_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,10 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include - #ifndef SPA_API_VIDEO_RAW_UTILS #ifdef SPA_API_IMPL #define SPA_API_VIDEO_RAW_UTILS SPA_API_IMPL @@ -117,6 +118,56 @@ spa_format_video_raw_build(struct spa_pod_builder *builder, uint32_t id, return (struct spa_pod*)spa_pod_builder_pop(builder, &f); } +static inline bool +spa_format_video_is_rgb(enum spa_video_format format) +{ + switch (format) { + case SPA_VIDEO_FORMAT_RGBx: + case SPA_VIDEO_FORMAT_BGRx: + case SPA_VIDEO_FORMAT_xRGB: + case SPA_VIDEO_FORMAT_xBGR: + case SPA_VIDEO_FORMAT_RGBA: + case SPA_VIDEO_FORMAT_BGRA: + case SPA_VIDEO_FORMAT_ARGB: + case SPA_VIDEO_FORMAT_ABGR: + case SPA_VIDEO_FORMAT_RGB: + case SPA_VIDEO_FORMAT_BGR: + case SPA_VIDEO_FORMAT_GRAY8: + case SPA_VIDEO_FORMAT_GRAY16_BE: + case SPA_VIDEO_FORMAT_GRAY16_LE: + case SPA_VIDEO_FORMAT_RGB16: + case SPA_VIDEO_FORMAT_BGR16: + case SPA_VIDEO_FORMAT_RGB15: + case SPA_VIDEO_FORMAT_BGR15: + case SPA_VIDEO_FORMAT_RGB8P: + case SPA_VIDEO_FORMAT_ARGB64: + case SPA_VIDEO_FORMAT_r210: + case SPA_VIDEO_FORMAT_GBR: + case SPA_VIDEO_FORMAT_GBR_10BE: + case SPA_VIDEO_FORMAT_GBR_10LE: + case SPA_VIDEO_FORMAT_GBRA: + case SPA_VIDEO_FORMAT_GBRA_10BE: + case SPA_VIDEO_FORMAT_GBRA_10LE: + case SPA_VIDEO_FORMAT_GBR_12BE: + case SPA_VIDEO_FORMAT_GBR_12LE: + case SPA_VIDEO_FORMAT_GBRA_12BE: + case SPA_VIDEO_FORMAT_GBRA_12LE: + case SPA_VIDEO_FORMAT_RGBA_F16: + case SPA_VIDEO_FORMAT_RGBA_F32: + case SPA_VIDEO_FORMAT_xRGB_210LE: + case SPA_VIDEO_FORMAT_xBGR_210LE: + case SPA_VIDEO_FORMAT_RGBx_102LE: + case SPA_VIDEO_FORMAT_BGRx_102LE: + case SPA_VIDEO_FORMAT_ARGB_210LE: + case SPA_VIDEO_FORMAT_ABGR_210LE: + case SPA_VIDEO_FORMAT_RGBA_102LE: + case SPA_VIDEO_FORMAT_BGRA_102LE: + return true; + default: + return false; + } +} + /** * \} */ diff --git a/spa/include/spa/param/video/raw.h b/spa/include/spa/param/video/raw.h index 84f78ad6a..971aac5ba 100644 --- a/spa/include/spa/param/video/raw.h +++ b/spa/include/spa/param/video/raw.h @@ -5,6 +5,11 @@ #ifndef SPA_VIDEO_RAW_H #define SPA_VIDEO_RAW_H +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -14,11 +19,6 @@ extern "C" { * \{ */ -#include -#include -#include -#include - #define SPA_VIDEO_MAX_PLANES 4 #define SPA_VIDEO_MAX_COMPONENTS 4 diff --git a/spa/include/spa/param/video/type-info.h b/spa/include/spa/param/video/type-info.h index 04e00c9c1..cc8d296e0 100644 --- a/spa/include/spa/param/video/type-info.h +++ b/spa/include/spa/param/video/type-info.h @@ -5,6 +5,7 @@ #ifndef SPA_VIDEO_TYPES_H #define SPA_VIDEO_TYPES_H +#include #include #endif /* SPA_VIDEO_TYPES_H */ diff --git a/spa/include/spa/pod/body.h b/spa/include/spa/pod/body.h new file mode 100644 index 000000000..51f8e8f74 --- /dev/null +++ b/spa/include/spa/pod/body.h @@ -0,0 +1,448 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_POD_BODY_H +#define SPA_POD_BODY_H + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef SPA_API_POD_BODY + #ifdef SPA_API_IMPL + #define SPA_API_POD_BODY SPA_API_IMPL + #else + #define SPA_API_POD_BODY static inline + #endif +#endif + +/** + * \addtogroup spa_pod + * \{ + */ + +struct spa_pod_frame { + struct spa_pod pod; + struct spa_pod_frame *parent; + uint32_t offset; + uint32_t flags; +}; + +SPA_API_POD_BODY uint32_t spa_pod_type_size(uint32_t type) +{ + switch (type) { + case SPA_TYPE_None: + case SPA_TYPE_Bytes: + case SPA_TYPE_Struct: + case SPA_TYPE_Pod: + return 0; + case SPA_TYPE_String: + return 1; + case SPA_TYPE_Bool: + case SPA_TYPE_Int: + return sizeof(int32_t); + case SPA_TYPE_Id: + return sizeof(uint32_t); + case SPA_TYPE_Long: + return sizeof(int64_t); + case SPA_TYPE_Float: + return sizeof(float); + case SPA_TYPE_Double: + return sizeof(double); + case SPA_TYPE_Rectangle: + return sizeof(struct spa_rectangle); + case SPA_TYPE_Fraction: + return sizeof(struct spa_fraction); + case SPA_TYPE_Bitmap: + return sizeof(uint8_t); + case SPA_TYPE_Array: + return sizeof(struct spa_pod_array_body); + case SPA_TYPE_Object: + return sizeof(struct spa_pod_object_body); + case SPA_TYPE_Sequence: + return sizeof(struct spa_pod_sequence_body); + case SPA_TYPE_Pointer: + return sizeof(struct spa_pod_pointer_body); + case SPA_TYPE_Fd: + return sizeof(int64_t); + case SPA_TYPE_Choice: + return sizeof(struct spa_pod_choice_body); + } + return 0; +} + +SPA_API_POD_BODY uint32_t spa_pod_choice_min_values(uint32_t choice_type) +{ + switch (choice_type) { + case SPA_CHOICE_Enum: + return 2; + case SPA_CHOICE_Range: + return 3; + case SPA_CHOICE_Step: + return 4; + case SPA_CHOICE_None: + case SPA_CHOICE_Flags: + default: + /* + * This must always return at least 1, because callers + * assume that n_vals >= spa_pod_choice_min_values() + * mean that n_vals is at least 1. + */ + return 1; + } +} + +SPA_API_POD_BODY int spa_pod_body_from_data(void *data, size_t maxsize, off_t offset, size_t size, + struct spa_pod *pod, const void **body) +{ + if (offset < 0 || offset > (int64_t)UINT32_MAX) + return -EINVAL; + if (size < sizeof(struct spa_pod) || + size > maxsize || + maxsize - size < (uint32_t)offset) + return -EINVAL; + memcpy(pod, SPA_PTROFF(data, offset, void), sizeof(struct spa_pod)); + if (!SPA_POD_IS_VALID(pod)) + return -EINVAL; + if (pod->size > size - sizeof(struct spa_pod)) + return -EINVAL; + *body = SPA_PTROFF(data, offset + sizeof(struct spa_pod), void); + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_none(const struct spa_pod *pod) +{ + return SPA_POD_CHECK_TYPE(pod, SPA_TYPE_None); +} + +SPA_API_POD_BODY int spa_pod_is_bool(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Bool, sizeof(int32_t)); +} + +#define SPA_POD_BODY_LOAD_ONCE(a, b) (*(a) = SPA_LOAD_ONCE((__typeof__(a))(b))) +#define SPA_POD_BODY_LOAD_FIELD_ONCE(a, b, field) ((a)->field = SPA_LOAD_ONCE(&((__typeof__(a))(b))->field)) + +SPA_API_POD_BODY int spa_pod_body_get_bool(const struct spa_pod *pod, const void *body, bool *value) +{ + if (!spa_pod_is_bool(pod)) + return -EINVAL; + *value = !!__atomic_load_n((const int32_t *)body, __ATOMIC_RELAXED); + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_id(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Id, sizeof(uint32_t)); +} + +SPA_API_POD_BODY int spa_pod_body_get_id(const struct spa_pod *pod, const void *body, uint32_t *value) +{ + if (!spa_pod_is_id(pod)) + return -EINVAL; + SPA_POD_BODY_LOAD_ONCE(value, body); + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_int(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Int, sizeof(int32_t)); +} + +SPA_API_POD_BODY int spa_pod_body_get_int(const struct spa_pod *pod, const void *body, int32_t *value) +{ + if (!spa_pod_is_int(pod)) + return -EINVAL; + SPA_POD_BODY_LOAD_ONCE(value, body); + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_long(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Long, sizeof(int64_t)); +} + +SPA_API_POD_BODY int spa_pod_body_get_long(const struct spa_pod *pod, const void *body, int64_t *value) +{ + if (!spa_pod_is_long(pod)) + return -EINVAL; + /* TODO this is wrong per C standard, but if it breaks so does the Linux kernel. */ + SPA_BARRIER; + memcpy(value, body, sizeof *value); + SPA_BARRIER; + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_float(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Float, sizeof(float)); +} + +SPA_API_POD_BODY int spa_pod_body_get_float(const struct spa_pod *pod, const void *body, float *value) +{ + if (!spa_pod_is_float(pod)) + return -EINVAL; + SPA_BARRIER; + memcpy(value, body, sizeof *value); + SPA_BARRIER; + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_double(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Double, sizeof(double)); +} + +SPA_API_POD_BODY int spa_pod_body_get_double(const struct spa_pod *pod, const void *body, double *value) +{ + if (!spa_pod_is_double(pod)) + return -EINVAL; + SPA_BARRIER; + memcpy(value, body, sizeof *value); + SPA_BARRIER; + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_string(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_String, 1); +} + +SPA_API_POD_BODY int spa_pod_body_get_string(const struct spa_pod *pod, + const void *body, const char **value) +{ + const char *s; + if (!spa_pod_is_string(pod)) + return -EINVAL; + s = (const char *)body; + if (((const volatile char *)s)[pod->size-1] != '\0') + return -EINVAL; + *value = s; + return 0; +} + +SPA_API_POD_BODY int spa_pod_body_copy_string(const struct spa_pod *pod, const void *body, + char *dest, size_t maxlen) +{ + const char *s; + if (spa_pod_body_get_string(pod, body, &s) < 0 || maxlen < 1) + return -EINVAL; + SPA_BARRIER; + strncpy(dest, s, maxlen-1); + SPA_BARRIER; + dest[maxlen-1]= '\0'; + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_bytes(const struct spa_pod *pod) +{ + return SPA_POD_CHECK_TYPE(pod, SPA_TYPE_Bytes); +} + +SPA_API_POD_BODY int spa_pod_body_get_bytes(const struct spa_pod *pod, const void *body, + const void **value, uint32_t *len) +{ + if (!spa_pod_is_bytes(pod)) + return -EINVAL; + *value = (const void *)body; + *len = pod->size; + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_pointer(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Pointer, sizeof(struct spa_pod_pointer_body)); +} + +SPA_API_POD_BODY int spa_pod_body_get_pointer(const struct spa_pod *pod, const void *body, + uint32_t *type, const void **value) +{ + if (!spa_pod_is_pointer(pod)) + return -EINVAL; + struct spa_pod_pointer_body b; + SPA_POD_BODY_LOAD_FIELD_ONCE(&b, body, type); + SPA_POD_BODY_LOAD_FIELD_ONCE(&b, body, value); + *type = b.type; + *value = b.value; + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_fd(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Fd, sizeof(int64_t)); +} + +SPA_API_POD_BODY int spa_pod_body_get_fd(const struct spa_pod *pod, const void *body, + int64_t *value) +{ + if (!spa_pod_is_fd(pod)) + return -EINVAL; + SPA_BARRIER; + memcpy(value, body, sizeof *value); + SPA_BARRIER; + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_rectangle(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Rectangle, sizeof(struct spa_rectangle)); +} + +SPA_API_POD_BODY int spa_pod_body_get_rectangle(const struct spa_pod *pod, const void *body, + struct spa_rectangle *value) +{ + if (!spa_pod_is_rectangle(pod)) + return -EINVAL; + SPA_POD_BODY_LOAD_FIELD_ONCE(value, body, width); + SPA_POD_BODY_LOAD_FIELD_ONCE(value, body, height); + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_fraction(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Fraction, sizeof(struct spa_fraction)); +} +SPA_API_POD_BODY int spa_pod_body_get_fraction(const struct spa_pod *pod, const void *body, + struct spa_fraction *value) +{ + if (!spa_pod_is_fraction(pod)) + return -EINVAL; + SPA_POD_BODY_LOAD_FIELD_ONCE(value, body, num); + SPA_POD_BODY_LOAD_FIELD_ONCE(value, body, denom); + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_bitmap(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Bitmap, sizeof(uint8_t)); +} +SPA_API_POD_BODY int spa_pod_body_get_bitmap(const struct spa_pod *pod, const void *body, + const uint8_t **value) +{ + if (!spa_pod_is_bitmap(pod)) + return -EINVAL; + *value = (const uint8_t *)body; + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_array(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Array, sizeof(struct spa_pod_array_body)); +} +SPA_API_POD_BODY int spa_pod_body_get_array(const struct spa_pod *pod, const void *body, + struct spa_pod_array *arr, const void **arr_body) +{ + if (!spa_pod_is_array(pod)) + return -EINVAL; + arr->pod = *pod; + SPA_POD_BODY_LOAD_FIELD_ONCE(&arr->body.child, body, type); + SPA_POD_BODY_LOAD_FIELD_ONCE(&arr->body.child, body, size); + *arr_body = SPA_PTROFF(body, sizeof(struct spa_pod_array_body), void); + return 0; +} +SPA_API_POD_BODY const void *spa_pod_array_body_get_values(const struct spa_pod_array *arr, + const void *body, uint32_t *n_values, uint32_t *val_size, uint32_t *val_type) +{ + uint32_t child_size = arr->body.child.size; + *n_values = child_size ? (arr->pod.size - sizeof(arr->body)) / child_size : 0; + *val_size = child_size; + *val_type = arr->body.child.type; + if (*val_size < spa_pod_type_size(*val_type)) + *n_values = 0; + return body; +} + +SPA_API_POD_BODY const void *spa_pod_body_get_array_values(const struct spa_pod *pod, + const void *body, uint32_t *n_values, uint32_t *val_size, uint32_t *val_type) +{ + struct spa_pod_array arr; + if (spa_pod_body_get_array(pod, body, &arr, &body) < 0) + return NULL; + return spa_pod_array_body_get_values(&arr, body, n_values, val_size, val_type); +} + +SPA_API_POD_BODY int spa_pod_is_choice(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Choice, sizeof(struct spa_pod_choice_body)); +} +SPA_API_POD_BODY int spa_pod_body_get_choice(const struct spa_pod *pod, const void *body, + struct spa_pod_choice *choice, const void **choice_body) +{ + if (!spa_pod_is_choice(pod)) + return -EINVAL; + choice->pod = *pod; + SPA_POD_BODY_LOAD_FIELD_ONCE(&choice->body, body, type); + SPA_POD_BODY_LOAD_FIELD_ONCE(&choice->body, body, flags); + SPA_POD_BODY_LOAD_FIELD_ONCE(&choice->body, body, child.size); + SPA_POD_BODY_LOAD_FIELD_ONCE(&choice->body, body, child.type); + *choice_body = SPA_PTROFF(body, sizeof(struct spa_pod_choice_body), void); + return 0; +} +SPA_API_POD_BODY const void *spa_pod_choice_body_get_values(const struct spa_pod_choice *pod, + const void *body, uint32_t *n_values, uint32_t *choice, + uint32_t *val_size, uint32_t *val_type) +{ + uint32_t child_size = pod->body.child.size; + *val_size = child_size; + *val_type = pod->body.child.type; + *n_values = child_size ? (pod->pod.size - sizeof(pod->body)) / child_size : 0; + *choice = pod->body.type; + if (*n_values < spa_pod_choice_min_values(*choice) || + *val_size < spa_pod_type_size(*val_type)) + *n_values = 0; + return body; +} + +SPA_API_POD_BODY int spa_pod_is_struct(const struct spa_pod *pod) +{ + return SPA_POD_CHECK_TYPE(pod, SPA_TYPE_Struct); +} + +SPA_API_POD_BODY int spa_pod_is_object(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Object, sizeof(struct spa_pod_object_body)); +} +SPA_API_POD_BODY int spa_pod_body_get_object(const struct spa_pod *pod, const void *body, + struct spa_pod_object *object, const void **object_body) +{ + if (!spa_pod_is_object(pod)) + return -EINVAL; + object->pod = *pod; + SPA_POD_BODY_LOAD_FIELD_ONCE(&object->body, body, type); + SPA_POD_BODY_LOAD_FIELD_ONCE(&object->body, body, id); + *object_body = SPA_PTROFF(body, sizeof(struct spa_pod_object_body), void); + return 0; +} + +SPA_API_POD_BODY int spa_pod_is_sequence(const struct spa_pod *pod) +{ + return SPA_POD_CHECK(pod, SPA_TYPE_Sequence, sizeof(struct spa_pod_sequence_body)); +} +SPA_API_POD_BODY int spa_pod_body_get_sequence(const struct spa_pod *pod, const void *body, + struct spa_pod_sequence *seq, const void **seq_body) +{ + if (!spa_pod_is_sequence(pod)) + return -EINVAL; + seq->pod = *pod; + SPA_POD_BODY_LOAD_FIELD_ONCE(&seq->body, body, unit); + SPA_POD_BODY_LOAD_FIELD_ONCE(&seq->body, body, pad); + *seq_body = SPA_PTROFF(body, sizeof(struct spa_pod_sequence_body), void); + return 0; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_POD_BODY_H */ diff --git a/spa/include/spa/pod/builder.h b/spa/include/spa/pod/builder.h index 553f75512..d1801832c 100644 --- a/spa/include/spa/pod/builder.h +++ b/spa/include/spa/pod/builder.h @@ -5,6 +5,12 @@ #ifndef SPA_POD_BUILDER_H #define SPA_POD_BUILDER_H +#include + +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -18,12 +24,6 @@ extern "C" { * \{ */ -#include - -#include -#include -#include - #ifndef SPA_API_POD_BUILDER #ifdef SPA_API_IMPL #define SPA_API_POD_BUILDER SPA_API_IMPL @@ -65,6 +65,11 @@ spa_pod_builder_get_state(struct spa_pod_builder *builder, struct spa_pod_builde *state = builder->state; } +SPA_API_POD_BUILDER bool spa_pod_builder_corrupted(const struct spa_pod_builder *builder) +{ + return builder->state.offset > builder->size; +} + SPA_API_POD_BUILDER void spa_pod_builder_set_callbacks(struct spa_pod_builder *builder, const struct spa_pod_builder_callbacks *callbacks, void *data) @@ -79,7 +84,7 @@ spa_pod_builder_reset(struct spa_pod_builder *builder, struct spa_pod_builder_st uint32_t size = builder->state.offset - state->offset; builder->state = *state; for (f = builder->state.frame; f ; f = f->parent) - f->pod.size -= size; + f->pod.size -= SPA_MIN(size, f->pod.size); } SPA_API_POD_BUILDER void spa_pod_builder_init(struct spa_pod_builder *builder, void *data, uint32_t size) @@ -88,21 +93,28 @@ SPA_API_POD_BUILDER void spa_pod_builder_init(struct spa_pod_builder *builder, v } SPA_API_POD_BUILDER struct spa_pod * -spa_pod_builder_deref(struct spa_pod_builder *builder, uint32_t offset) +spa_pod_builder_deref_fallback(struct spa_pod_builder *builder, uint32_t offset, struct spa_pod *fallback) { uint32_t size = builder->size; - if (offset + 8 <= size) { + if (offset + UINT64_C(8) <= size) { struct spa_pod *pod = SPA_PTROFF(builder->data, offset, struct spa_pod); - if (offset + SPA_POD_SIZE(pod) <= size) + if (offset + (uint64_t)SPA_POD_SIZE(pod) <= size && + SPA_POD_IS_VALID(pod)) return pod; } - return NULL; + return fallback; +} + +SPA_API_POD_BUILDER struct spa_pod * +spa_pod_builder_deref(struct spa_pod_builder *builder, uint32_t offset) +{ + return (struct spa_pod*)spa_pod_builder_deref_fallback(builder, offset, NULL); } SPA_API_POD_BUILDER struct spa_pod * spa_pod_builder_frame(struct spa_pod_builder *builder, struct spa_pod_frame *frame) { - if (frame->offset + SPA_POD_SIZE(&frame->pod) <= builder->size) + if (frame->offset + (uint64_t)SPA_POD_SIZE(&frame->pod) <= builder->size) return SPA_PTROFF(builder->data, frame->offset, struct spa_pod); return NULL; } @@ -129,8 +141,12 @@ SPA_API_POD_BUILDER int spa_pod_builder_raw(struct spa_pod_builder *builder, con struct spa_pod_frame *f; uint32_t offset = builder->state.offset; size_t data_offset = -1; + uint64_t total_size = offset + (uint64_t) size; + + if (total_size > builder->size) { + if (total_size > UINT32_MAX) + return -ENOSPC; - if (offset + size > builder->size) { /* data could be inside the data we will realloc */ if (spa_ptrinside(builder->data, builder->size, data, size, NULL)) data_offset = SPA_PTRDIFF(data, builder->data); @@ -139,7 +155,7 @@ SPA_API_POD_BUILDER int spa_pod_builder_raw(struct spa_pod_builder *builder, con if (offset <= builder->size) spa_callbacks_call_res(&builder->callbacks, struct spa_pod_builder_callbacks, res, - overflow, 0, offset + size); + overflow, 0, total_size); } if (res == 0 && data) { if (data_offset != (size_t) -1) @@ -148,7 +164,7 @@ SPA_API_POD_BUILDER int spa_pod_builder_raw(struct spa_pod_builder *builder, con memcpy(SPA_PTROFF(builder->data, offset, void), data, size); } - builder->state.offset += size; + builder->state.offset = total_size; for (f = builder->state.frame; f ; f = f->parent) f->pod.size += size; @@ -156,10 +172,20 @@ SPA_API_POD_BUILDER int spa_pod_builder_raw(struct spa_pod_builder *builder, con return res; } +SPA_API_POD_BUILDER void spa_pod_builder_remove(struct spa_pod_builder *builder, uint32_t size) +{ + struct spa_pod_frame *f; + builder->state.offset -= SPA_MIN(size, builder->state.offset); + for (f = builder->state.frame; f ; f = f->parent) + f->pod.size -= SPA_MIN(size, f->pod.size); +} + SPA_API_POD_BUILDER int spa_pod_builder_pad(struct spa_pod_builder *builder, uint32_t size) { uint64_t zeroes = 0; - size = SPA_ROUND_UP_N(size, 8) - size; + if (builder->state.flags == SPA_POD_BUILDER_FLAG_BODY) + return 0; + size = SPA_ROUND_UP_N(size, SPA_POD_ALIGN) - size; return size ? spa_pod_builder_raw(builder, &zeroes, size) : 0; } @@ -189,26 +215,30 @@ SPA_API_POD_BUILDER void *spa_pod_builder_pop(struct spa_pod_builder *builder, s return pod; } +SPA_API_POD_BUILDER int +spa_pod_builder_primitive_body(struct spa_pod_builder *builder, const struct spa_pod *p, + const void *body, uint32_t body_size, const char *suffix, uint32_t suffix_size) +{ + int res = 0, r; + uint32_t size = SPA_POD_SIZE(p) - body_size - suffix_size; + if (builder->state.flags != SPA_POD_BUILDER_FLAG_BODY) { + SPA_FLAG_CLEAR(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST); + res = spa_pod_builder_raw(builder, p, size); + } + if (body_size > 0 && (r = spa_pod_builder_raw(builder, body, body_size)) < 0) + res = r; + if (suffix_size > 0 && (r = spa_pod_builder_raw(builder, suffix, suffix_size)) < 0) + res = r; + if ((r = spa_pod_builder_pad(builder, builder->state.offset)) < 0) + res = r; + return res; +} + SPA_API_POD_BUILDER int spa_pod_builder_primitive(struct spa_pod_builder *builder, const struct spa_pod *p) { - const void *data; - uint32_t size; - int r, res; - - if (builder->state.flags == SPA_POD_BUILDER_FLAG_BODY) { - data = SPA_POD_BODY_CONST(p); - size = SPA_POD_BODY_SIZE(p); - } else { - data = p; - size = SPA_POD_SIZE(p); - SPA_FLAG_CLEAR(builder->state.flags, SPA_POD_BUILDER_FLAG_FIRST); - } - res = spa_pod_builder_raw(builder, data, size); - if (builder->state.flags != SPA_POD_BUILDER_FLAG_BODY) - if ((r = spa_pod_builder_pad(builder, size)) < 0) - res = r; - return res; + return spa_pod_builder_primitive_body(builder, p, + SPA_POD_BODY_CONST(p), p->size, NULL, 0); } #define SPA_POD_INIT(size,type) ((struct spa_pod) { (size), (type) }) @@ -294,10 +324,7 @@ SPA_API_POD_BUILDER int spa_pod_builder_string_len(struct spa_pod_builder *builder, const char *str, uint32_t len) { const struct spa_pod_string p = SPA_POD_INIT_String(len+1); - int r, res = spa_pod_builder_raw(builder, &p, sizeof(p)); - if ((r = spa_pod_builder_write_string(builder, str, len)) < 0) - res = r; - return res; + return spa_pod_builder_primitive_body(builder, &p.pod, str, len, "", 1); } SPA_API_POD_BUILDER int spa_pod_builder_string(struct spa_pod_builder *builder, const char *str) @@ -312,10 +339,7 @@ SPA_API_POD_BUILDER int spa_pod_builder_bytes(struct spa_pod_builder *builder, const void *bytes, uint32_t len) { const struct spa_pod_bytes p = SPA_POD_INIT_Bytes(len); - int r, res = spa_pod_builder_raw(builder, &p, sizeof(p)); - if ((r = spa_pod_builder_raw_padded(builder, bytes, len)) < 0) - res = r; - return res; + return spa_pod_builder_primitive_body(builder, &p.pod, bytes, len, NULL, 0); } SPA_API_POD_BUILDER void * spa_pod_builder_reserve_bytes(struct spa_pod_builder *builder, uint32_t len) @@ -323,7 +347,7 @@ spa_pod_builder_reserve_bytes(struct spa_pod_builder *builder, uint32_t len) uint32_t offset = builder->state.offset; if (spa_pod_builder_bytes(builder, NULL, len) < 0) return NULL; - return SPA_POD_BODY(spa_pod_builder_deref(builder, offset)); + return SPA_PTROFF(builder->data, offset + sizeof(struct spa_pod), void); } #define SPA_POD_INIT_Pointer(type,value) ((struct spa_pod_pointer){ { sizeof(struct spa_pod_pointer_body), SPA_TYPE_Pointer }, { (type), 0, (value) } }) @@ -381,10 +405,7 @@ spa_pod_builder_array(struct spa_pod_builder *builder, {(uint32_t)(sizeof(struct spa_pod_array_body) + n_elems * child_size), SPA_TYPE_Array}, {{child_size, child_type}} }; - int r, res = spa_pod_builder_raw(builder, &p, sizeof(p)); - if ((r = spa_pod_builder_raw_padded(builder, elems, child_size * n_elems)) < 0) - res = r; - return res; + return spa_pod_builder_primitive_body(builder, &p.pod, elems, n_elems * child_size, NULL, 0); } #define SPA_POD_INIT_CHOICE_BODY(type, flags, child_size, child_type) \ @@ -465,7 +486,7 @@ spa_pod_builder_control(struct spa_pod_builder *builder, uint32_t offset, uint32 return spa_pod_builder_raw(builder, &p, sizeof(p)); } -SPA_API_POD_BUILDER uint32_t spa_choice_from_id(char id) +SPA_API_POD_BUILDER uint32_t spa_choice_from_id_flags(char id, uint32_t *flags) { switch (id) { case 'r': @@ -474,6 +495,9 @@ SPA_API_POD_BUILDER uint32_t spa_choice_from_id(char id) return SPA_CHOICE_Step; case 'e': return SPA_CHOICE_Enum; + case 'F': + *flags |= SPA_POD_PROP_FLAG_DROP; + SPA_FALLTHROUGH; case 'f': return SPA_CHOICE_Flags; case 'n': @@ -481,6 +505,11 @@ SPA_API_POD_BUILDER uint32_t spa_choice_from_id(char id) return SPA_CHOICE_None; } } +SPA_API_POD_BUILDER uint32_t spa_choice_from_id(char id) +{ + uint32_t flags = 0; + return spa_choice_from_id_flags(id, &flags); +} #define SPA_POD_BUILDER_COLLECT(builder,type,args) \ do { \ @@ -574,6 +603,17 @@ do { \ spa_pod_builder_primitive(builder, pod); \ break; \ } \ + case 'Q': \ + case 'N': \ + case 'U': \ + case 'W': \ + { \ + struct spa_pod *pod = va_arg(args, struct spa_pod *); \ + const void *body = va_arg(args, const void *); \ + spa_pod_builder_primitive_body(builder, pod, \ + body, pod->size, NULL, 0); \ + break; \ + } \ } \ } while(false) @@ -589,39 +629,46 @@ spa_pod_builder_addv(struct spa_pod_builder *builder, va_list args) int n_values = 1; struct spa_pod_frame f; bool choice; + uint32_t key = 0, flags = 0, offset = 0, type = 0, ctype = 0; switch (ftype) { case SPA_TYPE_Object: - { - uint32_t key = va_arg(args, uint32_t); + key = va_arg(args, uint32_t); if (key == 0) goto exit; - spa_pod_builder_prop(builder, key, 0); + if (key == SPA_ID_INVALID) { + key = va_arg(args, uint32_t); + flags = va_arg(args, uint32_t); + } break; - } case SPA_TYPE_Sequence: - { - uint32_t offset = va_arg(args, uint32_t); - uint32_t type = va_arg(args, uint32_t); + offset = va_arg(args, uint32_t); + type = va_arg(args, uint32_t); if (type == 0) goto exit; - spa_pod_builder_control(builder, offset, type); - SPA_FALLTHROUGH - } - default: break; } + + if ((format = va_arg(args, const char *)) == NULL) break; choice = *format == '?'; if (choice) { - uint32_t type = spa_choice_from_id(*++format); + ctype = spa_choice_from_id_flags(*++format, &flags); if (*format != '\0') format++; - - spa_pod_builder_push_choice(builder, &f, type, 0); - + } + switch (ftype) { + case SPA_TYPE_Object: + spa_pod_builder_prop(builder, key, flags); + break; + case SPA_TYPE_Sequence: + spa_pod_builder_control(builder, offset, type); + break; + } + if (choice) { + spa_pod_builder_push_choice(builder, &f, ctype, 0); n_values = va_arg(args, int); } while (n_values-- > 0) diff --git a/spa/include/spa/pod/command.h b/spa/include/spa/pod/command.h index 330f56edc..5afcca8b8 100644 --- a/spa/include/spa/pod/command.h +++ b/spa/include/spa/pod/command.h @@ -5,13 +5,13 @@ #ifndef SPA_COMMAND_H #define SPA_COMMAND_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** * \addtogroup spa_pod * \{ diff --git a/spa/include/spa/pod/compare.h b/spa/include/spa/pod/compare.h index 508987561..144bd4a5a 100644 --- a/spa/include/spa/pod/compare.h +++ b/spa/include/spa/pod/compare.h @@ -5,10 +5,6 @@ #ifndef SPA_POD_COMPARE_H #define SPA_POD_COMPARE_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include @@ -20,6 +16,10 @@ extern "C" { #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_POD_COMPARE #ifdef SPA_API_IMPL #define SPA_API_POD_COMPARE SPA_API_IMPL @@ -51,19 +51,19 @@ SPA_API_POD_COMPARE int spa_pod_compare_value(uint32_t type, const void *r1, con case SPA_TYPE_Double: return SPA_CMP(*(double *)r1, *(double *)r2); case SPA_TYPE_String: - return strcmp((char *)r1, (char *)r2); - case SPA_TYPE_Bytes: - return memcmp((char *)r1, (char *)r2, size); + return strncmp((char *)r1, (char *)r2, size); case SPA_TYPE_Rectangle: { const struct spa_rectangle *rec1 = (struct spa_rectangle *) r1, *rec2 = (struct spa_rectangle *) r2; - if (rec1->width == rec2->width && rec1->height == rec2->height) - return 0; - else if (rec1->width < rec2->width || rec1->height < rec2->height) + uint64_t a1, a2; + a1 = ((uint64_t) rec1->width) * rec1->height; + a2 = ((uint64_t) rec2->width) * rec2->height; + if (a1 < a2) return -1; - else + if (a1 > a2) return 1; + return SPA_CMP(rec1->width, rec2->width); } case SPA_TYPE_Fraction: { @@ -75,7 +75,7 @@ SPA_API_POD_COMPARE int spa_pod_compare_value(uint32_t type, const void *r1, con return SPA_CMP(n1, n2); } default: - break; + return memcmp(r1, r2, size); } return 0; } @@ -96,10 +96,13 @@ SPA_API_POD_COMPARE int spa_pod_compare(const struct spa_pod *pod1, if (n_vals1 != n_vals2) return -EINVAL; - if (SPA_POD_TYPE(pod1) != SPA_POD_TYPE(pod2)) + if (pod1->type != pod2->type) return -EINVAL; - switch (SPA_POD_TYPE(pod1)) { + if (n_vals1 < 1) + return -EINVAL; /* empty choice */ + + switch (pod1->type) { case SPA_TYPE_Struct: { const struct spa_pod *p1, *p2; @@ -147,22 +150,111 @@ SPA_API_POD_COMPARE int spa_pod_compare(const struct spa_pod *pod1, } case SPA_TYPE_Array: { - if (SPA_POD_BODY_SIZE(pod1) != SPA_POD_BODY_SIZE(pod2)) + if (pod1->size != pod2->size) return -EINVAL; - res = memcmp(SPA_POD_BODY(pod1), SPA_POD_BODY(pod2), SPA_POD_BODY_SIZE(pod2)); + res = memcmp(SPA_POD_BODY(pod1), SPA_POD_BODY(pod2), pod2->size); break; } default: - if (SPA_POD_BODY_SIZE(pod1) != SPA_POD_BODY_SIZE(pod2)) + if (pod1->size != pod2->size) return -EINVAL; - res = spa_pod_compare_value(SPA_POD_TYPE(pod1), + if (pod1->size < spa_pod_type_size(pod1->type)) + return -EINVAL; + res = spa_pod_compare_value(pod1->type, SPA_POD_BODY(pod1), SPA_POD_BODY(pod2), - SPA_POD_BODY_SIZE(pod1)); + pod1->size); break; } return res; } +SPA_API_POD_COMPARE int spa_pod_compare_is_compatible_flags(uint32_t type, const void *r1, + const void *r2, uint32_t size SPA_UNUSED) +{ + switch (type) { + case SPA_TYPE_Int: + return ((*(int32_t *) r1) & (*(int32_t *) r2)) != 0; + case SPA_TYPE_Long: + return ((*(int64_t *) r1) & (*(int64_t *) r2)) != 0; + default: + return -ENOTSUP; + } + return 0; +} + + +SPA_API_POD_COMPARE int spa_pod_compare_is_step_of(uint32_t type, const void *r1, + const void *r2, uint32_t size) +{ + switch (type) { + case SPA_TYPE_Int: + if (*(int32_t *)r2 < 1) + return -EINVAL; + return *(int32_t *) r1 % *(int32_t *) r2 == 0; + case SPA_TYPE_Long: + if (*(int64_t *)r2 < 1) + return -EINVAL; + return *(int64_t *) r1 % *(int64_t *) r2 == 0; + case SPA_TYPE_Rectangle: + { + const struct spa_rectangle *rec1 = (struct spa_rectangle *) r1, + *rec2 = (struct spa_rectangle *) r2; + + if (rec2->width < 1 || rec2->height < 1) + return -EINVAL; + + return (rec1->width % rec2->width == 0 && + rec1->height % rec2->height == 0); + } + default: + return -ENOTSUP; + } + return 0; +} + +SPA_API_POD_COMPARE int spa_pod_compare_is_in_range(uint32_t type, const void *v, + const void *min, const void *max, const void *step, uint32_t size SPA_UNUSED) +{ + if (spa_pod_compare_value(type, v, min, size) < 0 || + spa_pod_compare_value(type, v, max, size) > 0) + return 0; + if (step != NULL) + return spa_pod_compare_is_step_of(type, v, step, size); + return 1; +} + +SPA_API_POD_COMPARE int spa_pod_compare_is_valid_choice(uint32_t type, uint32_t size, + const void *val, const void *vals, uint32_t n_vals, uint32_t choice) +{ + switch (choice) { + case SPA_CHOICE_None: + if (spa_pod_compare_value(type, val, vals, size) == 0) + return 1; + return 0; + case SPA_CHOICE_Enum: + { + const void *next = vals; + for (uint32_t i = 1; i < n_vals; i++) { + next = SPA_PTROFF(next, size, void); + if (spa_pod_compare_value(type, val, next, size) == 0) + return 1; + } + return 0; + } + case SPA_CHOICE_Range: + case SPA_CHOICE_Step: + { + void *min = SPA_PTROFF(vals,size,void); + void *max = SPA_PTROFF(min,size,void); + void *step = choice == SPA_CHOICE_Step ? SPA_PTROFF(max,size,void) : NULL; + return spa_pod_compare_is_in_range(type, val, min, max, step, size); + } + case SPA_CHOICE_Flags: + return 1; + } + return 0; +} + /** * \} */ diff --git a/spa/include/spa/pod/dynamic.h b/spa/include/spa/pod/dynamic.h index e9998cdb2..ac69381a9 100644 --- a/spa/include/spa/pod/dynamic.h +++ b/spa/include/spa/pod/dynamic.h @@ -5,13 +5,13 @@ #ifndef SPA_POD_DYNAMIC_H #define SPA_POD_DYNAMIC_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #ifndef SPA_API_POD_DYNAMIC #ifdef SPA_API_IMPL #define SPA_API_POD_DYNAMIC SPA_API_IMPL @@ -53,11 +53,21 @@ SPA_API_POD_DYNAMIC void spa_pod_dynamic_builder_init(struct spa_pod_dynamic_bui .overflow = spa_pod_dynamic_builder_overflow }; builder->b = SPA_POD_BUILDER_INIT(data, size); - spa_pod_builder_set_callbacks(&builder->b, &spa_pod_dynamic_builder_callbacks, builder); + if (extend > 0) + spa_pod_builder_set_callbacks(&builder->b, &spa_pod_dynamic_builder_callbacks, builder); builder->extend = extend; builder->data = data; } +SPA_API_POD_DYNAMIC void spa_pod_dynamic_builder_continue(struct spa_pod_dynamic_builder *builder, + struct spa_pod_builder *b) +{ + uint32_t remain = b->state.offset >= b->size ? 0 : b->size - b->state.offset; + spa_pod_dynamic_builder_init(builder, + remain ? SPA_PTROFF(b->data, b->state.offset, void) : NULL, + remain, b->callbacks.funcs == NULL ? 0 : 4096); +} + SPA_API_POD_DYNAMIC void spa_pod_dynamic_builder_clean(struct spa_pod_dynamic_builder *builder) { if (builder->data != builder->b.data) diff --git a/spa/include/spa/pod/event.h b/spa/include/spa/pod/event.h index c631bd392..00e66f194 100644 --- a/spa/include/spa/pod/event.h +++ b/spa/include/spa/pod/event.h @@ -5,12 +5,12 @@ #ifndef SPA_EVENT_H #define SPA_EVENT_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_pod * \{ diff --git a/spa/include/spa/pod/filter.h b/spa/include/spa/pod/filter.h index 6a47472e4..93d9a51ea 100644 --- a/spa/include/spa/pod/filter.h +++ b/spa/include/spa/pod/filter.h @@ -5,10 +5,6 @@ #ifndef SPA_POD_FILTER_H #define SPA_POD_FILTER_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include @@ -19,6 +15,11 @@ extern "C" { #include #include #include +#include + +#ifdef __cplusplus +extern "C" { +#endif #ifndef SPA_API_POD_FILTER #ifdef SPA_API_IMPL @@ -33,65 +34,16 @@ extern "C" { * \{ */ -SPA_API_POD_FILTER int spa_pod_choice_fix_default(struct spa_pod_choice *choice) -{ - void *val, *alt; - int i, nvals; - uint32_t type, size; - - nvals = SPA_POD_CHOICE_N_VALUES(choice); - type = SPA_POD_CHOICE_VALUE_TYPE(choice); - size = SPA_POD_CHOICE_VALUE_SIZE(choice); - alt = val = SPA_POD_CHOICE_VALUES(choice); - - switch (choice->body.type) { - case SPA_CHOICE_None: - break; - case SPA_CHOICE_Range: - case SPA_CHOICE_Step: - if (nvals > 1) { - alt = SPA_PTROFF(alt, size, void); - if (spa_pod_compare_value(type, val, alt, size) < 0) - memcpy(val, alt, size); - } - if (nvals > 2) { - alt = SPA_PTROFF(alt, size, void); - if (spa_pod_compare_value(type, val, alt, size) > 0) - memcpy(val, alt, size); - } - break; - case SPA_CHOICE_Flags: - case SPA_CHOICE_Enum: - { - void *best = NULL; - - for (i = 1; i < nvals; i++) { - alt = SPA_PTROFF(alt, size, void); - if (spa_pod_compare_value(type, val, alt, size) == 0) { - best = alt; - break; - } - if (best == NULL) - best = alt; - } - if (best) - memcpy(val, best, size); - - if (nvals <= 1) - choice->body.type = SPA_CHOICE_None; - break; - } - } - return 0; -} - SPA_API_POD_FILTER int spa_pod_filter_flags_value(struct spa_pod_builder *b, - uint32_t type, const void *r1, const void *r2, uint32_t size SPA_UNUSED) + uint32_t type, const void *r1, const void *r2, uint32_t size) { switch (type) { case SPA_TYPE_Int: { - int32_t val = (*(int32_t *) r1) & (*(int32_t *) r2); + int32_t val; + if (size < sizeof(int32_t)) + return -EINVAL; + val = (*(int32_t *) r1) & (*(int32_t *) r2); if (val == 0) return 0; spa_pod_builder_int(b, val); @@ -99,7 +51,10 @@ SPA_API_POD_FILTER int spa_pod_filter_flags_value(struct spa_pod_builder *b, } case SPA_TYPE_Long: { - int64_t val = (*(int64_t *) r1) & (*(int64_t *) r2); + int64_t val; + if (size < sizeof(int64_t)) + return -EINVAL; + val = (*(int64_t *) r1) & (*(int64_t *) r2); if (val == 0) return 0; spa_pod_builder_long(b, val); @@ -111,43 +66,27 @@ SPA_API_POD_FILTER int spa_pod_filter_flags_value(struct spa_pod_builder *b, return 1; } -SPA_API_POD_FILTER int spa_pod_filter_is_step_of(uint32_t type, const void *r1, - const void *r2, uint32_t size SPA_UNUSED) -{ - switch (type) { - case SPA_TYPE_Int: - return *(int32_t *) r1 % *(int32_t *) r2 == 0; - case SPA_TYPE_Long: - return *(int64_t *) r1 % *(int64_t *) r2 == 0; - case SPA_TYPE_Rectangle: - { - const struct spa_rectangle *rec1 = (struct spa_rectangle *) r1, - *rec2 = (struct spa_rectangle *) r2; - - return (rec1->width % rec2->width == 0 && - rec1->height % rec2->height == 0); - } - default: - return -ENOTSUP; - } - return 0; -} - SPA_API_POD_FILTER int spa_pod_filter_prop(struct spa_pod_builder *b, const struct spa_pod_prop *p1, const struct spa_pod_prop *p2) { const struct spa_pod *v1, *v2; - struct spa_pod_choice *nc; - uint32_t j, k, nalt1, nalt2; + struct spa_pod_choice *nc, dummy; + uint32_t j, k, nalt1, nalt2, nc_offs; void *alt1, *alt2, *a1, *a2; uint32_t type, size, p1c, p2c; struct spa_pod_frame f; + int res, n_copied = 0; v1 = spa_pod_get_values(&p1->value, &nalt1, &p1c); - alt1 = SPA_POD_BODY(v1); v2 = spa_pod_get_values(&p2->value, &nalt2, &p2c); + + /* empty/invalid choices */ + if (nalt1 < 1 || nalt2 < 1) + return -EINVAL; + + alt1 = SPA_POD_BODY(v1); alt2 = SPA_POD_BODY(v2); type = v1->type; @@ -157,175 +96,166 @@ spa_pod_filter_prop(struct spa_pod_builder *b, if (type != v2->type || size != v2->size || p1->key != p2->key) return -EINVAL; - if (p1c == SPA_CHOICE_None || p1c == SPA_CHOICE_Flags) { - nalt1 = 1; - } else { - alt1 = SPA_PTROFF(alt1, size, void); - nalt1--; - } - - if (p2c == SPA_CHOICE_None || p2c == SPA_CHOICE_Flags) { - nalt2 = 1; - } else { - alt2 = SPA_PTROFF(alt2, size, void); - nalt2--; - } - /* start with copying the property */ spa_pod_builder_prop(b, p1->key, p1->flags & p2->flags); - spa_pod_builder_push_choice(b, &f, 0, 0); - nc = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f); + spa_pod_builder_push_choice(b, &f, SPA_CHOICE_None, 0); + spa_zero(dummy); - /* default value */ - spa_pod_builder_primitive(b, v1); + nc_offs = f.offset; + + /* start with an empty child and we will select a good default + * below */ + spa_pod_builder_child(b, size, type); + + /* we should prefer alt2 values but only if they are within the + * range. Swap the order otherwise. */ + if (!spa_pod_compare_is_valid_choice(type, size, alt2, alt2, nalt2, p2c)) { + SPA_SWAP(alt2, alt1); + SPA_SWAP(nalt2, nalt1); + SPA_SWAP(p2c, p1c); + } if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_None) || (p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Enum) || (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_None) || (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Enum)) { - int n_copied = 0; - /* copy all equal values but don't copy the default value again */ - for (j = 0, a1 = alt1; j < nalt1; j++, a1 = SPA_PTROFF(a1, size, void)) { - for (k = 0, a2 = alt2; k < nalt2; k++, a2 = SPA_PTROFF(a2,size,void)) { + /* copy all equal values. Start with alt2 so that they are prefered. */ + for (j = 0, a2 = alt2; j < nalt2; j++, a2 = SPA_PTROFF(a2, size, void)) { + for (k = 0, a1 = alt1; k < nalt1; k++, a1 = SPA_PTROFF(a1,size,void)) { if (spa_pod_compare_value(type, a1, a2, size) == 0) { - if (p1c == SPA_CHOICE_Enum || j > 0) + if (n_copied++ == 0) spa_pod_builder_raw(b, a1, size); - n_copied++; + spa_pod_builder_raw(b, a1, size); } } } - if (n_copied == 0) - return -EINVAL; - nc->body.type = SPA_CHOICE_Enum; } - - if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Range) || - (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Range)) { - int n_copied = 0; - /* copy all values inside the range */ - for (j = 0, a1 = alt1, a2 = alt2; j < nalt1; j++, a1 = SPA_PTROFF(a1,size,void)) { - if (spa_pod_compare_value(type, a1, a2, size) < 0) - continue; - if (spa_pod_compare_value(type, a1, SPA_PTROFF(a2,size,void), size) > 0) - continue; - spa_pod_builder_raw(b, a1, size); - n_copied++; - } - if (n_copied == 0) - return -EINVAL; - nc->body.type = SPA_CHOICE_Enum; - } - - if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Step) || + else if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Range) || + (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Range) || + (p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Step) || (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Step)) { - int n_copied = 0; - for (j = 0, a1 = alt1, a2 = alt2; j < nalt1; j++, a1 = SPA_PTROFF(a1,size,void)) { - int res; - if (spa_pod_compare_value(type, a1, a2, size) < 0) - continue; - if (spa_pod_compare_value(type, a1, SPA_PTROFF(a2,size,void), size) > 0) - continue; + void *min = SPA_PTROFF(alt2,size,void); + void *max = SPA_PTROFF(min,size,void); + void *step = p2c == SPA_CHOICE_Step ? SPA_PTROFF(max,size,void) : NULL; + bool found_def = false; - res = spa_pod_filter_is_step_of(type, a1, SPA_PTROFF(a2,size*2,void), size); + /* we should prefer the alt2 range default value but only if valid */ + if (spa_pod_compare_value(type, alt2, min, size) >= 0 && + spa_pod_compare_value(type, alt2, max, size) <= 0) { + for (j = 0, a1 = alt1; j < nalt1; j++, a1 = SPA_PTROFF(a1,size,void)) { + if (spa_pod_compare_value(type, a1, alt2, size) == 0) { + /* it is in the enum, use as default then */ + spa_pod_builder_raw(b, a1, size); + found_def = true; + break; + } + } + } + /* copy all values inside the range */ + for (j = 0, a1 = alt1; j < nalt1; j++, a1 = SPA_PTROFF(a1,size,void)) { + if ((res = spa_pod_compare_is_in_range(type, a1, min, max, step, size)) < 0) + return res; if (res == 0) continue; - if (res == -ENOTSUP) - return -EINVAL; - + if (n_copied++ == 0 && !found_def) + spa_pod_builder_raw(b, a1, size); spa_pod_builder_raw(b, a1, size); - n_copied++; } - if (n_copied == 0) - return -EINVAL; - nc->body.type = SPA_CHOICE_Enum; } + else if ((p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_None) || + (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Enum) || + (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_None) || + (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Enum)) { + void *min = SPA_PTROFF(alt1,size,void); + void *max = SPA_PTROFF(min,size,void); + void *step = p1c == SPA_CHOICE_Step ? SPA_PTROFF(max,size,void) : NULL; - if ((p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_None) || - (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Enum)) { - int n_copied = 0; - /* copy all values inside the range */ - for (k = 0, a1 = alt1, a2 = alt2; k < nalt2; k++, a2 = SPA_PTROFF(a2,size,void)) { - if (spa_pod_compare_value(type, a2, a1, size) < 0) - continue; - if (spa_pod_compare_value(type, a2, SPA_PTROFF(a1,size,void), size) > 0) + /* copy all values inside the range, this will automatically prefer + * a valid alt2 value */ + for (j = 0, a2 = alt2; j < nalt2; j++, a2 = SPA_PTROFF(a2,size,void)) { + if ((res = spa_pod_compare_is_in_range(type, a2, min, max, step, size)) < 0) + return res; + if (res == 0) continue; + if (n_copied++ == 0) + spa_pod_builder_raw(b, a2, size); spa_pod_builder_raw(b, a2, size); - n_copied++; } - if (n_copied == 0) - return -EINVAL; - nc->body.type = SPA_CHOICE_Enum; } - - if ((p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Range) || + else if ((p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Range) || (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Step) || (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Range) || (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Step)) { - if (spa_pod_compare_value(type, alt1, alt2, size) < 0) - spa_pod_builder_raw(b, alt2, size); - else - spa_pod_builder_raw(b, alt1, size); + void *min1 = SPA_PTROFF(alt1,size,void); + void *max1 = SPA_PTROFF(min1,size,void); + void *min2 = SPA_PTROFF(alt2,size,void); + void *max2 = SPA_PTROFF(min2,size,void); - alt1 = SPA_PTROFF(alt1,size,void); - alt2 = SPA_PTROFF(alt2,size,void); + /* max of min */ + if (spa_pod_compare_value(type, min1, min2, size) < 0) + min1 = min2; + /* min of max */ + if (spa_pod_compare_value(type, max2, max1, size) < 0) + max1 = max2; - if (spa_pod_compare_value(type, alt1, alt2, size) < 0) - spa_pod_builder_raw(b, alt1, size); - else - spa_pod_builder_raw(b, alt2, size); + /* reject impossible range */ + if (spa_pod_compare_value(type, max1, min1, size) < 0) + return -EINVAL; + /* prefer alt2 if in new range */ + a1 = alt2; + if ((res = spa_pod_compare_is_in_range(type, a1, min1, max1, NULL, size)) < 0) + return res; + if (res == 0) { + /* try alt1 otherwise */ + a1 = alt1; + if ((res = spa_pod_compare_is_in_range(type, a1, min1, max1, NULL, size)) < 0) + return res; + /* fall back to new min value then */ + if (res == 0) + a1 = min1; + } + + spa_pod_builder_raw(b, a1, size); + spa_pod_builder_raw(b, min1, size); + spa_pod_builder_raw(b, max1, size); + nc = (struct spa_pod_choice*)spa_pod_builder_deref_fallback(b, nc_offs, &dummy.pod); nc->body.type = SPA_CHOICE_Range; } - - if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Flags) || + else if ((p1c == SPA_CHOICE_None && p2c == SPA_CHOICE_Flags) || (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_None) || (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Flags)) { if (spa_pod_filter_flags_value(b, type, alt1, alt2, size) != 1) return -EINVAL; + nc = (struct spa_pod_choice*)spa_pod_builder_deref_fallback(b, nc_offs, &dummy.pod); nc->body.type = SPA_CHOICE_Flags; } - - if (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Flags) + else if (p1c == SPA_CHOICE_Range && p2c == SPA_CHOICE_Flags) + return -ENOTSUP; + else if (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Flags) + return -ENOTSUP; + else if (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Flags) + return -ENOTSUP; + else if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Range) + return -ENOTSUP; + else if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Step) + return -ENOTSUP; + else if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Enum) return -ENOTSUP; - if (p1c == SPA_CHOICE_Enum && p2c == SPA_CHOICE_Flags) - return -ENOTSUP; - - if ((p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_None) || - (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Enum)) { - int n_copied = 0; - for (j = 0, a1 = alt1, a2 = alt2; j < nalt2; j++, a2 = SPA_PTROFF(a1,size,void)) { - int res; - if (spa_pod_compare_value(type, a2, a1, size) < 0) - continue; - if (spa_pod_compare_value(type, a2, SPA_PTROFF(a1,size,void), size) > 0) - continue; - - res = spa_pod_filter_is_step_of(type, a2, SPA_PTROFF(a1,size*2,void), size); - if (res == 0) - continue; - if (res == -ENOTSUP) - return -EINVAL; - - spa_pod_builder_raw(b, a2, size); - n_copied++; - } - if (n_copied == 0) + nc = (struct spa_pod_choice*)spa_pod_builder_deref_fallback(b, nc_offs, &dummy.pod); + if (nc->body.type == SPA_CHOICE_None) { + if (n_copied == 0) { return -EINVAL; - nc->body.type = SPA_CHOICE_Enum; + } else if (n_copied == 1) { + /* we always copy the default value twice, so remove it + * again when it was the only one added */ + spa_pod_builder_remove(b, size); + } else if (n_copied > 1) { + nc->body.type = SPA_CHOICE_Enum; + } } - if (p1c == SPA_CHOICE_Step && p2c == SPA_CHOICE_Flags) - return -ENOTSUP; - - if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Range) - return -ENOTSUP; - if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Step) - return -ENOTSUP; - if (p1c == SPA_CHOICE_Flags && p2c == SPA_CHOICE_Enum) - return -ENOTSUP; - spa_pod_builder_pop(b, &f); - spa_pod_choice_fix_default(nc); return 0; } @@ -344,14 +274,14 @@ SPA_API_POD_FILTER int spa_pod_filter_part(struct spa_pod_builder *b, uint32_t filter_offset = 0; struct spa_pod_frame f; - switch (SPA_POD_TYPE(pp)) { + switch (pp->type) { case SPA_TYPE_Object: if (pf != NULL) { struct spa_pod_object *op = (struct spa_pod_object *) pp; struct spa_pod_object *of = (struct spa_pod_object *) pf; const struct spa_pod_prop *p1, *p2; - if (SPA_POD_TYPE(pf) != SPA_POD_TYPE(pp)) + if (pf->type != pp->type) return -EINVAL; spa_pod_builder_push_object(b, &f, op->body.type, op->body.id); @@ -360,9 +290,9 @@ SPA_API_POD_FILTER int spa_pod_filter_part(struct spa_pod_builder *b, p2 = spa_pod_object_find_prop(of, p2, p1->key); if (p2 != NULL) res = spa_pod_filter_prop(b, p1, p2); - else if ((p1->flags & SPA_POD_PROP_FLAG_MANDATORY) != 0) + else if (SPA_FLAG_IS_SET(p1->flags, SPA_POD_PROP_FLAG_MANDATORY)) res = -EINVAL; - else + else if (!SPA_FLAG_IS_SET(p1->flags, SPA_POD_PROP_FLAG_DROP)) spa_pod_builder_raw_padded(b, p1, SPA_POD_PROP_SIZE(p1)); if (res < 0) break; @@ -373,11 +303,12 @@ SPA_API_POD_FILTER int spa_pod_filter_part(struct spa_pod_builder *b, p1 = spa_pod_object_find_prop(op, p1, p2->key); if (p1 != NULL) continue; - if ((p2->flags & SPA_POD_PROP_FLAG_MANDATORY) != 0) + if (SPA_FLAG_IS_SET(p2->flags, SPA_POD_PROP_FLAG_MANDATORY)) res = -EINVAL; + else if (!SPA_FLAG_IS_SET(p2->flags, SPA_POD_PROP_FLAG_DROP)) + spa_pod_builder_raw_padded(b, p2, SPA_POD_PROP_SIZE(p2)); if (res < 0) break; - spa_pod_builder_raw_padded(b, p2, SPA_POD_PROP_SIZE(p2)); } } spa_pod_builder_pop(b, &f); @@ -389,7 +320,7 @@ SPA_API_POD_FILTER int spa_pod_filter_part(struct spa_pod_builder *b, case SPA_TYPE_Struct: if (pf != NULL) { - if (SPA_POD_TYPE(pf) != SPA_POD_TYPE(pp)) + if (pf->type != pp->type) return -EINVAL; filter_offset = sizeof(struct spa_pod_struct); @@ -408,9 +339,9 @@ SPA_API_POD_FILTER int spa_pod_filter_part(struct spa_pod_builder *b, default: if (pf != NULL) { - if (SPA_POD_SIZE(pp) != SPA_POD_SIZE(pf)) + if (pp->size != pf->size) return -EINVAL; - if (memcmp(pp, pf, SPA_POD_SIZE(pp)) != 0) + if (memcmp(pp, pf, pp->size) != 0) return -EINVAL; do_advance = true; } @@ -443,14 +374,17 @@ spa_pod_filter(struct spa_pod_builder *b, spa_return_val_if_fail(b != NULL, -EINVAL); spa_pod_builder_get_state(b, &state); - if (filter == NULL) + if (filter == NULL) { res = spa_pod_builder_raw_padded(b, pod, SPA_POD_SIZE(pod)); - else - res = spa_pod_filter_part(b, pod, SPA_POD_SIZE(pod), filter, SPA_POD_SIZE(filter)); - - if (res < 0) { - spa_pod_builder_reset(b, &state); - } else if (result) { + } else { + struct spa_pod_dynamic_builder db; + spa_pod_dynamic_builder_continue(&db, b); + res = spa_pod_filter_part(&db.b, pod, SPA_POD_SIZE(pod), filter, SPA_POD_SIZE(filter)); + if (res >= 0) + res = spa_pod_builder_raw_padded(b, db.b.data, db.b.state.offset); + spa_pod_dynamic_builder_clean(&db); + } + if (res >= 0 && result) { *result = (struct spa_pod*)spa_pod_builder_deref(b, state.offset); if (*result == NULL) res = -ENOSPC; @@ -458,6 +392,36 @@ spa_pod_filter(struct spa_pod_builder *b, return res; } +SPA_API_POD_FILTER int spa_pod_filter_object_make(struct spa_pod_object *pod) +{ + struct spa_pod_prop *res; + int count = 0; + + SPA_POD_OBJECT_FOREACH(pod, res) { + if (spa_pod_is_choice(&res->value) && + !SPA_FLAG_IS_SET(res->flags, SPA_POD_PROP_FLAG_DONT_FIXATE)) { + uint32_t nvals, choice; + struct spa_pod *v = spa_pod_get_values(&res->value, &nvals, &choice); + const void *vals = SPA_POD_BODY(v); + + if (nvals < 1) + continue; + + if (spa_pod_compare_is_valid_choice(v->type, v->size, + vals, vals, nvals, choice)) { + ((struct spa_pod_choice*)&res->value)->body.type = SPA_CHOICE_None; + count++; + } + } + } + return count; +} +SPA_API_POD_FILTER int spa_pod_filter_make(struct spa_pod *pod) +{ + if (!spa_pod_is_object(pod)) + return -EINVAL; + return spa_pod_filter_object_make((struct spa_pod_object *)pod); +} /** * \} */ diff --git a/spa/include/spa/pod/iter.h b/spa/include/spa/pod/iter.h index 1bd569968..779286e0c 100644 --- a/spa/include/spa/pod/iter.h +++ b/spa/include/spa/pod/iter.h @@ -5,14 +5,15 @@ #ifndef SPA_POD_ITER_H #define SPA_POD_ITER_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#include + +#ifdef __cplusplus +extern "C" { +#endif #ifndef SPA_API_POD_ITER #ifdef SPA_API_IMPL @@ -27,24 +28,17 @@ extern "C" { * \{ */ -struct spa_pod_frame { - struct spa_pod pod; - struct spa_pod_frame *parent; - uint32_t offset; - uint32_t flags; -}; - SPA_API_POD_ITER bool spa_pod_is_inside(const void *pod, uint32_t size, const void *iter) { size_t remaining; return spa_ptr_type_inside(pod, size, iter, struct spa_pod, &remaining) && - remaining >= SPA_POD_BODY_SIZE(iter); + remaining >= SPA_POD_BODY_SIZE(iter); } SPA_API_POD_ITER void *spa_pod_next(const void *iter) { - return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_SIZE(iter), 8), void); + return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_SIZE(iter), SPA_POD_ALIGN), void); } SPA_API_POD_ITER struct spa_pod_prop *spa_pod_prop_first(const struct spa_pod_object_body *body) @@ -58,12 +52,12 @@ SPA_API_POD_ITER bool spa_pod_prop_is_inside(const struct spa_pod_object_body *b size_t remaining; return spa_ptr_type_inside(body, size, iter, struct spa_pod_prop, &remaining) && - remaining >= iter->value.size; + remaining >= iter->value.size; } SPA_API_POD_ITER struct spa_pod_prop *spa_pod_prop_next(const struct spa_pod_prop *iter) { - return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_PROP_SIZE(iter), 8), struct spa_pod_prop); + return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_PROP_SIZE(iter), SPA_POD_ALIGN), struct spa_pod_prop); } SPA_API_POD_ITER struct spa_pod_control *spa_pod_control_first(const struct spa_pod_sequence_body *body) @@ -77,12 +71,12 @@ SPA_API_POD_ITER bool spa_pod_control_is_inside(const struct spa_pod_sequence_bo size_t remaining; return spa_ptr_type_inside(body, size, iter, struct spa_pod_control, &remaining) && - remaining >= iter->value.size; + remaining >= iter->value.size; } SPA_API_POD_ITER struct spa_pod_control *spa_pod_control_next(const struct spa_pod_control *iter) { - return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_CONTROL_SIZE(iter), 8), struct spa_pod_control); + return SPA_PTROFF(iter, SPA_ROUND_UP_N(SPA_POD_CONTROL_SIZE(iter), SPA_POD_ALIGN), struct spa_pod_control); } #define SPA_POD_ARRAY_BODY_FOREACH(body, _size, iter) \ @@ -107,7 +101,7 @@ SPA_API_POD_ITER struct spa_pod_control *spa_pod_control_next(const struct spa_p (iter) = (__typeof__(iter))spa_pod_next(iter)) #define SPA_POD_STRUCT_FOREACH(obj, iter) \ - SPA_POD_FOREACH(SPA_POD_BODY(obj), SPA_POD_BODY_SIZE(obj), iter) + SPA_POD_FOREACH(SPA_POD_STRUCT_BODY(obj), SPA_POD_BODY_SIZE(obj), iter) #define SPA_POD_OBJECT_BODY_FOREACH(body, size, iter) \ for ((iter) = spa_pod_prop_first(body); \ @@ -125,259 +119,121 @@ SPA_API_POD_ITER struct spa_pod_control *spa_pod_control_next(const struct spa_p #define SPA_POD_SEQUENCE_FOREACH(seq, iter) \ SPA_POD_SEQUENCE_BODY_FOREACH(&(seq)->body, SPA_POD_BODY_SIZE(seq), iter) - SPA_API_POD_ITER void *spa_pod_from_data(void *data, size_t maxsize, off_t offset, size_t size) { - void *pod; - if (size < sizeof(struct spa_pod) || offset + size > maxsize) + struct spa_pod pod; + const void *body; + if (spa_pod_body_from_data(data, maxsize, offset, size, &pod, &body) < 0) return NULL; - pod = SPA_PTROFF(data, offset, void); - if (SPA_POD_SIZE(pod) > size) - return NULL; - return pod; -} - -SPA_API_POD_ITER int spa_pod_is_none(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_None); -} - -SPA_API_POD_ITER int spa_pod_is_bool(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Bool && SPA_POD_BODY_SIZE(pod) >= sizeof(int32_t)); + return SPA_PTROFF(data, offset, void); } SPA_API_POD_ITER int spa_pod_get_bool(const struct spa_pod *pod, bool *value) { - if (!spa_pod_is_bool(pod)) - return -EINVAL; - *value = !!SPA_POD_VALUE(struct spa_pod_bool, pod); - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_id(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Id && SPA_POD_BODY_SIZE(pod) >= sizeof(uint32_t)); + return spa_pod_body_get_bool(pod, SPA_POD_BODY_CONST(pod), value); } SPA_API_POD_ITER int spa_pod_get_id(const struct spa_pod *pod, uint32_t *value) { - if (!spa_pod_is_id(pod)) - return -EINVAL; - *value = SPA_POD_VALUE(struct spa_pod_id, pod); - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_int(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Int && SPA_POD_BODY_SIZE(pod) >= sizeof(int32_t)); + return spa_pod_body_get_id(pod, SPA_POD_BODY_CONST(pod), value); } SPA_API_POD_ITER int spa_pod_get_int(const struct spa_pod *pod, int32_t *value) { - if (!spa_pod_is_int(pod)) - return -EINVAL; - *value = SPA_POD_VALUE(struct spa_pod_int, pod); - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_long(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Long && SPA_POD_BODY_SIZE(pod) >= sizeof(int64_t)); + return spa_pod_body_get_int(pod, SPA_POD_BODY_CONST(pod), value); } SPA_API_POD_ITER int spa_pod_get_long(const struct spa_pod *pod, int64_t *value) { - if (!spa_pod_is_long(pod)) - return -EINVAL; - *value = SPA_POD_VALUE(struct spa_pod_long, pod); - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_float(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Float && SPA_POD_BODY_SIZE(pod) >= sizeof(float)); + return spa_pod_body_get_long(pod, SPA_POD_BODY_CONST(pod), value); } SPA_API_POD_ITER int spa_pod_get_float(const struct spa_pod *pod, float *value) { - if (!spa_pod_is_float(pod)) - return -EINVAL; - *value = SPA_POD_VALUE(struct spa_pod_float, pod); - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_double(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Double && SPA_POD_BODY_SIZE(pod) >= sizeof(double)); + return spa_pod_body_get_float(pod, SPA_POD_BODY_CONST(pod), value); } SPA_API_POD_ITER int spa_pod_get_double(const struct spa_pod *pod, double *value) { - if (!spa_pod_is_double(pod)) - return -EINVAL; - *value = SPA_POD_VALUE(struct spa_pod_double, pod); - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_string(const struct spa_pod *pod) -{ - const char *s = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod); - return (SPA_POD_TYPE(pod) == SPA_TYPE_String && - SPA_POD_BODY_SIZE(pod) > 0 && - s[SPA_POD_BODY_SIZE(pod)-1] == '\0'); + return spa_pod_body_get_double(pod, SPA_POD_BODY_CONST(pod), value); } SPA_API_POD_ITER int spa_pod_get_string(const struct spa_pod *pod, const char **value) { - if (!spa_pod_is_string(pod)) - return -EINVAL; - *value = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod); - return 0; + return spa_pod_body_get_string(pod, SPA_POD_BODY_CONST(pod), value); } SPA_API_POD_ITER int spa_pod_copy_string(const struct spa_pod *pod, size_t maxlen, char *dest) { - const char *s = (const char *)SPA_POD_CONTENTS(struct spa_pod_string, pod); - if (!spa_pod_is_string(pod) || maxlen < 1) - return -EINVAL; - strncpy(dest, s, maxlen-1); - dest[maxlen-1]= '\0'; - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_bytes(const struct spa_pod *pod) -{ - return SPA_POD_TYPE(pod) == SPA_TYPE_Bytes; + return spa_pod_body_copy_string(pod, SPA_POD_BODY_CONST(pod), dest, maxlen); } SPA_API_POD_ITER int spa_pod_get_bytes(const struct spa_pod *pod, const void **value, uint32_t *len) { - if (!spa_pod_is_bytes(pod)) - return -EINVAL; - *value = (const void *)SPA_POD_CONTENTS(struct spa_pod_bytes, pod); - *len = SPA_POD_BODY_SIZE(pod); - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_pointer(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Pointer && - SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_pointer_body)); + return spa_pod_body_get_bytes(pod, SPA_POD_BODY_CONST(pod), value, len); } SPA_API_POD_ITER int spa_pod_get_pointer(const struct spa_pod *pod, uint32_t *type, const void **value) { - if (!spa_pod_is_pointer(pod)) - return -EINVAL; - *type = ((struct spa_pod_pointer*)pod)->body.type; - *value = ((struct spa_pod_pointer*)pod)->body.value; - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_fd(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Fd && - SPA_POD_BODY_SIZE(pod) >= sizeof(int64_t)); + return spa_pod_body_get_pointer(pod, SPA_POD_BODY_CONST(pod), type, value); } SPA_API_POD_ITER int spa_pod_get_fd(const struct spa_pod *pod, int64_t *value) { - if (!spa_pod_is_fd(pod)) - return -EINVAL; - *value = SPA_POD_VALUE(struct spa_pod_fd, pod); - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_rectangle(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Rectangle && - SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_rectangle)); + return spa_pod_body_get_fd(pod, SPA_POD_BODY_CONST(pod), value); } SPA_API_POD_ITER int spa_pod_get_rectangle(const struct spa_pod *pod, struct spa_rectangle *value) { - if (!spa_pod_is_rectangle(pod)) - return -EINVAL; - *value = SPA_POD_VALUE(struct spa_pod_rectangle, pod); - return 0; -} - -SPA_API_POD_ITER int spa_pod_is_fraction(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Fraction && - SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_fraction)); + return spa_pod_body_get_rectangle(pod, SPA_POD_BODY_CONST(pod), value); } SPA_API_POD_ITER int spa_pod_get_fraction(const struct spa_pod *pod, struct spa_fraction *value) { - spa_return_val_if_fail(spa_pod_is_fraction(pod), -EINVAL); - *value = SPA_POD_VALUE(struct spa_pod_fraction, pod); - return 0; + return spa_pod_body_get_fraction(pod, SPA_POD_BODY_CONST(pod), value); } -SPA_API_POD_ITER int spa_pod_is_bitmap(const struct spa_pod *pod) +SPA_API_POD_ITER void *spa_pod_get_array_full(const struct spa_pod *pod, uint32_t *n_values, + uint32_t *val_size, uint32_t *val_type) { - return (SPA_POD_TYPE(pod) == SPA_TYPE_Bitmap && - SPA_POD_BODY_SIZE(pod) >= sizeof(uint8_t)); + return (void*)spa_pod_body_get_array_values(pod, SPA_POD_BODY(pod), n_values, val_size, val_type); } - -SPA_API_POD_ITER int spa_pod_is_array(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Array && - SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_array_body)); -} - SPA_API_POD_ITER void *spa_pod_get_array(const struct spa_pod *pod, uint32_t *n_values) { - spa_return_val_if_fail(spa_pod_is_array(pod), NULL); - *n_values = SPA_POD_ARRAY_N_VALUES(pod); - return SPA_POD_ARRAY_VALUES(pod); + uint32_t size, type; + return spa_pod_get_array_full(pod, n_values, &size, &type); } -SPA_API_POD_ITER uint32_t spa_pod_copy_array(const struct spa_pod *pod, uint32_t type, - void *values, uint32_t max_values) +SPA_API_POD_ITER uint32_t spa_pod_copy_array_full(const struct spa_pod *pod, uint32_t type, + uint32_t size, void *values, uint32_t max_values) { - uint32_t n_values; - void *v = spa_pod_get_array(pod, &n_values); - if (v == NULL || max_values == 0 || SPA_POD_ARRAY_VALUE_TYPE(pod) != type) + uint32_t n_values, val_size, val_type; + const void *v = spa_pod_get_array_full(pod, &n_values, &val_size, &val_type); + if (v == NULL || max_values == 0 || val_type != type || val_size != size) return 0; n_values = SPA_MIN(n_values, max_values); - memcpy(values, v, SPA_POD_ARRAY_VALUE_SIZE(pod) * n_values); + memcpy(values, v, val_size * n_values); return n_values; } -SPA_API_POD_ITER int spa_pod_is_choice(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Choice && - SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_choice_body)); -} +#define spa_pod_copy_array(pod,type,values,max_values) \ + spa_pod_copy_array_full(pod,type,sizeof(values[0]),values,max_values) -SPA_API_POD_ITER struct spa_pod *spa_pod_get_values(const struct spa_pod *pod, uint32_t *n_vals, uint32_t *choice) +SPA_API_POD_ITER struct spa_pod *spa_pod_get_values(const struct spa_pod *pod, + uint32_t *n_vals, uint32_t *choice) { - if (pod->type == SPA_TYPE_Choice) { - *n_vals = SPA_POD_CHOICE_N_VALUES(pod); - if ((*choice = SPA_POD_CHOICE_TYPE(pod)) == SPA_CHOICE_None) - *n_vals = SPA_MIN(1u, SPA_POD_CHOICE_N_VALUES(pod)); - return (struct spa_pod*)SPA_POD_CHOICE_CHILD(pod); + if (spa_pod_is_choice(pod)) { + const struct spa_pod_choice *p = (const struct spa_pod_choice*)pod; + uint32_t type, size; + spa_pod_choice_body_get_values(p, SPA_POD_BODY_CONST(p), n_vals, choice, &size, &type); + return (struct spa_pod*)&p->body.child; } else { - *n_vals = 1; + *n_vals = pod->size < spa_pod_type_size(pod->type) ? 0 : 1; *choice = SPA_CHOICE_None; return (struct spa_pod*)pod; } } -SPA_API_POD_ITER int spa_pod_is_struct(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Struct); -} - -SPA_API_POD_ITER int spa_pod_is_object(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Object && - SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_object_body)); -} - SPA_API_POD_ITER bool spa_pod_is_object_type(const struct spa_pod *pod, uint32_t type) { return (pod && spa_pod_is_object(pod) && SPA_POD_OBJECT_TYPE(pod) == type); @@ -388,12 +244,6 @@ SPA_API_POD_ITER bool spa_pod_is_object_id(const struct spa_pod *pod, uint32_t i return (pod && spa_pod_is_object(pod) && SPA_POD_OBJECT_ID(pod) == id); } -SPA_API_POD_ITER int spa_pod_is_sequence(const struct spa_pod *pod) -{ - return (SPA_POD_TYPE(pod) == SPA_TYPE_Sequence && - SPA_POD_BODY_SIZE(pod) >= sizeof(struct spa_pod_sequence_body)); -} - SPA_API_POD_ITER const struct spa_pod_prop *spa_pod_object_find_prop(const struct spa_pod_object *pod, const struct spa_pod_prop *start, uint32_t key) { @@ -422,16 +272,34 @@ SPA_API_POD_ITER const struct spa_pod_prop *spa_pod_find_prop(const struct spa_p return spa_pod_object_find_prop((const struct spa_pod_object *)pod, start, key); } +SPA_API_POD_ITER int spa_pod_object_has_props(const struct spa_pod_object *pod) +{ + struct spa_pod_prop *res; + SPA_POD_OBJECT_FOREACH(pod, res) + return 1; + return 0; +} + SPA_API_POD_ITER int spa_pod_object_fixate(struct spa_pod_object *pod) { struct spa_pod_prop *res; SPA_POD_OBJECT_FOREACH(pod, res) { - if (res->value.type == SPA_TYPE_Choice && + if (spa_pod_is_choice(&res->value) && !SPA_FLAG_IS_SET(res->flags, SPA_POD_PROP_FLAG_DONT_FIXATE)) ((struct spa_pod_choice*)&res->value)->body.type = SPA_CHOICE_None; } return 0; } +SPA_API_POD_ITER int spa_pod_object_is_fixated(const struct spa_pod_object *pod) +{ + struct spa_pod_prop *res; + SPA_POD_OBJECT_FOREACH(pod, res) { + if (spa_pod_is_choice(&res->value) && + ((struct spa_pod_choice*)&res->value)->body.type != SPA_CHOICE_None) + return 0; + } + return 1; +} SPA_API_POD_ITER int spa_pod_fixate(struct spa_pod *pod) { @@ -440,25 +308,6 @@ SPA_API_POD_ITER int spa_pod_fixate(struct spa_pod *pod) return spa_pod_object_fixate((struct spa_pod_object *)pod); } -SPA_API_POD_ITER int spa_pod_object_is_fixated(const struct spa_pod_object *pod) -{ - struct spa_pod_prop *res; - SPA_POD_OBJECT_FOREACH(pod, res) { - if (res->value.type == SPA_TYPE_Choice && - ((struct spa_pod_choice*)&res->value)->body.type != SPA_CHOICE_None) - return 0; - } - return 1; -} - -SPA_API_POD_ITER int spa_pod_object_has_props(const struct spa_pod_object *pod) -{ - struct spa_pod_prop *res; - SPA_POD_OBJECT_FOREACH(pod, res) - return 1; - return 0; -} - SPA_API_POD_ITER int spa_pod_is_fixated(const struct spa_pod *pod) { if (!spa_pod_is_object(pod)) @@ -474,4 +323,4 @@ SPA_API_POD_ITER int spa_pod_is_fixated(const struct spa_pod *pod) } /* extern "C" */ #endif -#endif /* SPA_POD_H */ +#endif /* SPA_POD_ITER_H */ diff --git a/spa/include/spa/pod/parser.h b/spa/include/spa/pod/parser.h index d2aa206ba..0e687ac9d 100644 --- a/spa/include/spa/pod/parser.h +++ b/spa/include/spa/pod/parser.h @@ -5,16 +5,16 @@ #ifndef SPA_POD_PARSER_H #define SPA_POD_PARSER_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include -#include +#include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_POD_PARSER #ifdef SPA_API_IMPL #define SPA_API_POD_PARSER SPA_API_IMPL @@ -55,6 +55,22 @@ SPA_API_POD_PARSER void spa_pod_parser_pod(struct spa_pod_parser *parser, spa_pod_parser_init(parser, pod, SPA_POD_SIZE(pod)); } +SPA_API_POD_PARSER void spa_pod_parser_init_pod_body(struct spa_pod_parser *parser, + const struct spa_pod *pod, const void *body) +{ + spa_pod_parser_init(parser, + SPA_PTROFF(body, -sizeof(struct spa_pod), const struct spa_pod), + pod->size + sizeof(struct spa_pod)); +} +SPA_API_POD_PARSER void spa_pod_parser_init_from_data(struct spa_pod_parser *parser, + const void *data, uint32_t maxsize, uint32_t offset, uint32_t size) +{ + size_t offs, sz; + offs = SPA_MIN(offset, maxsize); + sz = SPA_MIN(maxsize - offs, size); + spa_pod_parser_init(parser, SPA_PTROFF(data, offs, void), sz); +} + SPA_API_POD_PARSER void spa_pod_parser_get_state(struct spa_pod_parser *parser, struct spa_pod_parser_state *state) { @@ -67,23 +83,37 @@ spa_pod_parser_reset(struct spa_pod_parser *parser, struct spa_pod_parser_state parser->state = *state; } +SPA_API_POD_PARSER int +spa_pod_parser_read_header(struct spa_pod_parser *parser, uint32_t offset, uint32_t size, + void *header, uint32_t header_size, uint32_t pod_offset, const void **body) +{ + /* Cast to uint64_t to avoid wraparound. */ + const uint64_t long_offset = (uint64_t)offset + header_size; + if (long_offset <= size && (offset & 7) == 0) { + /* a barrier around the memcpy to make sure it is not moved around or + * duplicated after the size check below. We need to to work on shared + * memory while there could be updates happening while we read. */ + SPA_BARRIER; + memcpy(header, SPA_PTROFF(parser->data, offset, void), header_size); + SPA_BARRIER; + struct spa_pod *pod = SPA_PTROFF(header, pod_offset, struct spa_pod); + /* Check that the size (rounded to the next multiple of 8) is in bounds. */ + if (long_offset + SPA_ROUND_UP_N((uint64_t)pod->size, SPA_POD_ALIGN) <= size) { + *body = SPA_PTROFF(parser->data, long_offset, void); + return 0; + } + } + return -EPIPE; +} + SPA_API_POD_PARSER struct spa_pod * spa_pod_parser_deref(struct spa_pod_parser *parser, uint32_t offset, uint32_t size) { - /* Cast to uint64_t to avoid wraparound. Add 8 for the pod itself. */ - const uint64_t long_offset = (uint64_t)offset + 8; - if (long_offset <= size && (offset & 7) == 0) { - /* Use void* because creating a misaligned pointer is undefined. */ - void *pod = SPA_PTROFF(parser->data, offset, void); - /* - * Check that the pointer is aligned and that the size (rounded - * to the next multiple of 8) is in bounds. - */ - if (SPA_IS_ALIGNED(pod, __alignof__(struct spa_pod)) && - long_offset + SPA_ROUND_UP_N((uint64_t)SPA_POD_BODY_SIZE(pod), 8) <= size) - return (struct spa_pod *)pod; - } - return NULL; + struct spa_pod pod; + const void *body; + if (spa_pod_parser_read_header(parser, offset, size, &pod, sizeof(pod), 0, &body) < 0) + return NULL; + return SPA_PTROFF(body, -sizeof(pod), struct spa_pod); } SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_frame(struct spa_pod_parser *parser, struct spa_pod_frame *frame) @@ -101,198 +131,400 @@ SPA_API_POD_PARSER void spa_pod_parser_push(struct spa_pod_parser *parser, parser->state.frame = frame; } -SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_current(struct spa_pod_parser *parser) +SPA_API_POD_PARSER int spa_pod_parser_get_header(struct spa_pod_parser *parser, + void *header, uint32_t header_size, uint32_t pod_offset, const void **body) { struct spa_pod_frame *f = parser->state.frame; uint32_t size = f ? f->offset + SPA_POD_SIZE(&f->pod) : parser->size; - return spa_pod_parser_deref(parser, parser->state.offset, size); + return spa_pod_parser_read_header(parser, parser->state.offset, size, + header, header_size, pod_offset, body); +} + +SPA_API_POD_PARSER int spa_pod_parser_current_body(struct spa_pod_parser *parser, + struct spa_pod *pod, const void **body) +{ + return spa_pod_parser_get_header(parser, pod, sizeof(struct spa_pod), 0, body); +} + +SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_current(struct spa_pod_parser *parser) +{ + struct spa_pod pod; + const void *body; + if (spa_pod_parser_current_body(parser, &pod, &body) < 0) + return NULL; + return SPA_PTROFF(body, -sizeof(struct spa_pod), struct spa_pod); } SPA_API_POD_PARSER void spa_pod_parser_advance(struct spa_pod_parser *parser, const struct spa_pod *pod) { - parser->state.offset += SPA_ROUND_UP_N(SPA_POD_SIZE(pod), 8); + parser->state.offset += SPA_ROUND_UP_N(SPA_POD_SIZE(pod), SPA_POD_ALIGN); +} + +SPA_API_POD_PARSER int spa_pod_parser_next_body(struct spa_pod_parser *parser, + struct spa_pod *pod, const void **body) +{ + if (spa_pod_parser_current_body(parser, pod, body) < 0) + return -EINVAL; + spa_pod_parser_advance(parser, pod); + return 0; } SPA_API_POD_PARSER struct spa_pod *spa_pod_parser_next(struct spa_pod_parser *parser) { - struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod) - spa_pod_parser_advance(parser, pod); - return pod; + struct spa_pod pod; + const void *body; + if (spa_pod_parser_current_body(parser, &pod, &body) < 0) + return NULL; + spa_pod_parser_advance(parser, &pod); + return SPA_PTROFF(body, -sizeof(struct spa_pod), struct spa_pod); +} + +SPA_API_POD_PARSER void spa_pod_parser_restart(struct spa_pod_parser *parser, + struct spa_pod_frame *frame) +{ + parser->state.offset = frame->offset; +} + +SPA_API_POD_PARSER void spa_pod_parser_unpush(struct spa_pod_parser *parser, + struct spa_pod_frame *frame) +{ + spa_pod_parser_restart(parser, frame); + parser->state.frame = frame->parent; } SPA_API_POD_PARSER int spa_pod_parser_pop(struct spa_pod_parser *parser, struct spa_pod_frame *frame) { - parser->state.frame = frame->parent; - parser->state.offset = frame->offset + SPA_ROUND_UP_N(SPA_POD_SIZE(&frame->pod), 8); + spa_pod_parser_unpush(parser, frame); + spa_pod_parser_advance(parser, &frame->pod); return 0; } SPA_API_POD_PARSER int spa_pod_parser_get_bool(struct spa_pod_parser *parser, bool *value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_bool(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_bool(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_id(struct spa_pod_parser *parser, uint32_t *value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_id(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_id(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_int(struct spa_pod_parser *parser, int32_t *value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_int(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_int(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_long(struct spa_pod_parser *parser, int64_t *value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_long(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_long(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_float(struct spa_pod_parser *parser, float *value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_float(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_float(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_double(struct spa_pod_parser *parser, double *value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_double(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_double(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_string(struct spa_pod_parser *parser, const char **value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_string(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_string(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_bytes(struct spa_pod_parser *parser, const void **value, uint32_t *len) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_bytes(pod, value, len)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_bytes(&pod, body, value, len)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_pointer(struct spa_pod_parser *parser, uint32_t *type, const void **value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_pointer(pod, type, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_pointer(&pod, body, type, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_fd(struct spa_pod_parser *parser, int64_t *value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_fd(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_fd(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_rectangle(struct spa_pod_parser *parser, struct spa_rectangle *value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_rectangle(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_rectangle(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } SPA_API_POD_PARSER int spa_pod_parser_get_fraction(struct spa_pod_parser *parser, struct spa_fraction *value) { - int res = -EPIPE; - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod != NULL && (res = spa_pod_get_fraction(pod, value)) >= 0) - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_fraction(&pod, body, value)) >= 0) + spa_pod_parser_advance(parser, &pod); return res; } +SPA_API_POD_PARSER int spa_pod_parser_get_pod_body(struct spa_pod_parser *parser, + struct spa_pod *value, const void **body) +{ + int res; + if ((res = spa_pod_parser_current_body(parser, value, body)) < 0) + return res; + spa_pod_parser_advance(parser, value); + return 0; +} + SPA_API_POD_PARSER int spa_pod_parser_get_pod(struct spa_pod_parser *parser, struct spa_pod **value) { - struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod == NULL) - return -EPIPE; - *value = pod; - spa_pod_parser_advance(parser, pod); + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_get_pod_body(parser, &pod, &body)) < 0) + return res; + *value = SPA_PTROFF(body, -sizeof(struct spa_pod), struct spa_pod); return 0; } -SPA_API_POD_PARSER int spa_pod_parser_push_struct(struct spa_pod_parser *parser, - struct spa_pod_frame *frame) + +SPA_API_POD_PARSER int spa_pod_parser_init_struct_body(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, const struct spa_pod *pod, const void *body) { - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod == NULL) - return -EPIPE; if (!spa_pod_is_struct(pod)) return -EINVAL; + spa_pod_parser_init_pod_body(parser, pod, body); spa_pod_parser_push(parser, frame, pod, parser->state.offset); parser->state.offset += sizeof(struct spa_pod_struct); return 0; } -SPA_API_POD_PARSER int spa_pod_parser_push_object(struct spa_pod_parser *parser, - struct spa_pod_frame *frame, uint32_t type, uint32_t *id) +SPA_API_POD_PARSER int spa_pod_parser_push_struct_body(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, struct spa_pod *str, const void **str_body) { - const struct spa_pod *pod = spa_pod_parser_current(parser); - if (pod == NULL) - return -EPIPE; + int res; + if ((res = spa_pod_parser_current_body(parser, str, str_body)) < 0) + return res; + if (!spa_pod_is_struct(str)) + return -EINVAL; + spa_pod_parser_push(parser, frame, str, parser->state.offset); + parser->state.offset += sizeof(struct spa_pod_struct); + return 0; +} +SPA_API_POD_PARSER int spa_pod_parser_push_struct(struct spa_pod_parser *parser, + struct spa_pod_frame *frame) +{ + struct spa_pod pod; + const void *body; + return spa_pod_parser_push_struct_body(parser, frame, &pod, &body); +} + +SPA_API_POD_PARSER int spa_pod_parser_init_object_body(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, const struct spa_pod *pod, const void *body, + struct spa_pod_object *object, const void **object_body) +{ + int res; if (!spa_pod_is_object(pod)) return -EINVAL; - if (type != SPA_POD_OBJECT_TYPE(pod)) - return -EPROTO; - if (id != NULL) - *id = SPA_POD_OBJECT_ID(pod); + spa_pod_parser_init_pod_body(parser, pod, body); + if ((res = spa_pod_body_get_object(pod, body, object, object_body)) < 0) + return res; spa_pod_parser_push(parser, frame, pod, parser->state.offset); - parser->state.offset = parser->size; + parser->state.offset += sizeof(struct spa_pod_object); return 0; } -SPA_API_POD_PARSER bool spa_pod_parser_can_collect(const struct spa_pod *pod, char type) +SPA_API_POD_PARSER int spa_pod_parser_push_object_body(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, struct spa_pod_object *object, const void **object_body) { + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_object(&pod, body, object, object_body)) < 0) + return res; + spa_pod_parser_push(parser, frame, &pod, parser->state.offset); + parser->state.offset += sizeof(struct spa_pod_object); + return 0; +} +SPA_API_POD_PARSER int spa_pod_parser_push_object(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, uint32_t type, uint32_t *id) +{ + int res; + struct spa_pod_object obj; + const void *obj_body; + if ((res = spa_pod_parser_push_object_body(parser, frame, &obj, &obj_body)) < 0) + return res; + if (type != obj.body.type) { + spa_pod_parser_unpush(parser, frame); + return -EPROTO; + } + if (id != NULL) + *id = obj.body.id; + return 0; +} +SPA_API_POD_PARSER int spa_pod_parser_get_prop_body(struct spa_pod_parser *parser, + struct spa_pod_prop *prop, const void **body) +{ + int res; + if ((res = spa_pod_parser_get_header(parser, prop, + sizeof(struct spa_pod_prop), + offsetof(struct spa_pod_prop, value), body)) >= 0) + parser->state.offset += SPA_ROUND_UP_N(SPA_POD_PROP_SIZE(prop), SPA_POD_ALIGN); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_push_sequence_body(struct spa_pod_parser *parser, + struct spa_pod_frame *frame, struct spa_pod_sequence *seq, const void **seq_body) +{ + int res; + struct spa_pod pod; + const void *body; + if ((res = spa_pod_parser_current_body(parser, &pod, &body)) < 0) + return res; + if ((res = spa_pod_body_get_sequence(&pod, body, seq, seq_body)) < 0) + return res; + spa_pod_parser_push(parser, frame, &pod, parser->state.offset); + parser->state.offset += sizeof(struct spa_pod_sequence); + return 0; +} + +SPA_API_POD_PARSER int spa_pod_parser_get_control_body(struct spa_pod_parser *parser, + struct spa_pod_control *control, const void **body) +{ + int res; + if ((res = spa_pod_parser_get_header(parser, control, + sizeof(struct spa_pod_control), + offsetof(struct spa_pod_control, value), body)) >= 0) + parser->state.offset += SPA_ROUND_UP_N(SPA_POD_CONTROL_SIZE(control), SPA_POD_ALIGN); + return res; +} + +SPA_API_POD_PARSER int spa_pod_parser_object_find_prop(struct spa_pod_parser *parser, + uint32_t key, struct spa_pod_prop *prop, const void **body) +{ + uint32_t start_offset; + struct spa_pod_frame *f = parser->state.frame; + + if (f == NULL || f->pod.type != SPA_TYPE_Object) + return -EINVAL; + + start_offset = f->offset; + while (spa_pod_parser_get_prop_body(parser, prop, body) >= 0) { + if (prop->key == key) + return 0; + } + spa_pod_parser_restart(parser, f); + parser->state.offset += sizeof(struct spa_pod_object); + while (parser->state.offset != start_offset && + spa_pod_parser_get_prop_body(parser, prop, body) >= 0) { + if (prop->key == key) + return 0; + } + *body = NULL; + return -ENOENT; +} + +SPA_API_POD_PARSER bool spa_pod_parser_body_can_collect(const struct spa_pod *pod, const void *body, char type) +{ + struct spa_pod_choice choice; + if (pod == NULL) return false; - if (SPA_POD_TYPE(pod) == SPA_TYPE_Choice) { + if (pod->type == SPA_TYPE_Choice) { if (!spa_pod_is_choice(pod)) return false; - if (type == 'V') + if (type == 'V' || type == 'W') return true; - if (SPA_POD_CHOICE_TYPE(pod) != SPA_CHOICE_None) + if (spa_pod_body_get_choice(pod, body, &choice, &body) < 0) return false; - pod = SPA_POD_CHOICE_CHILD(pod); + if (choice.body.type != SPA_CHOICE_None) + return false; + pod = &choice.body.child; } switch (type) { case 'P': + case 'Q': return true; case 'b': return spa_pod_is_bool(pod); @@ -325,96 +557,193 @@ SPA_API_POD_PARSER bool spa_pod_parser_can_collect(const struct spa_pod *pod, ch case 'h': return spa_pod_is_fd(pod); case 'T': + case 'U': return spa_pod_is_struct(pod) || spa_pod_is_none(pod); + case 'N': case 'O': return spa_pod_is_object(pod) || spa_pod_is_none(pod); case 'V': + case 'W': default: return false; } } +SPA_API_POD_PARSER bool spa_pod_parser_can_collect(const struct spa_pod *pod, char type) +{ + return spa_pod_parser_body_can_collect(pod, SPA_POD_BODY_CONST(pod), type); +} -#define SPA_POD_PARSER_COLLECT(pod,_type,args) \ -do { \ +#define SPA_POD_PARSER_COLLECT_BODY(_pod,_body,_type,args) \ +({ \ + int res = 0; \ + struct spa_pod_choice choice; \ + const struct spa_pod *_p = _pod; \ + const void *_b = _body; \ + if (_p->type == SPA_TYPE_Choice && _type != 'V' && _type != 'W') { \ + if (spa_pod_body_get_choice(_p, _b, &choice, &_b) >= 0 && \ + choice.body.type == SPA_CHOICE_None) \ + _p = &choice.body.child; \ + } \ switch (_type) { \ case 'b': \ - *va_arg(args, bool*) = SPA_POD_VALUE(struct spa_pod_bool, pod); \ + { \ + bool *val = va_arg(args, bool*); \ + res = spa_pod_body_get_bool(_p, _b, val); \ break; \ + } \ case 'I': \ + { \ + uint32_t *val = va_arg(args, uint32_t*); \ + res = spa_pod_body_get_id(_p, _b, val); \ + break; \ + } \ case 'i': \ - *va_arg(args, int32_t*) = SPA_POD_VALUE(struct spa_pod_int, pod); \ + { \ + int32_t *val = va_arg(args, int32_t*); \ + res = spa_pod_body_get_int(_p, _b, val); \ break; \ + } \ case 'l': \ - *va_arg(args, int64_t*) = SPA_POD_VALUE(struct spa_pod_long, pod); \ + { \ + int64_t *val = va_arg(args, int64_t*); \ + res = spa_pod_body_get_long(_p, _b, val); \ break; \ + } \ case 'f': \ - *va_arg(args, float*) = SPA_POD_VALUE(struct spa_pod_float, pod); \ + { \ + float *val = va_arg(args, float*); \ + res = spa_pod_body_get_float(_p, _b, val); \ break; \ + } \ case 'd': \ - *va_arg(args, double*) = SPA_POD_VALUE(struct spa_pod_double, pod); \ + { \ + double *val = va_arg(args, double*); \ + res = spa_pod_body_get_double(_p, _b, val); \ break; \ + } \ case 's': \ - *va_arg(args, char**) = \ - ((pod) == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None) \ - ? NULL \ - : (char *)SPA_POD_CONTENTS(struct spa_pod_string, pod)); \ + { \ + const char **dest = va_arg(args, const char**); \ + if (_p->type == SPA_TYPE_None) \ + *dest = NULL; \ + else \ + res = spa_pod_body_get_string(_p, _b, dest); \ break; \ + } \ case 'S': \ { \ char *dest = va_arg(args, char*); \ uint32_t maxlen = va_arg(args, uint32_t); \ - strncpy(dest, (char *)SPA_POD_CONTENTS(struct spa_pod_string, pod), maxlen-1); \ - dest[maxlen-1] = '\0'; \ + res = spa_pod_body_copy_string(_p, _b, dest, maxlen); \ break; \ } \ case 'y': \ - *(va_arg(args, void **)) = SPA_POD_CONTENTS(struct spa_pod_bytes, pod); \ - *(va_arg(args, uint32_t *)) = SPA_POD_BODY_SIZE(pod); \ + { \ + const void **value = va_arg(args, const void**); \ + uint32_t *len = va_arg(args, uint32_t*); \ + res = spa_pod_body_get_bytes(_p, _b, value, len); \ break; \ + } \ case 'R': \ - *va_arg(args, struct spa_rectangle*) = \ - SPA_POD_VALUE(struct spa_pod_rectangle, pod); \ + { \ + struct spa_rectangle *val = va_arg(args, struct spa_rectangle*); \ + res = spa_pod_body_get_rectangle(_p, _b, val); \ break; \ + } \ case 'F': \ - *va_arg(args, struct spa_fraction*) = \ - SPA_POD_VALUE(struct spa_pod_fraction, pod); \ + { \ + struct spa_fraction *val = va_arg(args, struct spa_fraction*); \ + res = spa_pod_body_get_fraction(_p, _b, val); \ break; \ + } \ case 'B': \ - *va_arg(args, uint32_t **) = \ - (uint32_t *) SPA_POD_CONTENTS(struct spa_pod_bitmap, pod); \ + { \ + const uint8_t **val = va_arg(args, const uint8_t**); \ + res = spa_pod_body_get_bitmap(_p, _b, val); \ break; \ + } \ case 'a': \ - *va_arg(args, uint32_t*) = SPA_POD_ARRAY_VALUE_SIZE(pod); \ - *va_arg(args, uint32_t*) = SPA_POD_ARRAY_VALUE_TYPE(pod); \ - *va_arg(args, uint32_t*) = SPA_POD_ARRAY_N_VALUES(pod); \ - *va_arg(args, void**) = SPA_POD_ARRAY_VALUES(pod); \ + { \ + uint32_t *val_size = va_arg(args, uint32_t*); \ + uint32_t *val_type = va_arg(args, uint32_t*); \ + uint32_t *n_values = va_arg(args, uint32_t*); \ + const void **arr_body = va_arg(args, const void**); \ + *arr_body = spa_pod_body_get_array_values(_p, _b, \ + n_values, val_size, val_type); \ + if (*arr_body == NULL) \ + res = -EINVAL; \ break; \ + } \ case 'p': \ { \ - struct spa_pod_pointer_body *b = \ - (struct spa_pod_pointer_body *) SPA_POD_BODY(pod); \ - *(va_arg(args, uint32_t *)) = b->type; \ - *(va_arg(args, const void **)) = b->value; \ + uint32_t *type = va_arg(args, uint32_t*); \ + const void **value = va_arg(args, const void**); \ + res = spa_pod_body_get_pointer(_p, _b, type, value); \ break; \ } \ case 'h': \ - *va_arg(args, int64_t*) = SPA_POD_VALUE(struct spa_pod_fd, pod); \ - break; \ - case 'P': \ - case 'T': \ - case 'O': \ - case 'V': \ { \ - const struct spa_pod **d = va_arg(args, const struct spa_pod**); \ - if (d) \ - *d = ((pod) == NULL || (SPA_POD_TYPE(pod) == SPA_TYPE_None) \ - ? NULL : (pod)); \ + int64_t *val = va_arg(args, int64_t*); \ + res = spa_pod_body_get_fd(_p, _b, val); \ break; \ } \ default: \ + { \ + bool valid = false, do_body = false; \ + switch (_type) { \ + case 'Q': \ + do_body = true; \ + SPA_FALLTHROUGH; \ + case 'P': \ + valid = true; \ + break; \ + case 'U': \ + do_body = true; \ + SPA_FALLTHROUGH; \ + case 'T': \ + valid = spa_pod_is_struct(_p) || spa_pod_is_none(_p); \ + break; \ + case 'N': \ + do_body = true; \ + SPA_FALLTHROUGH; \ + case 'O': \ + valid = spa_pod_is_object(_p) || spa_pod_is_none(_p); \ + break; \ + case 'W': \ + do_body = true; \ + SPA_FALLTHROUGH; \ + case 'V': \ + valid = spa_pod_is_choice(_p) || spa_pod_is_none(_p); \ + break; \ + default: \ + res = -EINVAL; \ + break; \ + } \ + if (res >= 0 && do_body) { \ + struct spa_pod *p = va_arg(args, struct spa_pod*); \ + const void **v = va_arg(args, const void **); \ + if (valid && p && v) { \ + *p = *_p; \ + *v = _b; \ + } \ + } else if (res >= 0) { \ + const struct spa_pod **d = va_arg(args, const struct spa_pod**);\ + if (valid && d) \ + *d = (_p->type == SPA_TYPE_None) ? \ + NULL : \ + SPA_PTROFF((_b), -sizeof(struct spa_pod), \ + const struct spa_pod); \ + } \ + if (!valid) \ + res = -EINVAL; \ break; \ } \ -} while(false) + } \ + res; \ +}) + +#define SPA_POD_PARSER_COLLECT(pod,_type,args) \ + SPA_POD_PARSER_COLLECT_BODY(pod, SPA_POD_BODY_CONST(pod),_type,args) #define SPA_POD_PARSER_SKIP(_type,args) \ do { \ @@ -446,6 +775,10 @@ do { \ case 'P': \ case 'T': \ case 'O': \ + case 'W': \ + case 'Q': \ + case 'U': \ + case 'N': \ va_arg(args, void*); \ break; \ } \ @@ -454,50 +787,50 @@ do { \ SPA_API_POD_PARSER int spa_pod_parser_getv(struct spa_pod_parser *parser, va_list args) { struct spa_pod_frame *f = parser->state.frame; - uint32_t ftype = f ? f->pod.type : (uint32_t)SPA_TYPE_Struct; - const struct spa_pod_prop *prop = NULL; int count = 0; + if (f == NULL) + return -EINVAL; + do { bool optional; - const struct spa_pod *pod = NULL; + struct spa_pod pod = (struct spa_pod) { 0, SPA_TYPE_None }; + const void *body = NULL; const char *format; + struct spa_pod_prop prop; - if (f && ftype == SPA_TYPE_Object) { - uint32_t key = va_arg(args, uint32_t); - const struct spa_pod_object *object; + if (f->pod.type == SPA_TYPE_Object) { + uint32_t key = va_arg(args, uint32_t), *flags = NULL; if (key == 0) break; - - object = (const struct spa_pod_object *)spa_pod_parser_frame(parser, f); - prop = spa_pod_object_find_prop(object, prop, key); - pod = prop ? &prop->value : NULL; + if (key == SPA_ID_INVALID) { + key = va_arg(args, uint32_t); + flags = va_arg(args, uint32_t*); + } + if (spa_pod_parser_object_find_prop(parser, key, &prop, &body) >= 0) { + pod = prop.value; + if (flags) + *flags = prop.flags; + } } if ((format = va_arg(args, char *)) == NULL) break; - if (ftype == SPA_TYPE_Struct) - pod = spa_pod_parser_next(parser); + if (f->pod.type == SPA_TYPE_Struct) + spa_pod_parser_next_body(parser, &pod, &body); if ((optional = (*format == '?'))) format++; - if (!spa_pod_parser_can_collect(pod, *format)) { - if (!optional) { - if (pod == NULL) - return -ESRCH; - else - return -EPROTO; - } - SPA_POD_PARSER_SKIP(*format, args); - } else { - if (pod->type == SPA_TYPE_Choice && *format != 'V') - pod = SPA_POD_CHOICE_CHILD(pod); - - SPA_POD_PARSER_COLLECT(pod, *format, args); + if (SPA_POD_PARSER_COLLECT_BODY(&pod, body, *format, args) >= 0) { count++; + } else if (!optional) { + if (body == NULL) + return -ESRCH; + else + return -EPROTO; } } while (true); @@ -534,6 +867,10 @@ SPA_API_POD_PARSER int spa_pod_parser_get(struct spa_pod_parser *parser, ...) #define SPA_POD_OPT_PodObject(val) "?" SPA_POD_PodObject(val) #define SPA_POD_OPT_PodStruct(val) "?" SPA_POD_PodStruct(val) #define SPA_POD_OPT_PodChoice(val) "?" SPA_POD_PodChoice(val) +#define SPA_POD_OPT_PodBody(val,body) "?" SPA_POD_PodBody(val,body) +#define SPA_POD_OPT_PodBodyObject(val,body) "?" SPA_POD_PodBodyObject(val,body) +#define SPA_POD_OPT_PodBodyStruct(val,body) "?" SPA_POD_PodBodyStruct(val,body) +#define SPA_POD_OPT_PodBodyChoice(val,body) "?" SPA_POD_PodBodyChoice(val,body) #define spa_pod_parser_get_object(p,type,id,...) \ ({ \ @@ -557,20 +894,25 @@ SPA_API_POD_PARSER int spa_pod_parser_get(struct spa_pod_parser *parser, ...) _res; \ }) -#define spa_pod_parse_object(pod,type,id,...) \ +#define spa_pod_body_parse_object(pod,body,type,id,...) \ ({ \ struct spa_pod_parser _p; \ - spa_pod_parser_pod(&_p, pod); \ + spa_pod_parser_init_pod_body(&_p, pod, body); \ spa_pod_parser_get_object(&_p,type,id,##__VA_ARGS__); \ }) -#define spa_pod_parse_struct(pod,...) \ +#define spa_pod_parse_object(pod,type,id,...) \ + spa_pod_body_parse_object(pod,SPA_POD_BODY_CONST(pod),type,id,##__VA_ARGS__) + +#define spa_pod_body_parse_struct(pod,body,...) \ ({ \ struct spa_pod_parser _p; \ - spa_pod_parser_pod(&_p, pod); \ + spa_pod_parser_init_pod_body(&_p, pod, body); \ spa_pod_parser_get_struct(&_p,##__VA_ARGS__); \ }) +#define spa_pod_parse_struct(pod,...) \ + spa_pod_body_parse_struct(pod,SPA_POD_BODY_CONST(pod),##__VA_ARGS__) /** * \} */ diff --git a/spa/include/spa/pod/pod.h b/spa/include/spa/pod/pod.h index f7627f48e..e20990cc6 100644 --- a/spa/include/spa/pod/pod.h +++ b/spa/include/spa/pod/pod.h @@ -5,18 +5,21 @@ #ifndef SPA_POD_H #define SPA_POD_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** * \addtogroup spa_pod * \{ */ +#define SPA_POD_ALIGN 8 +#define SPA_POD_MAX_SIZE (1u<<20) + #define SPA_POD_BODY_SIZE(pod) (((struct spa_pod*)(pod))->size) #define SPA_POD_TYPE(pod) (((struct spa_pod*)(pod))->type) #define SPA_POD_SIZE(pod) ((uint64_t)sizeof(struct spa_pod) + SPA_POD_BODY_SIZE(pod)) @@ -27,6 +30,15 @@ extern "C" { #define SPA_POD_BODY(pod) SPA_PTROFF((pod),sizeof(struct spa_pod),void) #define SPA_POD_BODY_CONST(pod) SPA_PTROFF((pod),sizeof(struct spa_pod),const void) +#define SPA_POD_IS_VALID(pod) \ + (SPA_POD_BODY_SIZE(pod) < SPA_POD_MAX_SIZE) +#define SPA_POD_CHECK_TYPE(pod,_type) \ + (SPA_POD_IS_VALID(pod) && \ + (pod)->type == (_type)) +#define SPA_POD_CHECK(pod,_type,_size) \ + (SPA_POD_CHECK_TYPE(pod,_type) && (pod)->size >= (_size)) + + struct spa_pod { uint32_t size; /* size of the body */ uint32_t type; /* a basic id of enum spa_type */ @@ -94,8 +106,8 @@ struct spa_pod_bitmap { }; #define SPA_POD_ARRAY_CHILD(arr) (&((struct spa_pod_array*)(arr))->body.child) -#define SPA_POD_ARRAY_VALUE_TYPE(arr) (SPA_POD_TYPE(SPA_POD_ARRAY_CHILD(arr))) -#define SPA_POD_ARRAY_VALUE_SIZE(arr) (SPA_POD_BODY_SIZE(SPA_POD_ARRAY_CHILD(arr))) +#define SPA_POD_ARRAY_VALUE_TYPE(arr) (SPA_POD_ARRAY_CHILD(arr)->type) +#define SPA_POD_ARRAY_VALUE_SIZE(arr) (SPA_POD_ARRAY_CHILD(arr)->size) #define SPA_POD_ARRAY_N_VALUES(arr) (SPA_POD_ARRAY_VALUE_SIZE(arr) ? ((SPA_POD_BODY_SIZE(arr) - sizeof(struct spa_pod_array_body)) / SPA_POD_ARRAY_VALUE_SIZE(arr)) : 0) #define SPA_POD_ARRAY_VALUES(arr) SPA_POD_CONTENTS(struct spa_pod_array, arr) @@ -112,8 +124,8 @@ struct spa_pod_array { #define SPA_POD_CHOICE_CHILD(choice) (&((struct spa_pod_choice*)(choice))->body.child) #define SPA_POD_CHOICE_TYPE(choice) (((struct spa_pod_choice*)(choice))->body.type) #define SPA_POD_CHOICE_FLAGS(choice) (((struct spa_pod_choice*)(choice))->body.flags) -#define SPA_POD_CHOICE_VALUE_TYPE(choice) (SPA_POD_TYPE(SPA_POD_CHOICE_CHILD(choice))) -#define SPA_POD_CHOICE_VALUE_SIZE(choice) (SPA_POD_BODY_SIZE(SPA_POD_CHOICE_CHILD(choice))) +#define SPA_POD_CHOICE_VALUE_TYPE(choice) (SPA_POD_CHOICE_CHILD(choice)->type) +#define SPA_POD_CHOICE_VALUE_SIZE(choice) (SPA_POD_CHOICE_CHILD(choice)->size) #define SPA_POD_CHOICE_N_VALUES(choice) (SPA_POD_CHOICE_VALUE_SIZE(choice) ? ((SPA_POD_BODY_SIZE(choice) - sizeof(struct spa_pod_choice_body)) / SPA_POD_CHOICE_VALUE_SIZE(choice)) : 0) #define SPA_POD_CHOICE_VALUES(choice) (SPA_POD_CONTENTS(struct spa_pod_choice, choice)) @@ -122,7 +134,7 @@ enum spa_choice_type { SPA_CHOICE_Range, /**< range: default, min, max */ SPA_CHOICE_Step, /**< range with step: default, min, max, step */ SPA_CHOICE_Enum, /**< list: default, alternative,... */ - SPA_CHOICE_Flags, /**< flags: default, possible flags,... */ + SPA_CHOICE_Flags, /**< flags: first value is flags */ }; struct spa_pod_choice_body { @@ -138,6 +150,9 @@ struct spa_pod_choice { struct spa_pod_choice_body body; }; +#define SPA_POD_STRUCT_BODY(pod) SPA_PTROFF((pod),sizeof(struct spa_pod),struct spa_pod) +#define SPA_POD_STRUCT_BODY_CONST(pod) SPA_PTROFF((pod),sizeof(struct spa_pod),const struct spa_pod) + struct spa_pod_struct { struct spa_pod pod; /* one or more spa_pod follow */ @@ -186,8 +201,11 @@ struct spa_pod_prop { * Int : n_items, * (String : key, * String : value)*)) */ -#define SPA_POD_PROP_FLAG_MANDATORY (1u<<3) /**< is mandatory */ +#define SPA_POD_PROP_FLAG_MANDATORY (1u<<3) /**< is mandatory, when filtering, both sides + * need this property or filtering fails. */ #define SPA_POD_PROP_FLAG_DONT_FIXATE (1u<<4) /**< choices need no fixation */ +#define SPA_POD_PROP_FLAG_DROP (1u<<5) /**< drop property, when filtering, both sides + * need the property or it will be dropped. */ uint32_t flags; /**< flags for property */ struct spa_pod value; /* value follows */ diff --git a/spa/include/spa/pod/simplify.h b/spa/include/spa/pod/simplify.h new file mode 100644 index 000000000..1070e0a6f --- /dev/null +++ b/spa/include/spa/pod/simplify.h @@ -0,0 +1,187 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_POD_SIMPLIFY_H +#define SPA_POD_SIMPLIFY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifndef SPA_API_POD_SIMPLIFY + #ifdef SPA_API_IMPL + #define SPA_API_POD_SIMPLIFY SPA_API_IMPL + #else + #define SPA_API_POD_SIMPLIFY static inline + #endif +#endif + +/** + * \addtogroup spa_pod + * \{ + */ + +SPA_API_POD_SIMPLIFY int +spa_pod_simplify_merge(struct spa_pod_builder *b, const struct spa_pod *pod1, const struct spa_pod *pod2) +{ + const struct spa_pod_object *o1, *o2; + const struct spa_pod_prop *p1, *p2; + struct spa_pod_frame f[2]; + int res = 0, count = 0; + + if (!spa_pod_is_object(pod1) || + !spa_pod_is_object(pod2)) + return -ENOTSUP; + + o1 = (const struct spa_pod_object*) pod1; + o2 = (const struct spa_pod_object*) pod2; + + spa_pod_builder_push_object(b, &f[0], o1->body.type, o1->body.id); + p2 = NULL; + SPA_POD_OBJECT_FOREACH(o1, p1) { + p2 = spa_pod_object_find_prop(o2, p2, p1->key); + if (p2 == NULL) + goto error_enoent; + + if (spa_pod_compare(&p1->value, &p2->value) == 0) { + spa_pod_builder_raw_padded(b, p1, SPA_POD_PROP_SIZE(p1)); + } + else { + uint32_t i, n_vals1, n_vals2, choice1, choice2, size; + const struct spa_pod *vals1, *vals2; + void *alt1, *alt2, *a1, *a2; + + count++; + if (count > 1) + goto error_einval; + + vals1 = spa_pod_get_values(&p1->value, &n_vals1, &choice1); + vals2 = spa_pod_get_values(&p2->value, &n_vals2, &choice2); + + if (vals1->type != vals2->type || n_vals1 < 1 || n_vals2 < 1) + goto error_einval; + + size = vals1->size; + + alt1 = SPA_POD_BODY(vals1); + alt2 = SPA_POD_BODY(vals2); + + if ((choice1 == SPA_CHOICE_None && choice2 == SPA_CHOICE_None) || + (choice1 == SPA_CHOICE_None && choice2 == SPA_CHOICE_Enum) || + (choice1 == SPA_CHOICE_Enum && choice2 == SPA_CHOICE_None) || + (choice1 == SPA_CHOICE_Enum && choice2 == SPA_CHOICE_Enum)) { + spa_pod_builder_prop(b, p1->key, p1->flags); + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); + spa_pod_builder_child(b, size, vals1->type); + for (i = 0, a1 = alt1; i < n_vals1; i++, a1 = SPA_PTROFF(a1,size,void)) { + if (i == 0 && n_vals1 == 1) + spa_pod_builder_raw(b, a1, size); + spa_pod_builder_raw(b, a1, size); + } + for (i = 0, a2 = alt2; i < n_vals2; i++, a2 = SPA_PTROFF(a2,size,void)) { + spa_pod_builder_raw(b, a2, size); + } + spa_pod_builder_pop(b, &f[1]); + } else { + goto error_einval; + } + } + } + p1 = NULL; + SPA_POD_OBJECT_FOREACH(o2, p2) { + p1 = spa_pod_object_find_prop(o1, p1, p2->key); + if (p1 == NULL) + goto error_enoent; + } +done: + spa_pod_builder_pop(b, &f[0]); + return res; + +error_einval: + res = -EINVAL; + goto done; +error_enoent: + res = -ENOENT; + goto done; +} + +SPA_API_POD_SIMPLIFY int +spa_pod_simplify_struct(struct spa_pod_builder *b, const struct spa_pod *pod, uint32_t pod_size) +{ + struct spa_pod *p1 = NULL, *p2; + struct spa_pod_frame f; + struct spa_pod_builder_state state; + uint32_t p1offs; + + spa_pod_builder_push_struct(b, &f); + SPA_POD_STRUCT_FOREACH(pod, p2) { + spa_pod_builder_get_state(b, &state); + if (p1 == NULL || spa_pod_simplify_merge(b, p1, p2) < 0) { + spa_pod_builder_reset(b, &state); + spa_pod_builder_raw_padded(b, p2, SPA_POD_SIZE(p2)); + p1offs = state.offset; + p1 = SPA_PTROFF(b->data, p1offs, struct spa_pod); + } else { + void *pnew = SPA_PTROFF(b->data, state.offset, void); + p1 = SPA_PTROFF(b->data, p1offs, struct spa_pod); + spa_pod_builder_remove(b, SPA_POD_SIZE(p1)); + memmove(p1, pnew, SPA_POD_SIZE(pnew)); + } + } + spa_pod_builder_pop(b, &f); + return 0; +} + +SPA_API_POD_SIMPLIFY int +spa_pod_simplify(struct spa_pod_builder *b, struct spa_pod **result, const struct spa_pod *pod) +{ + int res = 0; + struct spa_pod_builder_state state; + + spa_return_val_if_fail(pod != NULL, -EINVAL); + spa_return_val_if_fail(b != NULL, -EINVAL); + + spa_pod_builder_get_state(b, &state); + + if (!spa_pod_is_struct(pod)) { + res = spa_pod_builder_raw_padded(b, pod, SPA_POD_SIZE(pod)); + } else { + struct spa_pod_dynamic_builder db; + spa_pod_dynamic_builder_continue(&db, b); + res = spa_pod_simplify_struct(&db.b, pod, SPA_POD_SIZE(pod)); + if (res >= 0) + res = spa_pod_builder_raw_padded(b, db.b.data, db.b.state.offset); + spa_pod_dynamic_builder_clean(&db); + } + + if (res >= 0 && result) { + *result = (struct spa_pod*)spa_pod_builder_deref(b, state.offset); + if (*result == NULL) + res = -ENOSPC; + } + return res; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_POD_SIMPLIFY_H */ diff --git a/spa/include/spa/pod/vararg.h b/spa/include/spa/pod/vararg.h index a30e11479..8e9137df1 100644 --- a/spa/include/spa/pod/vararg.h +++ b/spa/include/spa/pod/vararg.h @@ -5,14 +5,14 @@ #ifndef SPA_POD_VARARG_H #define SPA_POD_VARARG_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * \addtogroup spa_pod * \{ @@ -20,6 +20,8 @@ extern "C" { #define SPA_POD_Prop(key,...) \ key, ##__VA_ARGS__ +#define SPA_POD_Propf(key,flags,...) \ + SPA_ID_INVALID, key, flags, ##__VA_ARGS__ #define SPA_POD_Control(offset,type,...) \ offset, type, ##__VA_ARGS__ @@ -28,6 +30,7 @@ extern "C" { #define SPA_CHOICE_STEP(def,min,max,step) 4,(def),(min),(max),(step) #define SPA_CHOICE_ENUM(n_vals,...) (n_vals),##__VA_ARGS__ #define SPA_CHOICE_FLAGS(flags) 1, (flags) +#define SPA_CHOICE_FEATURES(features) 1, (features) #define SPA_CHOICE_BOOL(def) 3,(def),(def),!(def) #define SPA_POD_Bool(val) "b", val @@ -41,12 +44,14 @@ extern "C" { #define SPA_POD_CHOICE_RANGE_Int(def,min,max) "?ri", SPA_CHOICE_RANGE(def, min, max) #define SPA_POD_CHOICE_STEP_Int(def,min,max,step) "?si", SPA_CHOICE_STEP(def, min, max, step) #define SPA_POD_CHOICE_FLAGS_Int(flags) "?fi", SPA_CHOICE_FLAGS(flags) +#define SPA_POD_CHOICE_FEATURES_Int(features) "?Fi", SPA_CHOICE_FEATURES(features) #define SPA_POD_Long(val) "l", val #define SPA_POD_CHOICE_ENUM_Long(n_vals,...) "?el", SPA_CHOICE_ENUM(n_vals, __VA_ARGS__) #define SPA_POD_CHOICE_RANGE_Long(def,min,max) "?rl", SPA_CHOICE_RANGE(def, min, max) #define SPA_POD_CHOICE_STEP_Long(def,min,max,step) "?sl", SPA_CHOICE_STEP(def, min, max, step) #define SPA_POD_CHOICE_FLAGS_Long(flags) "?fl", SPA_CHOICE_FLAGS(flags) +#define SPA_POD_CHOICE_FEATURES_LONG(features) "?Fl", SPA_CHOICE_FEATURES(features) #define SPA_POD_Float(val) "f", val #define SPA_POD_CHOICE_ENUM_Float(n_vals,...) "?ef", SPA_CHOICE_ENUM(n_vals, __VA_ARGS__) @@ -82,6 +87,12 @@ extern "C" { #define SPA_POD_PodStruct(val) "T", val #define SPA_POD_PodChoice(val) "V", val +#define SPA_POD_PodBody(val,body) "Q", val, body +#define SPA_POD_PodBodyObject(val,body) "N", val, body +#define SPA_POD_PodBodyStruct(val,body) "U", val, body +#define SPA_POD_PodBodyChoice(val,body) "W", val, body + + /** * \} */ diff --git a/spa/include/spa/support/cpu.h b/spa/include/spa/support/cpu.h index ce8551e74..c69338855 100644 --- a/spa/include/spa/support/cpu.h +++ b/spa/include/spa/support/cpu.h @@ -5,16 +5,16 @@ #ifndef SPA_CPU_H #define SPA_CPU_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_CPU #ifdef SPA_API_IMPL #define SPA_API_CPU SPA_API_IMPL diff --git a/spa/include/spa/support/dbus.h b/spa/include/spa/support/dbus.h index 3908bfe53..d781bc1ba 100644 --- a/spa/include/spa/support/dbus.h +++ b/spa/include/spa/support/dbus.h @@ -5,12 +5,12 @@ #ifndef SPA_DBUS_H #define SPA_DBUS_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - #ifndef SPA_API_DBUS #ifdef SPA_API_IMPL #define SPA_API_DBUS SPA_API_IMPL diff --git a/spa/include/spa/support/i18n.h b/spa/include/spa/support/i18n.h index 3b258873f..859838e14 100644 --- a/spa/include/spa/support/i18n.h +++ b/spa/include/spa/support/i18n.h @@ -5,13 +5,13 @@ #ifndef SPA_I18N_H #define SPA_I18N_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #ifndef SPA_API_I18N #ifdef SPA_API_IMPL #define SPA_API_I18N SPA_API_IMPL diff --git a/spa/include/spa/support/log-impl.h b/spa/include/spa/support/log-impl.h index 8132d05bf..c1ce51b82 100644 --- a/spa/include/spa/support/log-impl.h +++ b/spa/include/spa/support/log-impl.h @@ -5,15 +5,15 @@ #ifndef SPA_LOG_IMPL_H #define SPA_LOG_IMPL_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * \addtogroup spa_log * \{ diff --git a/spa/include/spa/support/log.h b/spa/include/spa/support/log.h index e14414403..a1917886e 100644 --- a/spa/include/spa/support/log.h +++ b/spa/include/spa/support/log.h @@ -5,16 +5,16 @@ #ifndef SPA_LOG_H #define SPA_LOG_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_LOG #ifdef SPA_API_IMPL #define SPA_API_LOG SPA_API_IMPL diff --git a/spa/include/spa/support/loop.h b/spa/include/spa/support/loop.h index 520a465d5..1aec922de 100644 --- a/spa/include/spa/support/loop.h +++ b/spa/include/spa/support/loop.h @@ -5,16 +5,16 @@ #ifndef SPA_LOOP_H #define SPA_LOOP_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_LOOP #ifdef SPA_API_IMPL #define SPA_API_LOOP SPA_API_IMPL @@ -38,7 +38,7 @@ extern "C" { struct spa_loop { struct spa_interface iface; }; #define SPA_TYPE_INTERFACE_LoopControl SPA_TYPE_INFO_INTERFACE_BASE "LoopControl" -#define SPA_VERSION_LOOP_CONTROL 1 +#define SPA_VERSION_LOOP_CONTROL 2 struct spa_loop_control { struct spa_interface iface; }; #define SPA_TYPE_INTERFACE_LoopUtils SPA_TYPE_INFO_INTERFACE_BASE "LoopUtils" @@ -105,6 +105,7 @@ struct spa_loop_methods { /** Invoke a function in the context of this loop. * May be called from any thread and multiple threads at the same time. + * * If called from the loop's thread, all callbacks previously queued with * invoke() will be run synchronously, which might cause unexpected * reentrancy problems. @@ -119,8 +120,11 @@ struct spa_loop_methods { * an object that has identity. * \param size The size of data to copy. * \param block If \true, do not return until func has been called. Otherwise, - * returns immediately. Passing \true does not risk a deadlock because - * the data thread is never allowed to wait on any other thread. + * returns immediately. Passing \true can cause a deadlock when + * the calling thread is holding the loop context lock. A blocking + * invoke should never be done from a realtime thread. Also beware + * of blocking invokes between 2 threads as you can easily end up + * in a deadly embrace. * \param user_data An opaque pointer passed to func. * \return `-EPIPE` if the internal ring buffer filled up, * if block is \false, 0 if seq was SPA_ID_INVALID or @@ -133,6 +137,24 @@ struct spa_loop_methods { size_t size, bool block, void *user_data); + + /** Call a function with the loop lock acquired + * May be called from any thread and multiple threads at the same time. + * + * \param[in] object The callbacks data. + * \param func The function to be called. + * \param seq An opaque sequence number. This will be made + * available to func. + * \param[in] data Data that will be passed to func. + * \param size The size of data. + * \param user_data An opaque pointer passed to func. + * \return the return value of func. */ + int (*locked) (void *object, + spa_invoke_func_t func, + uint32_t seq, + const void *data, + size_t size, + void *user_data); }; SPA_API_LOOP int spa_loop_add_source(struct spa_loop *object, struct spa_source *source) @@ -158,6 +180,15 @@ SPA_API_LOOP int spa_loop_invoke(struct spa_loop *object, spa_loop, &object->iface, invoke, 0, func, seq, data, size, block, user_data); } +SPA_API_LOOP int spa_loop_locked(struct spa_loop *object, + spa_invoke_func_t func, uint32_t seq, const void *data, + size_t size, void *user_data) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop, &object->iface, locked, 0, func, seq, data, + size, user_data); +} + /** Control hooks. These hooks can't be removed from their * callbacks and must be removed from a safe place (when the loop @@ -166,10 +197,10 @@ struct spa_loop_control_hooks { #define SPA_VERSION_LOOP_CONTROL_HOOKS 0 uint32_t version; /** Executed right before waiting for events. It is typically used to - * release locks. */ + * release locks or integrate other fds into the loop. */ void (*before) (void *data); /** Executed right after waiting for events. It is typically used to - * reacquire locks. */ + * reacquire locks or integrate other fds into the loop. */ void (*after) (void *data); }; @@ -213,7 +244,7 @@ SPA_API_LOOP void spa_loop_control_hook_after(struct spa_hook_list *l) struct spa_loop_control_methods { /* the version of this structure. This can be used to expand this * structure in the future */ -#define SPA_VERSION_LOOP_CONTROL_METHODS 1 +#define SPA_VERSION_LOOP_CONTROL_METHODS 2 uint32_t version; /** get the loop fd @@ -244,7 +275,7 @@ struct spa_loop_control_methods { * This function should be called before calling iterate and is * typically used to capture the thread that this loop will run in. * It should ideally be called once from the thread that will run - * the loop. + * the loop. This function will lock the loop. */ void (*enter) (void *object); /** Leave a loop @@ -252,6 +283,8 @@ struct spa_loop_control_methods { * * It should ideally be called once after calling iterate when the loop * will no longer be iterated from the thread that called enter(). + * + * This function will unlock the loop. */ void (*leave) (void *object); @@ -260,8 +293,10 @@ struct spa_loop_control_methods { * \param timeout an optional timeout in milliseconds. * 0 for no timeout, -1 for infinite timeout. * - * This function will block - * up to \a timeout milliseconds and then dispatch the fds with activity. + * This function will first unlock the loop and then block + * up to \a timeout milliseconds, lock the loop again and then + * dispatch the fds with activity. + * * The number of dispatched fds is returned. */ int (*iterate) (void *object, int timeout); @@ -275,6 +310,70 @@ struct spa_loop_control_methods { * returns 1 on success, 0 or negative errno value on error. */ int (*check) (void *object); + + /** Lock the loop. + * This will ensure the loop is not in the process of dispatching + * callbacks. Since version 2:2 + * + * \param[in] object the control + * \return 0 on success or a negative return value on error. + */ + int (*lock) (void *object); + + /** Unlock the loop. + * Unlocks the loop again so that callbacks can be dispatched + * again. Since version 2:2 + * + * \param[in] object the control + * \return 0 on success or a negative return value on error. + */ + int (*unlock) (void *object); + + /** get the absolute time + * Get the current time with \ref timeout that can be used in wait. + * Since version 2:2 + * + * This function can be called from any thread. + */ + int (*get_time) (void *object, struct timespec *abstime, int64_t timeout); + + /** Wait for a signal + * Wait until a thread performs signal. Since version 2:2 + * + * This function must be called with the loop lock. Because this is a + * blocking call, it should not be performed from a realtime thread. + * + * \param[in] object the control + * \param[in] abstime the maximum time to wait for the signal or NULL + * \return 0 on success or a negative return value on error. + */ + int (*wait) (void *object, const struct timespec *abstime); + + /** Signal waiters + * Wake up all threads blocked in wait. Since version 2:2 + * When wait_for_accept is set, this functions blocks until all + * threads performed accept. + * + * This function must be called with the loop lock and is safe to + * call from a realtime thread source dispatch functions when + * wait_for_accept is false. + * + * \param[in] object the control + * \param[in] wait_for_accept block for accept + * \return 0 on success or a negative return value on error. + */ + int (*signal) (void *object, bool wait_for_accept); + + /** Accept signalers + * Resume the thread that signaled with wait_for accept. + * + * This function must be called with the loop lock and is safe to + * call from a realtime thread source dispatch functions. + * + * \param[in] object the control + * \return 0 on success or a negative return value on error. + */ + int (*accept) (void *object); }; SPA_API_LOOP int spa_loop_control_get_fd(struct spa_loop_control *object) @@ -314,6 +413,38 @@ SPA_API_LOOP int spa_loop_control_check(struct spa_loop_control *object) return spa_api_method_r(int, -ENOTSUP, spa_loop_control, &object->iface, check, 1); } +SPA_API_LOOP int spa_loop_control_lock(struct spa_loop_control *object) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_control, &object->iface, lock, 2); +} +SPA_API_LOOP int spa_loop_control_unlock(struct spa_loop_control *object) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_control, &object->iface, unlock, 2); +} +SPA_API_LOOP int spa_loop_control_get_time(struct spa_loop_control *object, + struct timespec *abstime, int64_t timeout) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_control, &object->iface, get_time, 2, abstime, timeout); +} +SPA_API_LOOP int spa_loop_control_wait(struct spa_loop_control *object, + const struct timespec *abstime) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_control, &object->iface, wait, 2, abstime); +} +SPA_API_LOOP int spa_loop_control_signal(struct spa_loop_control *object, bool wait_for_accept) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_control, &object->iface, signal, 2, wait_for_accept); +} +SPA_API_LOOP int spa_loop_control_accept(struct spa_loop_control *object) +{ + return spa_api_method_r(int, -ENOTSUP, + spa_loop_control, &object->iface, accept, 2); +} typedef void (*spa_source_io_func_t) (void *data, int fd, uint32_t mask); typedef void (*spa_source_idle_func_t) (void *data); diff --git a/spa/include/spa/support/plugin-loader.h b/spa/include/spa/support/plugin-loader.h index 9540853cd..0b059ec51 100644 --- a/spa/include/spa/support/plugin-loader.h +++ b/spa/include/spa/support/plugin-loader.h @@ -5,13 +5,13 @@ #ifndef SPA_PLUGIN_LOADER_H #define SPA_PLUGIN_LOADER_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #ifndef SPA_API_PLUGIN_LOADER #ifdef SPA_API_IMPL #define SPA_API_PLUGIN_LOADER SPA_API_IMPL diff --git a/spa/include/spa/support/plugin.h b/spa/include/spa/support/plugin.h index 576c19509..68299fe5b 100644 --- a/spa/include/spa/support/plugin.h +++ b/spa/include/spa/support/plugin.h @@ -5,16 +5,16 @@ #ifndef SPA_PLUGIN_H #define SPA_PLUGIN_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_PLUGIN #ifdef SPA_API_IMPL #define SPA_API_PLUGIN SPA_API_IMPL diff --git a/spa/include/spa/support/system.h b/spa/include/spa/support/system.h index aa140c958..07a31a55f 100644 --- a/spa/include/spa/support/system.h +++ b/spa/include/spa/support/system.h @@ -5,12 +5,6 @@ #ifndef SPA_SYSTEM_H #define SPA_SYSTEM_H -#ifdef __cplusplus -extern "C" { -#endif - -struct itimerspec; - #include #include #include @@ -18,6 +12,12 @@ struct itimerspec; #include #include +#ifdef __cplusplus +extern "C" { +#endif + +struct itimerspec; + #ifndef SPA_API_SYSTEM #ifdef SPA_API_IMPL #define SPA_API_SYSTEM SPA_API_IMPL diff --git a/spa/include/spa/support/thread.h b/spa/include/spa/support/thread.h index b69cb688c..bb523ce5c 100644 --- a/spa/include/spa/support/thread.h +++ b/spa/include/spa/support/thread.h @@ -5,10 +5,6 @@ #ifndef SPA_THREAD_H #define SPA_THREAD_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -16,6 +12,10 @@ extern "C" { #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_THREAD #ifdef SPA_API_IMPL #define SPA_API_THREAD SPA_API_IMPL diff --git a/spa/include/spa/utils/atomic.h b/spa/include/spa/utils/atomic.h index 549420b89..72a18ab8d 100644 --- a/spa/include/spa/utils/atomic.h +++ b/spa/include/spa/utils/atomic.h @@ -19,6 +19,8 @@ extern "C" { #define SPA_ATOMIC_DEC(s) __atomic_sub_fetch(&(s), 1, __ATOMIC_SEQ_CST) #define SPA_ATOMIC_INC(s) __atomic_add_fetch(&(s), 1, __ATOMIC_SEQ_CST) #define SPA_ATOMIC_LOAD(s) __atomic_load_n(&(s), __ATOMIC_SEQ_CST) +#define SPA_LOAD_ONCE(s) __atomic_load_n((s), __ATOMIC_RELAXED) +#define SPA_STORE_ONCE(s) __atomic_store_n((s), __ATOMIC_RELAXED) #define SPA_ATOMIC_STORE(s,v) __atomic_store_n(&(s), (v), __ATOMIC_SEQ_CST) #define SPA_ATOMIC_XCHG(s,v) __atomic_exchange_n(&(s), (v), __ATOMIC_SEQ_CST) diff --git a/spa/include/spa/utils/defs.h b/spa/include/spa/utils/defs.h index 1c1a73abf..371b1e658 100644 --- a/spa/include/spa/utils/defs.h +++ b/spa/include/spa/utils/defs.h @@ -5,6 +5,13 @@ #ifndef SPA_UTILS_DEFS_H #define SPA_UTILS_DEFS_H +#include +#include +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { # if __cplusplus >= 201103L @@ -34,13 +41,6 @@ extern "C" { #define SPA_CONCAT_NOEXPAND(a, b) a ## b #define SPA_CONCAT(a, b) SPA_CONCAT_NOEXPAND(a, b) -#include -#include -#include -#include -#include -#include - /** * \defgroup spa_utils_defs Miscellaneous * Helper macros and functions @@ -242,6 +242,7 @@ struct spa_fraction { #define SPA_UNUSED __attribute__ ((unused)) #define SPA_NORETURN __attribute__ ((noreturn)) #define SPA_WARN_UNUSED_RESULT __attribute__ ((warn_unused_result)) +#define SPA_BARRIER __asm__ __volatile__("": : :"memory") #else #define SPA_PRINTF_FUNC(fmt, arg1) #define SPA_FORMAT_ARG_FUNC(arg1) @@ -252,6 +253,7 @@ struct spa_fraction { #define SPA_UNUSED #define SPA_NORETURN #define SPA_WARN_UNUSED_RESULT +#define SPA_BARRIER #endif #ifndef SPA_API_IMPL diff --git a/spa/include/spa/utils/dict.h b/spa/include/spa/utils/dict.h index c88a833f4..979605d54 100644 --- a/spa/include/spa/utils/dict.h +++ b/spa/include/spa/utils/dict.h @@ -5,14 +5,14 @@ #ifndef SPA_DICT_H #define SPA_DICT_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_DICT #ifdef SPA_API_IMPL #define SPA_API_DICT SPA_API_IMPL diff --git a/spa/include/spa/utils/dll.h b/spa/include/spa/utils/dll.h index 7b8fd207e..824750f5d 100644 --- a/spa/include/spa/utils/dll.h +++ b/spa/include/spa/utils/dll.h @@ -5,15 +5,15 @@ #ifndef SPA_DLL_H #define SPA_DLL_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_DLL #ifdef SPA_API_IMPL #define SPA_API_DLL SPA_API_IMPL diff --git a/spa/include/spa/utils/enum-types.h b/spa/include/spa/utils/enum-types.h index 881ffd8b3..aebde1845 100644 --- a/spa/include/spa/utils/enum-types.h +++ b/spa/include/spa/utils/enum-types.h @@ -5,13 +5,13 @@ #ifndef SPA_ENUM_TYPES_H #define SPA_ENUM_TYPES_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** * \addtogroup spa_types * \{ diff --git a/spa/include/spa/utils/hook.h b/spa/include/spa/utils/hook.h index dbbb01976..d0ae744c8 100644 --- a/spa/include/spa/utils/hook.h +++ b/spa/include/spa/utils/hook.h @@ -5,13 +5,13 @@ #ifndef SPA_HOOK_H #define SPA_HOOK_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #ifndef SPA_API_HOOK #ifdef SPA_API_IMPL #define SPA_API_HOOK SPA_API_IMPL diff --git a/spa/include/spa/utils/json-core.h b/spa/include/spa/utils/json-core.h index 31bf772f1..5616bffe1 100644 --- a/spa/include/spa/utils/json-core.h +++ b/spa/include/spa/utils/json-core.h @@ -5,11 +5,6 @@ #ifndef SPA_UTILS_JSON_H #define SPA_UTILS_JSON_H -#ifdef __cplusplus -extern "C" { -#else -#include -#endif #include #include #include @@ -20,6 +15,12 @@ extern "C" { #include #include +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + #ifndef SPA_API_JSON #ifdef SPA_API_IMPL #define SPA_API_JSON SPA_API_IMPL @@ -53,6 +54,15 @@ SPA_API_JSON void spa_json_init(struct spa_json * iter, const char *data, size_t { *iter = SPA_JSON_INIT(data, size); } + +#define SPA_JSON_INIT_RELAX(type,data,size) \ + ((struct spa_json) { (data), (data)+(size), NULL, (uint32_t)((type) == '[' ? 0x10 : 0x0), 0 }) + +SPA_API_JSON void spa_json_init_relax(struct spa_json * iter, char type, const char *data, size_t size) +{ + *iter = SPA_JSON_INIT_RELAX(type, data, size); +} + #define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), (iter)->state & 0xff0, 0 }) SPA_API_JSON void spa_json_enter(struct spa_json * iter, struct spa_json * sub) @@ -268,6 +278,8 @@ SPA_API_JSON int spa_json_next(struct spa_json * iter, const char **value) if (--utf8_remain == 0) iter->state = __STRING | flag; continue; + default: + break; } _SPA_ERROR(CHARACTERS_NOT_ALLOWED); case __ESC: @@ -276,12 +288,17 @@ SPA_API_JSON int spa_json_next(struct spa_json * iter, const char **value) case 'n': case 'r': case 't': case 'u': iter->state = __STRING | flag; continue; + default: + break; } _SPA_ERROR(INVALID_ESCAPE); case __COMMENT: switch (cur) { case '\n': case '\r': iter->state = __STRUCT | flag; + break; + default: + break; } break; default: @@ -299,6 +316,8 @@ SPA_API_JSON int spa_json_next(struct spa_json * iter, const char **value) case __COMMENT: /* trailing comment */ return 0; + default: + break; } if ((iter->state & __SUB_FLAG) && (iter->state & __KEY_FLAG)) { diff --git a/spa/include/spa/utils/json-pod.h b/spa/include/spa/utils/json-pod.h index 22f2b2817..6dfc6241f 100644 --- a/spa/include/spa/utils/json-pod.h +++ b/spa/include/spa/utils/json-pod.h @@ -5,16 +5,16 @@ #ifndef SPA_UTILS_JSON_POD_H #define SPA_UTILS_JSON_POD_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_JSON_POD #ifdef SPA_API_IMPL #define SPA_API_JSON_POD SPA_API_IMPL diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h index a36554d1e..212637dab 100644 --- a/spa/include/spa/utils/json.h +++ b/spa/include/spa/utils/json.h @@ -5,11 +5,6 @@ #ifndef SPA_UTILS_JSON_UTILS_H #define SPA_UTILS_JSON_UTILS_H -#ifdef __cplusplus -extern "C" { -#else -#include -#endif #include #include #include @@ -19,6 +14,12 @@ extern "C" { #include +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + #ifndef SPA_API_JSON_UTILS #ifdef SPA_API_IMPL #define SPA_API_JSON_UTILS SPA_API_IMPL @@ -104,7 +105,7 @@ SPA_API_JSON_UTILS int spa_json_begin_container(struct spa_json * iter, spa_json_init(iter, data, size); res = spa_json_enter_container(iter, iter, type); if (res == -EPROTO && relax) - spa_json_init(iter, data, size); + spa_json_init_relax(iter, type, data, size); else if (res <= 0) return res; return 1; diff --git a/spa/include/spa/utils/list.h b/spa/include/spa/utils/list.h index 60c89f23b..05abc9518 100644 --- a/spa/include/spa/utils/list.h +++ b/spa/include/spa/utils/list.h @@ -5,12 +5,12 @@ #ifndef SPA_LIST_H #define SPA_LIST_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - #ifndef SPA_API_LIST #ifdef SPA_API_IMPL #define SPA_API_LIST SPA_API_IMPL diff --git a/spa/include/spa/utils/ratelimit.h b/spa/include/spa/utils/ratelimit.h index 2af2c26be..9dcd40405 100644 --- a/spa/include/spa/utils/ratelimit.h +++ b/spa/include/spa/utils/ratelimit.h @@ -5,15 +5,15 @@ #ifndef SPA_RATELIMIT_H #define SPA_RATELIMIT_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_RATELIMIT #ifdef SPA_API_IMPL #define SPA_API_RATELIMIT SPA_API_IMPL diff --git a/spa/include/spa/utils/result.h b/spa/include/spa/utils/result.h index 312a6bb0c..49f777e82 100644 --- a/spa/include/spa/utils/result.h +++ b/spa/include/spa/utils/result.h @@ -5,6 +5,10 @@ #ifndef SPA_UTILS_RESULT_H #define SPA_UTILS_RESULT_H +#include + +#include + #ifdef __cplusplus extern "C" { #endif @@ -19,10 +23,6 @@ extern "C" { * \{ */ -#include - -#include - #ifndef SPA_API_RESULT #ifdef SPA_API_IMPL #define SPA_API_RESULT SPA_API_IMPL diff --git a/spa/include/spa/utils/ringbuffer.h b/spa/include/spa/utils/ringbuffer.h index e8c5d6250..59bdd175f 100644 --- a/spa/include/spa/utils/ringbuffer.h +++ b/spa/include/spa/utils/ringbuffer.h @@ -5,6 +5,8 @@ #ifndef SPA_RINGBUFFER_H #define SPA_RINGBUFFER_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -21,8 +23,6 @@ extern "C" { struct spa_ringbuffer; -#include - #include #ifndef SPA_API_RINGBUFFER diff --git a/spa/include/spa/utils/string.h b/spa/include/spa/utils/string.h index 060ef7d62..4b6ad8bfd 100644 --- a/spa/include/spa/utils/string.h +++ b/spa/include/spa/utils/string.h @@ -5,10 +5,6 @@ #ifndef SPA_UTILS_STRING_H #define SPA_UTILS_STRING_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include @@ -17,6 +13,10 @@ extern "C" { #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef SPA_API_STRING #ifdef SPA_API_IMPL #define SPA_API_STRING SPA_API_IMPL diff --git a/spa/include/spa/utils/type-info.h b/spa/include/spa/utils/type-info.h index 9ee2f3abc..a099b9f0d 100644 --- a/spa/include/spa/utils/type-info.h +++ b/spa/include/spa/utils/type-info.h @@ -5,12 +5,19 @@ #ifndef SPA_TYPE_INFO_H #define SPA_TYPE_INFO_H +#include +#include +#include + +#include +#include +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** * \addtogroup spa_types * \{ @@ -20,15 +27,6 @@ extern "C" { #define SPA_TYPE_ROOT spa_types #endif - -#include -#include - -#include -#include -#include -#include - static const struct spa_type_info spa_types[] = { /* Basic types */ { SPA_TYPE_START, SPA_TYPE_START, SPA_TYPE_INFO_BASE, NULL }, diff --git a/spa/include/spa/utils/type.h b/spa/include/spa/utils/type.h index 88de2c624..758a9bd58 100644 --- a/spa/include/spa/utils/type.h +++ b/spa/include/spa/utils/type.h @@ -5,13 +5,13 @@ #ifndef SPA_TYPE_H #define SPA_TYPE_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #ifndef SPA_API_TYPE #ifdef SPA_API_IMPL #define SPA_API_TYPE SPA_API_IMPL diff --git a/spa/lib/lib.c b/spa/lib/lib.c index e2acb9cc2..0aa35fae3 100644 --- a/spa/lib/lib.c +++ b/spa/lib/lib.c @@ -1,4 +1,6 @@ +#undef SPA_AUDIO_MAX_CHANNELS + #define SPA_API_IMPL SPA_EXPORT #include #include @@ -34,6 +36,8 @@ #include #include #include +#include +#include #include #include #include @@ -46,6 +50,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -54,9 +63,12 @@ #include #include #include +#include #include #include #include +#include +#include #include #include #include @@ -65,6 +77,8 @@ #include #include #include +#include +#include #include #include #include @@ -118,6 +132,7 @@ #include #include #include +#include #include #include #include @@ -153,9 +168,3 @@ #include #include #include - - - - - - diff --git a/spa/meson.build b/spa/meson.build index 9b5f89604..f8acaec6a 100644 --- a/spa/meson.build +++ b/spa/meson.build @@ -18,6 +18,13 @@ spa_dep = declare_dependency( }, ) +spa_inc_dep = declare_dependency( + include_directories : [ + include_directories('include'), + include_directories('include-private'), + ], +) + meson.override_dependency('lib@0@'.format(spa_name), spa_dep) pkgconfig.generate(filebase : 'lib@0@'.format(spa_name), @@ -25,7 +32,7 @@ pkgconfig.generate(filebase : 'lib@0@'.format(spa_name), subdirs : spa_name, description : 'Simple Plugin API', version : spaversion, - extra_cflags : '-D_REENTRANT', + extra_cflags : ['-D_REENTRANT', '-fno-strict-aliasing', '-fno-strict-overflow'], variables : ['plugindir=${libdir}/@0@'.format(spa_name)], uninstalled_variables : ['plugindir=${prefix}/spa/plugins'], ) @@ -43,9 +50,13 @@ if get_option('spa-plugins').allowed() endif # plugin-specific dependencies - alsa_dep = dependency('alsa', version : '>=1.2.10', required: get_option('alsa')) + alsa_dep = dependency('alsa', version : '>=1.2.6', required: get_option('alsa')) summary({'ALSA': alsa_dep.found()}, bool_yn: true, section: 'Backend') + if alsa_dep.version().version_compare('>=1.2.11') + cdata.set('HAVE_ALSA_UMP', true) + endif + bluez_dep = dependency('bluez', version : '>= 4.101', required: get_option('bluez5')) bluez_gio_dep = dependency('gio-2.0', required : get_option('bluez5')) bluez_gio_unix_dep = dependency('gio-unix-2.0', required : get_option('bluez5')) @@ -66,6 +77,23 @@ if get_option('spa-plugins').allowed() summary({'LDAC': ldac_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') ldac_abr_dep = dependency('ldacBT-abr', required : get_option('bluez5-codec-ldac')) summary({'LDAC ABR': ldac_abr_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + + if get_option('bluez5-codec-ldac-dec').allowed() + ldac_dec_dep = dependency('ldacBT-dec', required : false) + if not ldac_dec_dep.found() + dep = cc.find_library('ldacBT_dec', required : false) + if dep.found() and cc.has_function('ldacBT_decode', dependencies : dep) + ldac_dec_dep = dep + endif + endif + if not ldac_dec_dep.found() and get_option('bluez5-codec-ldac-dec').enabled() + error('LDAC decoder library not found') + endif + else + ldac_dec_dep = dependency('', required: false) + endif + summary({'LDAC DEC': ldac_dec_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') + aptx_dep = dependency('libfreeaptx', required : get_option('bluez5-codec-aptx')) summary({'aptX': aptx_dep.found()}, bool_yn: true, section: 'Bluetooth audio codecs') fdk_aac_dep = dependency('fdk-aac', required : get_option('bluez5-codec-aac')) @@ -88,10 +116,11 @@ if get_option('spa-plugins').allowed() mm_dep = dependency('ModemManager', version : '>= 1.10.0', required : get_option('bluez5-backend-native-mm')) summary({'ModemManager': mm_dep.found()}, bool_yn: true, section: 'Bluetooth backends') endif - cdata.set('HAVE_LC3', get_option('bluez5-codec-lc3').allowed() and lc3_dep.found()) g722_codec_option = get_option('bluez5-codec-g722') summary({'G722': g722_codec_option.allowed()}, bool_yn: true, section: 'Bluetooth audio codecs') - cdata.set('HAVE_G722', g722_codec_option.allowed()) + + spandsp_dep = dependency('spandsp', required : get_option('bluez5-plc-spandsp')) + cdata.set('HAVE_SPANDSP', spandsp_dep.found()) endif have_vulkan = false @@ -110,7 +139,7 @@ if get_option('spa-plugins').allowed() cdata.set('HAVE_ALSA_COMPRESS_OFFLOAD', compress_offload_option.allowed()) # common dependencies - libudev_dep = dependency('libudev', required: get_option('udev').enabled()) + libudev_dep = dependency('libudev', required: get_option('udev')) cdata.set('HAVE_LIBUDEV', libudev_dep.found()) summary({'Udev': libudev_dep.found()}, bool_yn: true, section: 'Backend') @@ -120,9 +149,14 @@ if get_option('spa-plugins').allowed() lilv_lib = dependency('lilv-0', required: get_option('lv2')) summary({'lilv (for lv2 plugins)': lilv_lib.found()}, bool_yn: true, section: 'filter-graph') - ebur128_lib = dependency('libebur128', required: get_option('ebur128').enabled()) + ebur128_lib = dependency('libebur128', required: get_option('ebur128')) summary({'EBUR128': ebur128_lib.found()}, bool_yn: true, section: 'filter-graph') + summary({'ffmpeg': avfilter_dep.found()}, bool_yn: true, section: 'filter-graph') + + onnxruntime_dep = dependency('libonnxruntime', required: get_option('onnxruntime')) + summary({'onnxruntime': onnxruntime_dep.found()}, bool_yn: true, section: 'filter-graph') + cdata.set('HAVE_SPA_PLUGINS', true) subdir('plugins') endif diff --git a/spa/plugins/aec/aec-webrtc.cpp b/spa/plugins/aec/aec-webrtc.cpp index 74255aae4..7b8cafcde 100644 --- a/spa/plugins/aec/aec-webrtc.cpp +++ b/spa/plugins/aec/aec-webrtc.cpp @@ -119,15 +119,14 @@ static int webrtc_init2(void *object, const struct spa_dict *args, #elif defined(HAVE_WEBRTC1) bool voice_detection = webrtc_get_spa_bool(args, "webrtc.voice_detection", true); bool transient_suppression = webrtc_get_spa_bool(args, "webrtc.transient_suppression", true); + bool mobile_mode = webrtc_get_spa_bool(args, "webrtc.mobile_mode", false); +#elif defined(HAVE_WEBRTC2) + bool mobile_mode = webrtc_get_spa_bool(args, "webrtc.mobile_mode", false); #endif // Note: AGC seems to mess up with Agnostic Delay Detection, especially with speech, // result in very poor performance, disable by default bool gain_control = webrtc_get_spa_bool(args, "webrtc.gain_control", false); - // FIXME: Intelligibility enhancer is not currently supported - // This filter will modify playback buffer (when calling ProcessReverseStream), but now - // playback buffer modifications are discarded. - #if defined(HAVE_WEBRTC) webrtc::Config config; config.Set(new webrtc::ExtendedFilter(extended_filter)); @@ -175,6 +174,7 @@ static int webrtc_init2(void *object, const struct spa_dict *args, #elif defined(HAVE_WEBRTC1) webrtc::AudioProcessing::Config config; config.echo_canceller.enabled = true; + config.echo_canceller.mobile_mode = mobile_mode; config.pipeline.multi_channel_capture = rec_info->channels > 1; config.pipeline.multi_channel_render = play_info->channels > 1; // FIXME: Example code enables both gain controllers, but that seems sus @@ -191,6 +191,8 @@ static int webrtc_init2(void *object, const struct spa_dict *args, config.voice_detection.enabled = voice_detection; #elif defined(HAVE_WEBRTC2) webrtc::AudioProcessing::Config config; + config.echo_canceller.enabled = true; + config.echo_canceller.mobile_mode = mobile_mode; config.pipeline.multi_channel_capture = rec_info->channels > 1; config.pipeline.multi_channel_render = play_info->channels > 1; // FIXME: Example code enables both gain controllers, but that seems sus @@ -308,9 +310,6 @@ static int webrtc_run(void *object, const float *rec[], const float *play[], flo for (size_t j = 0; j < impl->out_info.channels; j++) impl->out_buffer[j] = out[j] + out_config.num_frames() * i; - /* FIXME: ProcessReverseStream may change the playback buffer, in which - * case we should use that, if we ever expose the intelligibility - * enhancer */ if ((res = impl->apm->ProcessReverseStream(impl->play_buffer.get(), play_config, play_config, impl->play_buffer.get())) != webrtc::AudioProcessing::kNoError) { diff --git a/spa/plugins/alsa/90-pipewire-alsa.rules b/spa/plugins/alsa/90-pipewire-alsa.rules index 9ef3d533b..b2e1f6886 100644 --- a/spa/plugins/alsa/90-pipewire-alsa.rules +++ b/spa/plugins/alsa/90-pipewire-alsa.rules @@ -118,6 +118,7 @@ ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="402e", ENV{ACP_PROFILE_SET}="dell-do ATTRS{idVendor}=="08bb", ATTRS{idProduct}=="2902", ENV{ACP_PROFILE_SET}="texas-instruments-pcm2902.conf" ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="0269", ENV{ACP_PROFILE_SET}="hp-tbt-dock-120w-g2.conf" ATTRS{idVendor}=="03f0", ATTRS{idProduct}=="0567", ENV{ACP_PROFILE_SET}="hp-tbt-dock-audio-module.conf" +ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0a4c", ENV{ACP_PROFILE_SET}="logi407.conf" # ID 1038:12ad is for the 2018 refresh of the Arctis 7. # ID 1038:1294 is for Arctis Pro Wireless (which works with the Arctis 7 configuration). @@ -137,8 +138,13 @@ ATTRS{idVendor}=="9886", ATTRS{idProduct}=="002c", ENV{ACP_PROFILE_SET}="usb-gam ATTRS{idVendor}=="9886", ATTRS{idProduct}=="0038", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" # ID 9886:0045 is for the Astro A20 Gen2 ATTRS{idVendor}=="9886", ATTRS{idProduct}=="0045", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" + # ID 1532:0520 is for the Razer Kraken Tournament Edition ATTRS{idVendor}=="1532", ATTRS{idProduct}=="0520", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# ID 1532:0579 is Razer BlackShark v3 (usb direct to headset) +ATTRS{idVendor}=="1532", ATTRS{idProduct}=="0579", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" +# ID 1532:057a is Razer BlackShark v3 (2.4GHz dongle) +ATTRS{idVendor}=="1532", ATTRS{idProduct}=="057a", ENV{ACP_PROFILE_SET}="usb-gaming-headset.conf" # ID 1038:1250 is for the Arctis 5 diff --git a/spa/plugins/alsa/acp-tool.c b/spa/plugins/alsa/acp-tool.c index cff9edb80..c12401100 100644 --- a/spa/plugins/alsa/acp-tool.c +++ b/spa/plugins/alsa/acp-tool.c @@ -132,6 +132,18 @@ static ACP_PRINTF_FUNC(6,0) void log_func(void *data, int level, const char *file, int line, const char *func, const char *fmt, va_list arg) { + static const char * const levels[] = { "E", "W", "N", "I", "D", "T" }; + const char *level_str = levels[SPA_CLAMP(level, 0, (int)SPA_N_ELEMENTS(levels) - 1)]; + + if (file) { + const char *p = strrchr(file, '/'); + if (p) + file = p + 1; + } + + fprintf(stderr, "%s %16s:%-5d ", level_str, file ? file : "", line); + while (level-- > 1) + fprintf(stderr, " "); vfprintf(stderr, fmt, arg); fprintf(stderr, "\n"); } diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c index 7b49c8be6..f6f03a5ff 100644 --- a/spa/plugins/alsa/acp/acp.c +++ b/spa/plugins/alsa/acp/acp.c @@ -8,7 +8,9 @@ #include #include +#include #include +#include int _acp_log_level = 1; acp_log_func _acp_log_func; @@ -16,7 +18,8 @@ void *_acp_log_data; struct spa_i18n *acp_i18n; -#define DEFAULT_RATE 48000 +#define DEFAULT_CHANNELS 255u +#define DEFAULT_RATE 48000u #define VOLUME_ACCURACY (PA_VOLUME_NORM/100) /* don't require volume adjustments to be perfectly correct. don't necessarily extend granularity in software unless the differences get greater than this level */ @@ -144,6 +147,15 @@ char *acp_channel_str(char *buf, size_t len, enum acp_channel ch) return buf; } +static enum acp_channel acp_channel_from_str(const char *buf, size_t len) +{ + for (unsigned long i = 0; i < ACP_N_ELEMENTS(channel_names); i++) { + if (strncmp(channel_names[i], buf, len) == 0) + return i; + } + + return ACP_CHANNEL_UNKNOWN; +} const char *acp_available_str(enum acp_available status) { @@ -183,6 +195,7 @@ static void device_free(void *data) pa_dynarray_clear(&dev->port_array); pa_proplist_free(dev->proplist); pa_hashmap_free(dev->ports); + free(dev->device.format.map); } static inline void channelmap_to_acp(pa_channel_map *m, uint32_t *map) @@ -213,9 +226,10 @@ static void init_device(pa_card *impl, pa_alsa_device *dev, pa_alsa_direction_t dev->device.format.format_mask = m->sample_spec.format; dev->device.format.rate_mask = m->sample_spec.rate; dev->device.format.channels = m->channel_map.channels; + dev->device.format.map = calloc(m->channel_map.channels, sizeof(uint32_t)); + channelmap_to_acp(&m->channel_map, dev->device.format.map); pa_cvolume_reset(&dev->real_volume, dev->device.format.channels); pa_cvolume_reset(&dev->soft_volume, dev->device.format.channels); - channelmap_to_acp(&m->channel_map, dev->device.format.map); dev->direction = direction; dev->proplist = pa_proplist_new(); pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->proplist); @@ -231,7 +245,7 @@ static void init_device(pa_card *impl, pa_alsa_device *dev, pa_alsa_direction_t pa_proplist_update(dev->proplist, PA_UPDATE_REPLACE, m->input_proplist); } if (m->split) { - char pos[2048]; + char pos[PA_CHANNELS_MAX*8]; struct spa_strbuf b; int i; @@ -369,7 +383,7 @@ static int add_pro_profile(pa_card *impl, uint32_t index) dev = -1; while (1) { - char desc[128], devstr[128], *name; + char desc[128], devstr[128]; if ((err = snd_ctl_pcm_next_device(ctl_hndl, &dev)) < 0) { pa_log_error("error iterating devices: %s", snd_strerror(err)); @@ -393,8 +407,13 @@ static int add_pro_profile(pa_card *impl, uint32_t index) pa_log_error("error pcm info: %s", snd_strerror(err)); } if (err >= 0) { + spa_autofree char *name = NULL; pa_assert_se(asprintf(&name, "Mapping pro-output-%d", dev) >= 0); m = pa_alsa_mapping_get(ps, name); + } else { + m = NULL; + } + if (m) { m->description = pa_xstrdup(desc); m->device_strings = pa_split_spaces_strv(devstr); @@ -416,7 +435,6 @@ static int add_pro_profile(pa_card *impl, uint32_t index) n_playback++; } pa_idxset_put(ap->output_mappings, m, NULL); - free(name); } snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE); @@ -425,8 +443,13 @@ static int add_pro_profile(pa_card *impl, uint32_t index) pa_log_error("error pcm info: %s", snd_strerror(err)); } if (err >= 0) { + spa_autofree char *name = NULL; pa_assert_se(asprintf(&name, "Mapping pro-input-%d", dev) >= 0); m = pa_alsa_mapping_get(ps, name); + } else { + m = NULL; + } + if (m) { m->description = pa_xstrdup(desc); m->device_strings = pa_split_spaces_strv(devstr); @@ -448,12 +471,18 @@ static int add_pro_profile(pa_card *impl, uint32_t index) n_capture++; } pa_idxset_put(ap->input_mappings, m, NULL); - free(name); } } snd_ctl_close(ctl_hndl); - if (n_capture == 1 && n_playback == 1) { + /* FireWire ALSA driver latency is determined by the buffer size and not the + * period. Timer-based scheduling is then not really useful on these devices as + * the latency is fixed. Enable IRQ scheduling unconditionally for these devices, + * so that controlling the latency works properly. + */ + bool is_firewire = spa_streq(pa_proplist_gets(impl->proplist, "device.bus"), "firewire"); + + if ((n_capture == 1 && n_playback == 1) || is_firewire) { PA_IDXSET_FOREACH(m, ap->output_mappings, idx) { pa_proplist_setf(m->output_proplist, "node.group", "pro-audio-%u", index); pa_proplist_setf(m->output_proplist, "node.link-group", "pro-audio-%u", index); @@ -498,6 +527,7 @@ static void add_profiles(pa_card *impl) int n_profiles, n_ports, n_devices; uint32_t idx; const char *arr; + bool broken_ucm = false; n_devices = 0; pa_dynarray_init(&impl->out.devices, device_free); @@ -509,7 +539,8 @@ static void add_profiles(pa_card *impl) ap->profile.flags = ACP_PROFILE_OFF; pa_hashmap_put(impl->profiles, ap->name, ap); - add_pro_profile(impl, impl->card.index); + if (!impl->disable_pro_audio) + add_pro_profile(impl, impl->card.index); PA_HASHMAP_FOREACH(ap, impl->profile_set->profiles, state) { pa_alsa_mapping *m; @@ -541,6 +572,9 @@ static void add_profiles(pa_card *impl) dev->ports, NULL); pa_dynarray_append(&ap->out.devices, dev); + + if (m->split && m->split->broken) + broken_ucm = true; } } @@ -564,6 +598,9 @@ static void add_profiles(pa_card *impl) dev->ports, NULL); pa_dynarray_append(&ap->out.devices, dev); + + if (m->split && m->split->broken) + broken_ucm = true; } } cp->n_devices = pa_dynarray_size(&ap->out.devices); @@ -571,6 +608,22 @@ static void add_profiles(pa_card *impl) pa_hashmap_put(impl->profiles, ap->name, cp); } + + /* Add a conspicuous notice if there are errors in the UCM profile */ + if (broken_ucm) { + const char *desc; + char *new_desc = NULL; + + desc = pa_proplist_gets(impl->proplist, PA_PROP_DEVICE_DESCRIPTION); + if (!desc) + desc = ""; + new_desc = spa_aprintf(_("%s [ALSA UCM error]"), desc); + pa_log_notice("Errors in ALSA UCM profile for card %s", desc); + if (new_desc) + pa_proplist_sets(impl->proplist, PA_PROP_DEVICE_DESCRIPTION, new_desc); + free(new_desc); + } + pa_dynarray_init(&impl->out.ports, NULL); n_ports = 0; PA_HASHMAP_FOREACH(dp, impl->ports, state) { @@ -1021,8 +1074,8 @@ static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask) { pa_card *impl = snd_mixer_elem_get_callback_private(melem); snd_hctl_elem_t **_elem = snd_mixer_elem_get_private(melem), *elem; - int device, i; - const char *old_monitor_name, *old_iec958_codec_list; + int device; + const char *old_monitor_name, *old_iec958_codec_list, *old_channels, *old_position; pa_device_port *p; pa_hdmi_eld eld; bool changed = false; @@ -1044,7 +1097,7 @@ static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask) memset(&eld, 0, sizeof(eld)); // Strip trailing whitespace from monitor_name (primarily an NVidia driver bug for now) - for (i = strlen(eld.monitor_name) - 1; i >= 0; i--) { + for (int i = strlen(eld.monitor_name) - 1; i >= 0; i--) { if (eld.monitor_name[i] == '\n' || eld.monitor_name[i] == '\r' || eld.monitor_name[i] == '\t' || eld.monitor_name[i] == ' ') eld.monitor_name[i] = 0; @@ -1072,6 +1125,67 @@ static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask) pa_proplist_sets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED, codecs); } + old_channels = pa_proplist_gets(p->proplist, ACP_KEY_AUDIO_CHANNELS_DETECTED); + if (eld.lpcm_channels == 0) { + changed |= old_channels != NULL; + pa_proplist_unset(p->proplist, ACP_KEY_AUDIO_CHANNELS_DETECTED); + } else { + char channels[4]; + snprintf(channels, sizeof(channels), "%u", eld.lpcm_channels); + changed |= (old_channels == NULL) || (!spa_streq(old_channels, channels)); + pa_proplist_sets(p->proplist, ACP_KEY_AUDIO_CHANNELS_DETECTED, channels); + } + + old_position = pa_proplist_gets(p->proplist, ACP_KEY_AUDIO_POSITION_DETECTED); + if (eld.speakers == 0) { + changed |= old_position != NULL; + pa_proplist_unset(p->proplist, ACP_KEY_AUDIO_POSITION_DETECTED); + } else { + uint32_t positions[eld.lpcm_channels]; + char position[eld.lpcm_channels * 8]; + struct spa_strbuf b; + int i = 0; + + if (eld.speakers & 0x01) { + positions[i++] = ACP_CHANNEL_FL; + positions[i++] = ACP_CHANNEL_FR; + } + if (eld.speakers & 0x02) { + positions[i++] = ACP_CHANNEL_LFE; + } + if (eld.speakers & 0x04) { + positions[i++] = ACP_CHANNEL_FC; + } + if (eld.speakers & 0x08) { + positions[i++] = ACP_CHANNEL_RL; + positions[i++] = ACP_CHANNEL_RR; + } + /* The rest are out of order in order of what channels we would prefer to use/expose first */ + if (eld.speakers & 0x40) { + /* Use SL/SR instead of RLC/RRC */ + positions[i++] = ACP_CHANNEL_SL; + positions[i++] = ACP_CHANNEL_SR; + } + if (eld.speakers & 0x20) { + positions[i++] = ACP_CHANNEL_RLC; + positions[i++] = ACP_CHANNEL_RRC; + } + if (eld.speakers & 0x10) { + positions[i++] = ACP_CHANNEL_RC; + } + while (i < eld.lpcm_channels) + positions[i++] = ACP_CHANNEL_UNKNOWN; + + spa_strbuf_init(&b, position, sizeof(position)); + spa_strbuf_append(&b, "["); + for (i = 0; i < eld.lpcm_channels; i++) + spa_strbuf_append(&b, "%s%s", i ? "," : "", channel_names[positions[i]]); + spa_strbuf_append(&b, "]"); + + changed |= (old_position == NULL) || (!spa_streq(old_position, position)); + pa_proplist_sets(p->proplist, ACP_KEY_AUDIO_POSITION_DETECTED, position); + } + pa_proplist_as_dict(p->proplist, &p->port.props); if (changed && mask != 0 && impl->events && impl->events->props_changed) @@ -1164,7 +1278,7 @@ uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *nam static void find_mixer(pa_card *impl, pa_alsa_device *dev, const char *element, bool ignore_dB) { - const char *mdev; + const char *mdev = NULL; pa_alsa_mapping *mapping = dev->mapping; if (!mapping && !element) @@ -1173,7 +1287,8 @@ static void find_mixer(pa_card *impl, pa_alsa_device *dev, const char *element, if (!element && mapping && pa_alsa_path_set_is_empty(dev->mixer_path_set)) return; - mdev = pa_proplist_gets(mapping->proplist, "alsa.mixer_device"); + if (mapping) + mdev = pa_proplist_gets(mapping->proplist, "alsa.mixer_device"); if (mdev) { dev->mixer_handle = pa_alsa_open_mixer_by_name(impl->ucm.mixers, mdev, true); } else { @@ -1240,8 +1355,15 @@ static int read_volume(pa_alsa_device *dev) if (!dev->mixer_handle) return 0; - if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0) - return res; + if (dev->mixer_path->has_volume_mute && dev->muted) { + /* Shift up by the base volume */ + pa_sw_cvolume_divide_scalar(&r, &dev->hardware_volume, dev->base_volume); + pa_log_debug("Reading cached volume only."); + } else { + if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, + &dev->mapping->channel_map, &r)) < 0) + return res; + } /* Shift down by the base volume, so that 0dB becomes maximum volume */ pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume); @@ -1268,6 +1390,7 @@ static int read_volume(pa_alsa_device *dev) static void set_volume(pa_alsa_device *dev, const pa_cvolume *v) { pa_cvolume r; + bool write_to_hw; if (v != &dev->real_volume) dev->real_volume = *v; @@ -1286,8 +1409,10 @@ static void set_volume(pa_alsa_device *dev, const pa_cvolume *v) /* Shift up by the base volume */ pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume); + write_to_hw = !(dev->mixer_path->has_volume_mute && dev->muted); + if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, - &r, false, true) < 0) + &r, false, write_to_hw) < 0) return; /* Shift down by the base volume, so that 0dB becomes maximum volume */ @@ -1344,8 +1469,18 @@ static int read_mute(pa_alsa_device *dev) if (!dev->mixer_handle) return 0; - if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0) - return res; + if (dev->mixer_path->has_volume_mute) { + pa_cvolume mute_vol; + pa_cvolume r; + + pa_cvolume_mute(&mute_vol, dev->mapping->channel_map.channels); + if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0) + return res; + mute = pa_cvolume_equal(&mute_vol, &r); + } else { + if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0) + return res; + } if (mute == dev->muted) return 0; @@ -1374,7 +1509,25 @@ static void set_mute(pa_alsa_device *dev, bool mute) if (!dev->mixer_handle) return; - pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute); + if (dev->mixer_path->has_volume_mute) { + pa_cvolume r; + + if (mute) { + pa_cvolume_mute(&r, dev->mapping->channel_map.channels); + pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, + &r, false, true); + } else { + /* Shift up by the base volume */ + pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume); + pa_log_debug("Restoring volume: %d", pa_cvolume_max(&dev->real_volume)); + if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, + &r, false, true) < 0) + pa_log_error("Unable to restore volume %d during unmute", + pa_cvolume_max(&dev->real_volume)); + } + } else { + pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute); + } } static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev) @@ -1410,6 +1563,11 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev) dev->base_volume = pa_sw_volume_from_dB(-dev->mixer_path->max_dB); dev->n_volume_steps = PA_VOLUME_NORM+1; + /* If minimum volume is set to -99999 dB, then volume control supports + * mute */ + if (dev->mixer_path->min_dB == -99999.99 && !dev->mixer_path->has_mute) + dev->mixer_path->has_volume_mute = true; + pa_log_info("Fixing base volume to %0.2f dB", pa_sw_volume_to_dB(dev->base_volume)); } else { dev->decibel_volume = false; @@ -1424,7 +1582,8 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev) dev->device.base_volume = (float)pa_sw_volume_to_linear(dev->base_volume); dev->device.volume_step = 1.0f / dev->n_volume_steps; - if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_mute) { + if (impl->soft_mixer || !dev->mixer_path || + (!dev->mixer_path->has_mute && !dev->mixer_path->has_volume_mute)) { dev->read_mute = NULL; dev->set_mute = NULL; pa_log_info("Driver does not support hardware mute control, falling back to software mute control."); @@ -1432,7 +1591,8 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev) } else { dev->read_mute = read_mute; dev->set_mute = set_mute; - pa_log_info("Using hardware mute control."); + pa_log_info("Using hardware %smute control.", + dev->mixer_path->has_volume_mute ? "volume-" : ""); dev->device.flags |= ACP_DEVICE_HW_MUTE; } } @@ -1530,7 +1690,6 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device { const char *mod_name; uint32_t i, port_index; - const char *codecs; pa_device_port *p; void *state = NULL; int res; @@ -1569,6 +1728,29 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device if (dev->active_port) dev->active_port->port.flags |= ACP_PORT_ACTIVE; + if (impl->use_eld_channels) { + while ((p = pa_hashmap_iterate(dev->ports, &state, NULL))) { + const char *channels = pa_proplist_gets(p->proplist, ACP_KEY_AUDIO_CHANNELS_DETECTED); + const char *positions = pa_proplist_gets(p->proplist, ACP_KEY_AUDIO_POSITION_DETECTED); + + if (channels && positions) { + const char *position, *split_state = NULL; + size_t i = 0, n; + + dev->device.format.channels = atoi(channels); + free(dev->device.format.map); + dev->device.format.map = calloc(dev->device.format.channels, sizeof(uint32_t)); + + while ((position = pa_split_in_place(positions, ",", &n, &split_state)) != NULL && + i < dev->device.format.channels) { + dev->device.format.map[i++] = acp_channel_from_str(position, n); + } + + break; + } + } + } + if ((res = setup_mixer(impl, dev, impl->ignore_dB)) < 0) return res; @@ -1583,13 +1765,16 @@ static int device_enable(pa_card *impl, pa_alsa_mapping *mapping, pa_alsa_device else dev->muted = false; + state = NULL; while ((p = pa_hashmap_iterate(dev->ports, &state, NULL))) { - codecs = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED); + const char *codecs = pa_proplist_gets(p->proplist, ACP_KEY_IEC958_CODECS_DETECTED); if (codecs) { dev->device.n_codecs = acp_iec958_codecs_from_json(codecs, dev->device.codecs, ACP_N_ELEMENTS(dev->device.codecs)); - break; } + + if (codecs) + break; } return 0; @@ -1754,7 +1939,7 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props) impl->auto_port = true; impl->ignore_dB = false; impl->rate = DEFAULT_RATE; - impl->pro_channels = 64; + impl->pro_channels = DEFAULT_CHANNELS; if (props) { if ((s = acp_dict_lookup(props, "api.alsa.use-ucm")) != NULL) @@ -1779,6 +1964,10 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props) impl->pro_channels = atoi(s); if ((s = acp_dict_lookup(props, "api.alsa.split-enable")) != NULL) impl->ucm.split_enable = spa_atob(s); + if ((s = acp_dict_lookup(props, "api.acp.disable-pro-audio")) != NULL) + impl->disable_pro_audio = spa_atob(s); + if ((s = acp_dict_lookup(props, "api.acp.use-eld-channels")) != NULL) + impl->use_eld_channels = spa_atob(s); } #if SND_LIB_VERSION < 0x10207 diff --git a/spa/plugins/alsa/acp/acp.h b/spa/plugins/alsa/acp/acp.h index 4b9f2c495..5fb5b96f7 100644 --- a/spa/plugins/alsa/acp/acp.h +++ b/spa/plugins/alsa/acp/acp.h @@ -5,18 +5,18 @@ #ifndef ACP_H #define ACP_H -#ifdef __cplusplus -extern "C" { -#else -#include -#endif - #include #include #include #include #include +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + #ifdef __GNUC__ #define ACP_PRINTF_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1))) #else @@ -24,7 +24,6 @@ extern "C" { #endif #define ACP_INVALID_INDEX ((uint32_t)-1) -#define ACP_MAX_CHANNELS 64 struct acp_dict_item { const char *key; @@ -93,7 +92,7 @@ struct acp_format { uint32_t format_mask; uint32_t rate_mask; uint32_t channels; - uint32_t map[ACP_MAX_CHANNELS]; + uint32_t *map; }; #define ACP_DICT_INIT(items,n_items) ((struct acp_dict) { 0, (n_items), (items) }) @@ -154,6 +153,18 @@ const char *acp_available_str(enum acp_available status); * values may be incorrect and/or might change, e.g. when external devices such * as receivers are powered on or off. */ +#define ACP_KEY_AUDIO_CHANNELS_DETECTED "audio.channels.detected" + /**< The number of channels detected detected via EDID-like data read from a device + * connected via HDMI/DisplayPort. This only serves as a hint, as the auto-detected + * values may be incorrect and/or might change, e.g. when external devices such + * as receivers are powered on or off. + */ +#define ACP_KEY_AUDIO_POSITION_DETECTED "audio.position.detected" + /**< The channel positions detected detected via EDID-like data read from a device + * connected via HDMI/DisplayPort. This only serves as a hint, as the auto-detected + * values may be incorrect and/or might change, e.g. when external devices such + * as receivers are powered on or off. + */ struct acp_device; diff --git a/spa/plugins/alsa/acp/alsa-mixer.c b/spa/plugins/alsa/acp/alsa-mixer.c index 1f494ee0b..b18d7c6c9 100644 --- a/spa/plugins/alsa/acp/alsa-mixer.c +++ b/spa/plugins/alsa/acp/alsa-mixer.c @@ -30,6 +30,16 @@ #include "alsa-mixer.h" #include "alsa-util.h" +/** + * ALSA (1/100) dB volume equal or below this is considered muted. + * The actual ALSA value is SND_CTL_TLV_DB_GAIN_MUTE = -9999999 + */ +#ifdef SND_CTL_TLV_DB_GAIN_MUTE +#define ALSA_DB_MUTED (SND_CTL_TLV_DB_GAIN_MUTE) +#else +#define ALSA_DB_MUTED (-9999999) +#endif + static int setting_select(pa_alsa_setting *s, snd_mixer_t *m); struct description_map { @@ -1046,6 +1056,52 @@ static long decibel_fix_get_step(pa_alsa_decibel_fix *db_fix, long *db_value, in return i + db_fix->min_step; } +/* Same as snd_mixer_selem_ask_playback/capture_dB_vol(), but if the result is muted, + * round volume up instead. + */ +static int element_ask_unmuted_dB_vol(snd_mixer_elem_t *me, pa_alsa_direction_t d, long value_dB, int rounding, long *alsa_val) { + int r = -1; + long val, dB, base_val; + + pa_assert(me); + + if (d == PA_ALSA_DIRECTION_OUTPUT) { + if ((r = snd_mixer_selem_ask_playback_dB_vol(me, value_dB, rounding, &val)) < 0) + return r; + + if ((r = snd_mixer_selem_ask_playback_vol_dB(me, val, &dB)) < 0) + return r; + + base_val = val; + + if (dB <= ALSA_DB_MUTED && value_dB > ALSA_DB_MUTED) + if ((r = snd_mixer_selem_ask_playback_dB_vol(me, value_dB, +1, &val)) < 0) + return r; + + *alsa_val = val; + } else { + if ((r = snd_mixer_selem_ask_capture_dB_vol(me, value_dB, rounding, &val)) < 0) + return r; + + if ((r = snd_mixer_selem_ask_capture_vol_dB(me, val, &dB)) < 0) + return r; + + base_val = val; + + if (dB <= ALSA_DB_MUTED && value_dB > ALSA_DB_MUTED) + if ((r = snd_mixer_selem_ask_capture_dB_vol(me, value_dB, +1, &val)) < 0) + return r; + + *alsa_val = val; + } + + if (val != base_val) + pa_log_debug("Volume rounded to mute: %ld -> %ld (dB/100) rounding:%d, correcting vol:%ld -> %ld", + value_dB, dB, rounding, base_val, val); + + return r; +} + /* Alsa lib documentation says for snd_mixer_selem_set_playback_dB() direction argument, * that "-1 = accurate or first below, 0 = accurate, 1 = accurate or first above". * But even with accurate nearest dB volume step is not selected, so that is why we need @@ -1164,17 +1220,19 @@ static int element_set_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_chann } } else { + long alsa_val; + if (write_to_hw) { if (deferred_volume) { if ((r = element_get_nearest_alsa_dB(me, c, PA_ALSA_DIRECTION_OUTPUT, &value)) >= 0) r = snd_mixer_selem_set_playback_dB(me, c, value, 0); } else { - if ((r = snd_mixer_selem_set_playback_dB(me, c, value, rounding)) >= 0) - r = snd_mixer_selem_get_playback_dB(me, c, &value); + if ((r = element_ask_unmuted_dB_vol(me, e->direction, value, rounding, &alsa_val)) >= 0) + if ((r = snd_mixer_selem_set_playback_volume(me, c, alsa_val)) >= 0) + r = snd_mixer_selem_get_playback_dB(me, c, &value); } } else { - long alsa_val; - if ((r = snd_mixer_selem_ask_playback_dB_vol(me, value, rounding, &alsa_val)) >= 0) + if ((r = element_ask_unmuted_dB_vol(me, e->direction, value, rounding, &alsa_val)) >= 0) r = snd_mixer_selem_ask_playback_vol_dB(me, alsa_val, &value); } } @@ -1192,17 +1250,19 @@ static int element_set_volume(pa_alsa_element *e, snd_mixer_t *m, const pa_chann } } else { + long alsa_val; + if (write_to_hw) { if (deferred_volume) { if ((r = element_get_nearest_alsa_dB(me, c, PA_ALSA_DIRECTION_INPUT, &value)) >= 0) r = snd_mixer_selem_set_capture_dB(me, c, value, 0); } else { - if ((r = snd_mixer_selem_set_capture_dB(me, c, value, rounding)) >= 0) - r = snd_mixer_selem_get_capture_dB(me, c, &value); + if ((r = element_ask_unmuted_dB_vol(me, e->direction, value, rounding, &alsa_val)) >= 0) + if ((r = snd_mixer_selem_set_capture_volume(me, c, alsa_val)) >= 0) + r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value); } } else { - long alsa_val; - if ((r = snd_mixer_selem_ask_capture_dB_vol(me, value, rounding, &alsa_val)) >= 0) + if ((r = element_ask_unmuted_dB_vol(me, e->direction, value, rounding, &alsa_val)) >= 0) r = snd_mixer_selem_ask_capture_vol_dB(me, alsa_val, &value); } } @@ -1398,13 +1458,18 @@ static int element_set_constant_volume(pa_alsa_element *e, snd_mixer_t *m) { else r = snd_mixer_selem_set_capture_volume_all(me, volume); } else { + int rounding = (e->direction == PA_ALSA_DIRECTION_OUTPUT) ? +1 : -1; + pa_assert(e->volume_use == PA_ALSA_VOLUME_ZERO); pa_assert(!e->db_fix); - if (e->direction == PA_ALSA_DIRECTION_OUTPUT) - r = snd_mixer_selem_set_playback_dB_all(me, 0, +1); - else - r = snd_mixer_selem_set_capture_dB_all(me, 0, -1); + r = element_ask_unmuted_dB_vol(me, e->direction, 0, rounding, &volume); + if (r >= 0) { + if (e->direction == PA_ALSA_DIRECTION_OUTPUT) + r = snd_mixer_selem_set_playback_volume_all(me, volume); + else + r = snd_mixer_selem_set_capture_volume_all(me, volume); + } } if (r < 0) { @@ -1654,11 +1719,15 @@ static bool element_probe_volume(pa_alsa_element *e, snd_mixer_elem_t *me) { else e->has_dB = snd_mixer_selem_get_capture_dB_range(me, &min_dB, &max_dB) >= 0; - /* Assume decibel data to be incorrect if max_dB is negative. */ - if (e->has_dB && max_dB < 0 && !e->db_fix) { + /* Assume decibel data to be incorrect if max_dB is negative and dB range is + * suspiciously small (< 10 dB). This can happen eg. if USB device is using volume + * values as arbitrary scale ignoring USB standard on their meaning. + */ + if (e->has_dB && max_dB < 0 && SPA_ABS(max_dB - min_dB) < 10*100 && !e->db_fix) { pa_alsa_mixer_id_to_string(buf, sizeof(buf), &e->alsa_id); - pa_log_warn("The decibel volume range for element %s (%li dB - %li dB) has negative maximum. " - "Disabling the decibel range.", buf, min_dB, max_dB); + pa_log_warn("The decibel volume range for element %s (%0.2f dB to %0.2f dB) has negative maximum " + "and suspiciously small range. " + "Disabling the decibel range.", buf, min_dB/100.0, max_dB/100.0); e->has_dB = false; } @@ -3558,9 +3627,9 @@ static bool element_is_subset(pa_alsa_element *a, pa_alsa_element *b, snd_mixer_ a_limit = a->constant_volume; else if (a->volume_use == PA_ALSA_VOLUME_ZERO) { long dB = 0; + int rounding = (a->direction == PA_ALSA_DIRECTION_OUTPUT ? +1 : -1); if (a->db_fix) { - int rounding = (a->direction == PA_ALSA_DIRECTION_OUTPUT ? +1 : -1); a_limit = decibel_fix_get_step(a->db_fix, &dB, rounding); } else { snd_mixer_selem_id_t *sid; @@ -3573,13 +3642,8 @@ static bool element_is_subset(pa_alsa_element *a, pa_alsa_element *b, snd_mixer_ return false; } - if (a->direction == PA_ALSA_DIRECTION_OUTPUT) { - if (snd_mixer_selem_ask_playback_dB_vol(me, dB, +1, &a_limit) < 0) - return false; - } else { - if (snd_mixer_selem_ask_capture_dB_vol(me, dB, -1, &a_limit) < 0) - return false; - } + if (element_ask_unmuted_dB_vol(me, a->direction, dB, rounding, &a_limit) < 0) + return false; } } else if (a->volume_use == PA_ALSA_VOLUME_OFF) a_limit = a->min_volume; diff --git a/spa/plugins/alsa/acp/alsa-mixer.h b/spa/plugins/alsa/acp/alsa-mixer.h index cbfac4ab7..75d4a03db 100644 --- a/spa/plugins/alsa/acp/alsa-mixer.h +++ b/spa/plugins/alsa/acp/alsa-mixer.h @@ -225,6 +225,7 @@ struct pa_alsa_path { bool has_mute:1; bool has_volume:1; bool has_dB:1; + bool has_volume_mute:1; bool mute_during_activation:1; /* These two are used during probing only */ bool has_req_any:1; diff --git a/spa/plugins/alsa/acp/alsa-ucm.c b/spa/plugins/alsa/acp/alsa-ucm.c index 9061a1f26..d67ac91b5 100644 --- a/spa/plugins/alsa/acp/alsa-ucm.c +++ b/spa/plugins/alsa/acp/alsa-ucm.c @@ -353,6 +353,15 @@ static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd const char *device_name; int i; uint32_t hw_channels; + const char *pcm_name; + const char *rule_name; + + if (spa_streq(prefix, "Playback")) + pcm_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SINK); + else + pcm_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_SOURCE); + if (!pcm_name) + pcm_name = ""; device_name = pa_proplist_gets(device->proplist, PA_ALSA_PROP_UCM_NAME); if (!device_name) @@ -372,16 +381,23 @@ static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd if (pa_atou(value, &idx) < 0) break; - if (idx >= hw_channels) - goto fail; + if (idx >= hw_channels) { + pa_log_notice("Error in ALSA UCM profile for %s (%s): %sChannel%d=%d >= %sChannels=%d", + pcm_name, device_name, prefix, i, idx, prefix, hw_channels); + split->broken = true; + } value = ucm_get_string(uc_mgr, "%sChannelPos%d/%s", prefix, i, device_name); - if (!value) + if (!value) { + rule_name = "ChannelPos"; goto fail; + } map = snd_pcm_chmap_parse_string(value); - if (!map) + if (!map) { + rule_name = "ChannelPos value"; goto fail; + } if (map->channels == 1) { pa_log_debug("Split %s channel %d -> device %s channel %d: %s (%d)", @@ -391,6 +407,7 @@ static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd free(map); } else { free(map); + rule_name = "channel map parsing"; goto fail; } } @@ -405,7 +422,7 @@ static pa_alsa_ucm_split *ucm_get_split_channels(pa_alsa_ucm_device *device, snd return split; fail: - pa_log_warn("Invalid SplitPCM ALSA UCM rule for device %s", device_name); + pa_log_warn("Invalid SplitPCM ALSA UCM %s for device %s (%s)", rule_name, pcm_name, device_name); pa_xfree(split); return NULL; } @@ -1724,7 +1741,7 @@ int pa_alsa_ucm_set_profile(pa_alsa_ucm_config *ucm, pa_card *card, pa_alsa_prof ret = -1; } - } else if (ucm->active_verb) { + } else if (ucm->active_verb && old_profile) { /* Disable modifiers not in new profile. Has to be done before * devices, because _dismod fails if a modifier's supported * devices are disabled. */ @@ -2383,7 +2400,7 @@ static void mapping_init_eld(pa_alsa_mapping *m, snd_pcm_t *pcm) dev->eld_device = pcm_device; } -static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, int mode) { +static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, int mode, bool max_channels) { snd_pcm_t* pcm; pa_sample_spec try_ss = ucm->default_sample_spec; pa_channel_map try_map; @@ -2391,6 +2408,11 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, bool exact_channels = m->channel_map.channels > 0; if (!m->split) { + if (max_channels) { + errno = EINVAL; + return NULL; + } + if (exact_channels) { try_map = m->channel_map; try_ss.channels = try_map.channels; @@ -2402,8 +2424,8 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, return NULL; } - exact_channels = true; - try_ss.channels = m->split->hw_channels; + exact_channels = false; + try_ss.channels = max_channels ? PA_CHANNELS_MAX : m->split->hw_channels; pa_channel_map_init_extend(&try_map, try_ss.channels, PA_CHANNEL_MAP_AUX); } @@ -2416,15 +2438,40 @@ static snd_pcm_t* mapping_open_pcm(pa_alsa_ucm_config *ucm, pa_alsa_mapping *m, &try_map, mode, &try_period_size, &try_buffer_size, 0, NULL, NULL, NULL, NULL, exact_channels); if (pcm) { - if (!exact_channels) + if (m->split) { + const char *mode_name = mode == SND_PCM_STREAM_PLAYBACK ? "Playback" : "Capture"; + + if (try_map.channels < m->split->hw_channels) { + pa_logl((max_channels ? PA_LOG_NOTICE : PA_LOG_DEBUG), + "Error in ALSA UCM profile for %s (%s): %sChannels=%d > avail %d", + m->device_strings[0], m->name, mode_name, m->split->hw_channels, try_map.channels); + + /* Retry with max channel count, in case ALSA rounded down */ + if (!max_channels) { + pa_alsa_close(&pcm); + return mapping_open_pcm(ucm, m, mode, true); + } + + /* Just accept whatever we got... Some of the routings won't get connected + * anywhere */ + m->split->hw_channels = try_map.channels; + m->split->broken = true; + } else if (try_map.channels > m->split->hw_channels) { + pa_log_notice("Error in ALSA UCM profile for %s (%s): %sChannels=%d < avail %d", + m->device_strings[0], m->name, mode_name, m->split->hw_channels, try_map.channels); + m->split->hw_channels = try_map.channels; + m->split->broken = true; + } + } else if (!exact_channels) { m->channel_map = try_map; + } mapping_init_eld(m, pcm); } return pcm; } -static void pa_alsa_init_proplist_split_pcm(pa_idxset *mappings, pa_alsa_mapping *leader, pa_direction_t direction) +static void pa_alsa_init_split_pcm(pa_idxset *mappings, pa_alsa_mapping *leader, pa_direction_t direction) { pa_proplist *props = pa_proplist_new(); uint32_t idx; @@ -2445,6 +2492,9 @@ static void pa_alsa_init_proplist_split_pcm(pa_idxset *mappings, pa_alsa_mapping pa_proplist_update(m->output_proplist, PA_UPDATE_REPLACE, props); else pa_proplist_update(m->input_proplist, PA_UPDATE_REPLACE, props); + + /* Update HW channel count to match probed one */ + m->split->hw_channels = leader->split->hw_channels; } pa_proplist_free(props); @@ -2464,7 +2514,7 @@ static void profile_finalize_probing(pa_alsa_profile *p) { if (!m->split) pa_alsa_init_proplist_pcm(NULL, m->output_proplist, m->output_pcm); else - pa_alsa_init_proplist_split_pcm(p->output_mappings, m, PA_DIRECTION_OUTPUT); + pa_alsa_init_split_pcm(p->output_mappings, m, PA_DIRECTION_OUTPUT); pa_alsa_close(&m->output_pcm); } @@ -2479,7 +2529,7 @@ static void profile_finalize_probing(pa_alsa_profile *p) { if (!m->split) pa_alsa_init_proplist_pcm(NULL, m->input_proplist, m->input_pcm); else - pa_alsa_init_proplist_split_pcm(p->input_mappings, m, PA_DIRECTION_INPUT); + pa_alsa_init_split_pcm(p->input_mappings, m, PA_DIRECTION_INPUT); pa_alsa_close(&m->input_pcm); } @@ -2521,7 +2571,7 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * pa_log_info("Set ucm verb to %s", verb_name); if ((snd_use_case_set(ucm->ucm_mgr, "_verb", verb_name)) < 0) { - pa_log("Failed to set verb %s", verb_name); + pa_log("Profile '%s': failed to set verb %s", p->name, verb_name); p->supported = false; continue; } @@ -2536,8 +2586,10 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * if (m->split && !m->split->leader) continue; - m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK); + m->output_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_PLAYBACK, false); if (!m->output_pcm) { + pa_log_info("Profile '%s' mapping '%s': output PCM open failed", + p->name, m->name); p->supported = false; break; } @@ -2554,8 +2606,10 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * if (m->split && !m->split->leader) continue; - m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE); + m->input_pcm = mapping_open_pcm(ucm, m, SND_PCM_STREAM_CAPTURE, false); if (!m->input_pcm) { + pa_log_info("Profile '%s' mapping '%s': input PCM open failed", + p->name, m->name); p->supported = false; break; } @@ -2564,6 +2618,7 @@ static void ucm_probe_profile_set(pa_alsa_ucm_config *ucm, pa_alsa_profile_set * if (!p->supported) { profile_finalize_probing(p); + pa_log_info("Profile %s not supported", p->name); continue; } diff --git a/spa/plugins/alsa/acp/alsa-ucm.h b/spa/plugins/alsa/acp/alsa-ucm.h index 74dbe8219..8e4c1a7d1 100644 --- a/spa/plugins/alsa/acp/alsa-ucm.h +++ b/spa/plugins/alsa/acp/alsa-ucm.h @@ -185,6 +185,7 @@ struct pa_alsa_ucm_split { int channels; int idx[PA_CHANNELS_MAX]; enum snd_pcm_chmap_position pos[PA_CHANNELS_MAX]; + bool broken; }; struct pa_alsa_ucm_device { diff --git a/spa/plugins/alsa/acp/alsa-util.c b/spa/plugins/alsa/acp/alsa-util.c index 96d6020cd..52b63eb87 100644 --- a/spa/plugins/alsa/acp/alsa-util.c +++ b/spa/plugins/alsa/acp/alsa-util.c @@ -2020,14 +2020,24 @@ int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld) { sad_count = 0; } + /* Look up speaker presence in Speaker Allocation Data Block */ + eld->speakers = elddata[7] & 0x7f; + + eld->lpcm_channels = 0; eld->iec958_codecs = 0; + for (unsigned i = 0; i < sad_count; i++) { uint8_t *sad = &elddata[20 + mnl + 3 * i]; + uint8_t lpcm_channels; /* https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#Audio_Data_Blocks */ switch ((sad[0] & 0x78) >> 3) { case 1: eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_PCM; + /* Lowest 3 bits are channel count - 1 */ + lpcm_channels = (sad[0] & 0x07) + 1; + if (lpcm_channels > eld->lpcm_channels) + eld->lpcm_channels = lpcm_channels; break; case 2: eld->iec958_codecs |= 1ULL << SPA_AUDIO_IEC958_CODEC_AC3; diff --git a/spa/plugins/alsa/acp/alsa-util.h b/spa/plugins/alsa/acp/alsa-util.h index 26c2698c9..f8c8622b5 100644 --- a/spa/plugins/alsa/acp/alsa-util.h +++ b/spa/plugins/alsa/acp/alsa-util.h @@ -175,7 +175,9 @@ void pa_alsa_mixer_free(pa_alsa_mixer *mixer); typedef struct pa_hdmi_eld pa_hdmi_eld; struct pa_hdmi_eld { char monitor_name[17]; + uint8_t speakers; uint64_t iec958_codecs; + uint8_t lpcm_channels; }; int pa_alsa_get_hdmi_eld(snd_hctl_elem_t *elem, pa_hdmi_eld *eld); diff --git a/spa/plugins/alsa/acp/array.h b/spa/plugins/alsa/acp/array.h index 84e9f65eb..dfb344025 100644 --- a/spa/plugins/alsa/acp/array.h +++ b/spa/plugins/alsa/acp/array.h @@ -5,13 +5,13 @@ #ifndef PA_ARRAY_H #define PA_ARRAY_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - typedef struct pa_array { void *data; /**< pointer to array data */ size_t size; /**< length of array in bytes */ diff --git a/spa/plugins/alsa/acp/card.h b/spa/plugins/alsa/acp/card.h index c1126fe23..f75222dbf 100644 --- a/spa/plugins/alsa/acp/card.h +++ b/spa/plugins/alsa/acp/card.h @@ -22,14 +22,14 @@ #ifndef PULSE_CARD_H #define PULSE_CARD_H +#include "compat.h" + #ifdef __cplusplus extern "C" { #else #include #endif -#include "compat.h" - typedef struct pa_card pa_card; struct pa_card { @@ -48,6 +48,8 @@ struct pa_card { bool auto_profile; bool auto_port; bool ignore_dB; + bool disable_pro_audio; + bool use_eld_channels; uint32_t rate; uint32_t pro_channels; diff --git a/spa/plugins/alsa/acp/channelmap.h b/spa/plugins/alsa/acp/channelmap.h index adb486809..2b13d2528 100644 --- a/spa/plugins/alsa/acp/channelmap.h +++ b/spa/plugins/alsa/acp/channelmap.h @@ -21,13 +21,17 @@ #ifndef PULSE_CHANNELMAP_H #define PULSE_CHANNELMAP_H +#include "spa/utils/defs.h" + #ifdef __cplusplus extern "C" { #endif -#include "spa/utils/defs.h" - +#ifdef SPA_AUDIO_MAX_CHANNELS +#define PA_CHANNELS_MAX ((int)SPA_AUDIO_MAX_CHANNELS) +#else #define PA_CHANNELS_MAX 64 +#endif #define PA_CHANNEL_MAP_SNPRINT_MAX (PA_CHANNELS_MAX * 32) @@ -451,7 +455,6 @@ static inline int pa_channel_map_equal(const pa_channel_map *a, const pa_channel static inline char* pa_channel_map_snprint(char *s, size_t l, const pa_channel_map *map) { unsigned channel; - bool first = true; char *e; if (!pa_channel_map_valid(map)) { pa_snprintf(s, l, "%s", _("(invalid)")); @@ -460,12 +463,10 @@ static inline char* pa_channel_map_snprint(char *s, size_t l, const pa_channel_m *(e = s) = 0; for (channel = 0; channel < map->channels && l > 1; channel++) { l -= pa_snprintf(e, l, "%s%s", - first ? "" : ",", + channel == 0 ? "" : ",", pa_channel_position_to_string(map->map[channel])); e = strchr(e, 0); - first = false; } - return s; } diff --git a/spa/plugins/alsa/acp/compat.c b/spa/plugins/alsa/acp/compat.c index e2f317b09..050f2ec54 100644 --- a/spa/plugins/alsa/acp/compat.c +++ b/spa/plugins/alsa/acp/compat.c @@ -18,13 +18,14 @@ along with PulseAudio; if not, see . ***/ +#include "config.h" + #include #include #include "compat.h" #include "device-port.h" #include "alsa-mixer.h" -#include "config.h" static const char *port_types[] = { [PA_DEVICE_PORT_TYPE_UNKNOWN] = "unknown", diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h index 7660e1c27..f7592e1a6 100644 --- a/spa/plugins/alsa/acp/compat.h +++ b/spa/plugins/alsa/acp/compat.h @@ -22,12 +22,6 @@ #ifndef PULSE_COMPAT_H #define PULSE_COMPAT_H -#ifdef __cplusplus -extern "C" { -#else -#include -#endif - #include #include #include @@ -39,6 +33,12 @@ extern "C" { #include +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + typedef struct pa_core pa_core; typedef void *(*pa_copy_func_t)(const void *p); @@ -327,18 +327,23 @@ static inline size_t pa_snprintf(char *str, size_t size, const char *format, ... return ret; } -#define pa_xstrdup(s) ((s) != NULL ? strdup(s) : NULL) -#define pa_xstrndup(s,n) ((s) != NULL ? strndup(s,n) : NULL) +#define pa_xnullcheck(p) ({ void *_mem_alloc = (p); spa_assert_se(_mem_alloc); _mem_alloc; }) +#define pa_xstrdup(s) ((s) != NULL ? pa_xnullcheck(strdup(s)) : NULL) +#define pa_xstrndup(s,n) ((s) != NULL ? pa_xnullcheck(strndup(s,n)) : NULL) #define pa_xfree free -#define pa_xmalloc malloc -#define pa_xnew0(t,n) calloc(n, sizeof(t)) +#define pa_xmalloc(n) pa_xnullcheck(malloc(n)) +#define pa_xnew0(t,n) pa_xnullcheck(calloc((n), sizeof(t))) #define pa_xnew(t,n) pa_xnew0(t,n) -#define pa_xrealloc realloc -#define pa_xrenew(t,p,n) ((t*) realloc(p, (n)*sizeof(t))) +#define pa_xrenew(t,p,n) ((t*) pa_xnullcheck(realloc(p, (n)*sizeof(t)))) static inline void* pa_xmemdup(const void *p, size_t l) { - return memcpy(malloc(l), p, l); - + if (!p) { + return NULL; + } else { + void *dst = pa_xmalloc(l); + memcpy(dst, p, l); + return dst; + } } #define pa_xnewdup(t,p,n) ((t*) pa_xmemdup((p), (n)*sizeof(t))) diff --git a/spa/plugins/alsa/acp/device-port.h b/spa/plugins/alsa/acp/device-port.h index d6bdc2297..d0224b4e0 100644 --- a/spa/plugins/alsa/acp/device-port.h +++ b/spa/plugins/alsa/acp/device-port.h @@ -22,14 +22,14 @@ #ifndef PULSE_DEVICE_PORT_H #define PULSE_DEVICE_PORT_H +#include "compat.h" + #ifdef __cplusplus extern "C" { #else #include #endif -#include "compat.h" - typedef struct pa_card pa_card; typedef struct pa_device_port pa_device_port; diff --git a/spa/plugins/alsa/acp/dynarray.h b/spa/plugins/alsa/acp/dynarray.h index eb7961847..87904d165 100644 --- a/spa/plugins/alsa/acp/dynarray.h +++ b/spa/plugins/alsa/acp/dynarray.h @@ -5,12 +5,12 @@ #ifndef PA_DYNARRAY_H #define PA_DYNARRAY_H +#include "compat.h" + #ifdef __cplusplus extern "C" { #endif -#include "array.h" - typedef struct pa_dynarray_item { void *ptr; } pa_dynarray_item; diff --git a/spa/plugins/alsa/acp/hashmap.h b/spa/plugins/alsa/acp/hashmap.h index 02a4950e0..e2c3ddd3b 100644 --- a/spa/plugins/alsa/acp/hashmap.h +++ b/spa/plugins/alsa/acp/hashmap.h @@ -5,12 +5,12 @@ #ifndef PA_HASHMAP_H #define PA_HASHMAP_H +#include "array.h" + #ifdef __cplusplus extern "C" { #endif -#include "array.h" - typedef unsigned (*pa_hash_func_t)(const void *p); typedef int (*pa_compare_func_t)(const void *a, const void *b); diff --git a/spa/plugins/alsa/acp/idxset.h b/spa/plugins/alsa/acp/idxset.h index 2638133da..598017eb6 100644 --- a/spa/plugins/alsa/acp/idxset.h +++ b/spa/plugins/alsa/acp/idxset.h @@ -5,12 +5,12 @@ #ifndef PA_IDXSET_H #define PA_IDXSET_H +#include "array.h" + #ifdef __cplusplus extern "C" { #endif -#include "array.h" - #define PA_IDXSET_INVALID ((uint32_t) -1) typedef unsigned (*pa_hash_func_t)(const void *p); diff --git a/spa/plugins/alsa/acp/proplist.h b/spa/plugins/alsa/acp/proplist.h index a647f520a..23d03d0b1 100644 --- a/spa/plugins/alsa/acp/proplist.h +++ b/spa/plugins/alsa/acp/proplist.h @@ -5,15 +5,15 @@ #ifndef PA_PROPLIST_H #define PA_PROPLIST_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include "array.h" #include "acp.h" +#ifdef __cplusplus +extern "C" { +#endif + #define PA_PROP_DEVICE_DESCRIPTION "device.description" #define PA_PROP_DEVICE_CLASS "device.class" diff --git a/spa/plugins/alsa/acp/volume.h b/spa/plugins/alsa/acp/volume.h index de786b7c2..5b5585953 100644 --- a/spa/plugins/alsa/acp/volume.h +++ b/spa/plugins/alsa/acp/volume.h @@ -21,12 +21,12 @@ #ifndef PA_VOLUME_H #define PA_VOLUME_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - typedef uint32_t pa_volume_t; #define PA_VOLUME_MUTED ((pa_volume_t) 0U) diff --git a/spa/plugins/alsa/alsa-acp-device.c b/spa/plugins/alsa/alsa-acp-device.c index 5d46feed4..44342a7a3 100644 --- a/spa/plugins/alsa/alsa-acp-device.c +++ b/spa/plugins/alsa/alsa-acp-device.c @@ -38,6 +38,7 @@ extern struct spa_i18n *acp_i18n; #define MAX_POLL 16 +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define DEFAULT_DEVICE "hw:0" #define DEFAULT_AUTO_PROFILE true @@ -155,12 +156,13 @@ static int emit_node(struct impl *this, struct acp_device *dev) const struct acp_dict_item *it; uint32_t n_items, i; char device_name[128], path[210], channels[16], ch[12], routes[16]; - char card_index[16], card_name[64], *p; - char positions[SPA_AUDIO_MAX_CHANNELS * 12]; + char card_index[16], card_name[64]; + char positions[MAX_CHANNELS * 12]; char codecs[512]; struct spa_device_object_info info; struct acp_card *card = this->card; - const char *stream, *card_id; + const char *stream, *card_id, *bus; + struct spa_strbuf b; info = SPA_DEVICE_OBJECT_INFO_INIT(); info.type = SPA_TYPE_INTERFACE_Node; @@ -175,7 +177,7 @@ static int emit_node(struct impl *this, struct acp_device *dev) info.change_mask = SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; - items = alloca((dev->props.n_items + 11) * sizeof(*items)); + items = alloca((dev->props.n_items + 12) * sizeof(*items)); n_items = 0; snprintf(card_index, sizeof(card_index), "%d", card->index); @@ -193,15 +195,19 @@ static int emit_node(struct impl *this, struct acp_device *dev) items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PCM_STREAM, stream); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, stream); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ICON_NAME, "audio-card-analog"); + bus = acp_dict_lookup(&card->props, SPA_KEY_DEVICE_BUS); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, bus); snprintf(channels, sizeof(channels), "%d", dev->format.channels); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_CHANNELS, channels); - p = positions; + spa_strbuf_init(&b, positions, sizeof(positions)); + spa_strbuf_append(&b, "["); for (i = 0; i < dev->format.channels; i++) { - p += snprintf(p, 12, "%s%s", i == 0 ? "" : ",", + spa_strbuf_append(&b, "%s%s", i == 0 ? " " : ", ", acp_channel_str(ch, sizeof(ch), dev->format.map[i])); } + spa_strbuf_append(&b, " ]"); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_AUDIO_POSITION, positions); if (dev->n_codecs > 0) { @@ -671,8 +677,8 @@ static int apply_device_props(struct impl *this, struct acp_device *dev, struct struct spa_pod_prop *prop; struct spa_pod_object *obj = (struct spa_pod_object *) props; int changed = 0; - float volumes[ACP_MAX_CHANNELS]; - uint32_t channels[ACP_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; + uint32_t channels[MAX_CHANNELS]; uint32_t n_volumes = 0; if (!spa_pod_is_object_type(props, SPA_TYPE_OBJECT_Props)) @@ -694,13 +700,13 @@ static int apply_device_props(struct impl *this, struct acp_device *dev, struct break; case SPA_PROP_channelVolumes: if ((n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - volumes, ACP_MAX_CHANNELS)) > 0) { + volumes, SPA_N_ELEMENTS(volumes))) > 0) { changed++; } break; case SPA_PROP_channelMap: if (spa_pod_copy_array(&prop->value, SPA_TYPE_Id, - channels, ACP_MAX_CHANNELS) > 0) { + channels, SPA_N_ELEMENTS(channels)) > 0) { changed++; } break; diff --git a/spa/plugins/alsa/alsa-compress-offload-sink.c b/spa/plugins/alsa/alsa-compress-offload-sink.c index d10d9c071..c9691a18f 100644 --- a/spa/plugins/alsa/alsa-compress-offload-sink.c +++ b/spa/plugins/alsa/alsa-compress-offload-sink.c @@ -684,7 +684,7 @@ static void stop_driver_timer(struct impl *this) /* Perform the actual stop within * the dataloop to avoid data races. */ - spa_loop_invoke(this->data_loop, do_remove_driver_timer_source, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_driver_timer_source, 0, NULL, 0, this); } static void on_driver_timeout(struct spa_source *source) @@ -795,7 +795,7 @@ static void reevaluate_following_state(struct impl *this) if (following != this->following) { spa_log_debug(this->log, "%p: following state changed: %d->%d", this, this->following, following); this->following = following; - spa_loop_invoke(this->data_loop, do_reevaluate_following_state, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_reevaluate_following_state, 0, NULL, 0, this); } } @@ -967,11 +967,15 @@ static int write_queued_output_buffers(struct impl *this) * If during the write attempts, only a portion of a chunk * is written, we must keep track of the portion that hasn't * been consumed yet. offset_within_oldest_output_buffer - * exists for this purpose. In this sink node, each SPA - * buffer has exactly one chunk, so when a chunk is fully - * consumed, the corresponding buffer is removed from the - * queued_output_buffers list, marked as available, and - * returned to the pool through spa_node_call_reuse_buffer(). */ + * exists for this purpose. This can happen when the + * device_write() call below returns 0. The loop is then + * aborted, and the chunk is not fully written. + * + * In this sink node, each SPA buffer has exactly one chunk, + * so when a chunk is fully consumed, the corresponding buffer + * is removed from the queued_output_buffers list, marked as + * available, and returned to the pool through + * spa_node_call_reuse_buffer(). */ again: total_num_written_bytes = 0; diff --git a/spa/plugins/alsa/alsa-pcm.c b/spa/plugins/alsa/alsa-pcm.c index 36834eaed..4f2fde8b6 100644 --- a/spa/plugins/alsa/alsa-pcm.c +++ b/spa/plugins/alsa/alsa-pcm.c @@ -20,6 +20,7 @@ static struct spa_list cards = SPA_LIST_INIT(&cards); static struct spa_list states = SPA_LIST_INIT(&states); +#define SPA_ALSA_DLL_BW_MIN 0.001 static struct card *find_card(uint32_t index) { @@ -162,6 +163,11 @@ static int alsa_set_param(struct state *state, const char *k, const char *s) int fmt_change = 0; if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) { state->default_channels = atoi(s); + if (state->default_channels > MAX_CHANNELS) { + spa_log_warn(state->log, "%p: %s: %s > %d, clamping", + state, k, s, MAX_CHANNELS); + state->default_channels = MAX_CHANNELS; + } fmt_change++; } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) { state->default_rate = atoi(s); @@ -193,6 +199,9 @@ static int alsa_set_param(struct state *state, const char *k, const char *s) state->disable_batch = spa_atob(s); } else if (spa_streq(k, "api.alsa.disable-tsched")) { state->disable_tsched = spa_atob(s); + } else if (spa_streq(k, "api.alsa.dll-bandwidth-max")) { + state->dll_bw_max = SPA_CLAMPD(spa_strtod(s, NULL), + SPA_ALSA_DLL_BW_MIN, SPA_DLL_BW_MAX); } else if (spa_streq(k, "api.alsa.use-chmap")) { state->props.use_chmap = spa_atob(s); } else if (spa_streq(k, "api.alsa.multi-rate")) { @@ -203,6 +212,8 @@ static int alsa_set_param(struct state *state, const char *k, const char *s) state->htimestamp_max_errors = atoi(s); } else if (spa_streq(k, "api.alsa.auto-link")) { state->auto_link = spa_atob(s); + } else if (spa_streq(k, "api.alsa.dsd-lsb")) { + state->dsd_lsb = spa_atob(s); } else if (spa_streq(k, "latency.internal.rate")) { state->process_latency.rate = atoi(s); } else if (spa_streq(k, "latency.internal.ns")) { @@ -229,35 +240,33 @@ static int alsa_set_param(struct state *state, const char *k, const char *s) static int position_to_string(struct channel_map *map, char *val, size_t len) { - uint32_t i, o = 0; - int r; - o += snprintf(val, len, "[ "); - for (i = 0; i < map->channels; i++) { - r = snprintf(val+o, len-o, "%s%s", i == 0 ? "" : ", ", - spa_debug_type_find_short_name(spa_type_audio_channel, - map->pos[i])); - if (r < 0 || o + r >= len) - return -ENOSPC; - o += r; + uint32_t i; + char pos[8]; + struct spa_strbuf b; + + spa_strbuf_init(&b, val, len); + spa_strbuf_append(&b, "["); + for (i = 0; i < map->n_pos; i++) { + spa_strbuf_append(&b, "%s%s", i == 0 ? " " : ", ", + spa_type_audio_channel_make_short_name(map->pos[i], + pos, sizeof(pos), "UNK")); } - if (len > o) - o += snprintf(val+o, len-o, " ]"); + if (spa_strbuf_append(&b, " ]") < 2) + return -ENOSPC; return 0; } static int uint32_array_to_string(uint32_t *vals, uint32_t n_vals, char *val, size_t len) { - uint32_t i, o = 0; - int r; - o += snprintf(val, len, "[ "); - for (i = 0; i < n_vals; i++) { - r = snprintf(val+o, len-o, "%s%d", i == 0 ? "" : ", ", vals[i]); - if (r < 0 || o + r >= len) - return -ENOSPC; - o += r; - } - if (len > o) - o += snprintf(val+o, len-o, " ]"); + uint32_t i; + struct spa_strbuf b; + + spa_strbuf_init(&b, val, len); + spa_strbuf_append(&b, "["); + for (i = 0; i < n_vals; i++) + spa_strbuf_append(&b, "%s%d", i == 0 ? " " : ", ", vals[i]); + if (spa_strbuf_append(&b, " ]") < 2) + return -ENOSPC; return 0; } @@ -764,7 +773,7 @@ static void bind_ctl_event(struct spa_source *source) snd_ctl_elem_id_alloca(&bound_id); snd_ctl_elem_value_alloca(&old_value); - while ((err = snd_ctl_read(state->ctl, ev) > 0)) { + while ((err = snd_ctl_read(state->ctl, ev)) > 0) { bool changed = false; if (snd_ctl_event_get_type(ev) != SND_CTL_EVENT_ELEM) @@ -961,13 +970,21 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info) snd_config_update_free_global(); - if ((str = spa_dict_lookup(info, "device.profile.pro")) != NULL) + if (info && (str = spa_dict_lookup(info, "device.profile.pro")) != NULL) state->is_pro = spa_atob(str); + if (info && spa_strstartswith(spa_dict_lookup(info, SPA_KEY_API_ALSA_CARD_NAME), "sof-") && + state->stream == SND_PCM_STREAM_PLAYBACK) { + state->use_period_size_min_as_headroom = true; + spa_log_info(state->log, + "ALSA SOF driver detected: default api.alsa.use-period-size-min-as-headroom=true"); + } + state->multi_rate = true; state->htimestamp = false; state->htimestamp_max_errors = MAX_HTIMESTAMP_ERROR; state->card_index = SPA_ID_INVALID; + state->dll_bw_max = SPA_DLL_BW_MAX; for (i = 0; info && i < info->n_items; i++) { const char *k = info->items[i].key; @@ -1000,6 +1017,8 @@ int spa_alsa_init(struct state *state, const struct spa_dict *info) state->num_bind_ctls = i; /* We'll do the actual binding after checking the card exists */ + } else if (spa_streq(k, SPA_KEY_DEVICE_BUS)) { + state->is_firewire = spa_streq(s, "firewire"); } else { alsa_set_param(state, k, s); } @@ -1563,14 +1582,17 @@ static int add_channels(struct state *state, bool all, uint32_t index, uint32_t spa_log_debug(state->log, "channels (%d %d) default:%d all:%d", min, max, state->default_channels, all); + min = SPA_MIN(min, MAX_CHANNELS); + max = SPA_MIN(max, MAX_CHANNELS); + if (state->default_channels != 0 && !all) { - if (min < state->default_channels) - min = state->default_channels; - if (max > state->default_channels) - max = state->default_channels; + if (min > state->default_channels || + max < state->default_channels) + spa_log_warn(state->log, "given audio.channels %d out of range:%d-%d", + state->default_channels, min, max); + else + min = max = state->default_channels; } - min = SPA_MIN(min, SPA_AUDIO_MAX_CHANNELS); - max = SPA_MIN(max, SPA_AUDIO_MAX_CHANNELS); spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_channels, 0); @@ -1609,39 +1631,34 @@ skip_channels: snd_pcm_free_chmaps(maps); } else { - const struct channel_map *map = NULL; - struct spa_pod_choice *choice; - if (index > 0) return 0; - spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_None, 0); - choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[0]); - spa_pod_builder_int(b, max); if (min != max) { + spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_Range, 0); + spa_pod_builder_int(b, max); spa_pod_builder_int(b, min); spa_pod_builder_int(b, max); - choice->body.type = SPA_CHOICE_Range; - } - spa_pod_builder_pop(b, &f[0]); - - if (min == max) { - if (state->default_pos.channels == min) { + spa_pod_builder_pop(b, &f[0]); + } else { + const struct channel_map *map = NULL; + spa_pod_builder_int(b, min); + if (state->default_pos.n_pos == min) { map = &state->default_pos; spa_log_debug(state->log, "%p: using provided default", state); } else if (min <= 8) { map = &default_map[min]; spa_log_debug(state->log, "%p: using default %d channel map", state, min); } - } - if (map) { - spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_position, 0); - spa_pod_builder_push_array(b, &f[0]); - for (i = 0; i < map->channels; i++) { - spa_log_debug(state->log, "%p: position %zd %d", state, i, map->pos[i]); - spa_pod_builder_id(b, map->pos[i]); + if (map) { + spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_position, 0); + spa_pod_builder_push_array(b, &f[0]); + for (i = 0; i < map->n_pos; i++) { + spa_log_debug(state->log, "%p: position %zd %d", state, i, map->pos[i]); + spa_pod_builder_id(b, map->pos[i]); + } + spa_pod_builder_pop(b, &f[0]); } - spa_pod_builder_pop(b, &f[0]); } } return 1; @@ -1842,10 +1859,12 @@ static int enum_iec958_formats(struct state *state, uint32_t index, uint32_t *ne spa_log_debug(state->log, "rate (%d %d)", rmin, rmax); if (state->default_rate != 0) { - if (rmin < state->default_rate) - rmin = state->default_rate; - if (rmax > state->default_rate) - rmax = state->default_rate; + if (rmin > state->default_rate || + rmax < state->default_rate) + spa_log_warn(state->log, "given audio.rate %d out of range:%d-%d", + state->default_rate, rmin, rmax); + else + rmin = rmax = state->default_rate; } spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_iec958Codec, 0); @@ -1913,7 +1932,7 @@ static int enum_dsd_formats(struct state *state, uint32_t index, uint32_t *next, 0); spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_bitorder, 0); - spa_pod_builder_id(b, SPA_PARAM_BITORDER_msb); + spa_pod_builder_id(b, state->dsd_lsb ? SPA_PARAM_BITORDER_lsb : SPA_PARAM_BITORDER_msb); spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_interleave, 0); spa_pod_builder_int(b, interleave); @@ -2020,7 +2039,12 @@ static void recalc_headroom(struct state *state) if (state->position != NULL) rate = state->position->clock.target_rate.denom; - state->headroom = state->default_headroom; + if (state->use_period_size_min_as_headroom) + state->headroom = state->default_headroom ? + state->default_headroom : state->period_size_min; + else + state->headroom = state->default_headroom; + if (!state->disable_tsched || state->resample) { /* When using timers, we might miss the pointer update for batch * devices so add some extra headroom. With IRQ, we know the pointers @@ -2042,13 +2066,20 @@ static void recalc_headroom(struct state *state) if (rate != 0 && state->rate != 0) latency = SPA_SCALE32_UP(latency, rate, state->rate); + if (state->is_firewire) { + /* XXX: For ALSA FireWire drivers, unlike for other ALSA drivers, buffer size + * XXX: contributes extra latency (as of kernel 6.16). + */ + latency += state->buffer_frames; + } + state->latency[state->port_direction].min_rate = state->latency[state->port_direction].max_rate = latency; } int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_t flags) { - unsigned int rrate, rchannels, val, rscale = 1; + unsigned int rrate, rchannels, val, rscale = 1, period_scale = 1; snd_pcm_uframes_t period_size; int err, dir; snd_pcm_hw_params_t *params; @@ -2064,7 +2095,7 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_ state->have_format, state->started); state->use_mmap = !state->disable_mmap; - state->force_rate = false; + state->force_quantum = state->disable_tsched; switch (fmt->media_subtype) { case SPA_MEDIA_SUBTYPE_raw: @@ -2127,7 +2158,7 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_ IEC958_AES0_CON_EMPHASIS_NONE | IEC958_AES0_NONAUDIO, IEC958_AES1_CON_ORIGINAL | IEC958_AES1_CON_PCM_CODER, 0, aes3); - state->force_rate = true; + state->force_quantum = true; break; } case SPA_MEDIA_SUBTYPE_dsd: @@ -2165,6 +2196,7 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_ default: return -ENOTSUP; } + state->force_quantum = true; break; } default: @@ -2295,10 +2327,13 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_ default_period = SPA_SCALE32_UP(DEFAULT_PERIOD, state->rate, DEFAULT_RATE); default_period = flp2(2 * default_period - 1); - /* no period size specified. If we are batch or not using timers, - * use the graph duration as the period */ - if (period_size == 0 && (state->is_batch || state->disable_tsched)) - period_size = state->position ? state->position->clock.target_duration : default_period; + /* no period size specified. If we are batch or forcing our quantum, + * use the graph requested quantum scaled by our rate */ + if (period_size == 0 && (state->is_batch || state->force_quantum) && state->position) { + period_size = SPA_SCALE32_UP(state->position->clock.target_duration, + state->rate, state->position->clock.target_rate.denom); + period_size = flp2(period_size); + } if (period_size == 0) period_size = default_period; @@ -2309,6 +2344,7 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_ * period size to our default so that we don't create too much * headroom. */ period_size = SPA_MIN(period_size, default_period) / 2; + period_scale = 2; } else { /* disable ALSA wakeups */ if (snd_pcm_hw_params_can_disable_period_wakeup(params)) @@ -2316,6 +2352,46 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_ } } + if (state->default_period_num != 0) { + /* period number given use that */ + periods = state->default_period_num; + } else if (state->disable_tsched) { + /* IRQ mode, use 3 periods. This is a bit of a workaround + * for Firewire devices, which seem to only work with 3 periods. + * For PipeWire it does not actually matter how many periods + * are used, we will always keep 1 filled, so we can work fine + * with anything from 2 periods to MAX. */ + periods = 3; + } else { + periods = UINT_MAX; + } + + /* Query the minimum period size for this configuration + * This information is used as headroom if use_period_size_min_as_headroom is + * set and default_headroom is 0 (not forced by user) + */ + CHECK(snd_pcm_hw_params_get_period_size_min(params, &state->period_size_min, &dir), "snd_pcm_hw_params_get_period_size_min"); + + if (state->default_period_size == 0) { + /* Some devices (FireWire) don't produce audio if period number is too + * small, so force a minimum. This will restrict possible period sizes if + * the device has small buffer (like FireWire), so force it only if + * period size was not set manually. + */ + snd_pcm_uframes_t period_size_max; + unsigned int periods_min = (periods == UINT_MAX) ? 3 : periods; + + err = snd_pcm_hw_params_set_periods_min(hndl, params, &periods_min, &dir); + if (!err) { + CHECK(snd_pcm_hw_params_get_period_size_max(params, &period_size_max, &dir), + "get_period_size_max"); + if (period_size > period_size_max) + period_size = SPA_MIN(period_size, flp2(period_size_max)); + } else { + spa_log_debug(state->log, "set_periods_min: %s", snd_strerror(err)); + } + } + CHECK(snd_pcm_hw_params_set_period_size_near(hndl, params, &period_size, &dir), "set_period_size_near"); if (period_size == 0) { @@ -2324,9 +2400,9 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_ } state->period_frames = period_size; + state->duration = period_size * period_scale; - if (state->default_period_num != 0) { - periods = state->default_period_num; + if (periods != UINT_MAX) { CHECK(snd_pcm_hw_params_set_periods_near(hndl, params, &periods, &dir), "set_periods"); state->buffer_frames = period_size * periods; } else { @@ -2357,14 +2433,14 @@ int spa_alsa_set_format(struct state *state, struct spa_audio_info *fmt, uint32_ recalc_headroom(state); spa_log_info(state->log, "%s: format:%s access:%s-%s rate:%d channels:%d " - "buffer frames %lu, period frames %lu, periods %u, frame_size %zd " - "headroom %u start-delay:%u batch:%u tsched:%u", + "buffer frames %lu, period frames %lu (min:%lu), periods %u, frame_size %zd " + "headroom %u start-delay:%u batch:%u tsched:%u resample:%u", state->name, snd_pcm_format_name(state->format), state->use_mmap ? "mmap" : "rw", planar ? "planar" : "interleaved", state->rate, state->channels, state->buffer_frames, state->period_frames, - periods, state->frame_size, state->headroom, state->start_delay, - state->is_batch, !state->disable_tsched); + state->period_size_min, periods, state->frame_size, state->headroom, + state->start_delay, state->is_batch, !state->disable_tsched, state->resample); /* write the parameters to device */ CHECK(snd_pcm_hw_params(hndl, params), "set_hw_params"); @@ -2564,6 +2640,7 @@ static int do_prepare(struct state *state) state->alsa_sync = true; state->alsa_sync_warning = false; state->alsa_started = false; + spa_dll_init(&state->dll); return 0; } @@ -2596,6 +2673,7 @@ static inline int do_start(struct state *state) } static inline int check_position_config(struct state *state, bool starting); +static void update_sources(struct state *state, bool active); static int alsa_recover(struct state *state) { @@ -2675,6 +2753,8 @@ recover: if (follower != driver && follower->linked) do_start(follower); } + + update_sources(state, true); return 0; } @@ -2787,6 +2867,12 @@ static int update_time(struct state *state, uint64_t current_time, snd_pcm_sfram double err, corr, avg; int32_t diff; + if (SPA_UNLIKELY(state->dll.bw == 0.0)) { + spa_dll_set_bw(&state->dll, state->dll_bw_max, state->threshold, state->rate); + state->next_time = current_time; + state->base_time = current_time; + } + if (state->disable_tsched && !follower) { err = (int64_t)(current_time - state->next_time); err = err / 1e9 * state->rate; @@ -2797,11 +2883,6 @@ static int update_time(struct state *state, uint64_t current_time, snd_pcm_sfram err = target - delay; } - if (SPA_UNLIKELY(state->dll.bw == 0.0)) { - spa_dll_set_bw(&state->dll, SPA_DLL_BW_MAX, state->threshold, state->rate); - state->next_time = current_time; - state->base_time = current_time; - } diff = (int32_t) (state->last_threshold - state->threshold); if (SPA_UNLIKELY(diff != 0)) { @@ -2850,7 +2931,7 @@ static int update_time(struct state *state, uint64_t current_time, snd_pcm_sfram err, state->max_error, state->max_resync, state->err_avg, state->err_var, bw); spa_dll_set_bw(&state->dll, - SPA_CLAMPD(bw, 0.001, SPA_DLL_BW_MAX), + SPA_CLAMPD(bw, SPA_ALSA_DLL_BW_MIN, state->dll_bw_max), state->threshold, state->rate); } @@ -2878,13 +2959,20 @@ static int update_time(struct state *state, uint64_t current_time, snd_pcm_sfram state->clock->next_nsec = state->next_time; } - spa_log_trace_fp(state->log, "%p: follower:%d %"PRIu64" %f %ld %ld %f %f %u", - state, follower, current_time, corr, delay, target, err, state->threshold * corr, - state->threshold); + spa_log_trace_fp(state->log, "%p: follower:%d %"PRIu64" %"PRIu64" %f %ld %ld %f %f %u %d", + state, follower, current_time, state->next_time, corr, delay, target, + err, state->threshold * corr, state->threshold, state->matching); return 0; } +static bool need_resample(struct state *state) +{ + return !state->pitch_elem && + ((state->rate != 0 && state->driver_rate.denom != 0 && + (uint32_t)state->rate != state->driver_rate.denom) || state->matching); +} + static int setup_matching(struct state *state) { state->matching = state->following; @@ -2898,8 +2986,10 @@ static int setup_matching(struct state *state) if (spa_streq(state->position->clock.name, state->clock_name)) state->matching = false; - state->resample = !state->pitch_elem && - (((uint32_t)state->rate != state->driver_rate.denom) || state->matching); + state->resample = need_resample(state); + + check_position_config(state, false); + recalc_headroom(state); spa_log_info(state->log, "driver clock:'%s'@%d our clock:'%s'@%d matching:%d resample:%d", @@ -2924,23 +3014,22 @@ static inline int check_position_config(struct state *state, bool starting) uint64_t target_duration; struct spa_fraction target_rate; struct spa_io_position *pos; + bool can_force; if (SPA_UNLIKELY((pos = state->position) == NULL)) return 0; - if (state->disable_tsched && (starting || state->started) && !state->following) { - target_duration = state->period_frames; + /* we can force rate/duration when we are the driver and started/starting */ + can_force = (starting || state->started) && !state->following; + + if (state->force_quantum && can_force) { target_rate = SPA_FRACTION(1, state->rate); - pos->clock.target_duration = target_duration; + target_duration = state->duration; pos->clock.target_rate = target_rate; + pos->clock.target_duration = target_duration; } else { + target_rate = pos->clock.target_rate; target_duration = pos->clock.target_duration; - if (state->force_rate && !state->following) { - target_rate = SPA_FRACTION(1, state->rate); - pos->clock.target_rate = target_rate; - } else { - target_rate = pos->clock.target_rate; - } } if (target_duration == 0 || target_rate.denom == 0) return -EIO; @@ -2957,8 +3046,7 @@ static inline int check_position_config(struct state *state, bool starting) state->max_error = SPA_MAX(256.0f, (state->threshold + state->headroom) / 2.0f); state->max_resync = SPA_MIN(state->threshold + state->headroom, state->max_error); state->err_wdw = (double)state->driver_rate.denom/state->driver_duration; - state->resample = !state->pitch_elem && - (((uint32_t)state->rate != state->driver_rate.denom) || state->matching); + state->resample = need_resample(state); state->alsa_sync = true; } return 0; @@ -2990,28 +3078,30 @@ static int alsa_write_sync(struct state *state, uint64_t current_time) if (SPA_UNLIKELY((res = update_time(state, current_time, delay, target, following)) < 0)) return res; - if (following && state->alsa_started && !state->linked) { + if (following && state->alsa_started) { if (SPA_UNLIKELY(state->alsa_sync)) { enum spa_log_level lev; - if (SPA_UNLIKELY(state->alsa_sync_warning)) - lev = SPA_LOG_LEVEL_WARN; - else - lev = SPA_LOG_LEVEL_INFO; + if (!state->linked) { + if (SPA_UNLIKELY(state->alsa_sync_warning)) + lev = SPA_LOG_LEVEL_WARN; + else + lev = SPA_LOG_LEVEL_INFO; - if ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) < 0) - lev = SPA_LOG_LEVEL_DEBUG; + if ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) < 0) + lev = SPA_LOG_LEVEL_DEBUG; - spa_log_lev(state->log, lev, "%s: follower avail:%lu delay:%ld " - "target:%ld thr:%u, resync (%d suppressed)", - state->name, avail, delay, - target, state->threshold, suppressed); + spa_log_lev(state->log, lev, "%s: follower avail:%lu delay:%ld " + "target:%ld thr:%u, resync (%d suppressed)", + state->name, avail, delay, + target, state->threshold, suppressed); - if (avail > target) - snd_pcm_rewind(state->hndl, avail - target); - else if (avail < target) - spa_alsa_silence(state, target - avail); - avail = target; + if (delay > target) + snd_pcm_rewind(state->hndl, delay - target); + else if (delay < target) + spa_alsa_silence(state, target - delay); + avail = target; + } spa_dll_init(&state->dll); state->alsa_sync = false; } else @@ -3256,27 +3346,28 @@ static int alsa_read_sync(struct state *state, uint64_t current_time) return res; max_read = state->buffer_frames; - if (following && !state->linked) { + if (following) { if (state->alsa_sync) { - enum spa_log_level lev; + if (!state->linked) { + enum spa_log_level lev; + if (SPA_UNLIKELY(state->alsa_sync_warning)) + lev = SPA_LOG_LEVEL_WARN; + else + lev = SPA_LOG_LEVEL_INFO; - if (SPA_UNLIKELY(state->alsa_sync_warning)) - lev = SPA_LOG_LEVEL_WARN; - else - lev = SPA_LOG_LEVEL_INFO; + if ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) < 0) + lev = SPA_LOG_LEVEL_DEBUG; - if ((suppressed = spa_ratelimit_test(&state->rate_limit, current_time)) < 0) - lev = SPA_LOG_LEVEL_DEBUG; + spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u " + "resample:%d, resync (%d suppressed)", state->name, delay, + target, state->threshold, state->resample, suppressed); - spa_log_lev(state->log, lev, "%s: follower delay:%ld target:%ld thr:%u " - "resample:%d, resync (%d suppressed)", state->name, delay, - target, state->threshold, state->resample, suppressed); - - if (avail < target) - max_read = target - avail; - else if (avail > target) { - snd_pcm_forward(state->hndl, avail - target); - avail = target; + if (avail < target) + max_read = target - avail; + else if (avail > target) { + snd_pcm_forward(state->hndl, avail - target); + avail = target; + } } state->alsa_sync = false; spa_dll_init(&state->dll); @@ -3358,6 +3449,10 @@ int spa_alsa_read(struct state *state) uint64_t current_time = state->position->clock.nsec; alsa_read_sync(state, current_time); } + else if (state->resample && state->rate_match) { + state->read_size = state->rate_match->size; + state->max_read = SPA_MIN(state->buffer_frames, state->read_size); + } return alsa_read_frames(state); } @@ -3399,11 +3494,13 @@ static int playback_ready(struct state *state) { struct spa_io_buffers *io = state->io; - spa_log_trace_fp(state->log, "%p: %d", state, io->status); + spa_log_trace_fp(state->log, "%p: %d", state, io ? io->status : 0); update_sources(state, false); - io->status = SPA_STATUS_NEED_DATA; + if (io != NULL) + io->status = SPA_STATUS_NEED_DATA; + return spa_node_call_ready(&state->callbacks, SPA_STATUS_NEED_DATA); } @@ -3606,7 +3703,7 @@ static int do_state_sync(struct spa_loop *loop, bool async, uint32_t seq, rt->driver = state->driver; spa_log_debug(state->log, "state:%p -> driver:%p", state, state->driver); - if(state->linked && state->matching) + if (state->linked && state->matching) try_unlink(state); } if (state->following) { @@ -3734,7 +3831,7 @@ int spa_alsa_start(struct state *state) } state->started = true; - spa_loop_invoke(state->data_loop, do_state_sync, 0, NULL, 0, true, state); + spa_loop_locked(state->data_loop, do_state_sync, 0, NULL, 0, state); return 0; } @@ -3778,7 +3875,7 @@ int spa_alsa_reassign_follower(struct state *state) } setup_matching(state); if (state->started) - spa_loop_invoke(state->data_loop, do_state_sync, 0, NULL, 0, true, state); + spa_loop_locked(state->data_loop, do_state_sync, 0, NULL, 0, state); else if (state->want_started) spa_alsa_start(state); @@ -3807,7 +3904,7 @@ int spa_alsa_pause(struct state *state) spa_log_debug(state->log, "%p: pause", state); state->started = false; - spa_loop_invoke(state->data_loop, do_state_sync, 0, NULL, 0, true, state); + spa_loop_locked(state->data_loop, do_state_sync, 0, NULL, 0, state); spa_list_for_each(follower, &state->followers, driver_link) spa_alsa_pause(follower); @@ -3834,7 +3931,7 @@ void spa_alsa_emit_node_info(struct state *state, bool full) items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, state->props.media_class); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_DRIVER, "true"); - if (state->have_format) + if (state->have_format && !state->disable_tsched) snprintf(latency, sizeof(latency), "%lu/%d", state->buffer_frames / (2 * state->frame_scale), state->rate); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_NODE_MAX_LATENCY, latency[0] ? latency : NULL); @@ -3849,7 +3946,7 @@ void spa_alsa_emit_node_info(struct state *state, bool full) snprintf(nperiods, sizeof(nperiods), "%lu", state->period_frames != 0 ? state->buffer_frames / state->period_frames : 0); else if (state->default_period_num) - snprintf(nperiods, sizeof(nperiods), "%u", state->default_period_size); + snprintf(nperiods, sizeof(nperiods), "%u", state->default_period_num); items[n_items++] = SPA_DICT_ITEM_INIT("api.alsa.period-num", nperiods[0] ? nperiods : NULL); if (state->have_format) diff --git a/spa/plugins/alsa/alsa-pcm.h b/spa/plugins/alsa/alsa-pcm.h index bd77abc2e..8b8a07721 100644 --- a/spa/plugins/alsa/alsa-pcm.h +++ b/spa/plugins/alsa/alsa-pcm.h @@ -5,10 +5,6 @@ #ifndef SPA_ALSA_UTILS_H #define SPA_ALSA_UTILS_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -35,8 +31,12 @@ extern "C" { #include "alsa.h" +#ifdef __cplusplus +extern "C" { +#endif #define MAX_RATES 16 +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define DEFAULT_PERIOD 1024u #define DEFAULT_RATE 48000u @@ -72,8 +72,8 @@ struct buffer { #define BW_PERIOD (3 * SPA_NSEC_PER_SEC) struct channel_map { - uint32_t channels; - uint32_t pos[SPA_AUDIO_MAX_CHANNELS]; + uint32_t n_pos; + uint32_t pos[MAX_CHANNELS]; }; struct card { @@ -155,6 +155,7 @@ struct state { unsigned int disable_batch:1; unsigned int disable_tsched:1; unsigned int is_split_parent:1; + unsigned int is_firewire:1; char clock_name[64]; uint32_t quantum_limit; @@ -169,6 +170,7 @@ struct state { uint32_t delay; uint32_t read_size; uint32_t max_read; + uint32_t duration; uint64_t port_info_all; struct spa_port_info port_info; @@ -202,6 +204,7 @@ struct state { int n_fds; uint32_t threshold; uint32_t last_threshold; + snd_pcm_uframes_t period_size_min; uint32_t headroom; uint32_t start_delay; uint32_t min_delay; @@ -229,9 +232,11 @@ struct state { unsigned int is_pro:1; unsigned int sources_added:1; unsigned int auto_link:1; + unsigned int dsd_lsb:1; unsigned int linked:1; unsigned int is_batch:1; - unsigned int force_rate:1; + unsigned int force_quantum:1; + unsigned int use_period_size_min_as_headroom:1; uint64_t iec958_codecs; @@ -244,6 +249,7 @@ struct state { uint64_t underrun; struct spa_dll dll; + double dll_bw_max; double max_error; double max_resync; double err_avg, err_var, err_wdw; @@ -309,7 +315,7 @@ void spa_alsa_emit_port_info(struct state *state, bool full); static inline void spa_alsa_parse_position(struct channel_map *map, const char *val, size_t len) { - spa_audio_parse_position(val, len, map->pos, &map->channels); + spa_audio_parse_position_n(val, len, map->pos, SPA_N_ELEMENTS(map->pos), &map->n_pos); } static inline uint32_t spa_alsa_parse_rates(uint32_t *rates, uint32_t max, const char *val, size_t len) diff --git a/spa/plugins/alsa/alsa-seq-bridge.c b/spa/plugins/alsa/alsa-seq-bridge.c index f85b41e28..06c5bc28f 100644 --- a/spa/plugins/alsa/alsa-seq-bridge.c +++ b/spa/plugins/alsa/alsa-seq-bridge.c @@ -275,7 +275,7 @@ static void emit_port_info(struct seq_state *this, struct seq_port *port, bool f snprintf(alias, sizeof(alias), "%s:%s", client_name, port_name); clean_name(alias); - items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_OBJECT_PATH, path); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, name); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, alias); @@ -294,13 +294,9 @@ static void emit_port_info(struct seq_state *this, struct seq_port *port, bool f static void emit_stream_info(struct seq_state *this, struct seq_stream *stream, bool full) { - uint32_t i; - - for (i = 0; i < MAX_PORTS; i++) { - struct seq_port *port = &stream->ports[i]; - if (port->valid) - emit_port_info(this, port, full); - } + struct seq_port *port; + spa_list_for_each(port, &stream->port_list, link) + emit_port_info(this, port, full); } static int @@ -353,11 +349,9 @@ static int impl_node_sync(void *object, int seq) static struct seq_port *find_port(struct seq_state *state, struct seq_stream *stream, const snd_seq_addr_t *addr) { - uint32_t i; - for (i = 0; i < stream->last_port; i++) { - struct seq_port *port = &stream->ports[i]; - if (port->valid && - port->addr.client == addr->client && + struct seq_port *port; + spa_list_for_each(port, &stream->port_list, link) { + if (port->addr.client == addr->client && port->addr.port == addr->port) return port; } @@ -366,36 +360,40 @@ static struct seq_port *find_port(struct seq_state *state, static struct seq_port *alloc_port(struct seq_state *state, struct seq_stream *stream) { + struct seq_port *port; uint32_t i; for (i = 0; i < MAX_PORTS; i++) { - struct seq_port *port = &stream->ports[i]; - if (!port->valid) { - port->id = i; - port->direction = stream->direction; - port->valid = true; - if (stream->last_port < i + 1) - stream->last_port = i + 1; - return port; - } + if (stream->ports[i] == NULL) + break; } - return NULL; + if (i == MAX_PORTS) + return NULL; + + if (!spa_list_is_empty(&state->free_list)) { + port = spa_list_first(&state->free_list, struct seq_port, link); + spa_list_remove(&port->link); + } else { + port = calloc(1, sizeof(struct seq_port)); + if (port == NULL) + return NULL; + } + port->id = i; + port->direction = stream->direction; + stream->ports[i] = port; + spa_list_append(&stream->port_list, &port->link); + + return port; } static void free_port(struct seq_state *state, struct seq_stream *stream, struct seq_port *port) { - port->valid = false; - - if (port->id + 1 == stream->last_port) { - int i; - for (i = stream->last_port - 1; i >= 0; i--) - if (stream->ports[i].valid) - break; - stream->last_port = i + 1; - } + stream->ports[port->id] = NULL; + spa_list_remove(&port->link); spa_node_emit_port_info(&state->hooks, port->direction, port->id, NULL); spa_zero(*port); + spa_list_append(&state->free_list, &port->link); } static void init_port(struct seq_state *state, struct seq_port *port, const snd_seq_addr_t *addr, @@ -529,8 +527,7 @@ impl_node_port_enum_params(void *object, int seq, param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), - SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int(1u<have_format = false; } else { struct spa_audio_info info = { 0 }; - uint32_t types; if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) return err; @@ -644,13 +639,6 @@ static int port_set_format(void *object, struct seq_port *port, info.media_subtype != SPA_MEDIA_SUBTYPE_control) return -EINVAL; - if ((err = spa_pod_parse_object(format, - SPA_TYPE_OBJECT_Format, NULL, - SPA_FORMAT_CONTROL_types, SPA_POD_Int(&types))) < 0) - return err; - if (types != 1u << SPA_CONTROL_UMP) - return -EINVAL; - port->current_format = info; port->have_format = true; } @@ -761,6 +749,36 @@ impl_node_port_use_buffers(void *object, return 0; } +struct io_info { + struct seq_state *state; + struct seq_port *port; + void *data; + size_t size; +}; + +static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct io_info *info = user_data; + struct seq_port *port = info->port; + struct seq_stream *stream = &info->state->streams[port->direction]; + + if (info->data == NULL || info->size < sizeof(struct spa_io_buffers)) { + port->io = NULL; + if (port->mixing) { + spa_list_remove(&port->mix_link); + port->mixing = false; + } + } else { + port->io = info->data; + if (!port->mixing) { + spa_list_append(&stream->mix_list, &port->mix_link); + port->mixing = true; + } + } + return 0; + } + static int impl_node_port_set_io(void *object, enum spa_direction direction, @@ -770,19 +788,25 @@ impl_node_port_set_io(void *object, { struct seq_state *this = object; struct seq_port *port; + struct io_info info; spa_return_val_if_fail(this != NULL, -EINVAL); spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); port = GET_PORT(this, direction, port_id); + info.state = this; + info.port = port; + info.data = data; + info.size = size; spa_log_debug(this->log, "%p: io %d.%d %d %p %zd", this, direction, port_id, id, data, size); switch (id) { case SPA_IO_Buffers: - port->io = data; + spa_loop_locked(this->data_loop, + do_port_set_io, SPA_ID_INVALID, NULL, 0, &info); break; default: return -ENOENT; @@ -931,6 +955,7 @@ impl_init(const struct spa_handle_factory *factory, this->quantum_limit = 8192; this->min_pool_size = 500; this->max_pool_size = 2000; + this->ump = true; for (i = 0; info && i < info->n_items; i++) { const char *k = info->items[i].key; @@ -949,6 +974,8 @@ impl_init(const struct spa_handle_factory *factory, spa_atou32(s, &this->min_pool_size, 0); } else if (spa_streq(k, "api.alsa.seq.max-pool")) { spa_atou32(s, &this->max_pool_size, 0); + } else if (spa_streq(k, "api.alsa.seq.ump")) { + this->ump = spa_atob(s); } } @@ -992,6 +1019,7 @@ static const struct spa_dict_item info_items[] = { "["SPA_KEY_API_ALSA_DISABLE_LONGNAME"=] " "[ api.alsa.seq.min-pool=] " "[ api.alsa.seq.max-pool=]" + "[ api.alsa.seq.ump = ]" }, }; diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index 2a4ebd2c4..8a4ba369c 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -24,7 +24,7 @@ #define CHECK(s,msg,...) if ((res = (s)) < 0) { spa_log_error(state->log, msg ": %s", ##__VA_ARGS__, snd_strerror(res)); return res; } -static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_queue) +static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_queue, bool probe_ump) { struct props *props = &state->props; int res; @@ -37,11 +37,46 @@ static int seq_open(struct seq_state *state, struct seq_conn *conn, bool with_qu 0)) < 0) return res; - if ((res = snd_seq_set_client_midi_version(conn->hndl, SND_SEQ_CLIENT_UMP_MIDI_2_0)) < 0) { - snd_seq_close(conn->hndl); - spa_log_info(state->log, "%p: ALSA failed to enable UMP MIDI: %s", - state, snd_strerror(res)); - return res; + if (!state->ump) { + spa_log_info(state->log, "%p: ALSA UMP MIDI disabled", state); + return 0; + } + +#ifdef HAVE_ALSA_UMP + res = snd_seq_set_client_midi_version(conn->hndl, SND_SEQ_CLIENT_UMP_MIDI_2_0); + if (!res) { + snd_seq_client_info_t *info = NULL; + + /* Double check client version */ + res = snd_seq_client_info_malloc(&info); + if (!res) + res = snd_seq_get_client_info(conn->hndl, info); + if (!res) { + res = snd_seq_client_info_get_midi_version(info); + if (res == SND_SEQ_CLIENT_UMP_MIDI_2_0) + res = 0; + else + res = -EIO; + } + if (info) + snd_seq_client_info_free(info); + } +#else + res = -EOPNOTSUPP; +#endif + + if (res < 0) { + spa_log_lev(state->log, (probe_ump ? SPA_LOG_LEVEL_INFO : SPA_LOG_LEVEL_ERROR), + "%p: ALSA failed to enable UMP MIDI: %s", state, snd_strerror(res)); + if (!probe_ump) { + snd_seq_close(conn->hndl); + return res; /* either all are UMP or none are UMP */ + } + + state->ump = false; + } else { + spa_log_debug(state->log, "%p: ALSA UMP MIDI enabled", state); + state->ump = true; } return 0; @@ -132,13 +167,16 @@ static int init_stream(struct seq_state *state, enum spa_direction direction) return res; } snd_midi_event_no_status(stream->codec, 1); - memset(stream->ports, 0, sizeof(stream->ports)); + + spa_list_init(&stream->port_list); + spa_list_init(&stream->mix_list); return 0; } static int uninit_stream(struct seq_state *state, enum spa_direction direction) { struct seq_stream *stream = &state->streams[direction]; + spa_list_insert_list(&state->free_list, &stream->port_list); if (stream->codec) snd_midi_event_free(stream->codec); stream->codec = NULL; @@ -172,45 +210,96 @@ static void init_ports(struct seq_state *state) } } -static void debug_event(struct seq_state *state, snd_seq_ump_event_t *ev) +static void debug_event(struct seq_state *state, const char *prefix, snd_seq_event_t *ev) { - if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) + enum spa_log_level lev = SPA_LOG_LEVEL_TRACE; + + if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, lev))) return; - spa_log_trace(state->log, "event type:%d flags:0x%x", ev->type, ev->flags); + spa_log_lev(state->log, lev, "%s: event type:%d flags:0x%x", prefix, ev->type, ev->flags); switch (ev->flags & SND_SEQ_TIME_STAMP_MASK) { case SND_SEQ_TIME_STAMP_TICK: - spa_log_trace(state->log, " time: %d ticks", ev->time.tick); + spa_log_lev(state->log, lev, "%s: time: %d ticks", prefix, ev->time.tick); break; case SND_SEQ_TIME_STAMP_REAL: - spa_log_trace(state->log, " time = %d.%09d", + spa_log_lev(state->log, lev, "%s: time = %d.%09d", prefix, (int)ev->time.time.tv_sec, (int)ev->time.time.tv_nsec); break; } - spa_log_trace(state->log, " source:%d.%d dest:%d.%d queue:%d", - ev->source.client, - ev->source.port, - ev->dest.client, - ev->dest.port, - ev->queue); + spa_log_lev(state->log, lev, "%s: source:%d.%d dest:%d.%d queue:%d", prefix, + ev->source.client, ev->source.port, ev->dest.client, + ev->dest.port, ev->queue); } +#ifdef HAVE_ALSA_UMP +static void debug_ump_event(struct seq_state *state, const char *prefix, snd_seq_ump_event_t *ev) +{ + enum spa_log_level lev = SPA_LOG_LEVEL_TRACE; + + if (SPA_LIKELY(!spa_log_level_topic_enabled(state->log, SPA_LOG_TOPIC_DEFAULT, lev))) + return; + + spa_log_lev(state->log, lev, "%s: event type:%d flags:0x%x", prefix, ev->type, ev->flags); + switch (ev->flags & SND_SEQ_TIME_STAMP_MASK) { + case SND_SEQ_TIME_STAMP_TICK: + spa_log_lev(state->log, lev, "%s: time: %d ticks", prefix, ev->time.tick); + break; + case SND_SEQ_TIME_STAMP_REAL: + spa_log_lev(state->log, lev, "%s: time = %d.%09d", prefix, + (int)ev->time.time.tv_sec, + (int)ev->time.time.tv_nsec); + break; + } + spa_log_lev(state->log, lev, "%s: source:%d.%d dest:%d.%d queue:%d %08x", + prefix, ev->source.client, ev->source.port, ev->dest.client, + ev->dest.port, ev->queue, ev->ump[0]); +} +#endif + static void alsa_seq_on_sys(struct spa_source *source) { struct seq_state *state = source->data; - snd_seq_ump_event_t *ev; + const bool ump = state->ump; int res; - while (snd_seq_ump_event_input(state->sys.hndl, &ev) > 0) { - const snd_seq_addr_t *addr = &ev->data.addr; + while (1) { + const snd_seq_addr_t *addr; + snd_seq_event_type_t type; + + if (ump) { +#ifdef HAVE_ALSA_UMP + snd_seq_ump_event_t *ev; + + res = snd_seq_ump_event_input(state->sys.hndl, &ev); + if (res <= 0) + break; + + debug_ump_event(state, "sys", ev); + + addr = &ev->data.addr; + type = ev->type; +#else + spa_assert_not_reached(); +#endif + } else { + snd_seq_event_t *ev; + + res = snd_seq_event_input(state->sys.hndl, &ev); + if (res <= 0) + break; + + debug_event(state, "sys", ev); + + addr = &ev->data.addr; + type = ev->type; + } if (addr->client == state->event.addr.client) continue; - debug_event(state, ev); - - switch (ev->type) { + switch (type) { case SND_SEQ_EVENT_CLIENT_START: case SND_SEQ_EVENT_CLIENT_CHANGE: spa_log_info(state->log, "client add/change %d", addr->client); @@ -243,8 +332,8 @@ static void alsa_seq_on_sys(struct spa_source *source) state->port_info(state->port_info_data, addr, NULL); break; default: - spa_log_info(state->log, "unhandled event %d: %d:%d", - ev->type, addr->client, addr->port); + spa_log_debug(state->log, "unhandled event %d: %d:%d", + type, addr->client, addr->port); break; } @@ -264,13 +353,14 @@ int spa_alsa_seq_open(struct seq_state *state) if (state->opened) return 0; + spa_list_init(&state->free_list); init_stream(state, SPA_DIRECTION_INPUT); init_stream(state, SPA_DIRECTION_OUTPUT); spa_zero(reserve); for (i = 0; i < 16; i++) { - spa_log_debug(state->log, "close %d", i); - if ((res = seq_open(state, &reserve[i], false)) < 0) + spa_log_debug(state->log, "open %d", i); + if ((res = seq_open(state, &reserve[i], false, (i == 0))) < 0) break; } if (i >= 2) { @@ -316,7 +406,6 @@ int spa_alsa_seq_open(struct seq_state *state) state->sys.source.func = alsa_seq_on_sys; state->sys.source.data = state; - spa_loop_add_source(state->main_loop, &state->sys.source); /* increase event queue timer resolution */ snd_seq_queue_timer_alloca(&timer); @@ -361,6 +450,8 @@ int spa_alsa_seq_open(struct seq_state *state) state->timerfd = res; + spa_loop_add_source(state->main_loop, &state->sys.source); + state->opened = true; return 0; @@ -374,6 +465,7 @@ error_close: int spa_alsa_seq_close(struct seq_state *state) { int res = 0; + struct seq_port *port; if (!state->opened) return 0; @@ -386,6 +478,10 @@ int spa_alsa_seq_close(struct seq_state *state) uninit_stream(state, SPA_DIRECTION_INPUT); uninit_stream(state, SPA_DIRECTION_OUTPUT); + spa_list_consume(port, &state->free_list, link) { + spa_list_remove(&port->link); + free(port); + } spa_system_close(state->data_system, state->timerfd); state->opened = false; @@ -408,11 +504,9 @@ static int set_timeout(struct seq_state *state, uint64_t time) static struct seq_port *find_port(struct seq_state *state, struct seq_stream *stream, const snd_seq_addr_t *addr) { - uint32_t i; - for (i = 0; i < stream->last_port; i++) { - struct seq_port *port = &stream->ports[i]; - if (port->valid && - port->addr.client == addr->client && + struct seq_port *port; + spa_list_for_each(port, &stream->mix_list, mix_link) { + if (port->addr.client == addr->client && port->addr.port == addr->port) return port; } @@ -506,15 +600,10 @@ static int prepare_buffer(struct seq_state *state, struct seq_port *port) static int process_recycle(struct seq_state *state) { struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; - uint32_t i; + struct seq_port *port; - for (i = 0; i < stream->last_port; i++) { - struct seq_port *port = &stream->ports[i]; + spa_list_for_each(port, &stream->mix_list, mix_link) { struct spa_io_buffers *io = port->io; - - if (!port->valid || io == NULL) - continue; - if (io->status != SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) { spa_alsa_seq_recycle_buffer(state, port, io->buffer_id); @@ -529,21 +618,57 @@ static int process_recycle(struct seq_state *state) static int process_read(struct seq_state *state) { - snd_seq_ump_event_t *ev; struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; - uint32_t i; + const bool ump = state->ump; uint32_t *data; + uint8_t midi1_data[MAX_EVENT_SIZE]; + uint32_t ump_data[MAX_EVENT_SIZE]; long size; - int res; + int res = -1; + struct seq_port *port; /* copy all new midi events into their port buffers */ - while ((res = snd_seq_ump_event_input(state->event.hndl, &ev)) > 0) { - const snd_seq_addr_t *addr = &ev->source; - struct seq_port *port; + while (1) { + const snd_seq_addr_t *addr; uint64_t ev_time, diff; uint32_t offset; + void *event; + uint8_t *midi1_ptr; + size_t midi1_size = 0; + uint64_t ump_state = 0; + snd_seq_event_type_t SPA_UNUSED type; - debug_event(state, ev); + if (ump) { +#ifdef HAVE_ALSA_UMP + snd_seq_ump_event_t *ev; + + res = snd_seq_ump_event_input(state->event.hndl, &ev); + if (res <= 0) + break; + + debug_ump_event(state, "read", ev); + + event = ev; + addr = &ev->source; + ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time); + type = ev->type; +#else + spa_assert_not_reached(); +#endif + } else { + snd_seq_event_t *ev; + + res = snd_seq_event_input(state->event.hndl, &ev); + if (res <= 0) + break; + + debug_event(state, "read", ev); + + event = ev; + addr = &ev->source; + ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time); + type = ev->type; + } if ((port = find_port(state, stream, addr)) == NULL) { spa_log_debug(state->log, "unknown port %d.%d", @@ -559,12 +684,8 @@ static int process_read(struct seq_state *state) continue; } - data = (uint32_t*)&ev->ump[0]; - size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4; - /* queue_time is the estimated current time of the queue as calculated by * the DLL. Calculate the age of the event. */ - ev_time = SPA_TIMESPEC_TO_NSEC(&ev->time.time); if (state->queue_time > ev_time) diff = state->queue_time - ev_time; else @@ -577,32 +698,63 @@ static int process_read(struct seq_state *state) else offset = 0; - spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d", - ev->type, ev_time, offset, size, addr->client, addr->port); + if (ump) { +#ifdef HAVE_ALSA_UMP + snd_seq_ump_event_t *ev = event; - spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&port->builder, data, size); + data = (uint32_t*)&ev->ump[0]; + size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4; +#else + spa_assert_not_reached(); +#endif + } else { + snd_seq_event_t *ev = event; - /* make sure we can fit at least one control event of max size otherwise - * we keep the event in the queue and try to copy it in the next cycle */ - if (port->builder.state.offset + - sizeof(struct spa_pod_control) + - MAX_EVENT_SIZE > port->buffer->buf->datas[0].maxsize) - break; + snd_midi_event_reset_decode(stream->codec); + if ((size = snd_midi_event_decode(stream->codec, midi1_data, sizeof(midi1_data), ev)) < 0) { + spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); + continue; + } + + midi1_ptr = midi1_data; + midi1_size = size; + } + + do { + if (!ump) { + data = ump_data; + size = spa_ump_from_midi(&midi1_ptr, &midi1_size, + ump_data, sizeof(ump_data), 0, &ump_state); + if (size <= 0) + break; + } + + spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%ld port:%d.%d", + type, ev_time, offset, size, addr->client, addr->port); + + spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&port->builder, data, size); + + /* make sure we can fit at least one control event of max size otherwise + * we keep the event in the queue and try to copy it in the next cycle */ + if (port->builder.state.offset + + sizeof(struct spa_pod_control) + + MAX_EVENT_SIZE > port->buffer->buf->datas[0].maxsize) + goto done; + + } while (!ump); } + +done: if (res < 0 && res != -EAGAIN) spa_log_warn(state->log, "event read failed: %s", snd_strerror(res)); /* prepare a buffer on each port, some ports might have their * buffer filled above */ res = 0; - for (i = 0; i < stream->last_port; i++) { - struct seq_port *port = &stream->ports[i]; + spa_list_for_each(port, &stream->mix_list, mix_link) { struct spa_io_buffers *io = port->io; - if (!port->valid || io == NULL) - continue; - if (prepare_buffer(state, port) >= 0) { spa_pod_builder_pop(&port->builder, &port->frame); @@ -651,22 +803,23 @@ static int process_read(struct seq_state *state) static int process_write(struct seq_state *state) { struct seq_stream *stream = &state->streams[SPA_DIRECTION_INPUT]; - uint32_t i; + const bool ump = state->ump; int err, res = 0; + struct seq_port *port; - for (i = 0; i < stream->last_port; i++) { - struct seq_port *port = &stream->ports[i]; + spa_list_for_each(port, &stream->mix_list, mix_link) { struct spa_io_buffers *io = port->io; struct buffer *buffer; - struct spa_pod_sequence *pod; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + const void *seq_body; struct spa_data *d; - struct spa_pod_control *c; - snd_seq_ump_event_t ev; + struct spa_pod_control c; + const void *c_body; uint64_t out_time; snd_seq_real_time_t out_rt; - - if (!port->valid || io == NULL) - continue; + bool first = true; if (io->status != SPA_STATUS_HAVE_DATA || io->buffer_id >= port->n_buffers) @@ -676,44 +829,92 @@ static int process_write(struct seq_state *state) d = &buffer->buf->datas[0]; io->status = SPA_STATUS_NEED_DATA; - spa_node_call_reuse_buffer(&state->callbacks, i, io->buffer_id); + spa_node_call_reuse_buffer(&state->callbacks, port->id, io->buffer_id); res |= SPA_STATUS_NEED_DATA; - pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size); - if (pod == NULL) { + spa_pod_parser_init_from_data(&parser, d->data, d->maxsize, + d->chunk->offset, d->chunk->size); + + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) { spa_log_warn(state->log, "invalid sequence in buffer max:%u offset:%u size:%u", d->maxsize, d->chunk->offset, d->chunk->size); continue; } - - SPA_POD_SEQUENCE_FOREACH(pod, c) { + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { size_t body_size; uint8_t *body; - if (c->type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_UMP) continue; - body = SPA_POD_BODY(&c->value); - body_size = SPA_POD_BODY_SIZE(&c->value); - spa_zero(ev); - - memcpy(ev.ump, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size)); - - snd_seq_ev_set_source(&ev, state->event.addr.port); - snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); - - out_time = state->queue_time + NSEC_FROM_CLOCK(&state->rate, c->offset); + body = (uint8_t*)c_body; + body_size = c.value.size; + out_time = state->queue_time + NSEC_FROM_CLOCK(&state->rate, c.offset); out_rt.tv_nsec = out_time % SPA_NSEC_PER_SEC; out_rt.tv_sec = out_time / SPA_NSEC_PER_SEC; - snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); - spa_log_trace_fp(state->log, "event %d time:%"PRIu64" offset:%d size:%zd port:%d.%d", - ev.type, out_time, c->offset, body_size, port->addr.client, port->addr.port); + spa_log_trace_fp(state->log, "event time:%"PRIu64" offset:%d size:%zd port:%d.%d", + out_time, c.offset, body_size, port->addr.client, port->addr.port); - if ((err = snd_seq_ump_event_output(state->event.hndl, &ev)) < 0) { - spa_log_warn(state->log, "failed to output event: %s", - snd_strerror(err)); + if (ump) { +#ifdef HAVE_ALSA_UMP + snd_seq_ump_event_t ev; + + snd_seq_ump_ev_clear(&ev); + snd_seq_ev_set_ump_data(&ev, body, SPA_MIN(sizeof(ev.ump), (size_t)body_size)); + snd_seq_ev_set_source(&ev, state->event.addr.port); + snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); + snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + + debug_ump_event(state, "send", &ev); + + if ((err = snd_seq_ump_event_output(state->event.hndl, &ev)) < 0) { + spa_log_warn(state->log, "failed to output event: %s", + snd_strerror(err)); + } +#else + spa_assert_not_reached(); +#endif + } else { + snd_seq_event_t ev; + uint8_t data[MAX_EVENT_SIZE]; + int size; + uint64_t st = 0; + + while (body_size > 0) { + if ((size = spa_ump_to_midi((const uint32_t **)&body, &body_size, + data, sizeof(data), &st)) <= 0) + break; + + if (first) + snd_seq_ev_clear(&ev); + + if ((size = snd_midi_event_encode(stream->codec, data, size, &ev)) < 0) { + spa_log_warn(state->log, "failed to encode event: %s", + snd_strerror(size)); + snd_midi_event_reset_encode(stream->codec); + first = true; + continue; + } + first = false; + if (ev.type == SND_SEQ_EVENT_NONE) + /* this can happen when the event is not complete yet, like + * a sysex message and we need to encode some more data. */ + continue; + + snd_seq_ev_set_source(&ev, state->event.addr.port); + snd_seq_ev_set_dest(&ev, port->addr.client, port->addr.port); + snd_seq_ev_schedule_real(&ev, state->event.queue_id, 0, &out_rt); + + debug_event(state, "send", &ev); + + if ((err = snd_seq_event_output(state->event.hndl, &ev)) < 0) { + spa_log_warn(state->log, "failed to output event: %s", + snd_strerror(err)); + } + first = true; + } } } } @@ -882,13 +1083,10 @@ static void reset_buffers(struct seq_state *this, struct seq_port *port) } static void reset_stream(struct seq_state *this, struct seq_stream *stream, bool active) { - uint32_t i; - for (i = 0; i < stream->last_port; i++) { - struct seq_port *port = &stream->ports[i]; - if (port->valid) { - reset_buffers(this, port); - spa_alsa_seq_activate_port(this, port, active); - } + struct seq_port *port; + spa_list_for_each(port, &stream->port_list, link) { + reset_buffers(this, port); + spa_alsa_seq_activate_port(this, port, active); } } @@ -980,7 +1178,7 @@ int spa_alsa_seq_reassign_follower(struct seq_state *state) if (following != state->following) { spa_log_debug(state->log, "alsa %p: reassign follower %d->%d", state, state->following, following); state->following = following; - spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state); + spa_loop_locked(state->data_loop, do_reassign_follower, 0, NULL, 0, state); } return 0; } @@ -1009,7 +1207,7 @@ int spa_alsa_seq_pause(struct seq_state *state) spa_log_debug(state->log, "alsa %p: pause", state); - spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state); + spa_loop_locked(state->data_loop, do_remove_source, 0, NULL, 0, state); if ((res = snd_seq_stop_queue(state->event.hndl, state->event.queue_id, NULL)) < 0) { spa_log_warn(state->log, "failed to stop queue: %s", snd_strerror(res)); diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h index 0f2c192c3..354fa1d5d 100644 --- a/spa/plugins/alsa/alsa-seq.h +++ b/spa/plugins/alsa/alsa-seq.h @@ -5,15 +5,15 @@ #ifndef SPA_ALSA_SEQ_H #define SPA_ALSA_SEQ_H -#ifdef __cplusplus -extern "C" { -#endif +#include "config.h" #include #include #include +#ifdef HAVE_ALSA_UMP #include +#endif #include #include @@ -29,6 +29,9 @@ extern "C" { #include "alsa.h" +#ifdef __cplusplus +extern "C" { +#endif struct props { char device[64]; @@ -50,6 +53,8 @@ struct buffer { }; struct seq_port { + struct spa_list link; + uint32_t id; enum spa_direction direction; snd_seq_addr_t addr; @@ -79,18 +84,21 @@ struct seq_port { struct spa_audio_info current_format; unsigned int have_format:1; - unsigned int valid:1; unsigned int active:1; struct spa_latency_info latency[2]; + + unsigned int mixing:1; + struct spa_list mix_link; }; struct seq_stream { enum spa_direction direction; unsigned int caps; snd_midi_event_t *codec; - struct seq_port ports[MAX_PORTS]; - uint32_t last_port; + struct seq_port *ports[MAX_PORTS]; + struct spa_list port_list; + struct spa_list mix_list; }; struct seq_conn { @@ -153,19 +161,21 @@ struct seq_state { unsigned int opened:1; unsigned int started:1; unsigned int following:1; + unsigned int ump:1; struct seq_stream streams[2]; + struct spa_list free_list; struct spa_dll dll; }; #define VALID_DIRECTION(this,d) ((d) == SPA_DIRECTION_INPUT || (d) == SPA_DIRECTION_OUTPUT) -#define VALID_PORT(this,d,p) ((p) < MAX_PORTS && this->streams[d].ports[p].id == (p)) +#define VALID_PORT(this,d,p) ((p) < MAX_PORTS && this->streams[d].ports[p] != NULL) #define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && VALID_PORT(this,d,p)) #define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && VALID_PORT(this,d,p)) #define CHECK_PORT(this,d,p) (VALID_DIRECTION(this,d) && VALID_PORT(this,d,p)) -#define GET_PORT(this,d,p) (&this->streams[d].ports[p]) +#define GET_PORT(this,d,p) (this->streams[d].ports[p]) int spa_alsa_seq_open(struct seq_state *state); int spa_alsa_seq_close(struct seq_state *state); diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c index 9420401f0..7d1ae9a3a 100644 --- a/spa/plugins/alsa/alsa-udev.c +++ b/spa/plugins/alsa/alsa-udev.c @@ -94,6 +94,7 @@ struct impl { struct spa_source notify; unsigned int use_acp:1; unsigned int expose_busy:1; + int use_ucm; }; static int impl_udev_open(struct impl *this) @@ -500,7 +501,7 @@ static int emit_added_object_info(struct impl *this, struct card *card) if (num_pcm_devices > 0) { struct spa_device_object_info info; char *cn = NULL, *cln = NULL; - struct spa_dict_item items[25]; + struct spa_dict_item items[26]; unsigned int n_items = 0; card->pcm_device_id = calc_pcm_device_id(card); @@ -521,6 +522,9 @@ static int emit_added_object_info(struct impl *this, struct card *card) items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ENUM_API, "udev"); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "alsa"); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device"); + if (this->use_ucm != -1) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_USE_UCM, + this->use_ucm ? "true" : "false"); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_PATH, path); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, path+3); if (snd_card_get_name(card->card_nr, &cn) >= 0) @@ -1105,6 +1109,7 @@ impl_init(const struct spa_handle_factory *factory, alsa_log_topic_init(this->log); this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); this->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); + this->use_ucm = -1; if (this->main_loop == NULL) { spa_log_error(this->log, "a main-loop is needed"); @@ -1131,6 +1136,8 @@ impl_init(const struct spa_handle_factory *factory, this->use_acp = spa_atob(str); else if ((str = spa_dict_lookup(info, "alsa.udev.expose-busy")) != NULL) this->expose_busy = spa_atob(str); + else if ((str = spa_dict_lookup(info, "alsa.use-ucm")) != NULL) + this->use_ucm = spa_atob(str) ? 1 : 0; } return 0; diff --git a/spa/plugins/alsa/compress-offload-api.c b/spa/plugins/alsa/compress-offload-api.c index 3f140c7a0..9ff0f8772 100644 --- a/spa/plugins/alsa/compress-offload-api.c +++ b/spa/plugins/alsa/compress-offload-api.c @@ -16,7 +16,6 @@ struct compress_offload_api_context { int fd; struct snd_compr_caps caps; struct spa_log *log; - bool was_configured; uint32_t fragment_size; uint32_t num_fragments; }; @@ -110,8 +109,6 @@ int compress_offload_api_set_params(struct compress_offload_api_context *context return -errno; } - context->was_configured = true; - return 0; } diff --git a/spa/plugins/alsa/mixer/paths/logi407-iec958-stereo-output.conf b/spa/plugins/alsa/mixer/paths/logi407-iec958-stereo-output.conf new file mode 100644 index 000000000..4a35f1fdb --- /dev/null +++ b/spa/plugins/alsa/mixer/paths/logi407-iec958-stereo-output.conf @@ -0,0 +1,13 @@ +; Mixer path for Logitech 407 PC Speakers +; +; See analog-output.conf.common for an explanation on the directives + +[General] +priority = 99 +description-key = iec958-stereo-output + +[Element PCM] +switch = mute +volume = merge +override-map.1 = all +override-map.2 = all-left,all-right diff --git a/spa/plugins/alsa/mixer/profile-sets/logi407.conf b/spa/plugins/alsa/mixer/profile-sets/logi407.conf new file mode 100644 index 000000000..61cea9554 --- /dev/null +++ b/spa/plugins/alsa/mixer/profile-sets/logi407.conf @@ -0,0 +1,38 @@ +# This file is part of PulseAudio. +# +# PulseAudio is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2.1 of the +# License, or (at your option) any later version. +# +# PulseAudio 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 Lesser General Public License +# along with PulseAudio; if not, see . + +; Logitech Z407 Stereo PC Speaker Set +; +; These are copies of the mappings we find in default.conf, but with +; the 'PCM' control used also in the iec958 output path +; +; See default.conf for an explanation on the directives used here. + +[General] +auto-profiles = yes + +# Based on analog-stereo +[Mapping analog-stereo] +device-strings = front:%f +channel-map = left,right +paths-output = analog-output +priority = 15 + +# Based on iec958-stereo +[Mapping iec958-stereo] +device-strings = iec958:%f +channel-map = left,right +paths-output = logi407-iec958-stereo-output +priority = 5 diff --git a/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf b/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf index 0ac157685..75eeffb2f 100644 --- a/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf +++ b/spa/plugins/alsa/mixer/profile-sets/sennheiser-gsx.conf @@ -30,6 +30,7 @@ auto-profiles = no [Mapping analog-chat-output] +description-key = gaming-headset-chat device-strings = hw:%f,0 channel-map = mono paths-output = analog-chat-output @@ -38,13 +39,16 @@ priority = 4000 intended-roles = phone [Mapping analog-output-surround71] +description-key = analog-surround-71 device-strings = hw:%f,1 channel-map = front-left,front-right,rear-left,rear-right,front-center,lfe,side-left,side-right +#channel-map = front-left,front-right,front-center,lfe,rear-left,rear-right,side-left,side-right # Swap channel fix that some devices require paths-output = virtual-surround-7.1 priority = 4100 direction = output [Mapping analog-chat-input] +description-key = gaming-headset-chat device-strings = hw:%f,0 channel-map = mono paths-input = analog-chat-input @@ -56,3 +60,17 @@ output-mappings = analog-output-surround71 analog-chat-output input-mappings = analog-chat-input priority = 5100 skip-probe = yes + +[Mapping stereo-output] +description = 2.0 HD +device-strings = hw:%f,1 +channel-map = stereo +priority = 3 +direction = output + +[Profile output:stereo-output+output:analog-chat-output+input:analog-chat-input] +description = 2.0 HD +output-mappings = stereo-output analog-chat-output +input-mappings = analog-chat-input +priority = 50 +skip-probe = yes diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c index 6e393bd0d..ccc9f48df 100644 --- a/spa/plugins/audioconvert/audioadapter.c +++ b/spa/plugins/audioconvert/audioadapter.c @@ -93,6 +93,7 @@ struct impl { struct spa_callbacks callbacks; unsigned int add_listener:1; + unsigned int have_rate_match:1; unsigned int have_format:1; unsigned int recheck_format:1; unsigned int started:1; @@ -142,19 +143,23 @@ static int follower_enum_params(struct impl *this, struct spa_pod_builder *builder) { int res; - if (result->next < 0x100000 && - this->follower != this->target) { - if ((res = node_enum_params_sync(this, this->target, - id, &result->next, filter, &result->param, builder)) == 1) - return res; + if (result->next < 0x100000) { + if (this->follower != this->target && + this->convert_params_flags[idx] & SPA_PARAM_INFO_READ) { + if ((res = node_enum_params_sync(this, this->target, + id, &result->next, filter, &result->param, builder)) == 1) + return res; + } result->next = 0x100000; } - if (result->next < 0x200000 && this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) { - result->next &= 0xfffff; - if ((res = node_enum_params_sync(this, this->follower, - id, &result->next, filter, &result->param, builder)) == 1) { - result->next |= 0x100000; - return res; + if (result->next < 0x200000) { + if (this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) { + result->next &= 0xfffff; + if ((res = node_enum_params_sync(this, this->follower, + id, &result->next, filter, &result->param, builder)) == 1) { + result->next |= 0x100000; + return res; + } } result->next = 0x200000; } @@ -279,7 +284,7 @@ static int link_io(struct impl *this) spa_zero(this->io_rate_match); this->io_rate_match.rate = 1.0; - if (this->follower == this->target) { + if (this->follower == this->target || !this->have_rate_match) { rate_match = NULL; rate_match_size = 0; } else { @@ -428,9 +433,12 @@ static int negotiate_buffers(struct impl *this) int res; bool follower_alloc, conv_alloc; uint32_t i, size, buffers, blocks, align, flags, stride = 0; - uint32_t *aligns; + uint32_t *aligns, data_flags; struct spa_data *datas; uint64_t follower_flags, conv_flags; + struct spa_node *alloc_node; + enum spa_direction alloc_direction; + uint32_t alloc_flags; spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers); @@ -442,28 +450,32 @@ static int negotiate_buffers(struct impl *this) state = 0; param = NULL; - if ((res = node_port_enum_params_sync(this, this->follower, - this->direction, 0, + if ((res = node_port_enum_params_sync(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) < 0) { if (res == -ENOENT) param = NULL; else { - debug_params(this, this->follower, this->direction, 0, - SPA_PARAM_Buffers, param, "follower buffers", res); + debug_params(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, param, "target buffers", res); return res; } } state = 0; - if ((res = node_port_enum_params_sync(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, + if ((res = node_port_enum_params_sync(this, this->follower, + this->direction, 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) != 1) { - debug_params(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_Buffers, param, "convert buffers", res); - return -ENOTSUP; + if (res == -ENOENT) + res = 0; + else { + debug_params(this, this->follower, this->direction, 0, + SPA_PARAM_Buffers, param, "follower buffers", res); + return res < 0 ? res : -ENOTSUP; + } } if (param == NULL) return -ENOTSUP; @@ -476,11 +488,10 @@ static int negotiate_buffers(struct impl *this) follower_alloc = SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); conv_alloc = SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); - flags = 0; + flags = alloc_flags = 0; if (conv_alloc || follower_alloc) { flags |= SPA_BUFFER_ALLOC_FLAG_NO_DATA; - if (conv_alloc) - follower_alloc = false; + alloc_flags = SPA_NODE_BUFFERS_FLAG_ALLOC; } align = DEFAULT_ALIGN; @@ -497,7 +508,7 @@ static int negotiate_buffers(struct impl *this) if (this->async) buffers = SPA_MAX(2u, buffers); - spa_log_debug(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", + spa_log_info(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", this, buffers, blocks, size, stride, align, follower_alloc, conv_alloc); align = SPA_MAX(align, this->max_align); @@ -505,9 +516,15 @@ static int negotiate_buffers(struct impl *this) datas = alloca(sizeof(struct spa_data) * blocks); memset(datas, 0, sizeof(struct spa_data) * blocks); aligns = alloca(sizeof(uint32_t) * blocks); + + data_flags = SPA_DATA_FLAG_READWRITE; + if (SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_DYNAMIC_DATA) && + SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_DYNAMIC_DATA)) + data_flags |= SPA_DATA_FLAG_DYNAMIC; + for (i = 0; i < blocks; i++) { datas[i].type = SPA_DATA_MemPtr; - datas[i].flags = SPA_DATA_FLAG_READWRITE | SPA_DATA_FLAG_DYNAMIC; + datas[i].flags = data_flags; datas[i].maxsize = size; aligns[i] = align; } @@ -518,15 +535,26 @@ static int negotiate_buffers(struct impl *this) return -errno; this->n_buffers = buffers; - if ((res = spa_node_port_use_buffers(this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - conv_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0, + /* prefer to let the follower alloc */ + if (follower_alloc) { + alloc_node = this->follower; + alloc_direction = this->direction; + } else { + alloc_node = this->target; + alloc_direction = SPA_DIRECTION_REVERSE(this->direction); + } + + if ((res = spa_node_port_use_buffers(alloc_node, + alloc_direction, 0, alloc_flags, this->buffers, this->n_buffers)) < 0) return res; - if ((res = spa_node_port_use_buffers(this->follower, - this->direction, 0, - follower_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0, + alloc_node = alloc_node == this->follower ? this->target : this->follower; + alloc_direction = SPA_DIRECTION_REVERSE(alloc_direction); + alloc_flags = 0; + + if ((res = spa_node_port_use_buffers(alloc_node, + alloc_direction, 0, alloc_flags, this->buffers, this->n_buffers)) < 0) return res; @@ -731,20 +759,20 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m this->mode = mode; - if (old_passthrough != passthrough) { - if (passthrough) { - /* add follower ports */ - spa_zero(l); - spa_node_add_listener(this->follower, &l, &follower_node_events, this); - spa_hook_remove(&l); - } else { - /* add converter ports */ - configure_convert(this, mode); - } - link_io(this); + if (old_passthrough != passthrough && passthrough) { + /* add follower ports */ + spa_zero(l); + spa_node_add_listener(this->follower, &l, &follower_node_events, this); + spa_hook_remove(&l); + } else { + /* add converter ports */ + configure_convert(this, mode); } + link_io(this); + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; - SPA_FLAG_CLEAR(this->info.flags, SPA_NODE_FLAG_NEED_CONFIGURE); + SPA_FLAG_UPDATE(this->info.flags, SPA_NODE_FLAG_NEED_CONFIGURE, + this->mode == SPA_PARAM_PORT_CONFIG_MODE_none); SPA_FLAG_UPDATE(this->info.flags, SPA_NODE_FLAG_ASYNC, this->async && this->follower == this->target); this->params[IDX_Props].user++; @@ -767,15 +795,30 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, switch (id) { case SPA_PARAM_Format: - if (this->started) + if (this->started) { + spa_log_error(this->log, "%p: cannot set Format param: " + "node already started", this); return -EIO; - if (param == NULL) + } + if (param == NULL) { + spa_log_error(this->log, "%p: attempted to set NULL Format POD", this); return -EINVAL; + } - if (spa_format_audio_parse(param, &info) < 0) + if (spa_format_audio_parse(param, &info) < 0) { + spa_log_error(this->log, "%p: cannot set Format param: " + "parsing the POD failed", this); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_ERROR, 0, NULL, param); return -EINVAL; - if (info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + } + if (info.media_subtype != SPA_MEDIA_SUBTYPE_raw) { + const char *subtype_name = spa_type_to_short_name(info.media_subtype, + spa_type_media_subtype, + ""); + spa_log_error(this->log, "%p: cannot set Format param: " + "expected raw subtype, got subtype \"%s\"", this, subtype_name); return -EINVAL; + } this->follower_current_format = info; break; @@ -787,7 +830,8 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, struct spa_pod *format = NULL; if (this->started) { - spa_log_error(this->log, "was started"); + spa_log_error(this->log, "%p: cannot set PortConfig param: " + "node already started", this); return -EIO; } @@ -795,26 +839,42 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, SPA_TYPE_OBJECT_ParamPortConfig, NULL, SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&dir), SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), - SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) { + spa_log_error(this->log, "%p: cannot set PortConfig param: " + "parsing the POD failed", this); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_ERROR, 0, NULL, param); return -EINVAL; + } if (format) { struct spa_audio_info info; spa_zero(info); - if ((res = spa_format_audio_parse(format, &info)) < 0) + if ((res = spa_format_audio_parse(format, &info)) < 0) { + spa_log_error(this->log, "%p: cannot set PortConfig param: " + "parsing format failed: %s", this, spa_strerror(res)); + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_ERROR, 0, NULL, format); return res; + } - if (info.media_subtype == SPA_MEDIA_SUBTYPE_raw) + if (info.media_subtype == SPA_MEDIA_SUBTYPE_raw) { info.info.raw.rate = 0; - else + } else { + const char *subtype_name = spa_type_to_short_name(info.media_subtype, + spa_type_media_subtype, + ""); + spa_log_error(this->log, "%p: cannot set PortConfig param: " + "subtype \"%s\" is not supported", this, subtype_name); return -ENOTSUP; + } this->default_format = info; } switch (mode) { case SPA_PARAM_PORT_CONFIG_MODE_none: + spa_log_error(this->log, "%p: cannot set PortConfig param: " + "\"none\" config mode is not supported", this); return -ENOTSUP; case SPA_PARAM_PORT_CONFIG_MODE_passthrough: if ((res = reconfigure_mode(this, mode, dir, format)) < 0) @@ -826,6 +886,8 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, return res; break; default: + spa_log_error(this->log, "%p: invalid config mode when setting PortConfig param", + this); return -EINVAL; } @@ -891,7 +953,7 @@ static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder * struct spa_pod_builder_state state; int res = 0; - if (o2 == NULL || SPA_POD_TYPE(o1) != SPA_POD_TYPE(o2)) + if (o2 == NULL || o1->pod.type != o2->pod.type) return (struct spa_pod*)o1; spa_pod_builder_push_object(b, &f, o1->body.type, o1->body.id); @@ -900,7 +962,7 @@ static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder * p2 = spa_pod_object_find_prop(o2, p2, p1->key); if (p2 != NULL) { spa_pod_builder_get_state(b, &state); - res = spa_pod_filter_prop(b, p1, p2); + res = spa_pod_filter_prop(b, p2, p1); if (res < 0) spa_pod_builder_reset(b, &state); } @@ -941,28 +1003,14 @@ static int negotiate_format(struct impl *this) spa_node_send_command(this->follower, &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin)); - /* first try the ideal converter format, which is likely passthrough */ - tstate = 0; - fres = node_port_enum_params_sync(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_EnumFormat, &tstate, - NULL, &format, &b); - if (fres == 1) { - fstate = 0; - res = node_port_enum_params_sync(this, this->follower, - this->direction, 0, - SPA_PARAM_EnumFormat, &fstate, - format, &format, &b); - if (res == 1) - goto found; - } - - /* then try something the follower can accept */ - for (fstate = 0;;) { + /* The target has been negotiated on its other ports and so it can propose + * a passthrough format or an ideal conversion. We use the suggestions of the + * target to find the best follower format */ + for (tstate = 0;;) { format = NULL; - res = node_port_enum_params_sync(this, this->follower, - this->direction, 0, - SPA_PARAM_EnumFormat, &fstate, + res = node_port_enum_params_sync(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, &tstate, NULL, &format, &b); if (res == -ENOENT) @@ -970,18 +1018,23 @@ static int negotiate_format(struct impl *this) else if (res <= 0) break; - tstate = 0; - fres = node_port_enum_params_sync(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_EnumFormat, &tstate, + if (format != NULL) + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, format); + + fstate = 0; + fres = node_port_enum_params_sync(this, this->follower, + this->direction, 0, + SPA_PARAM_EnumFormat, &fstate, format, &format, &b); if (fres == 0 && res == 1) continue; + if (format != NULL) + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, format); + res = fres; break; } -found: if (format == NULL) { debug_params(this, this->follower, this->direction, 0, SPA_PARAM_EnumFormat, format, "follower format", res); @@ -997,6 +1050,8 @@ found: format = merge_objects(this, &b, SPA_PARAM_Format, (struct spa_pod_object*)format, (struct spa_pod_object*)def); + if (format == NULL) + return -ENOSPC; spa_pod_fixate(format); @@ -1022,8 +1077,6 @@ static int impl_node_send_command(void *object, const struct spa_command *comman switch (SPA_NODE_COMMAND_ID(command)) { case SPA_NODE_COMMAND_Start: spa_log_debug(this->log, "%p: starting %d", this, this->started); - if (this->started) - return 0; if ((res = negotiate_format(this)) < 0) return res; this->ready = true; @@ -1043,7 +1096,10 @@ static int impl_node_send_command(void *object, const struct spa_command *comman break; } - if ((res = spa_node_send_command(this->target, command)) < 0) { + res = spa_node_send_command(this->target, command); + if (res == -ENOTSUP && this->target != this->follower) + res = 0; + if (res < 0) { spa_log_error(this->log, "%p: can't send command %d: %s", this, SPA_NODE_COMMAND_ID(command), spa_strerror(res)); @@ -1163,6 +1219,9 @@ static void follower_convert_port_info(void *data, case SPA_PARAM_Tag: idx = IDX_Tag; break; + case SPA_PARAM_EnumFormat: + idx = IDX_EnumFormat; + break; default: continue; } @@ -1190,6 +1249,11 @@ static void follower_convert_port_info(void *data, spa_log_debug(this->log, "tag: %d (%s)", res, spa_strerror(res)); } + if (idx == IDX_EnumFormat) { + spa_log_info(this->log, "new EnumFormat from converter"); + /* we will renegotiate when restarting */ + this->recheck_format = true; + } spa_log_debug(this->log, "param %d changed", info->params[i].id); } } @@ -1212,7 +1276,7 @@ static void convert_port_info(void *data, port_id--; } else if (info) { pi = *info; - pi.flags = this->follower_port_flags & + pi.flags |= this->follower_port_flags & (SPA_PORT_FLAG_LIVE | SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL); @@ -1249,8 +1313,8 @@ static void follower_info(void *data, const struct spa_node_info *info) struct impl *this = data; uint32_t i; - spa_log_debug(this->log, "%p: info change:%08"PRIx64, this, - info->change_mask); + spa_log_debug(this->log, "%p: info change:%08"PRIx64" %d:%d", this, + info->change_mask, info->max_input_ports, info->max_output_ports); if (this->follower_removing) return; @@ -1388,7 +1452,7 @@ static void follower_port_info(void *data, spa_strerror(res)); } if (idx == IDX_EnumFormat) { - spa_log_debug(this->log, "new formats"); + spa_log_debug(this->log, "new EnumFormat from follower"); /* we will renegotiate when restarting */ this->recheck_format = true; } @@ -1612,13 +1676,13 @@ port_enum_formats_for_convert(struct impl *this, int seq, enum spa_direction dir uint32_t count = 0; struct spa_result_node_params result; - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - result.id = id; result.next = start; next: result.index = result.next; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + if (result.next < 0x100000) { /* Enumerate follower formats first, until we have enough or we run out */ if ((res = node_port_enum_params_sync(this, this->follower, direction, port_id, id, @@ -1996,11 +2060,12 @@ static int do_auto_port_config(struct impl *this, const char *str) return -ENOENT; if (format.media_subtype == SPA_MEDIA_SUBTYPE_raw) { + uint32_t n_pos = SPA_MIN(SPA_N_ELEMENTS(format.info.raw.position), format.info.raw.channels); if (position == POSITION_AUX) { - for (i = 0; i < format.info.raw.channels; i++) + for (i = 0; i < n_pos; i++) format.info.raw.position[i] = SPA_AUDIO_CHANNEL_START_Aux + i; } else if (position == POSITION_UNKNOWN) { - for (i = 0; i < format.info.raw.channels; i++) + for (i = 0; i < n_pos; i++) format.info.raw.position[i] = SPA_AUDIO_CHANNEL_UNKNOWN; } } @@ -2014,9 +2079,8 @@ static int do_auto_port_config(struct impl *this, const char *str) SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(monitor), SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(control), SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); - impl_node_set_param(this, SPA_PARAM_PortConfig, 0, param); - return 0; + return impl_node_set_param(this, SPA_PARAM_PortConfig, 0, param); } static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) @@ -2094,6 +2158,9 @@ impl_init(const struct spa_handle_factory *factory, this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); spa_log_topic_init(this->log, &log_topic); + /* FIXME, we should check the IO params for SPA_IO_RateMatch */ + this->have_rate_match = true; + this->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); this->ploader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader); @@ -2134,6 +2201,7 @@ impl_init(const struct spa_handle_factory *factory, this->target = this->convert; /* the actual mode is selected below */ this->mode = SPA_PARAM_PORT_CONFIG_MODE_none; + configure_convert(this, this->mode); } this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | @@ -2163,8 +2231,9 @@ impl_init(const struct spa_handle_factory *factory, &this->convert_listener, &convert_node_events, this); if (info && (str = spa_dict_lookup(info, "adapter.auto-port-config")) != NULL) do_auto_port_config(this, str); - else - configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_dsp); + else { + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_none, this->direction, NULL); + } } else { reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_passthrough, this->direction, NULL); } diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index 47859e654..ebb057218 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -46,10 +47,11 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.audioconvert"); #define DEFAULT_RATE 48000 #define DEFAULT_CHANNELS 2 +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define MAX_ALIGN FMT_OPS_MAX_ALIGN #define MAX_BUFFERS 32 -#define MAX_DATAS SPA_AUDIO_MAX_CHANNELS -#define MAX_PORTS (SPA_AUDIO_MAX_CHANNELS+1) +#define MAX_DATAS MAX_CHANNELS +#define MAX_PORTS (MAX_CHANNELS+1) #define MAX_STAGES 64 #define MAX_GRAPH 9 /* 8 active + 1 replacement slot */ @@ -61,7 +63,7 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.audioconvert"); struct volumes { bool mute; uint32_t n_volumes; - float volumes[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; }; static void init_volumes(struct volumes *vol) @@ -69,7 +71,7 @@ static void init_volumes(struct volumes *vol) uint32_t i; vol->mute = DEFAULT_MUTE; vol->n_volumes = 0; - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + for (i = 0; i < MAX_CHANNELS; i++) vol->volumes[i] = DEFAULT_VOLUME; } @@ -79,6 +81,9 @@ struct volume_ramp_params { unsigned int volume_ramp_time; unsigned int volume_ramp_step_time; enum spa_audio_volume_ramp_scale scale; + float start; + float end; + uint32_t rate; }; struct props { @@ -87,11 +92,10 @@ struct props { float max_volume; float prev_volume; uint32_t n_channels; - uint32_t channel_map[SPA_AUDIO_MAX_CHANNELS]; + uint32_t channel_map[MAX_CHANNELS]; struct volumes channel; struct volumes soft; struct volumes monitor; - struct volume_ramp_params vrp; unsigned int have_soft_volume:1; unsigned int mix_disabled:1; unsigned int resample_disabled:1; @@ -109,7 +113,7 @@ static void props_reset(struct props *props) props->min_volume = DEFAULT_MIN_VOLUME; props->max_volume = DEFAULT_MAX_VOLUME; props->n_channels = 0; - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + for (i = 0; i < MAX_CHANNELS; i++) props->channel_map[i] = SPA_AUDIO_CHANNEL_UNKNOWN; init_volumes(&props->channel); init_volumes(&props->soft); @@ -127,6 +131,7 @@ static void props_reset(struct props *props) struct buffer { uint32_t id; #define BUFFER_FLAG_QUEUED (1<<0) +#define BUFFER_FLAG_MAPPED (1<<1) uint32_t flags; struct spa_list link; struct spa_buffer *buf; @@ -210,30 +215,39 @@ struct stage_context { uint32_t src_idx; uint32_t dst_idx; uint32_t final_idx; - uint32_t n_datas; + uint32_t tmp; +#define SRC_CONVERT_BIT (1<<0) +#define RESAMPLE_BIT (1<<1) +#define FILTER_BIT (1<<2) +#define MIX_BIT (1<<3) +#define DST_CONVERT_BIT (1<<4) + uint32_t bits; struct port *ctrlport; + bool empty; }; struct stage { struct impl *impl; - bool passthrough; uint32_t in_idx; uint32_t out_idx; - uint32_t n_in; - uint32_t n_out; void *data; void (*run) (struct stage *stage, struct stage_context *c); }; struct filter_graph { struct impl *impl; + struct spa_list link; int order; struct spa_handle *handle; struct spa_filter_graph *graph; struct spa_hook listener; uint32_t n_inputs; + uint32_t inputs_position[MAX_CHANNELS]; uint32_t n_outputs; - bool active; + uint32_t outputs_position[MAX_CHANNELS]; + uint32_t latency; + bool removing; + bool setup; }; struct impl { @@ -246,9 +260,13 @@ struct impl { struct spa_plugin_loader *loader; uint32_t n_graph; - uint32_t graph_index[MAX_GRAPH]; + struct filter_graph *filter_graph[MAX_GRAPH]; + + struct spa_list free_graphs; + struct spa_list active_graphs; + struct filter_graph graphs[MAX_GRAPH]; + struct spa_process_latency_info latency; - struct filter_graph filter_graph[MAX_GRAPH]; int in_filter_props; int filter_props_count; @@ -264,6 +282,7 @@ struct impl { struct props props; + struct spa_io_clock *io_clock; struct spa_io_position *io_position; struct spa_io_rate_match *io_rate_match; @@ -287,6 +306,7 @@ struct impl { struct volume volume; double rate_scale; struct spa_pod_sequence *vol_ramp_sequence; + void *vol_ramp_sequence_data; uint32_t vol_ramp_offset; uint32_t in_offset; @@ -294,7 +314,6 @@ struct impl { unsigned int started:1; unsigned int setup:1; unsigned int resample_peaks:1; - unsigned int is_passthrough:1; unsigned int ramp_volume:1; unsigned int drained:1; unsigned int rate_adjust:1; @@ -306,11 +325,13 @@ struct impl { char group_name[128]; + uint32_t maxsize; + uint32_t maxports; uint32_t scratch_size; uint32_t scratch_ports; float *empty; float *scratch; - float *tmp[2]; + float *tmp[2][MAX_PORTS]; float *tmp_datas[2][MAX_PORTS]; struct wav_file *wav_file; @@ -365,7 +386,7 @@ static void emit_port_info(struct impl *this, struct port *port, bool full) items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_IGNORE_LATENCY, "true"); } else if (PORT_IS_CONTROL(this, port->direction, port->id)) { items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control"); - items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"); } if (this->group_name[0] != '\0') items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, this->group_name); @@ -384,11 +405,26 @@ static void emit_port_info(struct impl *this, struct port *port, bool full) } } +static void emit_info(struct impl *this, bool full) +{ + struct port *p; + uint32_t i; + + emit_node_info(this, full); + for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) { + if ((p = GET_IN_PORT(this, i)) && p->valid) + emit_port_info(this, p, full); + } + for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) { + if ((p = GET_OUT_PORT(this, i)) && p->valid) + emit_port_info(this, p, full); + } +} + static int init_port(struct impl *this, enum spa_direction direction, uint32_t port_id, uint32_t position, bool is_dsp, bool is_monitor, bool is_control) { struct port *port = GET_PORT(this, direction, port_id); - const char *name; spa_assert(port_id < MAX_PORTS); @@ -403,13 +439,12 @@ static int init_port(struct impl *this, enum spa_direction direction, uint32_t p port->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); port->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); - name = spa_debug_type_find_short_name(spa_type_audio_channel, position); - snprintf(port->position, sizeof(port->position), "%s", name ? name : "UNK"); + spa_type_audio_channel_make_short_name(position, port->position, sizeof(port->position), "UNK"); - port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + port->info = SPA_PORT_INFO_INIT(); + port->info.change_mask = port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_PROPS | SPA_PORT_CHANGE_MASK_PARAMS; - port->info = SPA_PORT_INFO_INIT(); port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_DYNAMIC_DATA; port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); @@ -446,8 +481,6 @@ static int init_port(struct impl *this, enum spa_direction direction, uint32_t p spa_log_debug(this->log, "%p: add port %d:%d position:%s %d %d %d", this, direction, port_id, port->position, is_dsp, is_monitor, is_control); - emit_port_info(this, port, true); - return 0; } @@ -461,43 +494,15 @@ static int deinit_port(struct impl *this, enum spa_direction direction, uint32_t return 0; } -static int impl_node_enum_params(void *object, int seq, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) + +static int node_param_enum_port_config(struct impl *this, uint32_t id, uint32_t index, + struct spa_pod **param, struct spa_pod_builder *b) { - struct impl *this = object; - struct spa_pod *param; - struct spa_pod_builder b = { 0 }; - uint8_t buffer[4096]; - struct spa_result_node_params result; - uint32_t count = 0; - int res; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(num != 0, -EINVAL); - - result.id = id; - result.next = start; - next: - result.index = result.next++; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - - switch (id) { - case SPA_PARAM_EnumPortConfig: + switch (index) { + case 0 ... 1: { - struct dir *dir; - switch (result.index) { - case 0: - dir = &this->dir[SPA_DIRECTION_INPUT];; - break; - case 1: - dir = &this->dir[SPA_DIRECTION_OUTPUT];; - break; - default: - return 0; - } - param = spa_pod_builder_add_object(&b, + struct dir *dir = &this->dir[index]; + *param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_ParamPortConfig, id, SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), SPA_PARAM_PORT_CONFIG_mode, SPA_POD_CHOICE_ENUM_Id(4, @@ -509,23 +514,23 @@ static int impl_node_enum_params(void *object, int seq, SPA_PARAM_PORT_CONFIG_control, SPA_POD_CHOICE_Bool(false)); break; } - case SPA_PARAM_PortConfig: + default: + return 0; + } + return 1; +} + +static int node_param_port_config(struct impl *this, uint32_t id, uint32_t index, + struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0 ... 1: { - struct dir *dir; + struct dir *dir = &this->dir[index]; struct spa_pod_frame f[1]; - switch (result.index) { - case 0: - dir = &this->dir[SPA_DIRECTION_INPUT];; - break; - case 1: - dir = &this->dir[SPA_DIRECTION_OUTPUT];; - break; - default: - return 0; - } - spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_ParamPortConfig, id); - spa_pod_builder_add(&b, + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_ParamPortConfig, id); + spa_pod_builder_add(b, SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(dir->mode), SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(this->monitor), @@ -533,406 +538,455 @@ static int impl_node_enum_params(void *object, int seq, 0); if (dir->have_format) { - spa_pod_builder_prop(&b, SPA_PARAM_PORT_CONFIG_format, 0); - spa_format_audio_raw_build(&b, SPA_PARAM_PORT_CONFIG_format, - &dir->format.info.raw); - } - param = spa_pod_builder_pop(&b, &f[0]); - break; - } - case SPA_PARAM_PropInfo: - { - struct props *p = &this->props; - struct spa_pod_frame f[2]; - - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume), - SPA_PROP_INFO_description, SPA_POD_String("Volume"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, - DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME)); - break; - case 1: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_mute), - SPA_PROP_INFO_description, SPA_POD_String("Mute"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->channel.mute)); - break; - case 2: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_channelVolumes), - SPA_PROP_INFO_description, SPA_POD_String("Channel Volumes"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, - DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), - SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); - break; - case 3: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_channelMap), - SPA_PROP_INFO_description, SPA_POD_String("Channel Map"), - SPA_PROP_INFO_type, SPA_POD_Id(SPA_AUDIO_CHANNEL_UNKNOWN), - SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); - break; - case 4: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_monitorMute), - SPA_PROP_INFO_description, SPA_POD_String("Monitor Mute"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->monitor.mute)); - break; - case 5: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_monitorVolumes), - SPA_PROP_INFO_description, SPA_POD_String("Monitor Volumes"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, - DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), - SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); - break; - case 6: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_softMute), - SPA_PROP_INFO_description, SPA_POD_String("Soft Mute"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->soft.mute)); - break; - case 7: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_softVolumes), - SPA_PROP_INFO_description, SPA_POD_String("Soft Volumes"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, - DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), - SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); - break; - case 8: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("monitor.channel-volumes"), - SPA_PROP_INFO_description, SPA_POD_String("Monitor channel volume"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( - this->monitor_channel_volumes), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 9: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.disable"), - SPA_PROP_INFO_description, SPA_POD_String("Disable Channel mixing"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->mix_disabled), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 10: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.min-volume"), - SPA_PROP_INFO_description, SPA_POD_String("Minimum volume level"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->min_volume, - DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 11: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.max-volume"), - SPA_PROP_INFO_description, SPA_POD_String("Maximum volume level"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->max_volume, - DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 12: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.normalize"), - SPA_PROP_INFO_description, SPA_POD_String("Normalize Volumes"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( - SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_NORMALIZE)), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 13: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.mix-lfe"), - SPA_PROP_INFO_description, SPA_POD_String("Mix LFE into channels"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( - SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_MIX_LFE)), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 14: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix"), - SPA_PROP_INFO_description, SPA_POD_String("Enable upmixing"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( - SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_UPMIX)), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 15: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.lfe-cutoff"), - SPA_PROP_INFO_description, SPA_POD_String("LFE cutoff frequency"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( - this->mix.lfe_cutoff, 0.0, 1000.0), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 16: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.fc-cutoff"), - SPA_PROP_INFO_description, SPA_POD_String("FC cutoff frequency (Hz)"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( - this->mix.fc_cutoff, 0.0, 48000.0), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 17: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.rear-delay"), - SPA_PROP_INFO_description, SPA_POD_String("Rear channels delay (ms)"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( - this->mix.rear_delay, 0.0, 1000.0), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 18: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.stereo-widen"), - SPA_PROP_INFO_description, SPA_POD_String("Stereo widen"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( - this->mix.widen, 0.0, 1.0), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 19: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.hilbert-taps"), - SPA_PROP_INFO_description, SPA_POD_String("Taps for phase shift of rear"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int( - this->mix.hilbert_taps, 0, MAX_TAPS), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 20: - spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); - spa_pod_builder_add(&b, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix-method"), - SPA_PROP_INFO_description, SPA_POD_String("Upmix method to use"), - SPA_PROP_INFO_type, SPA_POD_String( - channelmix_upmix_info[this->mix.upmix].label), - SPA_PROP_INFO_params, SPA_POD_Bool(true), - 0); - - spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0); - spa_pod_builder_push_struct(&b, &f[1]); - SPA_FOR_EACH_ELEMENT_VAR(channelmix_upmix_info, i) { - spa_pod_builder_string(&b, i->label); - spa_pod_builder_string(&b, i->description); - } - spa_pod_builder_pop(&b, &f[1]); - param = spa_pod_builder_pop(&b, &f[0]); - break; - case 21: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_rate), - SPA_PROP_INFO_description, SPA_POD_String("Rate scaler"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Double(p->rate, 0.0, 10.0)); - break; - case 22: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_quality), - SPA_PROP_INFO_name, SPA_POD_String("resample.quality"), - SPA_PROP_INFO_description, SPA_POD_String("Resample Quality"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->resample_quality, 0, 14), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 23: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("resample.disable"), - SPA_PROP_INFO_description, SPA_POD_String("Disable Resampling"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->resample_disabled), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 24: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("dither.noise"), - SPA_PROP_INFO_description, SPA_POD_String("Add noise bits"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(this->dir[1].conv.noise_bits, 0, 16), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 25: - spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); - spa_pod_builder_add(&b, - SPA_PROP_INFO_name, SPA_POD_String("dither.method"), - SPA_PROP_INFO_description, SPA_POD_String("The dithering method"), - SPA_PROP_INFO_type, SPA_POD_String( - dither_method_info[this->dir[1].conv.method].label), - SPA_PROP_INFO_params, SPA_POD_Bool(true), - 0); - spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0); - spa_pod_builder_push_struct(&b, &f[1]); - SPA_FOR_EACH_ELEMENT_VAR(dither_method_info, i) { - spa_pod_builder_string(&b, i->label); - spa_pod_builder_string(&b, i->description); - } - spa_pod_builder_pop(&b, &f[1]); - param = spa_pod_builder_pop(&b, &f[0]); - break; - case 26: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("debug.wav-path"), - SPA_PROP_INFO_description, SPA_POD_String("Path to WAV file"), - SPA_PROP_INFO_type, SPA_POD_String(p->wav_path), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 27: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("channelmix.lock-volumes"), - SPA_PROP_INFO_description, SPA_POD_String("Disable volume updates"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->lock_volumes), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 28: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph.disable"), - SPA_PROP_INFO_description, SPA_POD_String("Disable Filter graph updates"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->filter_graph_disabled), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - case 29: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph"), - SPA_PROP_INFO_description, SPA_POD_String("A filter graph to load"), - SPA_PROP_INFO_type, SPA_POD_String(""), - SPA_PROP_INFO_params, SPA_POD_Bool(true)); - break; - default: - if (this->filter_graph[0].graph) { - res = spa_filter_graph_enum_prop_info(this->filter_graph[0].graph, - result.index - 30, &b, ¶m); - if (res <= 0) - return res; - } else - return 0; - } - break; - } - - case SPA_PARAM_Props: - { - struct props *p = &this->props; - struct spa_pod_frame f[2]; - - switch (result.index) { - case 0: - spa_pod_builder_push_object(&b, &f[0], - SPA_TYPE_OBJECT_Props, id); - spa_pod_builder_add(&b, - SPA_PROP_volume, SPA_POD_Float(p->volume), - SPA_PROP_mute, SPA_POD_Bool(p->channel.mute), - SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), - SPA_TYPE_Float, - p->channel.n_volumes, - p->channel.volumes), - SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t), - SPA_TYPE_Id, - p->n_channels, - p->channel_map), - SPA_PROP_softMute, SPA_POD_Bool(p->soft.mute), - SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float), - SPA_TYPE_Float, - p->soft.n_volumes, - p->soft.volumes), - SPA_PROP_monitorMute, SPA_POD_Bool(p->monitor.mute), - SPA_PROP_monitorVolumes, SPA_POD_Array(sizeof(float), - SPA_TYPE_Float, - p->monitor.n_volumes, - p->monitor.volumes), - 0); - spa_pod_builder_prop(&b, SPA_PROP_params, 0); - spa_pod_builder_push_struct(&b, &f[1]); - spa_pod_builder_string(&b, "monitor.channel-volumes"); - spa_pod_builder_bool(&b, this->monitor_channel_volumes); - spa_pod_builder_string(&b, "channelmix.disable"); - spa_pod_builder_bool(&b, this->props.mix_disabled); - spa_pod_builder_string(&b, "channelmix.min-volume"); - spa_pod_builder_float(&b, this->props.min_volume); - spa_pod_builder_string(&b, "channelmix.max-volume"); - spa_pod_builder_float(&b, this->props.max_volume); - spa_pod_builder_string(&b, "channelmix.normalize"); - spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options, - CHANNELMIX_OPTION_NORMALIZE)); - spa_pod_builder_string(&b, "channelmix.mix-lfe"); - spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options, - CHANNELMIX_OPTION_MIX_LFE)); - spa_pod_builder_string(&b, "channelmix.upmix"); - spa_pod_builder_bool(&b, SPA_FLAG_IS_SET(this->mix.options, - CHANNELMIX_OPTION_UPMIX)); - spa_pod_builder_string(&b, "channelmix.lfe-cutoff"); - spa_pod_builder_float(&b, this->mix.lfe_cutoff); - spa_pod_builder_string(&b, "channelmix.fc-cutoff"); - spa_pod_builder_float(&b, this->mix.fc_cutoff); - spa_pod_builder_string(&b, "channelmix.rear-delay"); - spa_pod_builder_float(&b, this->mix.rear_delay); - spa_pod_builder_string(&b, "channelmix.stereo-widen"); - spa_pod_builder_float(&b, this->mix.widen); - spa_pod_builder_string(&b, "channelmix.hilbert-taps"); - spa_pod_builder_int(&b, this->mix.hilbert_taps); - spa_pod_builder_string(&b, "channelmix.upmix-method"); - spa_pod_builder_string(&b, channelmix_upmix_info[this->mix.upmix].label); - spa_pod_builder_string(&b, "resample.quality"); - spa_pod_builder_int(&b, p->resample_quality); - spa_pod_builder_string(&b, "resample.disable"); - spa_pod_builder_bool(&b, p->resample_disabled); - spa_pod_builder_string(&b, "dither.noise"); - spa_pod_builder_int(&b, this->dir[1].conv.noise_bits); - spa_pod_builder_string(&b, "dither.method"); - spa_pod_builder_string(&b, dither_method_info[this->dir[1].conv.method].label); - spa_pod_builder_string(&b, "debug.wav-path"); - spa_pod_builder_string(&b, p->wav_path); - spa_pod_builder_string(&b, "channelmix.lock-volumes"); - spa_pod_builder_bool(&b, p->lock_volumes); - spa_pod_builder_string(&b, "audioconvert.filter-graph.disable"); - spa_pod_builder_bool(&b, p->filter_graph_disabled); - spa_pod_builder_string(&b, "audioconvert.filter-graph"); - spa_pod_builder_string(&b, ""); - spa_pod_builder_pop(&b, &f[1]); - param = spa_pod_builder_pop(&b, &f[0]); - break; - default: - if (result.index > MAX_GRAPH) - return 0; - - if (this->filter_graph[result.index-1].graph == NULL) - goto next; - - res = spa_filter_graph_get_props(this->filter_graph[result.index-1].graph, - &b, ¶m); - if (res < 0) - return res; - if (res == 0) - goto next; - break; + spa_pod_builder_prop(b, SPA_PARAM_PORT_CONFIG_format, 0); + spa_format_audio_raw_build(b, id, &dir->format.info.raw); } + *param = spa_pod_builder_pop(b, &f[0]); break; } default: return 0; } + return 1; +} - if (spa_pod_filter(&b, &result.param, param, filter) < 0) +static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, + struct spa_pod **param, struct spa_pod_builder *b) +{ + struct props *p = &this->props; + struct spa_pod_frame f[2]; + + switch (index) { + case 0: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume), + SPA_PROP_INFO_description, SPA_POD_String("Volume"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME)); + break; + case 1: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_mute), + SPA_PROP_INFO_description, SPA_POD_String("Mute"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->channel.mute)); + break; + case 2: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_channelVolumes), + SPA_PROP_INFO_description, SPA_POD_String("Channel Volumes"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); + break; + case 3: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_channelMap), + SPA_PROP_INFO_description, SPA_POD_String("Channel Map"), + SPA_PROP_INFO_type, SPA_POD_Id(SPA_AUDIO_CHANNEL_UNKNOWN), + SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); + break; + case 4: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_monitorMute), + SPA_PROP_INFO_description, SPA_POD_String("Monitor Mute"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->monitor.mute)); + break; + case 5: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_monitorVolumes), + SPA_PROP_INFO_description, SPA_POD_String("Monitor Volumes"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); + break; + case 6: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_softMute), + SPA_PROP_INFO_description, SPA_POD_String("Soft Mute"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->soft.mute)); + break; + case 7: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_softVolumes), + SPA_PROP_INFO_description, SPA_POD_String("Soft Volumes"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->volume, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + SPA_PROP_INFO_container, SPA_POD_Id(SPA_TYPE_Array)); + break; + case 8: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("monitor.channel-volumes"), + SPA_PROP_INFO_description, SPA_POD_String("Monitor channel volume"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( + this->monitor_channel_volumes), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 9: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.disable"), + SPA_PROP_INFO_description, SPA_POD_String("Disable Channel mixing"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->mix_disabled), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 10: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.min-volume"), + SPA_PROP_INFO_description, SPA_POD_String("Minimum volume level"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->min_volume, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 11: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.max-volume"), + SPA_PROP_INFO_description, SPA_POD_String("Maximum volume level"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->max_volume, + DEFAULT_MIN_VOLUME, DEFAULT_MAX_VOLUME), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 12: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.normalize"), + SPA_PROP_INFO_description, SPA_POD_String("Normalize Volumes"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( + SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_NORMALIZE)), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 13: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.mix-lfe"), + SPA_PROP_INFO_description, SPA_POD_String("Mix LFE into channels"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( + SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_MIX_LFE)), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 14: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix"), + SPA_PROP_INFO_description, SPA_POD_String("Enable upmixing"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( + SPA_FLAG_IS_SET(this->mix.options, CHANNELMIX_OPTION_UPMIX)), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 15: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.lfe-cutoff"), + SPA_PROP_INFO_description, SPA_POD_String("LFE cutoff frequency"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.lfe_cutoff, 0.0, 1000.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 16: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.fc-cutoff"), + SPA_PROP_INFO_description, SPA_POD_String("FC cutoff frequency (Hz)"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.fc_cutoff, 0.0, 48000.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 17: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.rear-delay"), + SPA_PROP_INFO_description, SPA_POD_String("Rear channels delay (ms)"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.rear_delay, 0.0, 1000.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 18: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.stereo-widen"), + SPA_PROP_INFO_description, SPA_POD_String("Stereo widen"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.widen, 0.0, 1.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 19: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.hilbert-taps"), + SPA_PROP_INFO_description, SPA_POD_String("Taps for phase shift of rear"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int( + this->mix.hilbert_taps, 0, MAX_TAPS), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 20: + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); + spa_pod_builder_add(b, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.upmix-method"), + SPA_PROP_INFO_description, SPA_POD_String("Upmix method to use"), + SPA_PROP_INFO_type, SPA_POD_String( + channelmix_upmix_info[this->mix.upmix].label), + SPA_PROP_INFO_params, SPA_POD_Bool(true), + 0); + + spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0); + spa_pod_builder_push_struct(b, &f[1]); + SPA_FOR_EACH_ELEMENT_VAR(channelmix_upmix_info, i) { + spa_pod_builder_string(b, i->label); + spa_pod_builder_string(b, i->description); + } + spa_pod_builder_pop(b, &f[1]); + *param = spa_pod_builder_pop(b, &f[0]); + break; + case 21: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_rate), + SPA_PROP_INFO_description, SPA_POD_String("Rate scaler"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Double(p->rate, 0.0, 10.0)); + break; + case 22: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_quality), + SPA_PROP_INFO_name, SPA_POD_String("resample.quality"), + SPA_PROP_INFO_description, SPA_POD_String("Resample Quality"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(p->resample_quality, 0, 14), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 23: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("resample.disable"), + SPA_PROP_INFO_description, SPA_POD_String("Disable Resampling"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->resample_disabled), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 24: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("dither.noise"), + SPA_PROP_INFO_description, SPA_POD_String("Add noise bits"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int(this->dir[1].conv.noise_bits, 0, 16), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 25: + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); + spa_pod_builder_add(b, + SPA_PROP_INFO_name, SPA_POD_String("dither.method"), + SPA_PROP_INFO_description, SPA_POD_String("The dithering method"), + SPA_PROP_INFO_type, SPA_POD_String( + dither_method_info[this->dir[1].conv.method].label), + SPA_PROP_INFO_params, SPA_POD_Bool(true), + 0); + spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0); + spa_pod_builder_push_struct(b, &f[1]); + SPA_FOR_EACH_ELEMENT_VAR(dither_method_info, i) { + spa_pod_builder_string(b, i->label); + spa_pod_builder_string(b, i->description); + } + spa_pod_builder_pop(b, &f[1]); + *param = spa_pod_builder_pop(b, &f[0]); + break; + case 26: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("debug.wav-path"), + SPA_PROP_INFO_description, SPA_POD_String("Path to WAV file"), + SPA_PROP_INFO_type, SPA_POD_String(p->wav_path), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 27: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.lock-volumes"), + SPA_PROP_INFO_description, SPA_POD_String("Disable volume updates"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->lock_volumes), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 28: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph.disable"), + SPA_PROP_INFO_description, SPA_POD_String("Disable Filter graph updates"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->filter_graph_disabled), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 29: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph.N"), + SPA_PROP_INFO_description, SPA_POD_String("A filter graph to load"), + SPA_PROP_INFO_type, SPA_POD_String(""), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + default: + if (this->filter_graph[0] && this->filter_graph[0]->graph) { + return spa_filter_graph_enum_prop_info(this->filter_graph[0]->graph, + index - 30, b, param); + } + return 0; + } + return 1; +} + +static int node_param_props(struct impl *this, uint32_t id, uint32_t index, + struct spa_pod **param, struct spa_pod_builder *b) +{ + struct props *p = &this->props; + struct spa_pod_frame f[2]; + + switch (index) { + case 0: + spa_pod_builder_push_object(b, &f[0], + SPA_TYPE_OBJECT_Props, id); + spa_pod_builder_add(b, + SPA_PROP_volume, SPA_POD_Float(p->volume), + SPA_PROP_mute, SPA_POD_Bool(p->channel.mute), + SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, + p->channel.n_volumes, + p->channel.volumes), + SPA_PROP_channelMap, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, + p->n_channels, + p->channel_map), + SPA_PROP_softMute, SPA_POD_Bool(p->soft.mute), + SPA_PROP_softVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, + p->soft.n_volumes, + p->soft.volumes), + SPA_PROP_monitorMute, SPA_POD_Bool(p->monitor.mute), + SPA_PROP_monitorVolumes, SPA_POD_Array(sizeof(float), + SPA_TYPE_Float, + p->monitor.n_volumes, + p->monitor.volumes), + 0); + spa_pod_builder_prop(b, SPA_PROP_params, 0); + spa_pod_builder_push_struct(b, &f[1]); + spa_pod_builder_string(b, "monitor.channel-volumes"); + spa_pod_builder_bool(b, this->monitor_channel_volumes); + spa_pod_builder_string(b, "channelmix.disable"); + spa_pod_builder_bool(b, this->props.mix_disabled); + spa_pod_builder_string(b, "channelmix.min-volume"); + spa_pod_builder_float(b, this->props.min_volume); + spa_pod_builder_string(b, "channelmix.max-volume"); + spa_pod_builder_float(b, this->props.max_volume); + spa_pod_builder_string(b, "channelmix.normalize"); + spa_pod_builder_bool(b, SPA_FLAG_IS_SET(this->mix.options, + CHANNELMIX_OPTION_NORMALIZE)); + spa_pod_builder_string(b, "channelmix.mix-lfe"); + spa_pod_builder_bool(b, SPA_FLAG_IS_SET(this->mix.options, + CHANNELMIX_OPTION_MIX_LFE)); + spa_pod_builder_string(b, "channelmix.upmix"); + spa_pod_builder_bool(b, SPA_FLAG_IS_SET(this->mix.options, + CHANNELMIX_OPTION_UPMIX)); + spa_pod_builder_string(b, "channelmix.lfe-cutoff"); + spa_pod_builder_float(b, this->mix.lfe_cutoff); + spa_pod_builder_string(b, "channelmix.fc-cutoff"); + spa_pod_builder_float(b, this->mix.fc_cutoff); + spa_pod_builder_string(b, "channelmix.rear-delay"); + spa_pod_builder_float(b, this->mix.rear_delay); + spa_pod_builder_string(b, "channelmix.stereo-widen"); + spa_pod_builder_float(b, this->mix.widen); + spa_pod_builder_string(b, "channelmix.hilbert-taps"); + spa_pod_builder_int(b, this->mix.hilbert_taps); + spa_pod_builder_string(b, "channelmix.upmix-method"); + spa_pod_builder_string(b, channelmix_upmix_info[this->mix.upmix].label); + spa_pod_builder_string(b, "resample.quality"); + spa_pod_builder_int(b, p->resample_quality); + spa_pod_builder_string(b, "resample.disable"); + spa_pod_builder_bool(b, p->resample_disabled); + spa_pod_builder_string(b, "dither.noise"); + spa_pod_builder_int(b, this->dir[1].conv.noise_bits); + spa_pod_builder_string(b, "dither.method"); + spa_pod_builder_string(b, dither_method_info[this->dir[1].conv.method].label); + spa_pod_builder_string(b, "debug.wav-path"); + spa_pod_builder_string(b, p->wav_path); + spa_pod_builder_string(b, "channelmix.lock-volumes"); + spa_pod_builder_bool(b, p->lock_volumes); + spa_pod_builder_string(b, "audioconvert.filter-graph.disable"); + spa_pod_builder_bool(b, p->filter_graph_disabled); + spa_pod_builder_string(b, "audioconvert.filter-graph"); + spa_pod_builder_string(b, ""); + spa_pod_builder_pop(b, &f[1]); + *param = spa_pod_builder_pop(b, &f[0]); + break; + default: + { + struct spa_filter_graph *graph; + int res; + + if (index-1 >= this->n_graph) + return 0; + + graph = this->filter_graph[index-1]->graph; + if (graph == NULL) + return 1; + + res = spa_filter_graph_get_props(graph, b, param); + if (res == 0) { + *param = NULL; + return 1; + } + return res; + } + } + return 1; +} + +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + int res = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; + next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + param = NULL; + switch (id) { + case SPA_PARAM_EnumPortConfig: + res = node_param_enum_port_config(this, id, result.index, ¶m, &b); + break; + case SPA_PARAM_PortConfig: + res = node_param_port_config(this, id, result.index, ¶m, &b); + break; + case SPA_PARAM_PropInfo: + res = node_param_prop_info(this, id, result.index, ¶m, &b); + break; + case SPA_PARAM_Props: + res = node_param_props(this, id, result.index, ¶m, &b); + break; + default: + return 0; + } + if (res <= 0) + return res; + + if (param == NULL || spa_pod_filter(&b, &result.param, param, filter) < 0) goto next; spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); @@ -952,22 +1006,146 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) spa_log_debug(this->log, "%p: io %d %p/%zd", this, id, data, size); switch (id) { - case SPA_IO_Position: - this->io_position = data; + case SPA_IO_Clock: + this->io_clock = data; break; + case SPA_IO_Position: + { + struct port *p; + uint32_t i; + + this->io_position = data; + + if (this->io_position && this->io_clock && + this->io_position->clock.target_rate.denom != this->io_clock->target_rate.denom && + !this->props.resample_disabled) { + spa_log_warn(this->log, "driver %d changed rate:%u -> %u", this->io_position->clock.id, + this->io_clock->target_rate.denom, + this->io_position->clock.target_rate.denom); + + this->io_clock->target_rate = this->io_position->clock.target_rate; + for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) { + if ((p = GET_IN_PORT(this, i)) && p->valid && !p->is_dsp && !p->is_control) { + p->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + p->params[IDX_EnumFormat].user++; + } + } + for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) { + if ((p = GET_OUT_PORT(this, i)) && p->valid && !p->is_dsp && !p->is_control) { + p->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + p->params[IDX_EnumFormat].user++; + } + } + } + break; + } default: return -ENOENT; } + emit_info(this, false); return 0; } +static void port_update_latency(struct port *port, + const struct spa_latency_info *info, bool valid) +{ + if (spa_latency_info_compare(info, &port->latency[info->direction]) != 0) { + port->latency[info->direction] = *info; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_Latency].user++; + } + port->have_latency = valid; +} + +static void recalc_latencies(struct impl *this, enum spa_direction direction) +{ + struct spa_latency_info info; + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + struct port *port; + uint32_t i; + bool have_latency = false; + + spa_latency_info_combine_start(&info, other); + for (i = 0; i < this->dir[direction].n_ports; i++) { + port = GET_PORT(this, direction, i); + if ((port->is_monitor) || !port->have_latency) + continue; + spa_log_debug(this->log, "%p: combine %d", this, i); + spa_latency_info_combine(&info, &port->latency[other]); + have_latency = true; + } + spa_latency_info_combine_finish(&info); + + spa_process_latency_info_add(&this->latency, &info); + + spa_log_debug(this->log, "%p: combined %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this, + info.direction == SPA_DIRECTION_INPUT ? "input" : "output", + info.min_quantum, info.max_quantum, + info.min_rate, info.max_rate, + info.min_ns, info.max_ns); + + for (i = 0; i < this->dir[other].n_ports; i++) { + port = GET_PORT(this, other, i); + if (port->is_monitor) + continue; + port_update_latency(port, &info, have_latency); + } +} + +static void recalc_graph_latency(struct impl *impl) +{ + struct filter_graph *g; + int32_t latency = 0; + + spa_list_for_each(g, &impl->active_graphs, link) + latency += g->latency; + + if (latency != impl->latency.rate) { + impl->latency.rate = latency; + recalc_latencies(impl, SPA_DIRECTION_INPUT); + recalc_latencies(impl, SPA_DIRECTION_OUTPUT); + } +} + +static void update_graph_latency(struct filter_graph *g, uint32_t latency) +{ + if (g->latency != latency) { + g->latency = latency; + recalc_graph_latency(g->impl); + } +} + static void graph_info(void *object, const struct spa_filter_graph_info *info) { struct filter_graph *g = object; - if (!g->active) + struct spa_dict *props = info->props; + uint32_t i; + + if (g->removing) return; + g->n_inputs = info->n_inputs; g->n_outputs = info->n_outputs; + for (i = 0; props && i < props->n_items; i++) { + const char *k = props->items[i].key; + const char *s = props->items[i].value; + if (spa_streq(k, "n_inputs")) + spa_atou32(s, &g->n_inputs, 0); + else if (spa_streq(k, "n_outputs")) + spa_atou32(s, &g->n_outputs, 0); + else if (spa_streq(k, "inputs.audio.position")) + spa_audio_parse_position_n(s, strlen(s), g->inputs_position, + SPA_N_ELEMENTS(g->inputs_position), &g->n_inputs); + else if (spa_streq(k, "outputs.audio.position")) + spa_audio_parse_position_n(s, strlen(s), g->outputs_position, + SPA_N_ELEMENTS(g->outputs_position), &g->n_outputs); + else if (spa_streq(k, "latency")) { + double latency; + if (spa_atod(s, &latency)) + update_graph_latency(g, (uint32_t)latency); + } + } + emit_info(g->impl, false); } static int apply_props(struct impl *impl, const struct spa_pod *props); @@ -976,20 +1154,22 @@ static void graph_apply_props(void *object, enum spa_direction direction, const { struct filter_graph *g = object; struct impl *impl = g->impl; - if (!g->active) + if (g->removing) return; - if (apply_props(impl, props) > 0) - emit_node_info(impl, false); + apply_props(impl, props); + + emit_info(impl, false); } static void graph_props_changed(void *object, enum spa_direction direction) { struct filter_graph *g = object; struct impl *impl = g->impl; - if (!g->active) + if (g->removing) return; impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; impl->params[IDX_Props].user++; + emit_info(impl, false); } struct spa_filter_graph_events graph_events = { @@ -999,61 +1179,196 @@ struct spa_filter_graph_events graph_events = { .props_changed = graph_props_changed, }; -static int setup_filter_graph(struct impl *this, struct spa_filter_graph *graph) +static int setup_filter_graph(struct impl *this, struct filter_graph *g, + uint32_t channels, uint32_t *position) { int res; - char rate_str[64]; - struct dir *in; + char rate_str[64], in_ports[64]; + struct dir *dir; - if (graph == NULL) + if (g->graph == NULL || g->setup) return 0; - in = &this->dir[SPA_DIRECTION_INPUT]; - snprintf(rate_str, sizeof(rate_str), "%d", in->format.info.raw.rate); + dir = &this->dir[SPA_DIRECTION_REVERSE(this->direction)]; + snprintf(rate_str, sizeof(rate_str), "%d", dir->format.info.raw.rate); + if (channels) { + snprintf(in_ports, sizeof(in_ports), "%d", channels); + g->n_inputs = channels; + if (position) { + memcpy(g->inputs_position, position, sizeof(uint32_t) * channels); + memcpy(g->outputs_position, position, sizeof(uint32_t) * channels); + } + } - spa_filter_graph_deactivate(graph); - res = spa_filter_graph_activate(graph, + spa_filter_graph_deactivate(g->graph); + res = spa_filter_graph_activate(g->graph, &SPA_DICT_ITEMS( - SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, rate_str))); + SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, rate_str), + SPA_DICT_ITEM("filter-graph.n_inputs", channels ? in_ports : NULL))); + + g->setup = res >= 0; + return res; } +static int setup_channelmix(struct impl *this, uint32_t channels, uint32_t *position); + +static void free_tmp(struct impl *this) +{ + uint32_t i; + + spa_log_debug(this->log, "free tmp %d", this->scratch_size); + + free(this->empty); + this->empty = NULL; + this->scratch_size = 0; + this->scratch_ports = 0; + free(this->scratch); + this->scratch = NULL; + for (i = 0; i < MAX_PORTS; i++) { + free(this->tmp[0][i]); + this->tmp[0][i] = NULL; + free(this->tmp[1][i]); + this->tmp[1][i] = NULL; + this->tmp_datas[0][i] = NULL; + this->tmp_datas[1][i] = NULL; + } +} + + +static int ensure_tmp(struct impl *this) +{ + uint32_t maxsize = this->maxsize, maxports = this->maxports; + uint32_t i; + float *empty, *scratch, *tmp[2]; + + if (maxsize > this->scratch_size) { + spa_log_info(this->log, "resize tmp %d -> %d", this->scratch_size, maxsize); + + if ((empty = realloc(this->empty, maxsize + MAX_ALIGN)) != NULL) + this->empty = empty; + if ((scratch = realloc(this->scratch, maxsize + MAX_ALIGN)) != NULL) + this->scratch = scratch; + if (empty == NULL || scratch == NULL) { + free_tmp(this); + return -ENOMEM; + } + memset(this->empty, 0, maxsize + MAX_ALIGN); + + for (i = 0; i < this->scratch_ports; i++) { + if ((tmp[0] = realloc(this->tmp[0][i], maxsize + MAX_ALIGN)) != NULL) + this->tmp[0][i] = tmp[0]; + if ((tmp[1] = realloc(this->tmp[1][i], maxsize + MAX_ALIGN)) != NULL) + this->tmp[1][i] = tmp[1]; + if (tmp[0] == NULL || tmp[1] == NULL) { + free_tmp(this); + return -ENOMEM; + } + this->tmp_datas[0][i] = SPA_PTR_ALIGN(this->tmp[0][i], MAX_ALIGN, void); + this->tmp_datas[1][i] = SPA_PTR_ALIGN(this->tmp[1][i], MAX_ALIGN, void); + } + this->scratch_size = maxsize; + } + if (maxports > this->scratch_ports) { + spa_log_info(this->log, "resize ports %d -> %d", this->scratch_ports, maxports); + + for (i = this->scratch_ports; i < maxports; i++) { + if ((tmp[0] = malloc(maxsize + MAX_ALIGN)) != NULL) + this->tmp[0][i] = tmp[0]; + if ((tmp[1] = malloc(maxsize + MAX_ALIGN)) != NULL) + this->tmp[1][i] = tmp[1]; + if (tmp[0] == NULL || tmp[1] == NULL) { + free_tmp(this); + return -ENOMEM; + } + this->tmp_datas[0][i] = SPA_PTR_ALIGN(this->tmp[0][i], MAX_ALIGN, void); + this->tmp_datas[0][i] = SPA_PTR_ALIGN(this->tmp[0][i], MAX_ALIGN, void); + } + this->scratch_ports = maxports; + } + return 0; +} + + +static int setup_filter_graphs(struct impl *impl, bool force) +{ + int res; + uint32_t channels, *position; + struct dir *in, *out; + struct filter_graph *g, *t; + + in = &impl->dir[SPA_DIRECTION_INPUT]; + out = &impl->dir[SPA_DIRECTION_OUTPUT]; + + channels = in->format.info.raw.channels; + position = in->format.info.raw.position; + impl->maxports = SPA_MAX(in->format.info.raw.channels, out->format.info.raw.channels); + + spa_list_for_each_safe(g, t, &impl->active_graphs, link) { + if (g->removing) + continue; + if (force) + g->setup = false; + if ((res = setup_filter_graph(impl, g, channels, position)) < 0) { + g->removing = true; + spa_log_warn(impl->log, "failed to activate graph %d: %s", g->order, + spa_strerror(res)); + } else { + channels = g->n_outputs; + position = g->outputs_position; + impl->maxports = SPA_MAX(impl->maxports, channels); + } + } + if ((res = ensure_tmp(impl)) < 0) + return res; + if ((res = setup_channelmix(impl, channels, position)) < 0) + return res; + + return 0; +} + static int do_sync_filter_graph(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { struct impl *impl = user_data; - uint32_t i, j; - impl->n_graph = 0; - for (i = 0; i < MAX_GRAPH; i++) { - struct filter_graph *g = &impl->filter_graph[i]; - if (g->graph == NULL || !g->active) - continue; - impl->graph_index[impl->n_graph++] = i; + struct filter_graph *g; + + impl->n_graph = 0; + spa_list_for_each(g, &impl->active_graphs, link) + if (g->setup && !g->removing) + impl->filter_graph[impl->n_graph++] = g; - for (j = impl->n_graph-1; j > 0; j--) { - if (impl->filter_graph[impl->graph_index[j]].order >= - impl->filter_graph[impl->graph_index[j-1]].order) - break; - SPA_SWAP(impl->graph_index[j], impl->graph_index[j-1]); - } - } impl->recalc = true; return 0; } static void clean_filter_handles(struct impl *impl, bool force) { - uint32_t i; - for (i = 0; i < MAX_GRAPH; i++) { - struct filter_graph *g = &impl->filter_graph[i]; - if (!g->active || force) { - if (g->graph) - spa_hook_remove(&g->listener); - if (g->handle) - spa_plugin_loader_unload(impl->loader, g->handle); - spa_zero(*g); - } + struct filter_graph *g, *t; + + spa_list_for_each_safe(g, t, &impl->active_graphs, link) { + if (!g->removing) + continue; + spa_list_remove(&g->link); + if (g->graph) + spa_hook_remove(&g->listener); + if (g->handle) + spa_plugin_loader_unload(impl->loader, g->handle); + spa_zero(*g); + spa_list_append(&impl->free_graphs, &g->link); } + recalc_graph_latency(impl); +} + +static inline void insert_graph(struct spa_list *graphs, struct filter_graph *pending) +{ + struct filter_graph *g; + + spa_list_for_each(g, graphs, link) { + if (g->order < pending->order) + break; + } + spa_list_append(&g->link, &pending->link); } static int load_filter_graph(struct impl *impl, const char *graph, int order) @@ -1062,35 +1377,29 @@ static int load_filter_graph(struct impl *impl, const char *graph, int order) int res; void *iface; struct spa_handle *new_handle = NULL; - uint32_t i, idx, n_graph; - struct filter_graph *pending, *old_active = NULL; + struct filter_graph *pending, *g, *t; if (impl->props.filter_graph_disabled) return -EPERM; /* find graph spot */ - idx = SPA_ID_INVALID; - n_graph = 0; - for (i = 0; i < MAX_GRAPH; i++) { - pending = &impl->filter_graph[i]; - /* find the first free spot for our new filter */ - if (!pending->active && idx == SPA_ID_INVALID) - idx = i; - /* deactivate an existing filter of the same order */ - if (pending->active) { - if (pending->order == order) - old_active = pending; - else - n_graph++; - } - } - /* we can at most have MAX_GRAPH-1 active filters */ - if (n_graph >= MAX_GRAPH-1) + if (spa_list_is_empty(&impl->free_graphs)) return -ENOSPC; - pending = &impl->filter_graph[idx]; + /* find free graph for our new filter */ + pending = spa_list_first(&impl->free_graphs, struct filter_graph, link); + pending->impl = impl; pending->order = order; + pending->removing = false; + + /* move active graphs with same order to inactive list */ + spa_list_for_each_safe(g, t, &impl->active_graphs, link) { + if (g->order == order) { + g->removing = true; + spa_log_info(impl->log, "removing filter-graph order:%d", order); + } + } if (graph != NULL && graph[0] != '\0') { snprintf(qlimit, sizeof(qlimit), "%u", impl->quantum_limit); @@ -1108,31 +1417,19 @@ static int load_filter_graph(struct impl *impl, const char *graph, int order) goto error; /* prepare new filter and swap it */ - res = setup_filter_graph(impl, iface); - if (res < 0) - goto error; pending->graph = iface; - pending->active = true; - spa_log_info(impl->log, "loading filter-graph order:%d in %d active:%d", - order, idx, n_graph + 1); - } else { - pending->active = false; - spa_log_info(impl->log, "removing filter-graph order:%d active:%d", - order, n_graph); - } - if (old_active) - old_active->active = false; - - /* we call this here on the pending_graph so that the n_input/n_output is updated - * before we switch */ - if (pending->active) + pending->handle = new_handle; spa_filter_graph_add_listener(pending->graph, &pending->listener, &graph_events, pending); + spa_list_remove(&pending->link); + insert_graph(&impl->active_graphs, pending); - spa_loop_invoke(impl->data_loop, do_sync_filter_graph, 0, NULL, 0, true, impl); + spa_log_info(impl->log, "loading filter-graph order:%d", order); + } + if (impl->setup) + res = setup_filter_graphs(impl, false); - if (pending->active) - pending->handle = new_handle; + spa_loop_locked(impl->data_loop, do_sync_filter_graph, 0, NULL, 0, impl); if (impl->in_filter_props == 0) clean_filter_handles(impl, false); @@ -1148,10 +1445,9 @@ error: return -ENOTSUP; } -static int audioconvert_set_param(struct impl *this, const char *k, const char *s) +static int audioconvert_set_param(struct impl *this, const char *k, const char *s, bool *disable_filter) { int res; - if (spa_streq(k, "monitor.channel-volumes")) this->monitor_channel_volumes = spa_atob(s); else if (spa_streq(k, "channelmix.disable")) @@ -1192,13 +1488,17 @@ static int audioconvert_set_param(struct impl *this, const char *k, const char * } else if (spa_streq(k, "channelmix.lock-volumes")) this->props.lock_volumes = spa_atob(s); - else if (spa_strstartswith(k, "audioconvert.filter-graph")) { - int order = atoi(k+ strlen("audioconvert.filter-graph.")); + else if (spa_strstartswith(k, "audioconvert.filter-graph.")) { + int order = atoi(k + strlen("audioconvert.filter-graph.")); if ((res = load_filter_graph(this, s, order)) < 0) { spa_log_warn(this->log, "Can't load filter-graph %d: %s", order, spa_strerror(res)); } } + else if (spa_streq(k, "audioconvert.filter-graph.disable")) { + if (!*disable_filter) + *disable_filter = spa_atob(s); + } else return 0; return 1; @@ -1209,6 +1509,7 @@ static int parse_prop_params(struct impl *this, struct spa_pod *params) struct spa_pod_parser prs; struct spa_pod_frame f; int changed = 0; + bool filter_graph_disabled = this->props.filter_graph_disabled; spa_pod_parser_pod(&prs, params); if (spa_pod_parser_push_struct(&prs, &f) < 0) @@ -1217,7 +1518,7 @@ static int parse_prop_params(struct impl *this, struct spa_pod *params) while (true) { const char *name; struct spa_pod *pod; - char value[512]; + char value[4096]; if (spa_pod_parser_get_string(&prs, &name) < 0) break; @@ -1236,6 +1537,9 @@ static int parse_prop_params(struct impl *this, struct spa_pod *params) } else if (spa_pod_is_int(pod)) { snprintf(value, sizeof(value), "%d", SPA_POD_VALUE(struct spa_pod_int, pod)); + } else if (spa_pod_is_long(pod)) { + snprintf(value, sizeof(value), "%"PRIi64, + SPA_POD_VALUE(struct spa_pod_long, pod)); } else if (spa_pod_is_bool(pod)) { snprintf(value, sizeof(value), "%s", SPA_POD_VALUE(struct spa_pod_bool, pod) ? @@ -1246,133 +1550,105 @@ static int parse_prop_params(struct impl *this, struct spa_pod *params) continue; spa_log_info(this->log, "key:'%s' val:'%s'", name, value); - changed += audioconvert_set_param(this, name, value); + changed += audioconvert_set_param(this, name, value, &filter_graph_disabled); } if (changed) { - channelmix_init(&this->mix); + this->props.filter_graph_disabled = filter_graph_disabled; + if (this->setup) + channelmix_init(&this->mix); } return changed; } -static int get_ramp_samples(struct impl *this) +static int get_ramp_samples(struct impl *this, struct volume_ramp_params *vrp) { - struct volume_ramp_params *vrp = &this->props.vrp; int samples = -1; if (vrp->volume_ramp_samples) samples = vrp->volume_ramp_samples; else if (vrp->volume_ramp_time) { - struct dir *d = &this->dir[SPA_DIRECTION_OUTPUT]; - unsigned int sample_rate = d->format.info.raw.rate; - samples = (vrp->volume_ramp_time * sample_rate) / 1000; + samples = (vrp->volume_ramp_time * vrp->rate) / 1000; spa_log_info(this->log, "volume ramp samples calculated from time is %d", samples); } - if (!samples) - samples = -1; - return samples; - } -static int get_ramp_step_samples(struct impl *this) +static int get_ramp_step_samples(struct impl *this, struct volume_ramp_params *vrp) { - struct volume_ramp_params *vrp = &this->props.vrp; int samples = -1; if (vrp->volume_ramp_step_samples) samples = vrp->volume_ramp_step_samples; else if (vrp->volume_ramp_step_time) { - struct dir *d = &this->dir[SPA_DIRECTION_OUTPUT]; - int sample_rate = d->format.info.raw.rate; - /* convert the step time which is in nano seconds to seconds */ - samples = (vrp->volume_ramp_step_time/1000) * (sample_rate/1000); + /* convert the step time which is in nano seconds to seconds, round up */ + samples = SPA_MAX(1u, vrp->volume_ramp_step_time/1000) * (vrp->rate/1000); spa_log_debug(this->log, "volume ramp step samples calculated from time is %d", samples); } - if (!samples) - samples = -1; - return samples; - } -static double get_volume_at_scale(struct impl *this, double value) +static float get_volume_at_scale(struct volume_ramp_params *vrp, float value) { - struct volume_ramp_params *vrp = &this->props.vrp; if (vrp->scale == SPA_AUDIO_VOLUME_RAMP_LINEAR || vrp->scale == SPA_AUDIO_VOLUME_RAMP_INVALID) return value; else if (vrp->scale == SPA_AUDIO_VOLUME_RAMP_CUBIC) return (value * value * value); - return 0.0; } -static struct spa_pod *generate_ramp_up_seq(struct impl *this) +static struct spa_pod *generate_ramp_seq(struct impl *this, struct volume_ramp_params *vrp, + void *buffer, size_t size) { struct spa_pod_dynamic_builder b; struct spa_pod_frame f[1]; - struct props *p = &this->props; - double volume_accum = p->prev_volume; - int ramp_samples = get_ramp_samples(this); - int ramp_step_samples = get_ramp_step_samples(this); - double volume_step = ((p->volume - p->prev_volume) / (ramp_samples / ramp_step_samples)); - uint32_t volume_offs = 0; + float start = vrp->start, end = vrp->end; + int samples = get_ramp_samples(this, vrp); + int step = get_ramp_step_samples(this, vrp); + int offs = 0; - spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); + if (samples < 0 || step < 0 || (samples > 0 && step == 0)) + return NULL; + + spa_pod_dynamic_builder_init(&b, buffer, size, 4096); spa_pod_builder_push_sequence(&b.b, &f[0], 0); - spa_log_info(this->log, "generating ramp up sequence from %f to %f with a" - " step value %f at scale %d", p->prev_volume, p->volume, volume_step, p->vrp.scale); - do { - spa_log_trace(this->log, "volume accum %f", get_volume_at_scale(this, volume_accum)); - spa_pod_builder_control(&b.b, volume_offs, SPA_CONTROL_Properties); + spa_log_info(this->log, "generating ramp sequence from %f to %f with " + "step %d/%d at scale %d", start, end, step, samples, vrp->scale); + + while (1) { + float pos = (samples == 0) ? end : + SPA_CLAMP(start + (end - start) * offs / samples, + SPA_MIN(start, end), SPA_MAX(start, end)); + float vas = get_volume_at_scale(vrp, pos); + + spa_log_trace(this->log, "volume %d accum %f", offs, vas); + spa_pod_builder_control(&b.b, offs, SPA_CONTROL_Properties); spa_pod_builder_add_object(&b.b, SPA_TYPE_OBJECT_Props, 0, - SPA_PROP_volume, - SPA_POD_Float(get_volume_at_scale(this, volume_accum))); - volume_accum += volume_step; - volume_offs += ramp_step_samples; - } while (volume_accum < p->volume); - return spa_pod_builder_pop(&b.b, &f[0]); -} + SPA_PROP_volume, SPA_POD_Float(vas)); -static struct spa_pod *generate_ramp_down_seq(struct impl *this) -{ - struct spa_pod_dynamic_builder b; - struct spa_pod_frame f[1]; - int ramp_samples = get_ramp_samples(this); - int ramp_step_samples = get_ramp_step_samples(this); - struct props *p = &this->props; - double volume_accum = p->prev_volume; - double volume_step = ((p->prev_volume - p->volume) / (ramp_samples / ramp_step_samples)); - uint32_t volume_offs = 0; + if (offs >= samples) + break; - spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); - - spa_pod_builder_push_sequence(&b.b, &f[0], 0); - spa_log_info(this->log, "generating ramp down sequence from %f to %f with a" - " step value %f at scale %d", p->prev_volume, p->volume, volume_step, p->vrp.scale); - do { - spa_log_trace(this->log, "volume accum %f", get_volume_at_scale(this, volume_accum)); - spa_pod_builder_control(&b.b, volume_offs, SPA_CONTROL_Properties); - spa_pod_builder_add_object(&b.b, - SPA_TYPE_OBJECT_Props, 0, - SPA_PROP_volume, - SPA_POD_Float(get_volume_at_scale(this, volume_accum))); - - volume_accum -= volume_step; - volume_offs += ramp_step_samples; - } while (volume_accum > p->volume); - return spa_pod_builder_pop(&b.b, &f[0]); -} - -static struct volume_ramp_params *reset_volume_ramp_params(struct impl *this) -{ - if (!this->vol_ramp_sequence) { - struct volume_ramp_params *vrp = &this->props.vrp; - spa_zero(this->props.vrp); - return vrp; + offs = SPA_MIN(samples, offs + step); } - return 0; + + return spa_pod_builder_pop(&b.b, &f[0]); +} + +static void generate_volume_ramp(struct impl *this, struct volume_ramp_params *vrp, + void *buffer, size_t size) +{ + void *sequence; + + sequence = generate_ramp_seq(this, vrp, buffer, size); + if (!sequence) + spa_log_error(this->log, "unable to generate sequence"); + + this->vol_ramp_sequence = (struct spa_pod_sequence *) sequence; + this->vol_ramp_sequence_data = (void*)sequence == buffer ? NULL : sequence; + this->vol_ramp_offset = 0; + this->recalc = true; } static int apply_props(struct impl *this, const struct spa_pod *param) @@ -1384,11 +1660,13 @@ static int apply_props(struct impl *this, const struct spa_pod *param) bool have_soft_volume = false; int changed = 0; int vol_ramp_params_changed = 0; - struct volume_ramp_params *vrp = reset_volume_ramp_params(this); + struct volume_ramp_params vrp; uint32_t n; int32_t value; uint32_t id; + spa_zero(vrp); + SPA_POD_OBJECT_FOREACH(obj, prop) { switch (prop->key) { case SPA_PROP_volume: @@ -1415,7 +1693,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param) } if (spa_pod_get_int(&prop->value, &value) == 0 && value) { - vrp->volume_ramp_samples = value; + vrp.volume_ramp_samples = value; spa_log_info(this->log, "%p volume ramp samples %d", this, value); vol_ramp_params_changed++; } @@ -1428,7 +1706,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param) } if (spa_pod_get_int(&prop->value, &value) == 0 && value) { - vrp->volume_ramp_step_samples = value; + vrp.volume_ramp_step_samples = value; spa_log_info(this->log, "%p volume ramp step samples is %d", this, value); } @@ -1441,7 +1719,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param) } if (spa_pod_get_int(&prop->value, &value) == 0 && value) { - vrp->volume_ramp_time = value; + vrp.volume_ramp_time = value; spa_log_info(this->log, "%p volume ramp time %d", this, value); vol_ramp_params_changed++; } @@ -1454,7 +1732,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param) } if (spa_pod_get_int(&prop->value, &value) == 0 && value) { - vrp->volume_ramp_step_time = value; + vrp.volume_ramp_step_time = value; spa_log_info(this->log, "%p volume ramp time %d", this, value); } break; @@ -1466,14 +1744,14 @@ static int apply_props(struct impl *this, const struct spa_pod *param) } if (spa_pod_get_id(&prop->value, &id) == 0 && id) { - vrp->scale = id; + vrp.scale = id; spa_log_info(this->log, "%p volume ramp scale %d", this, id); } break; case SPA_PROP_channelVolumes: if (!p->lock_volumes && (n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - p->channel.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) { + p->channel.volumes, SPA_N_ELEMENTS(p->channel.volumes))) > 0) { have_channel_volume = true; p->channel.n_volumes = n; changed++; @@ -1481,7 +1759,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param) break; case SPA_PROP_channelMap: if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, - p->channel_map, SPA_AUDIO_MAX_CHANNELS)) > 0) { + p->channel_map, SPA_N_ELEMENTS(p->channel_map))) > 0) { p->n_channels = n; changed++; } @@ -1496,7 +1774,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param) case SPA_PROP_softVolumes: if (!p->lock_volumes && (n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - p->soft.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) { + p->soft.volumes, SPA_N_ELEMENTS(p->soft.volumes))) > 0) { have_soft_volume = true; p->soft.n_volumes = n; changed++; @@ -1508,7 +1786,7 @@ static int apply_props(struct impl *this, const struct spa_pod *param) break; case SPA_PROP_monitorVolumes: if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - p->monitor.volumes, SPA_AUDIO_MAX_CHANNELS)) > 0) { + p->monitor.volumes, SPA_N_ELEMENTS(p->monitor.volumes))) > 0) { p->monitor.n_volumes = n; changed++; } @@ -1540,20 +1818,15 @@ static int apply_props(struct impl *this, const struct spa_pod *param) } if (!p->lock_volumes && vol_ramp_params_changed) { - void *sequence = NULL; - if (p->volume == p->prev_volume) - spa_log_error(this->log, "no change in volume, cannot ramp volume"); - else if (p->volume > p->prev_volume) - sequence = generate_ramp_up_seq(this); - else - sequence = generate_ramp_down_seq(this); - - if (!sequence) - spa_log_error(this->log, "unable to generate sequence"); - - this->vol_ramp_sequence = (struct spa_pod_sequence *) sequence; - this->vol_ramp_offset = 0; - this->recalc = true; + struct dir *dir = &this->dir[SPA_DIRECTION_REVERSE(this->direction)]; + vrp.start = p->prev_volume; + vrp.end = p->volume; + vrp.rate = dir->format.info.raw.rate; + generate_volume_ramp(this, &vrp, NULL, 0); + } + if (changed) { + this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; + this->params[IDX_Props].user++; } return changed; } @@ -1561,18 +1834,20 @@ static int apply_props(struct impl *this, const struct spa_pod *param) static int apply_midi(struct impl *this, const struct spa_pod *value) { struct props *p = &this->props; - uint8_t data[8]; - int size; + uint8_t ev[8]; + int ev_size; + const uint32_t *body = SPA_POD_BODY_CONST(value); + size_t size = SPA_POD_BODY_SIZE(value); + uint64_t state = 0; - size = spa_ump_to_midi(SPA_POD_BODY(value), SPA_POD_BODY_SIZE(value), - data, sizeof(data)); - if (size < 3) + ev_size = spa_ump_to_midi(&body, &size, ev, sizeof(ev), &state); + if (ev_size < 3) return -EINVAL; - if ((data[0] & 0xf0) != 0xb0 || data[1] != 7) + if ((ev[0] & 0xf0) != 0xb0 || ev[1] != 7) return 0; - p->volume = data[2] / 127.0f; + p->volume = ev[2] / 127.0f; set_volume(this); return 1; } @@ -1623,10 +1898,11 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m this->dir[SPA_DIRECTION_OUTPUT].n_ports = dir->n_ports + 1; for (i = 0; i < dir->n_ports; i++) { - init_port(this, direction, i, info->info.raw.position[i], true, false, false); + uint32_t pos = info->info.raw.position[i]; + init_port(this, direction, i, pos, true, false, false); if (this->monitor && direction == SPA_DIRECTION_INPUT) init_port(this, SPA_DIRECTION_OUTPUT, i+1, - info->info.raw.position[i], true, true, false); + pos, true, true, false); } break; } @@ -1638,6 +1914,7 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m break; } case SPA_PARAM_PORT_CONFIG_MODE_none: + dir->n_ports = 0; break; default: return -ENOTSUP; @@ -1646,6 +1923,8 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m i = dir->n_ports++; init_port(this, direction, i, 0, false, false, true); } + /* emit all port changes */ + emit_info(this, false); this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; @@ -1654,92 +1933,99 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m return 0; } -static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, - const struct spa_pod *param) +static int node_set_param_port_config(struct impl *this, uint32_t flags, + const struct spa_pod *param) { - struct impl *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); + struct spa_audio_info info = { 0, }, *infop = NULL; + struct spa_pod *format = NULL; + enum spa_direction direction; + enum spa_param_port_config_mode mode; + bool monitor = false, control = false; + int res; if (param == NULL) return 0; - switch (id) { - case SPA_PARAM_PortConfig: - { - struct spa_audio_info info = { 0, }, *infop = NULL; - struct spa_pod *format = NULL; - enum spa_direction direction; - enum spa_param_port_config_mode mode; - bool monitor = false, control = false; - int res; + if (spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamPortConfig, NULL, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_OPT_Bool(&monitor), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_OPT_Bool(&control), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + return -EINVAL; - if (spa_pod_parse_object(param, - SPA_TYPE_OBJECT_ParamPortConfig, NULL, - SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&direction), - SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), - SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_OPT_Bool(&monitor), - SPA_PARAM_PORT_CONFIG_control, SPA_POD_OPT_Bool(&control), - SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + if (format) { + if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format)) return -EINVAL; - if (format) { - if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format)) - return -EINVAL; - - if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) - return res; - - if (info.media_type != SPA_MEDIA_TYPE_audio || - info.media_subtype != SPA_MEDIA_SUBTYPE_raw) - return -EINVAL; - - if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) - return -EINVAL; - - if (info.info.raw.format == 0 || - info.info.raw.rate == 0 || - info.info.raw.channels == 0 || - info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) - return -EINVAL; - - infop = &info; - } - - if ((res = reconfigure_mode(this, mode, direction, monitor, control, infop)) < 0) + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) return res; - emit_node_info(this, false); - break; + if (info.media_type != SPA_MEDIA_TYPE_audio || + info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return -EINVAL; + + if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) + return -EINVAL; + + if (info.info.raw.channels == 0 || + info.info.raw.channels > MAX_CHANNELS) + return -EINVAL; + + infop = &info; } + return reconfigure_mode(this, mode, direction, monitor, control, infop); +} + +static int node_set_param_props(struct impl *this, uint32_t flags, + const struct spa_pod *param) +{ + bool have_graph = false; + struct filter_graph *g, *t; + + if (param == NULL) + return 0; + + this->filter_props_count = 0; + + spa_list_for_each_safe(g, t, &this->active_graphs, link) { + if (g->removing) + continue; + + have_graph = true; + this->in_filter_props++; + spa_filter_graph_set_props(g->graph, SPA_DIRECTION_INPUT, param); + this->filter_props_count++; + this->in_filter_props--; + } + if (!have_graph) + apply_props(this, param); + + clean_filter_handles(this, false); + return 0; +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_PortConfig: + res = node_set_param_port_config(this, flags, param); + break; case SPA_PARAM_Props: - { - uint32_t i; - bool have_graph = false; - this->filter_props_count = 0; - for (i = 0; i < MAX_GRAPH; i++) { - struct filter_graph *g = &this->filter_graph[i]; - if (!g->active) - continue; - - have_graph = true; - - this->in_filter_props++; - spa_filter_graph_set_props(g->graph, - SPA_DIRECTION_INPUT, param); - this->filter_props_count++; - this->in_filter_props--; - } - if (!have_graph && apply_props(this, param) > 0) - emit_node_info(this, false); - - clean_filter_handles(this, false); + res = node_set_param_props(this, flags, param); break; - } default: return -ENOENT; } - return 0; + emit_info(this, false); + return res; } static int int32_cmp(const void *v1, const void *v2) @@ -1774,22 +2060,24 @@ static int setup_in_convert(struct impl *this) dst_info.info.raw.rate); qsort(dst_info.info.raw.position, dst_info.info.raw.channels, - sizeof(uint32_t), int32_cmp); + sizeof(uint32_t), int32_cmp); for (i = 0; i < src_info.info.raw.channels; i++) { for (j = 0; j < dst_info.info.raw.channels; j++) { - if (src_info.info.raw.position[i] != - dst_info.info.raw.position[j]) + uint32_t pi, pj; + char b1[8], b2[8]; + + pi = src_info.info.raw.position[i]; + pj = dst_info.info.raw.position[j]; + if (pi != pj) continue; in->remap[i] = j; if (i != j) remap = true; spa_log_debug(this->log, "%p: channel %d (%d) -> %d (%s -> %s)", this, i, in->remap[i], j, - spa_debug_type_find_short_name(spa_type_audio_channel, - src_info.info.raw.position[i]), - spa_debug_type_find_short_name(spa_type_audio_channel, - dst_info.info.raw.position[j])); + spa_type_audio_channel_make_short_name(pi, b1, 8, "UNK"), + spa_type_audio_channel_make_short_name(pj, b2, 8, "UNK")); dst_info.info.raw.position[j] = -1; break; } @@ -1838,9 +2126,10 @@ static int remap_volumes(struct impl *this, const struct spa_audio_info *info) for (i = 0; i < p->n_channels; i++) { for (j = i; j < target; j++) { + uint32_t pj = info->info.raw.position[j]; spa_log_debug(this->log, "%d %d: %d <-> %d", i, j, - p->channel_map[i], info->info.raw.position[j]); - if (p->channel_map[i] != info->info.raw.position[j]) + p->channel_map[i], pj); + if (p->channel_map[i] != pj) continue; if (i != j) { SPA_SWAP(p->channel_map[i], p->channel_map[j]); @@ -1871,7 +2160,7 @@ static void set_volume(struct impl *this) { struct volumes *vol; uint32_t i; - float volumes[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; struct dir *dir = &this->dir[this->direction]; spa_log_debug(this->log, "%p set volume %f have_format:%d", this, this->props.volume, dir->have_format); @@ -1902,14 +2191,15 @@ static void set_volume(struct impl *this) static char *format_position(char *str, size_t len, uint32_t channels, uint32_t *position) { uint32_t i, idx = 0; + char buf[8]; for (i = 0; i < channels; i++) idx += snprintf(str + idx, len - idx, "%s%s", i == 0 ? "" : " ", - spa_debug_type_find_short_name(spa_type_audio_channel, - position[i])); + spa_type_audio_channel_make_short_name(position[i], + buf, sizeof(buf), "UNK")); return str; } -static int setup_channelmix(struct impl *this) +static int setup_channelmix(struct impl *this, uint32_t channels, uint32_t *position) { struct dir *in = &this->dir[SPA_DIRECTION_INPUT]; struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT]; @@ -1918,19 +2208,20 @@ static int setup_channelmix(struct impl *this) char str[1024]; int res; - src_chan = in->format.info.raw.channels; + src_chan = channels; dst_chan = out->format.info.raw.channels; for (i = 0, src_mask = 0; i < src_chan; i++) { - p = in->format.info.raw.position[i]; + p = position[i]; src_mask |= 1ULL << (p < 64 ? p : 0); } for (i = 0, dst_mask = 0; i < dst_chan; i++) { p = out->format.info.raw.position[i]; dst_mask |= 1ULL << (p < 64 ? p : 0); } + spa_log_info(this->log, "in %s (%016"PRIx64")", format_position(str, sizeof(str), - src_chan, in->format.info.raw.position), src_mask); + src_chan, position), src_mask); spa_log_info(this->log, "out %s (%016"PRIx64")", format_position(str, sizeof(str), dst_chan, out->format.info.raw.position), dst_mask); @@ -1971,13 +2262,19 @@ static int setup_resample(struct impl *this) struct dir *in = &this->dir[SPA_DIRECTION_INPUT]; struct dir *out = &this->dir[SPA_DIRECTION_OUTPUT]; int res; + uint32_t channels; + + if (this->direction == SPA_DIRECTION_INPUT) + channels = in->format.info.raw.channels; + else + channels = out->format.info.raw.channels; spa_log_info(this->log, "%p: %s/%d@%d->%s/%d@%d", this, spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32), - out->format.info.raw.channels, + channels, in->format.info.raw.rate, spa_debug_type_find_name(spa_type_audio_format, SPA_AUDIO_FORMAT_DSP_F32), - out->format.info.raw.channels, + channels, out->format.info.raw.rate); if (this->props.resample_disabled && !this->resample_peaks && @@ -1987,7 +2284,7 @@ static int setup_resample(struct impl *this) if (this->resample.free) resample_free(&this->resample); - this->resample.channels = out->format.info.raw.channels; + this->resample.channels = channels; this->resample.i_rate = in->format.info.raw.rate; this->resample.o_rate = out->format.info.raw.rate; this->resample.log = this->log; @@ -2055,12 +2352,16 @@ static int setup_out_convert(struct impl *this) dst_info.info.raw.rate); qsort(src_info.info.raw.position, src_info.info.raw.channels, - sizeof(uint32_t), int32_cmp); + sizeof(uint32_t), int32_cmp); for (i = 0; i < src_info.info.raw.channels; i++) { for (j = 0; j < dst_info.info.raw.channels; j++) { - if (src_info.info.raw.position[i] != - dst_info.info.raw.position[j]) + uint32_t pi, pj; + char b1[8], b2[8]; + + pi = src_info.info.raw.position[i]; + pj = dst_info.info.raw.position[j]; + if (pi != pj) continue; out->remap[i] = j; if (i != j) @@ -2068,10 +2369,9 @@ static int setup_out_convert(struct impl *this) spa_log_debug(this->log, "%p: channel %d (%d) -> %d (%s -> %s)", this, i, out->remap[i], j, - spa_debug_type_find_short_name(spa_type_audio_channel, - src_info.info.raw.position[i]), - spa_debug_type_find_short_name(spa_type_audio_channel, - dst_info.info.raw.position[j])); + spa_type_audio_channel_make_short_name(pi, b1, 8, "UNK"), + spa_type_audio_channel_make_short_name(pj, b2, 8, "UNK")); + dst_info.info.raw.position[j] = -1; break; } @@ -2097,63 +2397,6 @@ static int setup_out_convert(struct impl *this) return 0; } -static void free_tmp(struct impl *this) -{ - uint32_t i; - - spa_log_debug(this->log, "free tmp %d", this->scratch_size); - - free(this->empty); - this->empty = NULL; - this->scratch_size = 0; - this->scratch_ports = 0; - free(this->scratch); - this->scratch = NULL; - free(this->tmp[0]); - this->tmp[0] = NULL; - free(this->tmp[1]); - this->tmp[1] = NULL; - for (i = 0; i < MAX_PORTS; i++) { - this->tmp_datas[0][i] = NULL; - this->tmp_datas[1][i] = NULL; - } -} - -static int ensure_tmp(struct impl *this, uint32_t maxsize, uint32_t maxports) -{ - if (maxsize > this->scratch_size || maxports > this->scratch_ports) { - float *empty, *scratch, *tmp[2]; - uint32_t i; - - spa_log_debug(this->log, "resize tmp %d -> %d", this->scratch_size, maxsize); - - if ((empty = realloc(this->empty, maxsize + MAX_ALIGN)) != NULL) - this->empty = empty; - if ((scratch = realloc(this->scratch, maxsize + MAX_ALIGN)) != NULL) - this->scratch = scratch; - if ((tmp[0] = realloc(this->tmp[0], (maxsize + MAX_ALIGN) * maxports)) != NULL) - this->tmp[0] = tmp[0]; - if ((tmp[1] = realloc(this->tmp[1], (maxsize + MAX_ALIGN) * maxports)) != NULL) - this->tmp[1] = tmp[1]; - - if (empty == NULL || scratch == NULL || tmp[0] == NULL || tmp[1] == NULL) { - free_tmp(this); - return -ENOMEM; - } - memset(this->empty, 0, maxsize + MAX_ALIGN); - this->scratch_size = maxsize; - this->scratch_ports = maxports; - - for (i = 0; i < maxports; i++) { - this->tmp_datas[0][i] = SPA_PTROFF(tmp[0], maxsize * i, void); - this->tmp_datas[0][i] = SPA_PTR_ALIGN(this->tmp_datas[0][i], MAX_ALIGN, void); - this->tmp_datas[1][i] = SPA_PTROFF(tmp[1], maxsize * i, void); - this->tmp_datas[1][i] = SPA_PTR_ALIGN(this->tmp_datas[1][i], MAX_ALIGN, void); - } - } - return 0; -} - static uint32_t resample_update_rate_match(struct impl *this, bool passthrough, uint32_t size, uint32_t queued) { uint32_t delay, match_size; @@ -2216,7 +2459,7 @@ static inline bool resample_is_passthrough(struct impl *this) static int setup_convert(struct impl *this) { struct dir *in, *out; - uint32_t i, rate, maxsize, maxports, duration; + uint32_t i, rate, duration; struct port *p; int res; @@ -2265,31 +2508,23 @@ static int setup_convert(struct impl *this) if ((res = setup_in_convert(this)) < 0) return res; - for (i = 0; i < MAX_GRAPH; i++) { - struct filter_graph *g = &this->filter_graph[i]; - if (!g->active) - continue; - if ((res = setup_filter_graph(this, g->graph)) < 0) - return res; - } - if ((res = setup_channelmix(this)) < 0) + if ((res = setup_filter_graphs(this, true)) < 0) return res; if ((res = setup_resample(this)) < 0) return res; if ((res = setup_out_convert(this)) < 0) return res; - maxsize = this->quantum_limit * sizeof(float); + this->maxsize = this->quantum_limit * sizeof(float); for (i = 0; i < in->n_ports; i++) { p = GET_IN_PORT(this, i); - maxsize = SPA_MAX(maxsize, p->maxsize); + this->maxsize = SPA_MAX(this->maxsize, p->maxsize); } for (i = 0; i < out->n_ports; i++) { p = GET_OUT_PORT(this, i); - maxsize = SPA_MAX(maxsize, p->maxsize); + this->maxsize = SPA_MAX(this->maxsize, p->maxsize); } - maxports = SPA_MAX(in->format.info.raw.channels, out->format.info.raw.channels); - if ((res = ensure_tmp(this, maxsize, maxports)) < 0) + if ((res = ensure_tmp(this)) < 0) return res; resample_update_rate_match(this, resample_is_passthrough(this), duration, 0); @@ -2297,18 +2532,17 @@ static int setup_convert(struct impl *this) this->setup = true; this->recalc = true; - emit_node_info(this, false); - return 0; } static void reset_node(struct impl *this) { - uint32_t i; - for (i = 0; i < MAX_GRAPH; i++) { - struct filter_graph *g = &this->filter_graph[i]; + struct filter_graph *g; + + spa_list_for_each(g, &this->active_graphs, link) { if (g->graph) spa_filter_graph_deactivate(g->graph); + g->setup = false; } if (this->resample.reset) resample_reset(&this->resample); @@ -2333,6 +2567,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman this->started = true; break; case SPA_NODE_COMMAND_Suspend: + reset_node(this); this->setup = false; SPA_FALLTHROUGH; case SPA_NODE_COMMAND_Pause: @@ -2354,24 +2589,15 @@ impl_node_add_listener(void *object, void *data) { struct impl *this = object; - uint32_t i; struct spa_hook_list save; - struct port *p; spa_return_val_if_fail(this != NULL, -EINVAL); spa_log_trace(this->log, "%p: add listener %p", this, listener); spa_hook_list_isolate(&this->hooks, &save, listener, events, data); - emit_node_info(this, true); - for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) { - if ((p = GET_IN_PORT(this, i)) && p->valid) - emit_port_info(this, p, true); - } - for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) { - if ((p = GET_OUT_PORT(this, i)) && p->valid) - emit_port_info(this, p, true); - } + emit_info(this, true); + spa_hook_list_join(&this->hooks, &save); return 0; @@ -2397,36 +2623,28 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_ return -ENOTSUP; } -static int port_enum_formats(void *object, - enum spa_direction direction, uint32_t port_id, - uint32_t index, - struct spa_pod **param, - struct spa_pod_builder *builder) +static int port_param_enum_formats(struct impl *impl, struct port *port, uint32_t id, + uint32_t index, struct spa_pod **param, struct spa_pod_builder *b) { - struct impl *this = object; - switch (index) { case 0: - if (PORT_IS_DSP(this, direction, port_id)) { + if (port->is_dsp) { struct spa_audio_info_dsp info; info.format = SPA_AUDIO_FORMAT_DSP_F32; - *param = spa_format_audio_dsp_build(builder, - SPA_PARAM_EnumFormat, &info); - } else if (PORT_IS_CONTROL(this, direction, port_id)) { - *param = spa_pod_builder_add_object(builder, - SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + *param = spa_format_audio_dsp_build(b, id, &info); + } else if (port->is_control) { + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_Format, id, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), - SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int( - (1u<io_position ? - this->io_position->clock.target_rate.denom : DEFAULT_RATE; + uint32_t rate = impl->io_position ? + impl->io_position->clock.target_rate.denom : DEFAULT_RATE; - spa_pod_builder_push_object(builder, &f[0], - SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); - spa_pod_builder_add(builder, + spa_pod_builder_push_object(b, &f[0], + SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(25, @@ -2456,17 +2674,17 @@ static int port_enum_formats(void *object, SPA_AUDIO_FORMAT_ULAW, SPA_AUDIO_FORMAT_ALAW), 0); - if (!this->props.resample_disabled) { - spa_pod_builder_add(builder, + if (!impl->props.resample_disabled) { + spa_pod_builder_add(b, SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int( rate, 1, INT32_MAX), 0); } - spa_pod_builder_add(builder, + spa_pod_builder_add(b, SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int( - DEFAULT_CHANNELS, 1, SPA_AUDIO_MAX_CHANNELS), + DEFAULT_CHANNELS, 1, MAX_CHANNELS), 0); - *param = spa_pod_builder_pop(builder, &f[0]); + *param = spa_pod_builder_pop(b, &f[0]); } break; default: @@ -2475,6 +2693,136 @@ static int port_enum_formats(void *object, return 1; } +static int port_param_format(struct impl *impl, struct port *port, uint32_t id, + uint32_t index, struct spa_pod **param, struct spa_pod_builder *b) +{ + if (!port->have_format) + return -EIO; + if (index > 0) + return 0; + + if (port->is_dsp) + *param = spa_format_audio_dsp_build(b, id, &port->format.info.dsp); + else if (port->is_control) + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_Format, id, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + else + *param = spa_format_audio_raw_build(b, id, &port->format.info.raw); + + return 1; +} + +static int port_param_buffers(struct impl *impl, struct port *port, uint32_t id, + uint32_t index, struct spa_pod **param, struct spa_pod_builder *b) +{ + uint32_t size; + + if (!port->have_format) + return -EIO; + if (index > 0) + return 0; + + size = impl->quantum_limit; + + if (!port->is_dsp) { + uint32_t irate, orate; + struct dir *dir = &impl->dir[port->direction]; + + /* Convert ports are scaled so that they can always + * provide one quantum of data. irate is the rate of the + * data before it goes into the resampler. */ + irate = dir->format.info.raw.rate; + /* scale the size for adaptive resampling */ + size += size/2; + + /* collect the other port rate. This is the output of the resampler + * and is usually one quantum. */ + dir = &impl->dir[SPA_DIRECTION_REVERSE(port->direction)]; + if (dir->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp) + orate = impl->io_position ? impl->io_position->clock.target_rate.denom : DEFAULT_RATE; + else + orate = dir->format.info.raw.rate; + + /* scale the buffer size when we can. Only do this when we downsample because + * then we need to ask more input data for one quantum. */ + if (irate != 0 && orate != 0 && irate > orate) + size = SPA_SCALE32_UP(size, irate, orate); + } + + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(port->blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + size * port->stride, + 16 * port->stride, + INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride)); + + return 1; +} + +static int port_param_meta(struct impl *impl, struct port *port, uint32_t id, + uint32_t index, struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + break; + default: + return 0; + } + return 1; +} +static int port_param_io(struct impl *impl, struct port *port, uint32_t id, + uint32_t index, struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + break; + default: + return 0; + } + return 1; +} + +static int port_param_latency(struct impl *impl, struct port *port, uint32_t id, + uint32_t index, struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0 ... 1: + *param = spa_latency_build(b, id, &port->latency[index]); + break; + default: + return 0; + } + return 1; +} + +static int port_param_tag(struct impl *impl, struct port *port, uint32_t id, + uint32_t index, struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0 ... 1: + if (port->is_monitor) + index = index ^ 1; + *param = impl->dir[index].tag; + break; + default: + return 0; + } + return 1; +} + static int impl_node_port_enum_params(void *object, int seq, enum spa_direction direction, uint32_t port_id, @@ -2507,133 +2855,36 @@ impl_node_port_enum_params(void *object, int seq, spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = NULL; switch (id) { case SPA_PARAM_EnumFormat: - if ((res = port_enum_formats(object, direction, port_id, result.index, ¶m, &b)) <= 0) - return res; + res = port_param_enum_formats(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_Format: - if (!port->have_format) - return -EIO; - if (result.index > 0) - return 0; - - if (PORT_IS_DSP(this, direction, port_id)) - param = spa_format_audio_dsp_build(&b, id, &port->format.info.dsp); - else if (PORT_IS_CONTROL(this, direction, port_id)) - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_Format, id, - SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), - SPA_FORMAT_CONTROL_types, SPA_POD_Int( - (1u<format.info.raw); + res = port_param_format(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_Buffers: - { - uint32_t size; - - if (!port->have_format) - return -EIO; - if (result.index > 0) - return 0; - - size = this->quantum_limit; - - if (!PORT_IS_DSP(this, direction, port_id)) { - uint32_t irate, orate; - struct dir *dir = &this->dir[direction]; - - /* Convert ports are scaled so that they can always - * provide one quantum of data. irate is the rate of the - * data before it goes into the resampler. */ - irate = dir->format.info.raw.rate; - /* scale the size for adaptive resampling */ - size += size/2; - - /* collect the other port rate. This is the output of the resampler - * and is usually one quantum. */ - dir = &this->dir[SPA_DIRECTION_REVERSE(direction)]; - if (dir->mode == SPA_PARAM_PORT_CONFIG_MODE_dsp) - orate = this->io_position ? this->io_position->clock.target_rate.denom : DEFAULT_RATE; - else - orate = dir->format.info.raw.rate; - - /* scale the buffer size when we can. Only do this when we downsample because - * then we need to ask more input data for one quantum. */ - if (irate != 0 && orate != 0 && irate > orate) - size = SPA_SCALE32_UP(size, irate, orate); - } - - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamBuffers, id, - SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS), - SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(port->blocks), - SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( - size * port->stride, - 16 * port->stride, - INT32_MAX), - SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride)); + res = port_param_buffers(this, port, id, result.index, ¶m, &b); break; - } case SPA_PARAM_Meta: - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamMeta, id, - SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), - SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); - break; - default: - return 0; - } + res = port_param_meta(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_IO: - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamIO, id, - SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), - SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); - break; - default: - return 0; - } + res = port_param_io(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_Latency: - switch (result.index) { - case 0: case 1: - { - uint32_t idx = result.index; - param = spa_latency_build(&b, id, &port->latency[idx]); - break; - } - default: - return 0; - } + res = port_param_latency(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_Tag: - switch (result.index) { - case 0: case 1: - { - uint32_t idx = result.index; - if (port->is_monitor) - idx = idx ^ 1; - param = this->dir[idx].tag; - if (param == NULL) - goto next; - break; - } - default: - return 0; - } + res = port_param_tag(this, port, id, result.index, ¶m, &b); break; default: return -ENOENT; } + if (res <= 0) + return res; - if (spa_pod_filter(&b, &result.param, param, filter) < 0) + if (param == NULL || spa_pod_filter(&b, &result.param, param, filter) < 0) goto next; spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); @@ -2646,11 +2897,25 @@ impl_node_port_enum_params(void *object, int seq, static int clear_buffers(struct impl *this, struct port *port) { - if (port->n_buffers > 0) { - spa_log_debug(this->log, "%p: clear buffers %p", this, port); - port->n_buffers = 0; - spa_list_init(&port->queue); + uint32_t i, j; + + spa_log_debug(this->log, "%p: clear buffers %p %d", this, port, port->n_buffers); + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) { + for (j = 0; j < b->buf->n_datas; j++) { + if (b->datas[j]) { + spa_log_debug(this->log, "%p: unmap buffer %d data %d %p", + this, i, j, b->datas[j]); + munmap(b->datas[j], b->buf->datas[j].maxsize); + b->datas[j] = NULL; + } + } + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_MAPPED); + } } + port->n_buffers = 0; + spa_list_init(&port->queue); return 0; } @@ -2664,8 +2929,7 @@ static int port_set_latency(void *object, struct port *port, *oport; enum spa_direction other = SPA_DIRECTION_REVERSE(direction); struct spa_latency_info info; - bool have_latency, emit = false;; - uint32_t i; + bool have_latency;; spa_log_debug(this->log, "%p: set latency direction:%d id:%d %p", this, direction, port_id, latency); @@ -2680,11 +2944,8 @@ static int port_set_latency(void *object, return -EINVAL; have_latency = true; } - emit = spa_latency_info_compare(&info, &port->latency[other]) != 0 || - port->have_latency == have_latency; - port->latency[other] = info; - port->have_latency = have_latency; + port_update_latency(port, &info, have_latency); spa_log_debug(this->log, "%p: set %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this, info.direction == SPA_DIRECTION_INPUT ? "input" : "output", @@ -2700,48 +2961,10 @@ static int port_set_latency(void *object, else return 0; - if (oport != NULL && - spa_latency_info_compare(&info, &oport->latency[other]) != 0) { - oport->latency[other] = info; - oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; - oport->params[IDX_Latency].user++; - emit_port_info(this, oport, false); - } - } else { - spa_latency_info_combine_start(&info, other); - for (i = 0; i < this->dir[direction].n_ports; i++) { - oport = GET_PORT(this, direction, i); - if ((oport->is_monitor) || !oport->have_latency) - continue; - spa_log_debug(this->log, "%p: combine %d", this, i); - spa_latency_info_combine(&info, &oport->latency[other]); - } - spa_latency_info_combine_finish(&info); - - spa_log_debug(this->log, "%p: combined %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, this, - info.direction == SPA_DIRECTION_INPUT ? "input" : "output", - info.min_quantum, info.max_quantum, - info.min_rate, info.max_rate, - info.min_ns, info.max_ns); - - for (i = 0; i < this->dir[other].n_ports; i++) { - oport = GET_PORT(this, other, i); - if (oport->is_monitor) - continue; - spa_log_debug(this->log, "%p: change %d", this, i); - if (spa_latency_info_compare(&info, &oport->latency[other]) != 0) { - oport->latency[other] = info; - oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; - oport->params[IDX_Latency].user++; - emit_port_info(this, oport, false); - } - } - } - if (emit) { - port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; - port->params[IDX_Latency].user++; - emit_port_info(this, port, false); + if (oport != NULL) + port_update_latency(oport, &info, have_latency); } + recalc_latencies(this, direction); return 0; } @@ -2778,12 +3001,10 @@ static int port_set_tag(void *object, oport = GET_PORT(this, other, i); oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; oport->params[IDX_Tag].user++; - emit_port_info(this, oport, false); } } port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; port->params[IDX_Tag].user++; - emit_port_info(this, port, false); return 0; } @@ -2854,7 +3075,7 @@ static int port_set_format(void *object, if (info.info.raw.format == 0 || (!this->props.resample_disabled && info.info.raw.rate == 0) || info.info.raw.channels == 0 || - info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) { + info.info.raw.channels > MAX_CHANNELS) { spa_log_error(this->log, "invalid format:%d rate:%d channels:%d", info.info.raw.format, info.info.raw.rate, info.info.raw.channels); @@ -2886,12 +3107,9 @@ static int port_set_format(void *object, port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); } - emit_port_info(this, port, false); - return 0; } - static int impl_node_port_set_param(void *object, enum spa_direction direction, uint32_t port_id, @@ -2899,6 +3117,7 @@ impl_node_port_set_param(void *object, const struct spa_pod *param) { struct impl *this = object; + int res = 0; spa_return_val_if_fail(this != NULL, -EINVAL); @@ -2909,14 +3128,19 @@ impl_node_port_set_param(void *object, switch (id) { case SPA_PARAM_Latency: - return port_set_latency(this, direction, port_id, flags, param); + res = port_set_latency(this, direction, port_id, flags, param); + break; case SPA_PARAM_Tag: - return port_set_tag(this, direction, port_id, flags, param); + res = port_set_tag(this, direction, port_id, flags, param); + break; case SPA_PARAM_Format: - return port_set_format(this, direction, port_id, flags, param); + res = port_set_format(this, direction, port_id, flags, param); + break; default: return -ENOENT; } + emit_info(this, false); + return res; } static inline void queue_buffer(struct impl *this, struct port *port, uint32_t id) @@ -2966,6 +3190,7 @@ impl_node_port_use_buffers(void *object, struct impl *this = object; struct port *port; uint32_t i, j, maxsize; + int res; spa_return_val_if_fail(this != NULL, -EINVAL); @@ -2976,12 +3201,16 @@ impl_node_port_use_buffers(void *object, spa_log_debug(this->log, "%p: use buffers %d on port %d:%d", this, n_buffers, direction, port_id); - clear_buffers(this, port); + if (n_buffers > 0 && !port->have_format) { + res = -EIO; + goto error; + } + if (n_buffers > MAX_BUFFERS) { + res = -ENOSPC; + goto error; + } - if (n_buffers > 0 && !port->have_format) - return -EIO; - if (n_buffers > MAX_BUFFERS) - return -ENOSPC; + clear_buffers(this, port); maxsize = this->quantum_limit * sizeof(float); @@ -2990,6 +3219,11 @@ impl_node_port_use_buffers(void *object, uint32_t n_datas = buffers[i]->n_datas; struct spa_data *d = buffers[i]->datas; + if (n_datas > MAX_DATAS) { + res = -ENOSPC; + goto error; + } + b = &port->buffers[i]; b->id = i; b->flags = 0; @@ -3002,30 +3236,49 @@ impl_node_port_use_buffers(void *object, } for (j = 0; j < n_datas; j++) { - if (d[j].data == NULL) { - spa_log_error(this->log, "%p: invalid memory %d on buffer %d %d %p", - this, j, i, d[j].type, d[j].data); - return -EINVAL; + void *data = d[j].data; + if (data == NULL && SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_MAPPABLE)) { + int prot = 0; + if (SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_READABLE)) + prot |= PROT_READ; + if (SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_WRITABLE)) + prot |= PROT_WRITE; + data = mmap(NULL, d[j].maxsize, + prot, MAP_SHARED, d[j].fd, d[j].mapoffset); + if (data == MAP_FAILED) { + spa_log_error(this->log, "%p: mmap failed %d on buffer %d %d %p: %m", + this, j, i, d[j].type, data); + res = -EINVAL; + goto error; + } + SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); + spa_log_debug(this->log, "%p: mmap %d on buffer %d %d %p %p", + this, j, i, d[j].type, data, b); } - if (!SPA_IS_ALIGNED(d[j].data, this->max_align)) { + if (data == NULL) { + spa_log_error(this->log, "%p: invalid memory %d on buffer %d %d %p", + this, j, i, d[j].type, data); + res = -EINVAL; + goto error; + } else if (!SPA_IS_ALIGNED(data, this->max_align)) { spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned", this, j, i); } - if (direction == SPA_DIRECTION_OUTPUT && - !SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_DYNAMIC)) - this->is_passthrough = false; - b->datas[j] = d[j].data; + b->datas[j] = data; maxsize = SPA_MAX(maxsize, d[j].maxsize); } if (direction == SPA_DIRECTION_OUTPUT) queue_buffer(this, port, i); + port->n_buffers++; } port->maxsize = maxsize; - port->n_buffers = n_buffers; return 0; +error: + clear_buffers(this, port); + return res; } struct io_data { @@ -3063,7 +3316,7 @@ impl_node_port_set_io(void *object, case SPA_IO_Buffers: if (this->data_loop) { struct io_data d = { .port = port, .data = data, .size = size }; - spa_loop_invoke(this->data_loop, do_set_port_io, 0, NULL, 0, true, &d); + spa_loop_locked(this->data_loop, do_set_port_io, 0, NULL, 0, &d); } else port->io = data; @@ -3182,6 +3435,16 @@ static uint64_t get_time_ns(struct impl *impl) return SPA_TIMESPEC_TO_NSEC(&now); } +static uint32_t get_dst_idx(struct stage_context *ctx) +{ + uint32_t res; + if (ctx->bits == 0) + res = ctx->final_idx; + else + res = CTX_DATA_TMP_0 + ((ctx->tmp++) & 1); + return res; +} + static void run_wav_stage(struct stage *stage, struct stage_context *c) { struct impl *impl = stage->impl; @@ -3214,11 +3477,8 @@ static void add_wav_stage(struct impl *impl, struct stage_context *ctx) { struct stage *s = &impl->stages[impl->n_stages]; s->impl = impl; - s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = ctx->src_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_wav_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3230,7 +3490,7 @@ static void run_dst_remap_stage(struct stage *s, struct stage_context *c) struct impl *impl = s->impl; struct dir *dir = &impl->dir[SPA_DIRECTION_OUTPUT]; uint32_t i; - for (i = 0; i < s->n_in; i++) { + for (i = 0; i < dir->conv.n_channels; i++) { c->datas[s->out_idx][i] = c->datas[s->in_idx][dir->remap[i]]; spa_log_trace_fp(impl->log, "%p: output remap %d -> %d", impl, i, dir->remap[i]); } @@ -3239,11 +3499,8 @@ static void add_dst_remap_stage(struct impl *impl, struct stage_context *ctx) { struct stage *s = &impl->stages[impl->n_stages]; s->impl = impl; - s->passthrough = false; s->in_idx = ctx->dst_idx; s->out_idx = CTX_DATA_REMAP_DST; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_dst_remap_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3266,11 +3523,8 @@ static void add_src_remap_stage(struct impl *impl, struct stage_context *ctx) { struct stage *s = &impl->stages[impl->n_stages]; s->impl = impl; - s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = CTX_DATA_REMAP_SRC; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_src_remap_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3295,22 +3549,23 @@ static void run_src_convert_stage(struct stage *s, struct stage_context *c) } else { dst = c->datas[s->out_idx]; } - convert_process(&dir->conv, dst, (const void**)c->datas[s->in_idx], c->n_samples); + if (c->empty && dir->conv.clear) + convert_clear(&dir->conv, dst, c->n_samples); + else + convert_process(&dir->conv, dst, (const void**)c->datas[s->in_idx], c->n_samples); } static void add_src_convert_stage(struct impl *impl, struct stage_context *ctx) { struct stage *s = &impl->stages[impl->n_stages]; + SPA_FLAG_CLEAR(ctx->bits, SRC_CONVERT_BIT); s->impl = impl; - s->passthrough = false; s->in_idx = ctx->src_idx; - s->out_idx = ctx->dst_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; + s->out_idx = get_dst_idx(ctx); s->data = NULL; s->run = run_src_convert_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); impl->n_stages++; - ctx->src_idx = ctx->dst_idx; + ctx->src_idx = s->out_idx; } static void run_resample_stage(struct stage *s, struct stage_context *c) @@ -3330,17 +3585,36 @@ static void run_resample_stage(struct stage *s, struct stage_context *c) static void add_resample_stage(struct impl *impl, struct stage_context *ctx) { struct stage *s = &impl->stages[impl->n_stages]; + SPA_FLAG_CLEAR(ctx->bits, RESAMPLE_BIT); s->impl = impl; - s->passthrough = false; s->in_idx = ctx->src_idx; - s->out_idx = ctx->dst_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; + s->out_idx = get_dst_idx(ctx); s->data = NULL; s->run = run_resample_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); impl->n_stages++; - ctx->src_idx = ctx->dst_idx; + ctx->src_idx = s->out_idx; +} + +static void run_filter_stage(struct stage *s, struct stage_context *c) +{ + struct filter_graph *fg = s->data; + + spa_log_trace_fp(s->impl->log, "%p: filter-graph %d", s->impl, c->n_samples); + spa_filter_graph_process(fg->graph, (const void **)c->datas[s->in_idx], + c->datas[s->out_idx], c->n_samples); +} +static void add_filter_stage(struct impl *impl, uint32_t i, struct filter_graph *fg, struct stage_context *ctx) +{ + struct stage *s = &impl->stages[impl->n_stages]; + s->impl = impl; + s->in_idx = ctx->src_idx; + s->out_idx = get_dst_idx(ctx); + s->data = fg; + s->run = run_filter_stage; + spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); + impl->n_stages++; + ctx->src_idx = s->out_idx; } static void run_channelmix_stage(struct stage *s, struct stage_context *c) @@ -3360,7 +3634,8 @@ static void run_channelmix_stage(struct stage *s, struct stage_context *c) } else if (impl->vol_ramp_sequence) { if (channelmix_process_apply_sequence(impl, impl->vol_ramp_sequence, &impl->vol_ramp_offset, out_datas, in_datas, c->n_samples) == 1) { - free(impl->vol_ramp_sequence); + free(impl->vol_ramp_sequence_data); + impl->vol_ramp_sequence_data = NULL; impl->vol_ramp_sequence = NULL; } } else { @@ -3368,44 +3643,18 @@ static void run_channelmix_stage(struct stage *s, struct stage_context *c) } } -static void run_filter_stage(struct stage *s, struct stage_context *c) -{ - struct filter_graph *fg = s->data; - - spa_log_trace_fp(s->impl->log, "%p: filter-graph %d", s->impl, c->n_samples); - spa_filter_graph_process(fg->graph, (const void **)c->datas[s->in_idx], - c->datas[s->out_idx], c->n_samples); -} -static void add_filter_stage(struct impl *impl, uint32_t i, struct filter_graph *fg, struct stage_context *ctx) -{ - struct stage *s = &impl->stages[impl->n_stages]; - s->impl = impl; - s->passthrough = false; - s->in_idx = ctx->src_idx; - s->out_idx = ctx->dst_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; - s->data = fg; - s->run = run_filter_stage; - spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); - impl->n_stages++; - ctx->src_idx = ctx->dst_idx; -} - static void add_channelmix_stage(struct impl *impl, struct stage_context *ctx) { struct stage *s = &impl->stages[impl->n_stages]; + SPA_FLAG_CLEAR(ctx->bits, MIX_BIT); s->impl = impl; - s->passthrough = false; s->in_idx = ctx->src_idx; - s->out_idx = ctx->dst_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; + s->out_idx = get_dst_idx(ctx); s->data = NULL; s->run = run_channelmix_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); impl->n_stages++; - ctx->src_idx = ctx->dst_idx; + ctx->src_idx = s->out_idx; } static void run_dst_convert_stage(struct stage *s, struct stage_context *c) @@ -3425,17 +3674,17 @@ static void run_dst_convert_stage(struct stage *s, struct stage_context *c) } else { src = c->datas[s->in_idx]; } - convert_process(&dir->conv, c->datas[s->out_idx], (const void **)src, c->n_samples); + if (c->empty && dir->conv.clear) + convert_clear(&dir->conv, c->datas[s->out_idx], c->n_samples); + else + convert_process(&dir->conv, c->datas[s->out_idx], (const void **)src, c->n_samples); } static void add_dst_convert_stage(struct impl *impl, struct stage_context *ctx) { struct stage *s = &impl->stages[impl->n_stages]; s->impl = impl; - s->passthrough = false; s->in_idx = ctx->src_idx; s->out_idx = ctx->final_idx; - s->n_in = ctx->n_datas; - s->n_out = ctx->n_datas; s->data = NULL; s->run = run_dst_convert_stage; spa_log_trace(impl->log, "%p: stage %d", impl, impl->n_stages); @@ -3446,8 +3695,7 @@ static void add_dst_convert_stage(struct impl *impl, struct stage_context *ctx) static void recalc_stages(struct impl *this, struct stage_context *ctx) { struct dir *dir; - bool filter_passthrough, in_passthrough, mix_passthrough, resample_passthrough, out_passthrough; - int tmp = 0; + bool test, do_wav; struct port *ctrlport = ctx->ctrlport; bool in_need_remap, out_need_remap; uint32_t i; @@ -3455,36 +3703,44 @@ static void recalc_stages(struct impl *this, struct stage_context *ctx) this->recalc = false; this->n_stages = 0; + ctx->tmp = 0; + ctx->bits = 0; + ctx->src_idx = CTX_DATA_SRC; + ctx->dst_idx = CTX_DATA_DST; + ctx->final_idx = CTX_DATA_DST; + + /* set bits for things we need to do */ dir = &this->dir[SPA_DIRECTION_INPUT]; - in_passthrough = dir->conv.is_passthrough; + SPA_FLAG_UPDATE(ctx->bits, SRC_CONVERT_BIT, !dir->conv.is_passthrough); in_need_remap = dir->need_remap; dir = &this->dir[SPA_DIRECTION_OUTPUT]; - out_passthrough = dir->conv.is_passthrough; + SPA_FLAG_UPDATE(ctx->bits, DST_CONVERT_BIT, !dir->conv.is_passthrough); out_need_remap = dir->need_remap; - resample_passthrough = resample_is_passthrough(this); - filter_passthrough = this->n_graph == 0; - this->resample_passthrough = resample_passthrough; - mix_passthrough = SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY) && + this->resample_passthrough = resample_is_passthrough(this); + SPA_FLAG_UPDATE(ctx->bits, RESAMPLE_BIT, !this->resample_passthrough); + + SPA_FLAG_UPDATE(ctx->bits, FILTER_BIT, this->n_graph != 0); + + test = SPA_FLAG_IS_SET(this->mix.flags, CHANNELMIX_FLAG_IDENTITY) && (ctrlport == NULL || ctrlport->ctrl == NULL) && (this->vol_ramp_sequence == NULL); + SPA_FLAG_UPDATE(ctx->bits, MIX_BIT, !test); - if (in_passthrough && filter_passthrough && mix_passthrough && resample_passthrough) - out_passthrough = false; + /* if we have nothing to do, force a conversion to the destination to make sure we + * actually write something to the destination buffer */ + if (ctx->bits == 0) + SPA_FLAG_SET(ctx->bits, DST_CONVERT_BIT); - if (out_passthrough && out_need_remap) + do_wav = this->props.wav_path[0] || this->wav_file != NULL; + + if (!SPA_FLAG_IS_SET(ctx->bits, DST_CONVERT_BIT) && out_need_remap) add_dst_remap_stage(this, ctx); - if (this->direction == SPA_DIRECTION_INPUT && - (this->props.wav_path[0] || this->wav_file != NULL)) + if (this->direction == SPA_DIRECTION_INPUT && do_wav) add_wav_stage(this, ctx); - if (!in_passthrough) { - if (filter_passthrough && mix_passthrough && resample_passthrough && out_passthrough) - ctx->dst_idx = ctx->final_idx; - else - ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); - + if (SPA_FLAG_IS_SET(ctx->bits, SRC_CONVERT_BIT)) { add_src_convert_stage(this, ctx); } else { if (in_need_remap) @@ -3492,62 +3748,41 @@ static void recalc_stages(struct impl *this, struct stage_context *ctx) } if (this->direction == SPA_DIRECTION_INPUT) { - if (!resample_passthrough) { - if (filter_passthrough && mix_passthrough && out_passthrough) - ctx->dst_idx = ctx->final_idx; - else - ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); - + if (SPA_FLAG_IS_SET(ctx->bits, RESAMPLE_BIT)) add_resample_stage(this, ctx); - resample_passthrough = true; - } } - if (!filter_passthrough) { + if (SPA_FLAG_IS_SET(ctx->bits, FILTER_BIT)) { for (i = 0; i < this->n_graph; i++) { - struct filter_graph *fg = &this->filter_graph[this->graph_index[i]]; + struct filter_graph *fg = this->filter_graph[i]; - if (mix_passthrough && resample_passthrough && out_passthrough && - i + 1 == this->n_graph) - ctx->dst_idx = ctx->final_idx; - else - ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); + if (i + 1 == this->n_graph) + SPA_FLAG_CLEAR(ctx->bits, FILTER_BIT); add_filter_stage(this, i, fg, ctx); } } - if (!mix_passthrough) { - if (resample_passthrough && out_passthrough) - ctx->dst_idx = ctx->final_idx; - else - ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); - + if (SPA_FLAG_IS_SET(ctx->bits, MIX_BIT)) add_channelmix_stage(this, ctx); - } - if (this->direction == SPA_DIRECTION_OUTPUT) { - if (!resample_passthrough) { - if (out_passthrough) - ctx->dst_idx = ctx->final_idx; - else - ctx->dst_idx = CTX_DATA_TMP_0 + ((tmp++) & 1); + if (this->direction == SPA_DIRECTION_OUTPUT) { + if (SPA_FLAG_IS_SET(ctx->bits, RESAMPLE_BIT)) add_resample_stage(this, ctx); - } } - if (!out_passthrough) { + + if (SPA_FLAG_IS_SET(ctx->bits, DST_CONVERT_BIT)) add_dst_convert_stage(this, ctx); - } - if (this->direction == SPA_DIRECTION_OUTPUT && - (this->props.wav_path[0] || this->wav_file != NULL)) + + if (this->direction == SPA_DIRECTION_OUTPUT && do_wav) add_wav_stage(this, ctx); - spa_log_trace(this->log, "got %u processing stages", this->n_stages); + spa_log_debug(this->log, "got %u processing stages", this->n_stages); } static int impl_node_process(void *object) { struct impl *this = object; const void *src_datas[MAX_PORTS]; - void *dst_datas[MAX_PORTS], *remap_src_datas[MAX_PORTS], *remap_dst_datas[MAX_PORTS]; + void *dst_datas[MAX_PORTS], *remap_src_datas[MAX_PORTS], *remap_dst_datas[MAX_PORTS], *data; uint32_t i, j, n_src_datas = 0, n_dst_datas = 0, n_mon_datas = 0, remap; uint32_t n_samples, max_in, n_out, max_out, quant_samples; struct port *port, *ctrlport = NULL; @@ -3556,7 +3791,7 @@ static int impl_node_process(void *object) struct dir *dir; int res = 0, suppressed; bool in_avail = false, flush_in = false, flush_out = false; - bool draining = false, in_empty = this->out_offset == 0; + bool draining = false, in_empty = this->out_offset == 0, out_empty; struct spa_io_buffers *io; const struct spa_pod_sequence *ctrl = NULL; uint64_t current_time; @@ -3610,6 +3845,7 @@ static int impl_node_process(void *object) if (io->status & SPA_STATUS_DRAINED) { spa_log_debug(this->log, "%p: port %d drained", this, port->id); in_avail = flush_in = draining = true; + in_empty = false; } else { spa_log_trace_fp(this->log, "%p: empty input port %d %p %d %d %d", this, port->id, io, io->status, io->buffer_id, @@ -3649,6 +3885,7 @@ static int impl_node_process(void *object) uint32_t offs, size; bd = &buf->buf->datas[j]; + data = bd->data ? bd->data : buf->datas[j]; offs = SPA_MIN(bd->chunk->offset, bd->maxsize); size = SPA_MIN(bd->maxsize - offs, bd->chunk->size); @@ -3659,7 +3896,7 @@ static int impl_node_process(void *object) spa_log_trace_fp(this->log, "%p: control %d", this, i * port->blocks + j); ctrlport = port; - ctrl = spa_pod_from_data(bd->data, bd->maxsize, + ctrl = spa_pod_from_data(data, bd->maxsize, bd->chunk->offset, bd->chunk->size); if (ctrl && !spa_pod_is_sequence(&ctrl->pod)) ctrl = NULL; @@ -3673,7 +3910,7 @@ static int impl_node_process(void *object) remap = n_src_datas++; offs += this->in_offset * port->stride; - src_datas[remap] = SPA_PTROFF(bd->data, offs, void); + src_datas[remap] = SPA_PTROFF(data, offs, void); spa_log_trace_fp(this->log, "%p: input %d:%d:%d %d %d %d->%d", this, offs, size, port->stride, this->in_offset, max_in, @@ -3748,6 +3985,7 @@ static int impl_node_process(void *object) } else { for (j = 0; j < port->blocks; j++) { bd = &buf->buf->datas[j]; + data = bd->data ? bd->data : buf->datas[j]; bd->chunk->offset = 0; bd->chunk->size = 0; @@ -3767,7 +4005,7 @@ static int impl_node_process(void *object) mon_max = SPA_MIN(bd->maxsize / port->stride, max_in); - volume_process(&this->volume, bd->data, src_datas[remap], + volume_process(&this->volume, data, src_datas[remap], volume, mon_max); bd->chunk->size = mon_max * port->stride; @@ -3784,7 +4022,7 @@ static int impl_node_process(void *object) spa_log_trace_fp(this->log, "%p: control %d", this, j); } else { remap = n_dst_datas++; - dst_datas[remap] = SPA_PTROFF(bd->data, + dst_datas[remap] = SPA_PTROFF(data, this->out_offset * port->stride, void); max_out = SPA_MIN(max_out, bd->maxsize / port->stride); @@ -3828,13 +4066,10 @@ static int impl_node_process(void *object) ctx.in_samples = n_samples; ctx.n_samples = n_samples; ctx.n_out = n_out; - ctx.src_idx = CTX_DATA_SRC; - ctx.dst_idx = CTX_DATA_DST; - ctx.final_idx = CTX_DATA_DST; - ctx.n_datas = dir->conv.n_channels; ctx.ctrlport = ctrlport; + ctx.empty = in_empty; - if (this->recalc) + if (SPA_UNLIKELY(this->recalc)) recalc_stages(this, &ctx); for (i = 0; i < this->n_stages; i++) { @@ -3843,6 +4078,7 @@ static int impl_node_process(void *object) } this->in_offset += ctx.in_samples; this->out_offset += ctx.n_samples; + out_empty = ctx.empty; spa_log_trace_fp(this->log, "%d/%d %d/%d %d->%d", this->in_offset, max_in, this->out_offset, max_out, n_samples, n_out); @@ -3884,7 +4120,7 @@ static int impl_node_process(void *object) bd = &buf->buf->datas[j]; bd->chunk->size = this->out_offset * port->stride; bd->chunk->stride = port->stride; - SPA_FLAG_UPDATE(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY, in_empty); + SPA_FLAG_UPDATE(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY, out_empty); spa_log_trace_fp(this->log, "out: offs:%d stride:%d size:%d", this->out_offset, port->stride, bd->chunk->size); } @@ -3991,7 +4227,7 @@ static int impl_clear(struct spa_handle *handle) resample_free(&this->resample); if (this->wav_file != NULL) wav_file_close(this->wav_file); - free (this->vol_ramp_sequence); + free (this->vol_ramp_sequence_data); return 0; } @@ -4011,8 +4247,7 @@ impl_init(const struct spa_handle_factory *factory, { struct impl *this; uint32_t i; - const char *str; - bool filter_graph_disabled; + bool filter_graph_disabled = false; spa_return_val_if_fail(factory != NULL, -EINVAL); spa_return_val_if_fail(handle != NULL, -EINVAL); @@ -4035,6 +4270,13 @@ impl_init(const struct spa_handle_factory *factory, props_reset(&this->props); filter_graph_disabled = this->props.filter_graph_disabled; + spa_list_init(&this->active_graphs); + spa_list_init(&this->free_graphs); + for (i = 0; i < MAX_GRAPH; i++) { + struct filter_graph *g = &this->graphs[i]; + g->impl = this; + spa_list_append(&this->free_graphs, &g->link); + } this->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; this->rate_limit.burst = 1; @@ -4047,13 +4289,12 @@ impl_init(const struct spa_handle_factory *factory, this->mix.rear_delay = 0.0f; this->mix.widen = 0.0f; - if (info && (str = spa_dict_lookup(info, "clock.quantum-limit")) != NULL) - spa_atou32(str, &this->quantum_limit, 0); - for (i = 0; info && i < info->n_items; i++) { const char *k = info->items[i].key; const char *s = info->items[i].value; - if (spa_streq(k, "resample.peaks")) + if (spa_streq(k, "clock.quantum-limit")) + spa_atou32(s, &this->quantum_limit, 0); + else if (spa_streq(k, "resample.peaks")) this->resample_peaks = spa_atob(s); else if (spa_streq(k, "resample.prefill")) SPA_FLAG_UPDATE(this->resample.options, @@ -4065,9 +4306,18 @@ impl_init(const struct spa_handle_factory *factory, this->direction = SPA_DIRECTION_INPUT; } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { - if (s != NULL) - spa_audio_parse_position(s, strlen(s), this->props.channel_map, - &this->props.n_channels); + if (s == NULL) + continue; + spa_audio_parse_position_n(s, strlen(s), + this->props.channel_map, SPA_N_ELEMENTS(this->props.channel_map), + &this->props.n_channels); + } + else if (spa_streq(k, SPA_KEY_AUDIO_LAYOUT)) { + if (s == NULL) + continue; + spa_audio_parse_layout(s, + this->props.channel_map, SPA_N_ELEMENTS(this->props.channel_map), + &this->props.n_channels); } else if (spa_streq(k, SPA_KEY_PORT_IGNORE_LATENCY)) this->port_ignore_latency = spa_atob(s); @@ -4075,12 +4325,7 @@ impl_init(const struct spa_handle_factory *factory, spa_scnprintf(this->group_name, sizeof(this->group_name), "%s", s); else if (spa_streq(k, "monitor.passthrough")) this->monitor_passthrough = spa_atob(s); - else if (spa_streq(k, "audioconvert.filter-graph.disable")) - filter_graph_disabled = spa_atob(s); - else - audioconvert_set_param(this, k, s); } - this->props.filter_graph_disabled = filter_graph_disabled; this->props.channel.n_volumes = this->props.n_channels; this->props.soft.n_volumes = this->props.n_channels; this->props.monitor.n_volumes = this->props.n_channels; @@ -4118,6 +4363,14 @@ impl_init(const struct spa_handle_factory *factory, reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_INPUT, false, false, NULL); reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, SPA_DIRECTION_OUTPUT, false, false, NULL); + filter_graph_disabled = this->props.filter_graph_disabled; + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + audioconvert_set_param(this, k, s, &filter_graph_disabled); + } + this->props.filter_graph_disabled = filter_graph_disabled; + return 0; } diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c index d774112bc..ce67042e1 100644 --- a/spa/plugins/audioconvert/channelmix-ops.c +++ b/spa/plugins/audioconvert/channelmix-ops.c @@ -129,6 +129,7 @@ static const struct channelmix_info *find_channelmix_info(uint32_t src_chan, uin #define STEREO (_MASK(FL)|_MASK(FR)) #define REAR (_MASK(RL)|_MASK(RR)) #define SIDE (_MASK(SL)|_MASK(SR)) +#define CHANNEL_BITS (64u) static uint32_t mask_to_ch(struct channelmix *mix, uint64_t mask) { @@ -141,34 +142,34 @@ static uint32_t mask_to_ch(struct channelmix *mix, uint64_t mask) } static void distribute_mix(struct channelmix *mix, - float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS], + float matrix[MAX_CHANNELS][MAX_CHANNELS], uint64_t mask) { uint32_t i, ch = mask_to_ch(mix, mask); - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + for (i = 0; i < MAX_CHANNELS; i++) matrix[i][ch]= 1.0f; } static void average_mix(struct channelmix *mix, - float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS], + float matrix[MAX_CHANNELS][MAX_CHANNELS], uint64_t mask) { uint32_t i, ch = mask_to_ch(mix, mask); - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + for (i = 0; i < MAX_CHANNELS; i++) matrix[ch][i]= 1.0f; } -static void pair_mix(float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS]) +static void pair_mix(float matrix[MAX_CHANNELS][MAX_CHANNELS]) { uint32_t i; - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + for (i = 0; i < MAX_CHANNELS; i++) matrix[i][i]= 1.0f; } static bool match_mix(struct channelmix *mix, - float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS], + float matrix[MAX_CHANNELS][MAX_CHANNELS], uint64_t src_mask, uint64_t dst_mask) { bool matched = false; uint32_t i; - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + for (i = 0; i < CHANNEL_BITS; i++) { if ((src_mask & dst_mask & (1ULL << i))) { spa_log_info(mix->log, "matched channel %u (%f)", i, 1.0f); matrix[i][i] = 1.0f; @@ -180,7 +181,7 @@ static bool match_mix(struct channelmix *mix, static int make_matrix(struct channelmix *mix) { - float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS] = {{ 0.0f }}; + float matrix[MAX_CHANNELS][MAX_CHANNELS] = {{ 0.0f }}; uint64_t src_mask = mix->src_mask, src_paired; uint64_t dst_mask = mix->dst_mask, dst_paired; uint32_t src_chan = mix->src_chan; @@ -292,7 +293,7 @@ static int make_matrix(struct channelmix *mix) keep &= ~STEREO; } else if (dst_mask & _MASK(MONO)){ spa_log_info(mix->log, "assign FC to MONO (%f)", 1.0f); - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + for (i = 0; i < MAX_CHANNELS; i++) matrix[i][_CH(FC)]= 1.0f; normalize = true; } else { @@ -312,7 +313,7 @@ static int make_matrix(struct channelmix *mix) keep &= ~FRONT; } else if ((dst_mask & _MASK(MONO))){ spa_log_info(mix->log, "assign STEREO to MONO (%f)", 1.0f); - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + for (i = 0; i < MAX_CHANNELS; i++) { matrix[i][_CH(FL)]= 1.0f; matrix[i][_CH(FR)]= 1.0f; } @@ -351,7 +352,7 @@ static int make_matrix(struct channelmix *mix) _MATRIX(FC,RC) += slev * SQRT1_2; } else if (dst_mask & _MASK(MONO)){ spa_log_info(mix->log, "assign RC to MONO (%f)", 1.0f); - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + for (i = 0; i < MAX_CHANNELS; i++) matrix[i][_CH(RC)]= 1.0f; normalize = true; } else { @@ -397,7 +398,7 @@ static int make_matrix(struct channelmix *mix) _MATRIX(FC,RR)+= slev * SQRT1_2; } else if (dst_mask & _MASK(MONO)){ spa_log_info(mix->log, "assign RL+RR to MONO (%f)", 1.0f); - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + for (i = 0; i < MAX_CHANNELS; i++) { matrix[i][_CH(RL)]= 1.0f; matrix[i][_CH(RR)]= 1.0f; } @@ -449,7 +450,7 @@ static int make_matrix(struct channelmix *mix) _MATRIX(FC,SR) += slev * SQRT1_2; } else if (dst_mask & _MASK(MONO)){ spa_log_info(mix->log, "assign SL+SR to MONO (%f)", 1.0f); - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + for (i = 0; i < MAX_CHANNELS; i++) { matrix[i][_CH(SL)]= 1.0f; matrix[i][_CH(SR)]= 1.0f; } @@ -470,7 +471,7 @@ static int make_matrix(struct channelmix *mix) _MATRIX(FC,FRC)+= SQRT1_2; } else if (dst_mask & _MASK(MONO)){ spa_log_info(mix->log, "assign FLC+FRC to MONO (%f)", 1.0f); - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + for (i = 0; i < MAX_CHANNELS; i++) { matrix[i][_CH(FLC)]= 1.0f; matrix[i][_CH(FRC)]= 1.0f; } @@ -491,7 +492,7 @@ static int make_matrix(struct channelmix *mix) _MATRIX(FR,LFE) += llev * SQRT1_2; } else if ((dst_mask & _MASK(MONO))){ spa_log_info(mix->log, "assign LFE to MONO (%f)", 1.0f); - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) + for (i = 0; i < MAX_CHANNELS; i++) matrix[i][_CH(LFE)]= 1.0f; normalize = true; } else { @@ -627,7 +628,7 @@ done: if (src_paired == 0) src_paired = ~0LU; - for (jc = 0, ic = 0, i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + for (jc = 0, ic = 0, i = 0; i < CHANNEL_BITS; i++) { float sum = 0.0f; char str1[1024], str2[1024]; struct spa_strbuf sb1, sb2; @@ -637,7 +638,7 @@ done: if ((dst_paired & (1UL << i)) == 0) continue; - for (jc = 0, j = 0; j < SPA_AUDIO_MAX_CHANNELS; j++) { + for (jc = 0, j = 0; j < CHANNEL_BITS; j++) { if ((src_paired & (1UL << j)) == 0) continue; if (ic >= dst_chan || jc >= src_chan) @@ -689,7 +690,7 @@ done: static void impl_channelmix_set_volume(struct channelmix *mix, float volume, bool mute, uint32_t n_channel_volumes, float *channel_volumes) { - float volumes[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; float vol = mute ? 0.0f : volume, t; uint32_t i, j; uint32_t src_chan = mix->src_chan; @@ -759,8 +760,8 @@ int channelmix_init(struct channelmix *mix) { const struct channelmix_info *info; - if (mix->src_chan > SPA_AUDIO_MAX_CHANNELS || - mix->dst_chan > SPA_AUDIO_MAX_CHANNELS) + if (mix->src_chan > MAX_CHANNELS || + mix->dst_chan > MAX_CHANNELS) return -EINVAL; info = find_channelmix_info(mix->src_chan, mix->src_mask, mix->dst_chan, mix->dst_mask, diff --git a/spa/plugins/audioconvert/channelmix-ops.h b/spa/plugins/audioconvert/channelmix-ops.h index 26e2efc3a..6ea2b9451 100644 --- a/spa/plugins/audioconvert/channelmix-ops.h +++ b/spa/plugins/audioconvert/channelmix-ops.h @@ -24,6 +24,7 @@ #define BUFFER_SIZE 4096 #define MAX_TAPS 255u +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define CHANNELMIX_OPS_MAX_ALIGN 16 @@ -50,8 +51,8 @@ struct channelmix { #define CHANNELMIX_FLAG_EQUAL (1<<2) /**< all values are equal */ #define CHANNELMIX_FLAG_COPY (1<<3) /**< 1 on diagonal, can be nxm */ uint32_t flags; - float matrix_orig[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS]; - float matrix[SPA_AUDIO_MAX_CHANNELS][SPA_AUDIO_MAX_CHANNELS]; + float matrix_orig[MAX_CHANNELS][MAX_CHANNELS]; + float matrix[MAX_CHANNELS][MAX_CHANNELS]; float freq; /* sample frequency */ float lfe_cutoff; /* in Hz, 0 is disabled */ @@ -59,7 +60,7 @@ struct channelmix { float rear_delay; /* in ms, 0 is disabled */ float widen; /* stereo widen. 0 is disabled */ uint32_t hilbert_taps; /* to phase shift, 0 disabled */ - struct lr4 lr4[SPA_AUDIO_MAX_CHANNELS]; + struct lr4 lr4[MAX_CHANNELS]; float buffer_mem[2 * BUFFER_SIZE*2 + CHANNELMIX_OPS_MAX_ALIGN/4]; float *buffer[2]; diff --git a/spa/plugins/audioconvert/fmt-ops-c.c b/spa/plugins/audioconvert/fmt-ops-c.c index e92b5bf31..800951ce0 100644 --- a/spa/plugins/audioconvert/fmt-ops-c.c +++ b/spa/plugins/audioconvert/fmt-ops-c.c @@ -411,3 +411,53 @@ MAKE_INTERLEAVE(24, 24, uint24_t, (uint24_t)); MAKE_INTERLEAVE(32, 32, uint32_t, (uint32_t)); MAKE_INTERLEAVE(32, 32s, uint32_t, bswap_32); MAKE_INTERLEAVE(64, 64, uint64_t, (uint64_t)); + +#define MAKE_CLEAR(size) \ +void conv_clear_ ##size## d_c(struct convert *conv, \ + void * SPA_RESTRICT dst[], uint32_t n_samples) \ +{ \ + uint32_t i, n_channels = conv->n_channels; \ + for (i = 0; i < n_channels; i++) \ + memset(dst[i], 0, n_samples * (size>>3)); \ +} \ +void conv_clear_ ##size## _c(struct convert *conv, \ + void * SPA_RESTRICT dst[], uint32_t n_samples) \ +{ \ + memset(dst[0], 0, n_samples * conv->n_channels * (size>>3)); \ +} + +MAKE_CLEAR(8); +MAKE_CLEAR(16); +MAKE_CLEAR(24); +MAKE_CLEAR(32); +MAKE_CLEAR(64); + +#define MAKE_CLEAR_VAL(size,dtype,val) \ +void conv_clear_ ##size## d_c(struct convert *conv, \ + void * SPA_RESTRICT dst[], uint32_t n_samples) \ +{ \ + uint32_t i, j, n_channels = conv->n_channels; \ + for (i = 0; i < n_channels; i++) { \ + dtype *d = dst[i]; \ + for (j = 0; j < n_samples; j++) \ + d[j] = val; \ + } \ +} \ +void conv_clear_ ##size## _c(struct convert *conv, \ + void * SPA_RESTRICT dst[], uint32_t n_samples) \ +{ \ + uint32_t j; \ + dtype *d = dst[0]; \ + n_samples *= conv->n_channels; \ + for (j = 0; j < n_samples; j++) \ + d[j] = val; \ +} + +MAKE_CLEAR_VAL(alaw, uint8_t, 0x55); +MAKE_CLEAR_VAL(ulaw, uint8_t, 0xff); +MAKE_CLEAR_VAL(u8, uint8_t, 0x80); +MAKE_CLEAR_VAL(u16, uint16_t, 0x8000); +MAKE_CLEAR_VAL(u24, uint24_t, U32_TO_U24(0x800000)); +MAKE_CLEAR_VAL(u24_32, uint32_t, 0x800000); +MAKE_CLEAR_VAL(u32, uint32_t, 0x80000000); + diff --git a/spa/plugins/audioconvert/fmt-ops.c b/spa/plugins/audioconvert/fmt-ops.c index 4df31323a..3fc2c5f0a 100644 --- a/spa/plugins/audioconvert/fmt-ops.c +++ b/spa/plugins/audioconvert/fmt-ops.c @@ -376,6 +376,64 @@ static const struct conv_info *find_conv_info(uint32_t src_fmt, uint32_t dst_fmt return NULL; } + +typedef void (*clear_func_t) (struct convert *conv, void * SPA_RESTRICT dst[], + uint32_t n_samples); + +struct clear_info { + uint32_t fmt; + + clear_func_t clear; + const char *name; + + uint32_t cpu_flags; +}; + +#define MAKE(fmt,func,...) \ + { SPA_AUDIO_FORMAT_ ##fmt, func, #func , __VA_ARGS__ } + +static struct clear_info clear_table[] = +{ + MAKE(U8, conv_clear_u8_c), + MAKE(U8P, conv_clear_u8d_c), + MAKE(S8, conv_clear_8_c), + MAKE(S8P, conv_clear_8d_c), + MAKE(U16, conv_clear_u16_c), + MAKE(S16, conv_clear_16_c), + MAKE(S16_OE, conv_clear_16_c), + MAKE(S16P, conv_clear_16d_c), + MAKE(U24, conv_clear_u24_c), + MAKE(S24, conv_clear_24_c), + MAKE(S24_OE, conv_clear_24_c), + MAKE(S24P, conv_clear_24d_c), + MAKE(U24_32, conv_clear_u24_32_c), + MAKE(S24_32, conv_clear_32_c), + MAKE(S24_32_OE, conv_clear_32_c), + MAKE(U32, conv_clear_u32_c), + MAKE(S32, conv_clear_32_c), + MAKE(S32_OE, conv_clear_32_c), + MAKE(S32P, conv_clear_32d_c), + MAKE(F32, conv_clear_32_c), + MAKE(F32_OE, conv_clear_32_c), + MAKE(F32P, conv_clear_32d_c), + MAKE(F64, conv_clear_64_c), + MAKE(F64_OE, conv_clear_64_c), + MAKE(F64P, conv_clear_64d_c), + MAKE(ALAW, conv_clear_alaw_c), + MAKE(ULAW, conv_clear_ulaw_c), +}; +#undef MAKE + +static const struct clear_info *find_clear_info(uint32_t fmt, uint32_t cpu_flags) +{ + SPA_FOR_EACH_ELEMENT_VAR(clear_table, c) { + if (c->fmt == fmt && + MATCH_CPU_FLAGS(c->cpu_flags, cpu_flags)) + return c; + } + return NULL; +} + typedef void (*noise_func_t) (struct convert *conv, float * noise, uint32_t n_samples); struct noise_info { @@ -492,7 +550,8 @@ int convert_init(struct convert *conv) const struct conv_info *info; const struct dither_info *dinfo; const struct noise_info *ninfo; - uint32_t i, conv_flags, data_size[3]; + const struct clear_info *cinfo; + uint32_t i, conv_flags, data_size[4]; /* we generate int32 bits of random values. With this scale * factor, we bring this in the [-1.0, 1.0] range */ @@ -549,20 +608,24 @@ int convert_init(struct convert *conv) if (ninfo == NULL) return -ENOTSUP; + cinfo = find_clear_info(conv->dst_fmt, conv->cpu_flags); + conv->noise_size = NOISE_SIZE; data_size[0] = SPA_ROUND_UP(conv->noise_size * sizeof(float), FMT_OPS_MAX_ALIGN); data_size[1] = SPA_ROUND_UP(RANDOM_SIZE * sizeof(uint32_t), FMT_OPS_MAX_ALIGN); data_size[2] = SPA_ROUND_UP(RANDOM_SIZE * sizeof(int32_t), FMT_OPS_MAX_ALIGN); + data_size[3] = SPA_ROUND_UP(conv->n_channels * sizeof(struct shaper), FMT_OPS_MAX_ALIGN); conv->data = calloc(FMT_OPS_MAX_ALIGN + - data_size[0] + data_size[1] + data_size[2], 1); + data_size[0] + data_size[1] + data_size[2] + data_size[3], 1); if (conv->data == NULL) return -errno; conv->noise = SPA_PTR_ALIGN(conv->data, FMT_OPS_MAX_ALIGN, float); conv->random = SPA_PTROFF(conv->noise, data_size[0], uint32_t); conv->prev = SPA_PTROFF(conv->random, data_size[1], int32_t); + conv->shaper = SPA_PTROFF(conv->prev, data_size[2], struct shaper); for (i = 0; i < RANDOM_SIZE; i++) conv->random[i] = random(); @@ -571,6 +634,7 @@ int convert_init(struct convert *conv) conv->cpu_flags = info->cpu_flags; conv->update_noise = ninfo->noise; conv->process = info->process; + conv->clear = cinfo ? cinfo->clear : NULL; conv->free = impl_convert_free; conv->func_name = info->name; diff --git a/spa/plugins/audioconvert/fmt-ops.h b/spa/plugins/audioconvert/fmt-ops.h index 7aed0bc65..f738e3858 100644 --- a/spa/plugins/audioconvert/fmt-ops.h +++ b/spa/plugins/audioconvert/fmt-ops.h @@ -236,11 +236,12 @@ struct convert { uint32_t noise_size; const float *ns; uint32_t n_ns; - struct shaper shaper[64]; + struct shaper *shaper; void (*update_noise) (struct convert *conv, float *noise, uint32_t n_samples); void (*process) (struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], uint32_t n_samples); + void (*clear) (struct convert *conv, void * SPA_RESTRICT dst[], uint32_t n_samples); void (*free) (struct convert *conv); void *data; @@ -278,6 +279,7 @@ static inline uint32_t dither_method_from_label(const char *label) #define convert_update_noise(conv,...) (conv)->update_noise(conv, __VA_ARGS__) #define convert_process(conv,...) (conv)->process(conv, __VA_ARGS__) +#define convert_clear(conv,...) (conv)->clear(conv, __VA_ARGS__) #define convert_free(conv) (conv)->free(conv) #define DEFINE_NOISE_FUNCTION(name,arch) \ @@ -490,3 +492,28 @@ DEFINE_FUNCTION(f32d_to_s16, avx2); #endif #undef DEFINE_FUNCTION + +#define DEFINE_CLEAR_FUNCTION(name,arch) \ +void conv_clear_##name##_##arch(struct convert *conv, void * SPA_RESTRICT dst[], \ + uint32_t n_samples) + +DEFINE_CLEAR_FUNCTION(alaw, c); +DEFINE_CLEAR_FUNCTION(ulaw, c); +DEFINE_CLEAR_FUNCTION(8, c); +DEFINE_CLEAR_FUNCTION(8d, c); +DEFINE_CLEAR_FUNCTION(16, c); +DEFINE_CLEAR_FUNCTION(16d, c); +DEFINE_CLEAR_FUNCTION(24, c); +DEFINE_CLEAR_FUNCTION(24d, c); +DEFINE_CLEAR_FUNCTION(32, c); +DEFINE_CLEAR_FUNCTION(32d, c); +DEFINE_CLEAR_FUNCTION(64, c); +DEFINE_CLEAR_FUNCTION(64d, c); +DEFINE_CLEAR_FUNCTION(u8, c); +DEFINE_CLEAR_FUNCTION(u8d, c); +DEFINE_CLEAR_FUNCTION(u16, c); +DEFINE_CLEAR_FUNCTION(u24, c); +DEFINE_CLEAR_FUNCTION(u24_32, c); +DEFINE_CLEAR_FUNCTION(u32, c); + +#undef DEFINE_CLEAR_FUNCTION diff --git a/spa/plugins/audioconvert/hilbert.h b/spa/plugins/audioconvert/hilbert.h index aa00940ba..2a794592c 100644 --- a/spa/plugins/audioconvert/hilbert.h +++ b/spa/plugins/audioconvert/hilbert.h @@ -5,14 +5,13 @@ #ifndef HILBERT_H #define HILBERT_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif static inline void blackman_window(float *taps, int n_taps) { diff --git a/spa/plugins/audioconvert/meson.build b/spa/plugins/audioconvert/meson.build index 394bc11eb..64379c845 100644 --- a/spa/plugins/audioconvert/meson.build +++ b/spa/plugins/audioconvert/meson.build @@ -125,7 +125,7 @@ sparesampledumpcoeffs_sources = [ sparesampledumpcoeffs = executable( 'spa-resample-dump-coeffs', sparesampledumpcoeffs_sources, - c_args : [ cc_flags_native, '-DRESAMPLE_DISABLE_PRECOMP' ], + c_args : [ '-DRESAMPLE_DISABLE_PRECOMP' ], dependencies : [ spa_dep, mathlib_native ], install : false, native : true, diff --git a/spa/plugins/audioconvert/peaks-ops.h b/spa/plugins/audioconvert/peaks-ops.h index 1629e8edf..24092a4f7 100644 --- a/spa/plugins/audioconvert/peaks-ops.h +++ b/spa/plugins/audioconvert/peaks-ops.h @@ -6,6 +6,11 @@ #include #include +#include + +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &resample_log_topic +extern struct spa_log_topic resample_log_topic; struct peaks { uint32_t cpu_flags; @@ -49,4 +54,5 @@ DEFINE_MIN_MAX_FUNCTION(sse); DEFINE_ABS_MAX_FUNCTION(sse); #endif -#undef DEFINE_FUNCTION +#undef DEFINE_MIN_MAX_FUNCTION +#undef DEFINE_ABS_MAX_FUNCTION diff --git a/spa/plugins/audioconvert/resample-native-impl.h b/spa/plugins/audioconvert/resample-native-impl.h index e3ad54dba..11a0851b3 100644 --- a/spa/plugins/audioconvert/resample-native-impl.h +++ b/spa/plugins/audioconvert/resample-native-impl.h @@ -5,13 +5,30 @@ #include #include +#include #include "resample.h" +#undef SPA_LOG_TOPIC_DEFAULT +#define SPA_LOG_TOPIC_DEFAULT &resample_log_topic +extern struct spa_log_topic resample_log_topic; + typedef void (*resample_func_t)(struct resample *r, const void * SPA_RESTRICT src[], uint32_t ioffs, uint32_t *in_len, void * SPA_RESTRICT dst[], uint32_t ooffs, uint32_t *out_len); +#define FIXP_SHIFT 32 +#define FIXP_SCALE ((uint64_t)1 << FIXP_SHIFT) +#define FIXP_MASK (FIXP_SCALE - 1) +#define UINT32_TO_FIXP(v) ((struct fixp) { (uint64_t)((uint32_t)(v)) << FIXP_SHIFT }) +#define FLOAT_TO_FIXP(d) ((struct fixp) { (uint64_t)((d) * (float)FIXP_SCALE) }) +#define FIXP_TO_UINT32(f) ((f).value >> FIXP_SHIFT) +#define FIXP_TO_FLOAT(f) ((f).value / (float)FIXP_SCALE) + +struct fixp { + uint64_t value; +}; + struct resample_info { uint32_t format; resample_func_t process_copy; @@ -27,13 +44,15 @@ struct native_data { double rate; uint32_t n_taps; uint32_t n_phases; - uint32_t in_rate; + struct fixp in_rate; uint32_t out_rate; - float phase; + struct fixp phase; + float pm; uint32_t inc; - uint32_t frac; + struct fixp frac; uint32_t filter_stride; uint32_t filter_stride_os; + uint32_t gcd; uint32_t hist; float **history; resample_func_t func; @@ -84,25 +103,26 @@ DEFINE_RESAMPLER(full,arch) \ { \ struct native_data *data = r->data; \ uint32_t n_taps = data->n_taps, stride = data->filter_stride_os; \ - uint32_t index, phase, n_phases = data->out_rate; \ + uint32_t index; \ uint32_t c, o, olen = *out_len, ilen = *in_len; \ - uint32_t inc = data->inc, frac = data->frac, ch = r->channels; \ + uint32_t inc = data->inc, ch = r->channels; \ + uint64_t frac = data->frac.value, phase = data->phase.value; \ + uint64_t denom = UINT32_TO_FIXP(data->out_rate).value; \ \ index = ioffs; \ - phase = (uint32_t)data->phase; \ for (o = ooffs; o < olen && index + n_taps <= ilen; o++) { \ - float *filter = &data->filter[phase * stride]; \ + float *filter = &data->filter[(phase >> FIXP_SHIFT) * stride]; \ for (c = 0; c < ch; c++) { \ const float *s = src[c]; \ float *d = dst[c]; \ inner_product_##arch(&d[o], &s[index], \ filter, n_taps); \ } \ - INC(index, phase, n_phases); \ + INC(index, phase, denom); \ } \ *in_len = index; \ *out_len = o; \ - data->phase = phase; \ + data->phase.value = phase; \ } #define MAKE_RESAMPLER_INTER(arch) \ @@ -110,17 +130,18 @@ DEFINE_RESAMPLER(inter,arch) \ { \ struct native_data *data = r->data; \ uint32_t index, stride = data->filter_stride; \ - uint32_t n_phases = data->n_phases, out_rate = data->out_rate; \ uint32_t n_taps = data->n_taps; \ uint32_t c, o, olen = *out_len, ilen = *in_len; \ - uint32_t inc = data->inc, frac = data->frac, ch = r->channels; \ - float phase; \ + uint32_t inc = data->inc, ch = r->channels; \ + uint32_t ph_max = data->n_phases - 1; \ + uint64_t frac = data->frac.value, phase = data->phase.value; \ + uint64_t denom = UINT32_TO_FIXP(data->out_rate).value; \ + float pm = data->pm; \ \ index = ioffs; \ - phase = data->phase; \ for (o = ooffs; o < olen && index + n_taps <= ilen; o++) { \ - float ph = phase * n_phases / out_rate; \ - uint32_t offset = (uint32_t)floorf(ph); \ + float ph = phase * pm; \ + uint32_t offset = SPA_MIN((uint32_t)floorf(ph), ph_max); \ float *filter0 = &data->filter[(offset+0) * stride]; \ float *filter1 = &data->filter[(offset+1) * stride]; \ float pho = ph - offset; \ @@ -130,11 +151,11 @@ DEFINE_RESAMPLER(inter,arch) \ inner_product_ip_##arch(&d[o], &s[index], \ filter0, filter1, pho, n_taps); \ } \ - INC(index, phase, out_rate); \ + INC(index, phase, denom); \ } \ *in_len = index; \ *out_len = o; \ - data->phase = phase; \ + data->phase.value = phase; \ } diff --git a/spa/plugins/audioconvert/resample-native.c b/spa/plugins/audioconvert/resample-native.c index f393e3dce..985f25497 100644 --- a/spa/plugins/audioconvert/resample-native.c +++ b/spa/plugins/audioconvert/resample-native.c @@ -11,6 +11,8 @@ #include "resample-native-precomp.h" #endif +SPA_LOG_TOPIC_DEFINE(resample_log_topic, "spa.resample"); + struct quality { uint32_t n_taps; double cutoff; @@ -137,45 +139,54 @@ static inline uint32_t calc_gcd(uint32_t a, uint32_t b) static void impl_native_update_rate(struct resample *r, double rate) { struct native_data *data = r->data; - uint32_t in_rate, out_rate, gcd, old_out_rate; - float phase; + struct fixp in_rate; + uint32_t out_rate; if (SPA_LIKELY(data->rate == rate)) return; - old_out_rate = data->out_rate; - in_rate = (uint32_t)(r->i_rate / rate); - out_rate = r->o_rate; - phase = data->phase; - - gcd = calc_gcd(in_rate, out_rate); - in_rate /= gcd; - out_rate /= gcd; - data->rate = rate; - data->phase = phase * out_rate / (float)old_out_rate; + in_rate = UINT32_TO_FIXP(r->i_rate); + out_rate = r->o_rate; + + if (rate != 1.0) { + in_rate.value = (uint64_t)round(in_rate.value / rate); + data->func = data->info->process_inter; + } + else if (in_rate.value == UINT32_TO_FIXP(out_rate).value) { + data->func = data->info->process_copy; + } + else { + in_rate.value /= data->gcd; + out_rate /= data->gcd; + data->func = data->info->process_full; + } + + if (data->out_rate != out_rate) { + /* Cast to double to avoid overflows */ + data->phase.value = (uint64_t)(data->phase.value * (double)out_rate / data->out_rate); + if (data->phase.value >= UINT32_TO_FIXP(out_rate).value) + data->phase.value = UINT32_TO_FIXP(out_rate).value - 1; + } + data->in_rate = in_rate; data->out_rate = out_rate; - data->inc = data->in_rate / data->out_rate; - data->frac = data->in_rate % data->out_rate; + data->inc = in_rate.value / UINT32_TO_FIXP(out_rate).value; + data->frac.value = in_rate.value % UINT32_TO_FIXP(out_rate).value; - if (data->in_rate == data->out_rate && rate == 1.0) { - data->func = data->info->process_copy; - r->func_name = data->info->copy_name; - } - else if (rate == 1.0) { - data->func = data->info->process_full; - r->func_name = data->info->full_name; - } - else { - data->func = data->info->process_inter; - r->func_name = data->info->inter_name; - } - - spa_log_trace_fp(r->log, "native %p: rate:%f in:%d out:%d gcd:%d phase:%f inc:%d frac:%d", r, - rate, r->i_rate, r->o_rate, gcd, data->phase, data->inc, data->frac); + spa_log_trace_fp(r->log, "native %p: rate:%f in:%d out:%d phase:%f inc:%d frac:%f", r, + rate, r->i_rate, r->o_rate, FIXP_TO_FLOAT(data->phase), + data->inc, FIXP_TO_FLOAT(data->frac)); +} +static uint64_t fixp_floor_a_plus_bc(struct fixp a, uint32_t b, struct fixp c) +{ + /* (a + b*c) >> FIXP_SHIFT, with bigger overflow threshold */ + uint64_t hi, lo; + hi = (a.value >> FIXP_SHIFT) + b * (c.value >> FIXP_SHIFT); + lo = (a.value & FIXP_MASK) + b * (c.value & FIXP_MASK); + return hi + (lo >> FIXP_SHIFT); } static uint32_t impl_native_in_len(struct resample *r, uint32_t out_len) @@ -183,7 +194,7 @@ static uint32_t impl_native_in_len(struct resample *r, uint32_t out_len) struct native_data *data = r->data; uint32_t in_len; - in_len = (uint32_t)((data->phase + out_len * data->frac) / data->out_rate); + in_len = fixp_floor_a_plus_bc(data->phase, out_len, data->frac) / data->out_rate; in_len += out_len * data->inc + (data->n_taps - data->hist); spa_log_trace_fp(r->log, "native %p: hist:%d %d->%d", r, data->hist, out_len, in_len); @@ -196,9 +207,9 @@ static uint32_t impl_native_out_len(struct resample *r, uint32_t in_len) struct native_data *data = r->data; uint32_t out_len; - in_len = in_len - SPA_MIN(in_len, (data->n_taps - data->hist) + 1); - out_len = (uint32_t)(in_len * data->out_rate - data->phase); - out_len = (out_len + data->in_rate - 1) / data->in_rate; + in_len = in_len - SPA_MIN(in_len, data->n_taps - data->hist); + out_len = in_len * data->out_rate - FIXP_TO_UINT32(data->phase); + out_len = (UINT32_TO_FIXP(out_len).value + data->in_rate.value - 1) / data->in_rate.value; spa_log_trace_fp(r->log, "native %p: hist:%d %d->%d", r, data->hist, in_len, out_len); @@ -306,7 +317,7 @@ static void impl_native_reset (struct resample *r) d->hist = d->n_taps - 1; else d->hist = d->n_taps / 2; - d->phase = 0; + d->phase.value = 0; } static uint32_t impl_native_delay (struct resample *r) @@ -321,13 +332,13 @@ static float impl_native_phase (struct resample *r) float pho = 0; if (d->func == d->info->process_full) { - pho = -(float)((int32_t)d->phase) / d->out_rate; + pho = -(float)FIXP_TO_UINT32(d->phase) / d->out_rate; /* XXX: this is how it seems to behave, but not clear why */ if (d->hist >= d->n_taps - 1) pho += 1.0f; } else if (d->func == d->info->process_inter) { - pho = -d->phase / d->out_rate; + pho = -FIXP_TO_FLOAT(d->phase) / d->out_rate; /* XXX: this is how it seems to behave, but not clear why */ if (d->hist >= d->n_taps - 1) @@ -391,8 +402,10 @@ int resample_native_init(struct resample *r) r->data = d; d->n_taps = n_taps; d->n_phases = n_phases; - d->in_rate = in_rate; + d->in_rate = UINT32_TO_FIXP(in_rate); d->out_rate = out_rate; + d->gcd = gcd; + d->pm = (float)n_phases / r->o_rate / FIXP_SCALE; d->filter = SPA_PTROFF_ALIGN(d, sizeof(struct native_data), 64, float); d->hist_mem = SPA_PTROFF_ALIGN(d->filter, filter_size, 64, float); d->history = SPA_PTROFF(d->hist_mem, history_size, float*); @@ -436,5 +449,12 @@ int resample_native_init(struct resample *r) impl_native_reset(r); impl_native_update_rate(r, 1.0); + if (d->func == d->info->process_copy) + r->func_name = d->info->copy_name; + else if (d->func == d->info->process_full) + r->func_name = d->info->full_name; + else + r->func_name = d->info->inter_name; + return 0; } diff --git a/spa/plugins/audioconvert/test-audioconvert.c b/spa/plugins/audioconvert/test-audioconvert.c index bc6e2a5ef..de3ebb8b5 100644 --- a/spa/plugins/audioconvert/test-audioconvert.c +++ b/spa/plugins/audioconvert/test-audioconvert.c @@ -25,7 +25,8 @@ SPA_LOG_IMPL(logger); extern const struct spa_handle_factory test_source_factory; -#define MAX_PORTS (SPA_AUDIO_MAX_CHANNELS+1) +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS +#define MAX_PORTS (MAX_CHANNELS+1) struct context { struct spa_handle *convert_handle; @@ -598,7 +599,7 @@ static int run_convert(struct context *ctx, struct data *in_data, for (j = 0; j < in_data->planes; j++, k++) { b->datas[j].type = SPA_DATA_MemPtr; - b->datas[j].flags = 0; + b->datas[j].flags = SPA_DATA_FLAG_READABLE; b->datas[j].fd = -1; b->datas[j].mapoffset = 0; b->datas[j].maxsize = in_data->size; @@ -629,7 +630,7 @@ static int run_convert(struct context *ctx, struct data *in_data, for (j = 0; j < out_data->planes; j++) { b->datas[j].type = SPA_DATA_MemPtr; - b->datas[j].flags = 0; + b->datas[j].flags = SPA_DATA_FLAG_READWRITE; b->datas[j].fd = -1; b->datas[j].mapoffset = 0; b->datas[j].maxsize = out_data->size; diff --git a/spa/plugins/audioconvert/test-resample.c b/spa/plugins/audioconvert/test-resample.c index 1d77a3b23..8c0a7de86 100644 --- a/spa/plugins/audioconvert/test-resample.c +++ b/spa/plugins/audioconvert/test-resample.c @@ -15,6 +15,7 @@ SPA_LOG_IMPL(logger); #include "resample.h" +#include "resample-native-impl.h" #define N_SAMPLES 253 #define N_CHANNELS 11 @@ -69,7 +70,7 @@ static void test_native(void) resample_free(&r); } -static void pull_blocks(struct resample *r, uint32_t first, uint32_t size) +static void pull_blocks(struct resample *r, uint32_t first, uint32_t size, uint32_t count) { uint32_t i; float in[SPA_MAX(size, first) * 2]; @@ -82,7 +83,7 @@ static void pull_blocks(struct resample *r, uint32_t first, uint32_t size) src[0] = in; dst[0] = out; - for (i = 0; i < 500; i++) { + for (i = 0; i < count; i++) { pout_len = out_len = i == 0 ? first : size; pin_len = in_len = resample_in_len(r, out_len); @@ -97,7 +98,52 @@ static void pull_blocks(struct resample *r, uint32_t first, uint32_t size) } } -static void test_in_len(void) +static void pull_blocks_out(struct resample *r, uint32_t first, uint32_t size, uint32_t count) +{ + uint32_t i; + float in[SPA_MAX(size, first) * 2]; + float out[SPA_MAX(size, first) * 2]; + const void *src[1]; + void *dst[1]; + uint32_t in_len, out_len; + uint32_t pin_len, pout_len; + + src[0] = in; + dst[0] = out; + + for (i = 0; i < count; i++) { + pin_len = in_len = i == 0 ? first : size; + pout_len = out_len = resample_out_len(r, in_len); + + resample_process(r, src, &pin_len, dst, &pout_len); + + fprintf(stderr, "%d: %d %d %d %d %d\n", i, + in_len, pin_len, out_len, pout_len, + resample_out_len(r, size)); + + spa_assert_se(in_len == pin_len); + spa_assert_se(out_len == pout_len); + } +} + +static void check_inout_len(struct resample *r, uint32_t first, uint32_t size, double rate, float phase) +{ + struct native_data *data = r->data; + + resample_reset(r); + resample_update_rate(r, rate); + if (phase != 0.0) + data->phase = FLOAT_TO_FIXP(phase); + pull_blocks(r, first, size, 500); + + resample_reset(r); + resample_update_rate(r, rate); + if (phase != 0.0) + data->phase = FLOAT_TO_FIXP(phase); + pull_blocks_out(r, first, size, 500); +} + +static void test_inout_len(void) { struct resample r; @@ -109,7 +155,7 @@ static void test_in_len(void) r.quality = RESAMPLE_DEFAULT_QUALITY; resample_native_init(&r); - pull_blocks(&r, 1024, 1024); + check_inout_len(&r, 1024, 1024, 1.0, 0); resample_free(&r); spa_zero(r); @@ -120,7 +166,7 @@ static void test_in_len(void) r.quality = RESAMPLE_DEFAULT_QUALITY; resample_native_init(&r); - pull_blocks(&r, 1024, 1024); + check_inout_len(&r, 1024, 1024, 1.0, 0); resample_free(&r); spa_zero(r); @@ -131,7 +177,7 @@ static void test_in_len(void) r.quality = RESAMPLE_DEFAULT_QUALITY; resample_native_init(&r); - pull_blocks(&r, 1024, 1024); + check_inout_len(&r, 1024, 1024, 1.0, 0); resample_free(&r); spa_zero(r); @@ -142,7 +188,71 @@ static void test_in_len(void) r.quality = RESAMPLE_DEFAULT_QUALITY; resample_native_init(&r); - pull_blocks(&r, 513, 64); + check_inout_len(&r, 513, 64, 1.0, 0); + resample_free(&r); + + spa_zero(r); + r.log = &logger.log; + r.channels = 1; + r.i_rate = 32000; + r.o_rate = 48000; + r.quality = RESAMPLE_DEFAULT_QUALITY; + resample_native_init(&r); + + check_inout_len(&r, 513, 64, 1.02, 0); + resample_free(&r); + + spa_zero(r); + r.log = &logger.log; + r.channels = 1; + r.i_rate = 32000; + r.o_rate = 48000; + r.quality = RESAMPLE_DEFAULT_QUALITY; + resample_native_init(&r); + + check_inout_len(&r, 513, 64, 1.0002, 0); + resample_free(&r); + + spa_zero(r); + r.log = &logger.log; + r.channels = 1; + r.i_rate = 32000; + r.o_rate = 48000; + r.quality = RESAMPLE_DEFAULT_QUALITY; + r.options = RESAMPLE_OPTION_PREFILL; + resample_native_init(&r); + + check_inout_len(&r, 513, 64, 1.0002, 0); + resample_free(&r); + + /* Test value of phase that in floating-point arithmetic produces + * inconsistent in_len + */ + spa_zero(r); + r.log = &logger.log; + r.channels = 1; + r.i_rate = 8000; + r.o_rate = 8000; + r.quality = RESAMPLE_DEFAULT_QUALITY; + r.options = RESAMPLE_OPTION_PREFILL; + resample_native_init(&r); + + check_inout_len(&r, 64, 64, 1.0 + 1e-10, 7999.99f); + resample_free(&r); + + /* Test value of phase that overflows filter buffer due to floating point rounding + * up to nearest + */ + spa_zero(r); + r.log = &logger.log; + r.channels = 1; + r.i_rate = 8000; + r.o_rate = 8000; + r.quality = RESAMPLE_DEFAULT_QUALITY; + r.options = RESAMPLE_OPTION_PREFILL; + resample_native_init(&r); + + check_inout_len(&r, 64, 64, 1.0 + 1e-10, nextafterf(8000, 0)); resample_free(&r); } @@ -151,7 +261,7 @@ int main(int argc, char *argv[]) logger.log.level = SPA_LOG_LEVEL_TRACE; test_native(); - test_in_len(); + test_inout_len(); return 0; } diff --git a/spa/plugins/audiomixer/audiomixer.c b/spa/plugins/audiomixer/audiomixer.c index bab300713..f7558ef47 100644 --- a/spa/plugins/audiomixer/audiomixer.c +++ b/spa/plugins/audiomixer/audiomixer.c @@ -31,7 +31,6 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.audiomixer"); #define MAX_BUFFERS 64 #define MAX_PORTS 512 -#define MAX_CHANNELS 64 #define MAX_ALIGN MIX_OPS_MAX_ALIGN #define PORT_DEFAULT_VOLUME 1.0 @@ -60,6 +59,8 @@ struct buffer { }; struct port { + struct spa_list link; + uint32_t direction; uint32_t id; @@ -71,7 +72,6 @@ struct port { struct spa_port_info info; struct spa_param_info params[8]; - unsigned int valid:1; unsigned int have_format:1; struct buffer buffers[MAX_BUFFERS]; @@ -79,6 +79,9 @@ struct port { struct spa_list queue; size_t queued_bytes; + + struct spa_list mix_link; + bool active; }; struct impl { @@ -104,10 +107,10 @@ struct impl { struct spa_hook_list hooks; - uint32_t port_count; - uint32_t last_port; struct port *in_ports[MAX_PORTS]; struct port out_ports[1]; + struct spa_list port_list; + struct spa_list free_list; struct buffer *mix_buffers[MAX_PORTS]; const void *mix_datas[MAX_PORTS]; @@ -119,12 +122,13 @@ struct impl { unsigned int started:1; uint32_t stride; uint32_t blocks; + + struct spa_list mix_list; }; -#define PORT_VALID(p) ((p) != NULL && (p)->valid) #define CHECK_ANY_IN(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == SPA_ID_INVALID) -#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && !PORT_VALID(this->in_ports[(p)])) -#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && PORT_VALID(this->in_ports[(p)])) +#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && this->in_ports[(p)] == NULL) +#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && this->in_ports[(p)] != NULL) #define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) #define CHECK_PORT(this,d,p) (CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p)) #define CHECK_PORT_ANY(this,d,p) (CHECK_ANY_IN(this,d,p) || CHECK_PORT(this,d,p)) @@ -210,7 +214,7 @@ static int impl_node_add_listener(void *object, { struct impl *this = object; struct spa_hook_list save; - uint32_t i; + struct port *port; spa_return_val_if_fail(this != NULL, -EINVAL); @@ -218,10 +222,8 @@ static int impl_node_add_listener(void *object, emit_node_info(this, true); emit_port_info(this, GET_OUT_PORT(this, 0), true); - for (i = 0; i < this->last_port; i++) { - if (PORT_VALID(this->in_ports[i])) - emit_port_info(this, GET_IN_PORT(this, i), true); - } + spa_list_for_each(port, &this->port_list, link) + emit_port_info(this, port, true); spa_hook_list_join(&this->hooks, &save); @@ -236,6 +238,18 @@ impl_node_set_callbacks(void *object, return 0; } +static struct port *get_free_port(struct impl *this) +{ + struct port *port; + if (!spa_list_is_empty(&this->free_list)) { + port = spa_list_first(&this->free_list, struct port, link); + spa_list_remove(&port->link); + } else { + port = calloc(1, sizeof(struct port)); + } + return port; +} + static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, const struct spa_dict *props) { @@ -245,13 +259,9 @@ static int impl_node_add_port(void *object, enum spa_direction direction, uint32 spa_return_val_if_fail(this != NULL, -EINVAL); spa_return_val_if_fail(CHECK_FREE_IN_PORT(this, direction, port_id), -EINVAL); - port = GET_IN_PORT(this, port_id); - if (port == NULL) { - port = calloc(1, sizeof(struct port)); - if (port == NULL) - return -errno; - this->in_ports[port_id] = port; - } + if ((port = get_free_port(this)) == NULL) + return -errno; + port->direction = SPA_DIRECTION_INPUT; port->id = port_id; @@ -273,13 +283,10 @@ static int impl_node_add_port(void *object, enum spa_direction direction, uint32 port->info.params = port->params; port->info.n_params = 5; - this->port_count++; - if (this->last_port <= port_id) - this->last_port = port_id + 1; - port->valid = true; + this->in_ports[port_id] = port; + spa_list_append(&this->port_list, &port->link); - spa_log_debug(this->log, "%p: add port %d:%d %d", this, - direction, port_id, this->last_port); + spa_log_debug(this->log, "%p: add port %d:%d", this, direction, port_id); emit_port_info(this, port, true); return 0; @@ -295,26 +302,18 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_ spa_return_val_if_fail(CHECK_IN_PORT(this, direction, port_id), -EINVAL); port = GET_IN_PORT(this, port_id); + this->in_ports[port_id] = NULL; + spa_list_remove(&port->link); - port->valid = false; - this->port_count--; if (port->have_format && this->have_format) { if (--this->n_formats == 0) this->have_format = false; } spa_memzero(port, sizeof(struct port)); + spa_list_append(&this->free_list, &port->link); - if (port_id + 1 == this->last_port) { - int i; - - for (i = this->last_port - 1; i >= 0; i--) - if (PORT_VALID(GET_IN_PORT(this, i))) - break; - - this->last_port = i + 1; - } - spa_log_debug(this->log, "%p: remove port %d:%d %d", this, - direction, port_id, this->last_port); + spa_log_debug(this->log, "%p: remove port %d:%d", this, + direction, port_id); spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); @@ -343,7 +342,7 @@ static int port_enum_formats(void *object, struct port *port, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), - SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Int(12, + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id(12, SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_S16, @@ -698,6 +697,7 @@ impl_node_port_use_buffers(void *object, } struct io_info { + struct impl *impl; struct port *port; void *data; size_t size; @@ -707,17 +707,29 @@ static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { struct io_info *info = user_data; - if (info->size >= sizeof(struct spa_io_async_buffers)) { - struct spa_io_async_buffers *ab = info->data; - info->port->io[0] = &ab->buffers[info->port->direction]; - info->port->io[1] = &ab->buffers[info->port->direction^1]; - } else if (info->size >= sizeof(struct spa_io_buffers)) { - info->port->io[0] = info->data; - info->port->io[1] = info->data; - } else { - info->port->io[0] = NULL; - info->port->io[1] = NULL; - } + struct port *port = info->port; + + if (info->data == NULL || info->size < sizeof(struct spa_io_buffers)) { + port->io[0] = NULL; + port->io[1] = NULL; + if (port->active) { + spa_list_remove(&port->mix_link); + port->active = false; + } + } else { + if (info->size >= sizeof(struct spa_io_async_buffers)) { + struct spa_io_async_buffers *ab = info->data; + port->io[0] = &ab->buffers[port->direction]; + port->io[1] = &ab->buffers[port->direction^1]; + } else { + port->io[0] = info->data; + port->io[1] = info->data; + } + if (!port->active) { + spa_list_append(&info->impl->mix_list, &port->mix_link); + port->active = true; + } + } return 0; } @@ -738,6 +750,7 @@ impl_node_port_set_io(void *object, spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); port = GET_PORT(this, direction, port_id); + info.impl = this; info.port = port; info.data = data; info.size = size; @@ -745,8 +758,8 @@ impl_node_port_set_io(void *object, switch (id) { case SPA_IO_Buffers: case SPA_IO_AsyncBuffers: - spa_loop_invoke(this->data_loop, - do_port_set_io, SPA_ID_INVALID, NULL, 0, true, &info); + spa_loop_locked(this->data_loop, + do_port_set_io, SPA_ID_INVALID, NULL, 0, &info); break; default: return -ENOENT; @@ -772,9 +785,9 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t static int impl_node_process(void *object) { struct impl *this = object; - struct port *outport; + struct port *outport, *inport; struct spa_io_buffers *outio; - uint32_t n_buffers, i, maxsize; + uint32_t n_buffers, maxsize; struct buffer **buffers; struct buffer *outb; const void **datas; @@ -804,24 +817,17 @@ static int impl_node_process(void *object) maxsize = UINT32_MAX; - for (i = 0; i < this->last_port; i++) { - struct port *inport = GET_IN_PORT(this, i); - struct spa_io_buffers *inio = NULL; + spa_list_for_each(inport, &this->mix_list, mix_link) { + struct spa_io_buffers *inio = inport->io[cycle]; struct buffer *inb; struct spa_data *bd; uint32_t size, offs; - if (SPA_UNLIKELY(!PORT_VALID(inport) || (inio = inport->io[cycle]) == NULL)) { - spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d io:%p/%p/%d", - this, i, PORT_VALID(inport), - inport->io[0], inport->io[1], cycle); - continue; - } if (inio->buffer_id >= inport->n_buffers || inio->status != SPA_STATUS_HAVE_DATA) { spa_log_trace_fp(this->log, "%p: skip input idx:%d " "io:%p status:%d buf_id:%d n_buffers:%d", this, - i, inio, inio->status, inio->buffer_id, inport->n_buffers); + inport->id, inio, inio->status, inio->buffer_id, inport->n_buffers); continue; } @@ -833,7 +839,7 @@ static int impl_node_process(void *object) maxsize = SPA_MIN(size, maxsize); spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d %d:%d/%d", this, - i, inio, outio, inio->status, inio->buffer_id, + inport->id, inio, outio, inio->status, inio->buffer_id, offs, size, this->stride); if (!SPA_FLAG_IS_SET(bd->chunk->flags, SPA_CHUNK_FLAG_EMPTY)) { @@ -913,15 +919,20 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void static int impl_clear(struct spa_handle *handle) { struct impl *this; - uint32_t i; + struct port *port; spa_return_val_if_fail(handle != NULL, -EINVAL); this = (struct impl *) handle; - for (i = 0; i < MAX_PORTS; i++) - free(this->in_ports[i]); - mix_ops_free(&this->ops); + spa_list_insert_list(&this->free_list, &this->port_list); + spa_list_consume(port, &this->free_list, link) { + spa_list_remove(&port->link); + free(port); + } + spa_list_init(&this->mix_list); + if (this->ops.free) + mix_ops_free(&this->ops); return 0; } @@ -974,6 +985,9 @@ impl_init(const struct spa_handle_factory *factory, } spa_hook_list_init(&this->hooks); + spa_list_init(&this->port_list); + spa_list_init(&this->free_list); + spa_list_init(&this->mix_list); this->node.iface = SPA_INTERFACE_INIT( SPA_TYPE_INTERFACE_Node, @@ -984,15 +998,16 @@ impl_init(const struct spa_handle_factory *factory, this->info.max_output_ports = 1; this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; this->info.flags = SPA_NODE_FLAG_RT | SPA_NODE_FLAG_IN_DYNAMIC_PORTS; + this->info_all = this->info.change_mask; port = GET_OUT_PORT(this, 0); - port->valid = true; port->direction = SPA_DIRECTION_OUTPUT; port->id = 0; port->info = SPA_PORT_INFO_INIT(); port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA; port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->info_all = port->info.change_mask; port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); diff --git a/spa/plugins/audiomixer/mixer-dsp.c b/spa/plugins/audiomixer/mixer-dsp.c index fb396c14e..5bd4e1a1e 100644 --- a/spa/plugins/audiomixer/mixer-dsp.c +++ b/spa/plugins/audiomixer/mixer-dsp.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -27,6 +28,7 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.mixer-dsp"); #define MAX_BUFFERS 64 +#define MAX_DATAS SPA_AUDIO_MAX_CHANNELS #define MAX_PORTS 512 #define MAX_ALIGN MIX_OPS_MAX_ALIGN @@ -47,15 +49,20 @@ static void port_props_reset(struct port_props *props) struct buffer { uint32_t id; #define BUFFER_FLAG_QUEUED (1 << 0) +#define BUFFER_FLAG_MAPPED (1 << 1) uint32_t flags; struct spa_list link; struct spa_buffer *buffer; struct spa_meta_header *h; struct spa_buffer buf; + + void *datas[MAX_DATAS]; }; struct port { + struct spa_list link; + uint32_t direction; uint32_t id; @@ -67,7 +74,6 @@ struct port { struct spa_port_info info; struct spa_param_info params[8]; - unsigned int valid:1; unsigned int have_format:1; struct buffer buffers[MAX_BUFFERS]; @@ -75,6 +81,9 @@ struct port { struct spa_list queue; size_t queued_bytes; + + struct spa_list mix_link; + bool active:1; }; struct impl { @@ -100,10 +109,10 @@ struct impl { struct spa_hook_list hooks; - uint32_t port_count; - uint32_t last_port; struct port *in_ports[MAX_PORTS]; struct port out_ports[1]; + struct spa_list port_list; + struct spa_list free_list; struct buffer *mix_buffers[MAX_PORTS]; const void *mix_datas[MAX_PORTS]; @@ -114,12 +123,13 @@ struct impl { unsigned int have_format:1; unsigned int started:1; + + struct spa_list mix_list; }; -#define PORT_VALID(p) ((p) != NULL && (p)->valid) #define CHECK_ANY_IN(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == SPA_ID_INVALID) -#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && !PORT_VALID(this->in_ports[(p)])) -#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && PORT_VALID(this->in_ports[(p)])) +#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && this->in_ports[(p)] == NULL) +#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && this->in_ports[(p)] != NULL) #define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) #define CHECK_PORT(this,d,p) (CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p)) #define CHECK_PORT_ANY(this,d,p) (CHECK_ANY_IN(this,d,p) || CHECK_PORT(this,d,p)) @@ -205,7 +215,7 @@ static int impl_node_add_listener(void *object, { struct impl *this = object; struct spa_hook_list save; - uint32_t i; + struct port *port; spa_return_val_if_fail(this != NULL, -EINVAL); @@ -213,10 +223,8 @@ static int impl_node_add_listener(void *object, emit_node_info(this, true); emit_port_info(this, GET_OUT_PORT(this, 0), true); - for (i = 0; i < this->last_port; i++) { - if (PORT_VALID(this->in_ports[i])) - emit_port_info(this, GET_IN_PORT(this, i), true); - } + spa_list_for_each(port, &this->port_list, link) + emit_port_info(this, port, true); spa_hook_list_join(&this->hooks, &save); @@ -231,6 +239,18 @@ impl_node_set_callbacks(void *object, return 0; } +static struct port *get_free_port(struct impl *this) +{ + struct port *port; + if (!spa_list_is_empty(&this->free_list)) { + port = spa_list_first(&this->free_list, struct port, link); + spa_list_remove(&port->link); + } else { + port = calloc(1, sizeof(struct port)); + } + return port; +} + static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, const struct spa_dict *props) { @@ -240,13 +260,8 @@ static int impl_node_add_port(void *object, enum spa_direction direction, uint32 spa_return_val_if_fail(this != NULL, -EINVAL); spa_return_val_if_fail(CHECK_FREE_IN_PORT(this, direction, port_id), -EINVAL); - port = GET_IN_PORT (this, port_id); - if (port == NULL) { - port = calloc(1, sizeof(struct port)); - if (port == NULL) - return -errno; - this->in_ports[port_id] = port; - } + if ((port = get_free_port(this)) == NULL) + return -errno; port->direction = direction; port->id = port_id; @@ -269,13 +284,10 @@ static int impl_node_add_port(void *object, enum spa_direction direction, uint32 port->info.params = port->params; port->info.n_params = 5; - this->port_count++; - if (this->last_port <= port_id) - this->last_port = port_id + 1; - port->valid = true; + this->in_ports[port_id] = port; + spa_list_append(&this->port_list, &port->link); - spa_log_debug(this->log, "%p: add port %d:%d %d", this, - direction, port_id, this->last_port); + spa_log_debug(this->log, "%p: add port %d:%d", this, direction, port_id); emit_port_info(this, port, true); return 0; @@ -291,26 +303,18 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_ spa_return_val_if_fail(CHECK_IN_PORT(this, direction, port_id), -EINVAL); port = GET_IN_PORT (this, port_id); + this->in_ports[port_id] = NULL; + spa_list_remove(&port->link); - port->valid = false; - this->port_count--; if (port->have_format && this->have_format) { if (--this->n_formats == 0) this->have_format = false; } spa_memzero(port, sizeof(struct port)); + spa_list_append(&this->free_list, &port->link); - if (port_id + 1 == this->last_port) { - int i; - - for (i = this->last_port - 1; i >= 0; i--) - if (PORT_VALID(GET_IN_PORT(this, i))) - break; - - this->last_port = i + 1; - } - spa_log_debug(this->log, "%p: remove port %d:%d %d", this, - direction, port_id, this->last_port); + spa_log_debug(this->log, "%p: remove port %d:%d", this, + direction, port_id); spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); @@ -451,11 +455,25 @@ next: static int clear_buffers(struct impl *this, struct port *port) { - if (port->n_buffers > 0) { - spa_log_debug(this->log, "%p: clear buffers %p", this, port); - port->n_buffers = 0; - spa_list_init(&port->queue); + uint32_t i, j; + + spa_log_debug(this->log, "%p: clear buffers %p %d", this, port, port->n_buffers); + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) { + for (j = 0; j < b->buffer->n_datas; j++) { + if (b->datas[j]) { + spa_log_debug(this->log, "%p: unmap buffer %d data %d %p", + this, i, j, b->datas[j]); + munmap(b->datas[j], b->buffer->datas[j].maxsize); + b->datas[j] = NULL; + } + } + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_MAPPED); + } } + port->n_buffers = 0; + spa_list_init(&port->queue); return 0; } @@ -582,7 +600,8 @@ impl_node_port_use_buffers(void *object, { struct impl *this = object; struct port *port; - uint32_t i; + uint32_t i, j; + int res; spa_return_val_if_fail(this != NULL, -EINVAL); @@ -595,17 +614,27 @@ impl_node_port_use_buffers(void *object, spa_return_val_if_fail(!this->started || port->io == NULL, -EIO); - clear_buffers(this, port); + if (n_buffers > 0 && !port->have_format) { + res = -EIO; + goto error; + } + if (n_buffers > MAX_BUFFERS) { + res = -ENOSPC; + goto error; + } - if (n_buffers > 0 && !port->have_format) - return -EIO; - if (n_buffers > MAX_BUFFERS) - return -ENOSPC; + clear_buffers(this, port); for (i = 0; i < n_buffers; i++) { struct buffer *b; + uint32_t n_datas = buffers[i]->n_datas; struct spa_data *d = buffers[i]->datas; + if (n_datas > MAX_DATAS) { + res = -ENOSPC; + goto error; + } + b = &port->buffers[i]; b->buffer = buffers[i]; b->flags = 0; @@ -613,26 +642,55 @@ impl_node_port_use_buffers(void *object, b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); b->buf = *buffers[i]; - if (d[0].data == NULL) { - spa_log_error(this->log, "%p: invalid memory on buffer %d", this, i); - return -EINVAL; - } - if (!SPA_IS_ALIGNED(d[0].data, this->max_align)) { - spa_log_warn(this->log, "%p: memory on buffer %d not aligned", this, i); + for (j = 0; j < n_datas; j++) { + void *data = d[j].data; + if (data == NULL && SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_MAPPABLE)) { + int prot = 0; + if (SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_READABLE)) + prot |= PROT_READ; + if (SPA_FLAG_IS_SET(d[j].flags, SPA_DATA_FLAG_WRITABLE)) + prot |= PROT_WRITE; + data = mmap(NULL, d[j].maxsize, + prot, MAP_SHARED, d[j].fd, d[j].mapoffset); + if (data == MAP_FAILED) { + spa_log_error(this->log, "%p: mmap failed %d on buffer %d %d %p: %m", + this, j, i, d[j].type, data); + res = -EINVAL; + goto error; + } + SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); + spa_log_debug(this->log, "%p: mmap %d on buffer %d %d %p %p", + this, j, i, d[j].type, data, b); + } + if (data == NULL) { + spa_log_error(this->log, "%p: invalid memory %d on buffer %d %d %p", + this, j, i, d[j].type, data); + res = -EINVAL; + goto error; + } else if (!SPA_IS_ALIGNED(data, this->max_align)) { + spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned", + this, j, i); + } + + d[j].data = b->datas[j] = data; } if (direction == SPA_DIRECTION_OUTPUT) queue_buffer(this, port, b); + port->n_buffers++; spa_log_debug(this->log, "%p: port %d:%d buffer:%d n_data:%d data:%p maxsize:%d", this, direction, port_id, i, buffers[i]->n_datas, d[0].data, d[0].maxsize); } - port->n_buffers = n_buffers; return 0; +error: + clear_buffers(this, port); + return res; } struct io_info { + struct impl *impl; struct port *port; void *data; size_t size; @@ -642,16 +700,28 @@ static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { struct io_info *info = user_data; - if (info->size >= sizeof(struct spa_io_async_buffers)) { - struct spa_io_async_buffers *ab = info->data; - info->port->io[0] = &ab->buffers[info->port->direction]; - info->port->io[1] = &ab->buffers[info->port->direction^1]; - } else if (info->size >= sizeof(struct spa_io_buffers)) { - info->port->io[0] = info->data; - info->port->io[1] = info->data; + struct port *port = info->port; + + if (info->data == NULL || info->size < sizeof(struct spa_io_buffers)) { + port->io[0] = NULL; + port->io[1] = NULL; + if (port->active) { + spa_list_remove(&port->mix_link); + port->active = false; + } } else { - info->port->io[0] = NULL; - info->port->io[1] = NULL; + if (info->size >= sizeof(struct spa_io_async_buffers)) { + struct spa_io_async_buffers *ab = info->data; + port->io[0] = &ab->buffers[port->direction]; + port->io[1] = &ab->buffers[port->direction^1]; + } else { + port->io[0] = info->data; + port->io[1] = info->data; + } + if (!port->active) { + spa_list_append(&info->impl->mix_list, &port->mix_link); + port->active = true; + } } return 0; } @@ -673,6 +743,7 @@ impl_node_port_set_io(void *object, spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); port = GET_PORT(this, direction, port_id); + info.impl = this; info.port = port; info.data = data; info.size = size; @@ -680,8 +751,8 @@ impl_node_port_set_io(void *object, switch (id) { case SPA_IO_Buffers: case SPA_IO_AsyncBuffers: - spa_loop_invoke(this->data_loop, - do_port_set_io, SPA_ID_INVALID, NULL, 0, true, &info); + spa_loop_locked(this->data_loop, + do_port_set_io, SPA_ID_INVALID, NULL, 0, &info); break; default: return -ENOENT; @@ -707,13 +778,14 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t static int impl_node_process(void *object) { struct impl *this = object; - struct port *outport; + struct port *outport, *inport; struct spa_io_buffers *outio; - uint32_t n_buffers, i, maxsize; + uint32_t n_buffers, maxsize; struct buffer **buffers; struct buffer *outb; const void **datas; uint32_t cycle = this->position->clock.cycle & 1; + struct spa_data *d; spa_return_val_if_fail(this != NULL, -EINVAL); @@ -739,24 +811,17 @@ static int impl_node_process(void *object) maxsize = UINT32_MAX; - for (i = 0; i < this->last_port; i++) { - struct port *inport = GET_IN_PORT(this, i); - struct spa_io_buffers *inio = NULL; + spa_list_for_each(inport, &this->mix_list, mix_link) { + struct spa_io_buffers *inio = inport->io[cycle]; struct buffer *inb; struct spa_data *bd; uint32_t size, offs; - if (SPA_UNLIKELY(!PORT_VALID(inport) || (inio = inport->io[cycle]) == NULL)) { - spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d io:%p/%p/%d", - this, i, PORT_VALID(inport), - inport->io[0], inport->io[1], cycle); - continue; - } if (inio->buffer_id >= inport->n_buffers || inio->status != SPA_STATUS_HAVE_DATA) { spa_log_trace_fp(this->log, "%p: skip input idx:%d " "io:%p status:%d buf_id:%d n_buffers:%d", this, - i, inio, inio->status, inio->buffer_id, inport->n_buffers); + inport->id, inio, inio->status, inio->buffer_id, inport->n_buffers); continue; } @@ -768,7 +833,7 @@ static int impl_node_process(void *object) maxsize = SPA_MIN(maxsize, size); spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d/%d %d:%d/%d %u", this, - i, inio, outio, inio->status, inio->buffer_id, inport->n_buffers, + inport->id, inio, outio, inio->status, inio->buffer_id, inport->n_buffers, offs, size, (int)sizeof(float), bd->chunk->flags); @@ -787,11 +852,12 @@ static int impl_node_process(void *object) return -EPIPE; } - if (n_buffers == 1) { + d = outb->buf.datas; + + if (n_buffers == 1 && SPA_FLAG_IS_SET(d[0].flags, SPA_DATA_FLAG_DYNAMIC)) { + spa_log_trace_fp(this->log, "%p: %d passthrough", this, n_buffers); *outb->buffer = *buffers[0]->buffer; } else { - struct spa_data *d = outb->buf.datas; - *outb->buffer = outb->buf; maxsize = SPA_MIN(maxsize, d[0].maxsize); @@ -851,14 +917,17 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void static int impl_clear(struct spa_handle *handle) { struct impl *this; - uint32_t i; + struct port *port; spa_return_val_if_fail(handle != NULL, -EINVAL); this = (struct impl *) handle; - for (i = 0; i < MAX_PORTS; i++) - free(this->in_ports[i]); + spa_list_insert_list(&this->free_list, &this->port_list); + spa_list_consume(port, &this->free_list, link) { + spa_list_remove(&port->link); + free(port); + } return 0; } @@ -911,6 +980,9 @@ impl_init(const struct spa_handle_factory *factory, } spa_hook_list_init(&this->hooks); + spa_list_init(&this->port_list); + spa_list_init(&this->free_list); + spa_list_init(&this->mix_list); this->node.iface = SPA_INTERFACE_INIT( SPA_TYPE_INTERFACE_Node, @@ -921,15 +993,16 @@ impl_init(const struct spa_handle_factory *factory, this->info.max_output_ports = 1; this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; this->info.flags = SPA_NODE_FLAG_RT | SPA_NODE_FLAG_IN_DYNAMIC_PORTS; + this->info_all = this->info.change_mask; port = GET_OUT_PORT(this, 0); - port->valid = true; port->direction = SPA_DIRECTION_OUTPUT; port->id = 0; port->info = SPA_PORT_INFO_INIT(); port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; port->info.flags = SPA_PORT_FLAG_DYNAMIC_DATA; port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->info_all = port->info.change_mask; port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); diff --git a/spa/plugins/audiotestsrc/audiotestsrc.c b/spa/plugins/audiotestsrc/audiotestsrc.c index 273e4a5a3..a3ad10230 100644 --- a/spa/plugins/audiotestsrc/audiotestsrc.c +++ b/spa/plugins/audiotestsrc/audiotestsrc.c @@ -83,6 +83,7 @@ struct port { struct spa_io_buffers *io; struct spa_io_sequence *io_control; + uint32_t io_control_size; bool have_format; struct spa_audio_info current_format; @@ -872,6 +873,7 @@ impl_node_port_set_io(void *object, break; case SPA_IO_Control: port->io_control = data; + port->io_control_size = size; break; default: return -ENOENT; @@ -909,16 +911,24 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t return 0; } -static int process_control(struct impl *this, struct spa_pod_sequence *sequence) +static int process_control(struct impl *this, struct spa_pod_sequence *sequence, uint32_t size) { - struct spa_pod_control *c; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + const void *seq_body, *c_body; + struct spa_pod_control c; - SPA_POD_SEQUENCE_FOREACH(sequence, c) { - switch (c->type) { + spa_pod_parser_init_from_data(&parser, sequence, size, 0, size); + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) + return 0; + + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { + switch (c.type) { case SPA_CONTROL_Properties: { struct props *p = &this->props; - spa_pod_parse_object(&c->value, + spa_pod_body_parse_object(&c.value, c_body, SPA_TYPE_OBJECT_Props, NULL, SPA_PROP_frequency, SPA_POD_OPT_Float(&p->freq), SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume)); @@ -946,7 +956,7 @@ static int impl_node_process(void *object) return -EIO; if (port->io_control) - process_control(this, &port->io_control->sequence); + process_control(this, &port->io_control->sequence, port->io_control_size); if (io->status == SPA_STATUS_HAVE_DATA) return SPA_STATUS_HAVE_DATA; @@ -1013,7 +1023,7 @@ static int impl_clear(struct spa_handle *handle) this = (struct impl *) handle; if (this->data_loop) - spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_timer, 0, NULL, 0, this); spa_system_close(this->data_system, this->timer_source.fd); return 0; diff --git a/spa/plugins/avb/avb-pcm.c b/spa/plugins/avb/avb-pcm.c index 3f608d7f4..5585cb141 100644 --- a/spa/plugins/avb/avb-pcm.c +++ b/spa/plugins/avb/avb-pcm.c @@ -40,7 +40,13 @@ static int avb_set_param(struct state *state, const char *k, const char *s) state->default_format = spa_type_audio_format_from_short_name(s); fmt_change++; } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { - spa_audio_parse_position(s, strlen(s), state->default_pos.pos, + spa_audio_parse_position_n(s, strlen(s), state->default_pos.pos, + SPA_N_ELEMENTS(state->default_pos.pos), + &state->default_pos.channels); + fmt_change++; + } else if (spa_streq(k, SPA_KEY_AUDIO_LAYOUT)) { + spa_audio_parse_layout(s, state->default_pos.pos, + SPA_N_ELEMENTS(state->default_pos.pos), &state->default_pos.channels); fmt_change++; } else if (spa_streq(k, SPA_KEY_AUDIO_ALLOWED_RATES)) { @@ -85,11 +91,12 @@ static int position_to_string(struct channel_map *map, char *val, size_t len) { uint32_t i, o = 0; int r; + char pos[8]; o += snprintf(val, len, "[ "); for (i = 0; i < map->channels; i++) { r = snprintf(val+o, len-o, "%s%s", i == 0 ? "" : ", ", - spa_debug_type_find_short_name(spa_type_audio_channel, - map->pos[i])); + spa_type_audio_channel_make_short_name(map->pos[i], + pos, sizeof(pos), "UNK")); if (r < 0 || o + r >= len) return -ENOSPC; o += r; @@ -1131,7 +1138,7 @@ int spa_avb_reassign_follower(struct state *state) if (following != state->following) { spa_log_debug(state->log, "%p: reassign follower %d->%d", state, state->following, following); state->following = following; - spa_loop_invoke(state->data_loop, do_reassign_follower, 0, NULL, 0, true, state); + spa_loop_locked(state->data_loop, do_reassign_follower, 0, NULL, 0, state); } freewheel = state->position && @@ -1208,7 +1215,7 @@ int spa_avb_pause(struct state *state) spa_log_debug(state->log, "%p: pause", state); - spa_loop_invoke(state->data_loop, do_remove_source, 0, NULL, 0, true, state); + spa_loop_locked(state->data_loop, do_remove_source, 0, NULL, 0, state); state->started = false; diff --git a/spa/plugins/avb/avb-pcm.h b/spa/plugins/avb/avb-pcm.h index d4dfa03fb..41f803d4c 100644 --- a/spa/plugins/avb/avb-pcm.h +++ b/spa/plugins/avb/avb-pcm.h @@ -5,10 +5,6 @@ #ifndef SPA_AVB_PCM_H #define SPA_AVB_PCM_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include @@ -37,6 +33,10 @@ extern "C" { #include "avb.h" +#ifdef __cplusplus +extern "C" { +#endif + #define MAX_RATES 16 #define DEFAULT_IFNAME "eth0" @@ -109,6 +109,7 @@ static inline char *format_streamid(char *str, size_t size, const uint64_t strea return str; } +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define MAX_BUFFERS 32 struct buffer { @@ -127,7 +128,7 @@ struct buffer { struct channel_map { uint32_t channels; - uint32_t pos[SPA_AUDIO_MAX_CHANNELS]; + uint32_t pos[MAX_CHANNELS]; }; struct port { diff --git a/spa/plugins/bluez5/README-Telephony.md b/spa/plugins/bluez5/README-Telephony.md index 93910b057..994480af0 100644 --- a/spa/plugins/bluez5/README-Telephony.md +++ b/spa/plugins/bluez5/README-Telephony.md @@ -102,10 +102,10 @@ NOTE: This method is implemented only on the `org.ofono.VoiceCallManager` interface, for compatibility. Call announcements are normally made available via the standard `org.freedesktop.DBus.ObjectManager` interface. -`object Dial(string number)` +`void Dial(string number)` -Initiates a new outgoing call. Returns the object path to the newly created -call. +Initiates a new outgoing call. If this succeeds, the new call is announced via +the signals. The number must be a string containing the following characters: `[0-9+*#,ABCD]{1,80}` In other words, it must be a non-empty string consisting @@ -176,12 +176,12 @@ Possible Errors: * org.pipewire.Telephony.Error.InvalidState * org.freedesktop.DBus.Error.Failed -`array{object} CreateMultiparty()` +`void CreateMultiparty()` Joins active and held calls together into a multi-party call. If one of the calls is already a multi-party call, then the other call is added to the -multiparty conversation. Returns the new list of calls participating in the -multiparty call. +multiparty conversation. Changes to the call objects are announced via the +signals. There can only be one subscriber controlled multi-party call according to the GSM specification. diff --git a/spa/plugins/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c index bf0b735f7..6d4db7fc4 100644 --- a/spa/plugins/bluez5/a2dp-codec-aac.c +++ b/spa/plugins/bluez5/a2dp-codec-aac.c @@ -200,7 +200,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, a2dp_aac_t conf; struct spa_pod_frame f[2]; struct spa_pod_choice *choice; - uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t position[2]; uint32_t i = 0; if (caps_size < sizeof(conf)) @@ -370,11 +370,14 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, /* If object type has multiple bits set (invalid per spec, see above), * assume the device usually means AAC-LC. */ - if (conf->object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC | - AAC_OBJECT_TYPE_MPEG4_AAC_LC)) { + if (conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LC) { res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC); if (res != AACENC_OK) goto error; + } else if (conf->object_type & AAC_OBJECT_TYPE_MPEG2_AAC_LC) { + res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_MP2_AAC_LC); + if (res != AACENC_OK) + goto error; } else if (conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_ELD) { res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_ER_AAC_ELD); if (res != AACENC_OK) @@ -578,7 +581,8 @@ static int codec_start_decode (void *data, const struct rtp_header *header = src; size_t header_size = sizeof(struct rtp_header); - spa_return_val_if_fail (src_size > header_size, -EINVAL); + if (src_size <= header_size) + return -EINVAL; if (seqnum) *seqnum = ntohs(header->sequence_number); @@ -680,6 +684,7 @@ static void codec_set_log(struct spa_log *global_log) const struct media_codec a2dp_codec_aac = { .id = SPA_BLUETOOTH_AUDIO_CODEC_AAC, + .kind = MEDIA_CODEC_A2DP, .codec_id = A2DP_CODEC_MPEG24, .name = "aac", .description = "AAC", @@ -705,6 +710,7 @@ const struct media_codec a2dp_codec_aac = { const struct media_codec a2dp_codec_aac_eld = { .id = SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD, + .kind = MEDIA_CODEC_A2DP, .codec_id = A2DP_CODEC_MPEG24, .name = "aac_eld", .description = "AAC-ELD", diff --git a/spa/plugins/bluez5/a2dp-codec-aptx.c b/spa/plugins/bluez5/a2dp-codec-aptx.c index e10691e78..75ceb229f 100644 --- a/spa/plugins/bluez5/a2dp-codec-aptx.c +++ b/spa/plugins/bluez5/a2dp-codec-aptx.c @@ -205,7 +205,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, a2dp_aptx_t conf; struct spa_pod_frame f[2]; struct spa_pod_choice *choice; - uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t position[2]; uint32_t i = 0; if (caps_size < sizeof(conf)) @@ -426,7 +426,8 @@ static int codec_start_decode (void *data, const struct rtp_header *header = src; size_t header_size = sizeof(struct rtp_header); - spa_return_val_if_fail(src_size > header_size, -EINVAL); + if (src_size <= header_size) + return -EINVAL; if (seqnum) *seqnum = ntohs(header->sequence_number); @@ -617,6 +618,7 @@ static int msbc_decode(void *data, const struct media_codec a2dp_codec_aptx = { .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX, + .kind = MEDIA_CODEC_A2DP, .codec_id = A2DP_CODEC_VENDOR, .vendor = { .vendor_id = APTX_VENDOR_ID, .codec_id = APTX_CODEC_ID }, @@ -641,6 +643,7 @@ const struct media_codec a2dp_codec_aptx = { const struct media_codec a2dp_codec_aptx_hd = { .id = SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, + .kind = MEDIA_CODEC_A2DP, .codec_id = A2DP_CODEC_VENDOR, .vendor = { .vendor_id = APTX_HD_VENDOR_ID, .codec_id = APTX_HD_CODEC_ID }, @@ -663,6 +666,7 @@ const struct media_codec a2dp_codec_aptx_hd = { }; #define APTX_LL_COMMON_DEFS \ + .kind = MEDIA_CODEC_A2DP, \ .codec_id = A2DP_CODEC_VENDOR, \ .description = "aptX-LL", \ .fill_caps = codec_fill_caps, \ diff --git a/spa/plugins/bluez5/a2dp-codec-caps.h b/spa/plugins/bluez5/a2dp-codec-caps.h index d775ce101..b58e33624 100644 --- a/spa/plugins/bluez5/a2dp-codec-caps.h +++ b/spa/plugins/bluez5/a2dp-codec-caps.h @@ -116,8 +116,10 @@ #define AAC_SAMPLING_FREQ_88200 0x0002 #define AAC_SAMPLING_FREQ_96000 0x0001 -#define AAC_CHANNELS_1 0x02 -#define AAC_CHANNELS_2 0x01 +#define AAC_CHANNELS_1 0x08 +#define AAC_CHANNELS_2 0x04 +#define AAC_CHANNELS_5_1 0x02 +#define AAC_CHANNELS_7_1 0x01 #define AAC_GET_BITRATE(a) ((a).bitrate1 << 16 | \ (a).bitrate2 << 8 | (a).bitrate3) @@ -341,8 +343,7 @@ typedef struct { typedef struct { uint8_t object_type; uint8_t frequency1; - uint8_t rfa:2; - uint8_t channels:2; + uint8_t channels:4; uint8_t frequency2:4; uint8_t bitrate1:7; uint8_t vbr:1; @@ -403,8 +404,7 @@ typedef struct { uint8_t object_type; uint8_t frequency1; uint8_t frequency2:4; - uint8_t channels:2; - uint8_t rfa:2; + uint8_t channels:4; uint8_t vbr:1; uint8_t bitrate1:7; uint8_t bitrate2; diff --git a/spa/plugins/bluez5/a2dp-codec-faststream.c b/spa/plugins/bluez5/a2dp-codec-faststream.c index bbf9e96ab..edc833292 100644 --- a/spa/plugins/bluez5/a2dp-codec-faststream.c +++ b/spa/plugins/bluez5/a2dp-codec-faststream.c @@ -114,7 +114,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, a2dp_faststream_t conf; struct spa_pod_frame f[2]; struct spa_pod_choice *choice; - uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t position[2]; uint32_t i = 0; if (caps_size < sizeof(conf)) @@ -301,6 +301,9 @@ static int codec_encode(void *data, static SPA_UNUSED int codec_start_decode (void *data, const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) { + if (!src_size) + return -EINVAL; + return 0; } @@ -584,6 +587,7 @@ static const struct media_codec duplex_codec = { }; #define FASTSTREAM_COMMON_DEFS \ + .kind = MEDIA_CODEC_A2DP, \ .codec_id = A2DP_CODEC_VENDOR, \ .vendor = { .vendor_id = FASTSTREAM_VENDOR_ID, \ .codec_id = FASTSTREAM_CODEC_ID }, \ diff --git a/spa/plugins/bluez5/a2dp-codec-lc3plus.c b/spa/plugins/bluez5/a2dp-codec-lc3plus.c index 05a3aa209..ee25fef0c 100644 --- a/spa/plugins/bluez5/a2dp-codec-lc3plus.c +++ b/spa/plugins/bluez5/a2dp-codec-lc3plus.c @@ -175,7 +175,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, a2dp_lc3plus_hr_t conf; struct spa_pod_frame f[2]; struct spa_pod_choice *choice; - uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t position[2]; uint32_t i = 0; if (caps_size < sizeof(conf)) @@ -637,7 +637,8 @@ static SPA_UNUSED int codec_start_decode (void *data, const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void); size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); - spa_return_val_if_fail (src_size > header_size, -EINVAL); + if (src_size <= header_size) + return -EINVAL; if (seqnum) *seqnum = ntohs(header->sequence_number); @@ -740,6 +741,7 @@ static int codec_increase_bitpool(void *data) const struct media_codec a2dp_codec_lc3plus_hr = { .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR, + .kind = MEDIA_CODEC_A2DP, .name = "lc3plus_hr", .codec_id = A2DP_CODEC_VENDOR, .vendor = { .vendor_id = LC3PLUS_HR_VENDOR_ID, diff --git a/spa/plugins/bluez5/a2dp-codec-ldac.c b/spa/plugins/bluez5/a2dp-codec-ldac.c index f5d0b155f..11185800e 100644 --- a/spa/plugins/bluez5/a2dp-codec-ldac.c +++ b/spa/plugins/bluez5/a2dp-codec-ldac.c @@ -19,6 +19,13 @@ #include #endif +#if defined(ENABLE_LDAC_DEC) +int ldacBT_decode(HANDLE_LDAC_BT handle, unsigned char *src, unsigned char *dst, + LDACBT_SMPL_FMT_T fmt, int src_size, int *consumed, int *dst_out); +int ldacBT_init_handle_decode(HANDLE_LDAC_BT handle, int channel_mode, int frequency, + int dummy1, int dummy2, int dummy3); +#endif + #include "rtp.h" #include "media-codecs.h" @@ -35,15 +42,25 @@ #define LDAC_ABR_SOCK_BUFFER_SIZE (LDAC_ABR_THRESHOLD_CRITICAL * LDAC_ABR_MAX_PACKET_NBYTES) +static struct spa_log *log_; + struct props { int eqmid; }; +struct dec_data { + int frames; + size_t max_frame_bytes; +}; + struct impl { HANDLE_LDAC_BT ldac; #ifdef ENABLE_LDAC_ABR HANDLE_LDAC_ABR ldac_abr; +#endif +#ifdef ENABLE_LDAC_DEC + HANDLE_LDAC_BT ldac_dec; #endif bool enable_abr; @@ -57,6 +74,8 @@ struct impl { int codesize; int frame_length; int frame_count; + + struct dec_data d; }; static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, @@ -139,7 +158,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, struct spa_pod_frame f[2]; struct spa_pod_choice *choice; uint32_t i = 0; - uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t position[2]; if (caps_size < sizeof(conf)) return -EINVAL; @@ -367,6 +386,66 @@ static int codec_set_props(void *props, const struct spa_pod *param) return prev_eqmid != p->eqmid; } +static const char *ldac_strerror(int ldac_error) +{ +#define LDAC_BT_ERROR_CASE(name) case name: return #name + switch (ldac_error) { + LDAC_BT_ERROR_CASE(LDACBT_ERR_NONE); + LDAC_BT_ERROR_CASE(LDACBT_ERR_NON_FATAL); + LDAC_BT_ERROR_CASE(LDACBT_ERR_BIT_ALLOCATION); + LDAC_BT_ERROR_CASE(LDACBT_ERR_NOT_IMPLEMENTED); + LDAC_BT_ERROR_CASE(LDACBT_ERR_NON_FATAL_ENCODE); + LDAC_BT_ERROR_CASE(LDACBT_ERR_FATAL); + LDAC_BT_ERROR_CASE(LDACBT_ERR_SYNTAX_BAND); + LDAC_BT_ERROR_CASE(LDACBT_ERR_SYNTAX_GRAD_A); + LDAC_BT_ERROR_CASE(LDACBT_ERR_SYNTAX_GRAD_B); + LDAC_BT_ERROR_CASE(LDACBT_ERR_SYNTAX_GRAD_C); + LDAC_BT_ERROR_CASE(LDACBT_ERR_SYNTAX_GRAD_D); + LDAC_BT_ERROR_CASE(LDACBT_ERR_SYNTAX_GRAD_E); + LDAC_BT_ERROR_CASE(LDACBT_ERR_SYNTAX_IDSF); + LDAC_BT_ERROR_CASE(LDACBT_ERR_SYNTAX_SPEC); + LDAC_BT_ERROR_CASE(LDACBT_ERR_BIT_PACKING); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ALLOC_MEMORY); + LDAC_BT_ERROR_CASE(LDACBT_ERR_FATAL_HANDLE); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ILL_SYNCWORD); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ILL_SMPL_FORMAT); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ILL_PARAM); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ASSERT_SAMPLING_FREQ); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ASSERT_SUP_SAMPLING_FREQ); + LDAC_BT_ERROR_CASE(LDACBT_ERR_CHECK_SAMPLING_FREQ); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ASSERT_CHANNEL_CONFIG); + LDAC_BT_ERROR_CASE(LDACBT_ERR_CHECK_CHANNEL_CONFIG); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ASSERT_FRAME_LENGTH); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ASSERT_SUP_FRAME_LENGTH); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ASSERT_FRAME_STATUS); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ASSERT_NSHIFT); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ASSERT_CHANNEL_MODE); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ENC_INIT_ALLOC); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ENC_ILL_GRADMODE); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ENC_ILL_GRADPAR_A); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ENC_ILL_GRADPAR_B); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ENC_ILL_GRADPAR_C); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ENC_ILL_GRADPAR_D); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ENC_ILL_NBANDS); + LDAC_BT_ERROR_CASE(LDACBT_ERR_PACK_BLOCK_FAILED); + LDAC_BT_ERROR_CASE(LDACBT_ERR_DEC_INIT_ALLOC); + LDAC_BT_ERROR_CASE(LDACBT_ERR_INPUT_BUFFER_SIZE); + LDAC_BT_ERROR_CASE(LDACBT_ERR_UNPACK_BLOCK_FAILED); + LDAC_BT_ERROR_CASE(LDACBT_ERR_UNPACK_BLOCK_ALIGN); + LDAC_BT_ERROR_CASE(LDACBT_ERR_UNPACK_FRAME_ALIGN); + LDAC_BT_ERROR_CASE(LDACBT_ERR_FRAME_LENGTH_OVER); + LDAC_BT_ERROR_CASE(LDACBT_ERR_FRAME_ALIGN_OVER); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ALTER_EQMID_LIMITED); + LDAC_BT_ERROR_CASE(LDACBT_ERR_HANDLE_NOT_INIT); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ILL_EQMID); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ILL_SAMPLING_FREQ); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ILL_NUM_CHANNEL); + LDAC_BT_ERROR_CASE(LDACBT_ERR_ILL_MTU_SIZE); + LDAC_BT_ERROR_CASE(LDACBT_ERR_DEC_CONFIG_UPDATED); + default: return "other error"; + } +} + static void *codec_init(const struct media_codec *codec, uint32_t flags, void *config, size_t config_len, const struct spa_audio_info *info, void *props, size_t mtu) @@ -384,6 +463,12 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, if (this->ldac == NULL) goto error_errno; +#ifdef ENABLE_LDAC_DEC + this->ldac_dec = ldacBT_get_handle(); + if (this->ldac_dec == NULL) + goto error_errno; +#endif + #ifdef ENABLE_LDAC_ABR this->ldac_abr = ldac_ABR_get_handle(); if (this->ldac_abr == NULL) @@ -424,14 +509,34 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, goto error; } + this->d.max_frame_bytes = (size_t)this->codesize * LDACBT_MAX_LSU / LDACBT_ENC_LSU; + res = ldacBT_init_handle_encode(this->ldac, this->mtu, this->eqmid, conf->channel_mode, this->fmt, this->frequency); - if (res < 0) + if (res < 0) { + res = ldacBT_get_error_code(this->ldac); + spa_log_error(log_, "LDAC encoder initialization failed: %s (%d)", + ldac_strerror(LDACBT_API_ERR(res)), res); + res = -EIO; goto error; + } + +#ifdef ENABLE_LDAC_DEC + res = ldacBT_init_handle_decode(this->ldac_dec, + conf->channel_mode, + this->frequency, 0, 0, 0); + if (res < 0) { + res = ldacBT_get_error_code(this->ldac_dec); + spa_log_error(log_, "LDAC decoder initialization failed: %s (%d)", + ldac_strerror(LDACBT_API_ERR(res)), res); + res = -EIO; + goto error; + } +#endif #ifdef ENABLE_LDAC_ABR res = ldac_ABR_Init(this->ldac_abr, LDAC_ABR_INTERVAL_MS); @@ -552,6 +657,71 @@ static int codec_encode(void *data, return src_used; } +#ifdef ENABLE_LDAC_DEC + +static int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + struct impl *this = data; + const struct rtp_header *header = src; + const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), struct rtp_payload); + size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); + + if (src_size <= header_size) + return -EINVAL; + + if (seqnum) + *seqnum = ntohs(header->sequence_number); + if (timestamp) + *timestamp = ntohl(header->timestamp); + + this->d.frames = payload->frame_count; + + return header_size; +} + +static int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + void *to = dst; + size_t avail = dst_size; + size_t processed = 0; + + *dst_out = 0; + + for (; this->d.frames > 0; --this->d.frames) { + int consumed; + int written; + + if (avail < this->d.max_frame_bytes) + return -EINVAL; + + if (ldacBT_decode(this->ldac_dec, (void *)src, to, this->fmt, src_size, + &consumed, &written) != 0) { + return -EINVAL; + } + if (consumed < 0 || (size_t)consumed > src_size) + return -EINVAL; + if (written < 0 || (size_t)written > avail) + return -EINVAL; + + src = SPA_PTROFF(src, consumed, const void); + src_size -= consumed; + processed += consumed; + + to = SPA_PTROFF(to, written, void); + avail -= written; + *dst_out += written; + } + + return processed; +} + +#endif + static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) { struct impl *this = data; @@ -571,8 +741,15 @@ static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) *decoder = 0; } +static void codec_set_log(struct spa_log *global_log) +{ + log_ = global_log; + spa_log_topic_init(log_, &codec_plugin_log_topic); +} + const struct media_codec a2dp_codec_ldac = { .id = SPA_BLUETOOTH_AUDIO_CODEC_LDAC, + .kind = MEDIA_CODEC_A2DP, .codec_id = A2DP_CODEC_VENDOR, .vendor = { .vendor_id = LDAC_VENDOR_ID, .codec_id = LDAC_CODEC_ID }, @@ -595,9 +772,14 @@ const struct media_codec a2dp_codec_ldac = { .abr_process = codec_abr_process, .start_encode = codec_start_encode, .encode = codec_encode, +#ifdef ENABLE_LDAC_DEC + .start_decode = codec_start_decode, + .decode = codec_decode, +#endif .reduce_bitpool = codec_reduce_bitpool, .increase_bitpool = codec_increase_bitpool, .get_delay = codec_get_delay, + .set_log = codec_set_log, }; MEDIA_CODEC_EXPORT_DEF( diff --git a/spa/plugins/bluez5/a2dp-codec-opus-g.c b/spa/plugins/bluez5/a2dp-codec-opus-g.c index 85a36bf8d..130dea433 100644 --- a/spa/plugins/bluez5/a2dp-codec-opus-g.c +++ b/spa/plugins/bluez5/a2dp-codec-opus-g.c @@ -164,7 +164,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, { a2dp_opus_g_t conf; struct spa_pod_frame f[1]; - uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t position[2]; int channels; if (caps_size < sizeof(conf)) @@ -444,7 +444,8 @@ static int codec_start_decode (void *data, const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void); size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); - spa_return_val_if_fail (src_size > header_size, -EINVAL); + if (src_size <= header_size) + return -EINVAL; if (seqnum) *seqnum = ntohs(header->sequence_number); @@ -512,6 +513,7 @@ static void codec_set_log(struct spa_log *global_log) const struct media_codec a2dp_codec_opus_g = { .id = SPA_BLUETOOTH_AUDIO_CODEC_OPUS_G, + .kind = MEDIA_CODEC_A2DP, .codec_id = A2DP_CODEC_VENDOR, .vendor = { .vendor_id = OPUS_G_VENDOR_ID, .codec_id = OPUS_G_CODEC_ID }, diff --git a/spa/plugins/bluez5/a2dp-codec-opus.c b/spa/plugins/bluez5/a2dp-codec-opus.c index e65b62fac..c5b042b20 100644 --- a/spa/plugins/bluez5/a2dp-codec-opus.c +++ b/spa/plugins/bluez5/a2dp-codec-opus.c @@ -58,6 +58,8 @@ static struct spa_log *log; #define BITRATE_DUPLEX_BIDI 160000 +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS + #define OPUS_05_MAX_BYTES (15 * 1024) struct props { @@ -251,14 +253,8 @@ static const struct surround_encoder_mapping surround_encoders[] = { static uint32_t bt_channel_from_name(const char *name) { size_t i; - enum spa_audio_channel position = SPA_AUDIO_CHANNEL_UNKNOWN; + enum spa_audio_channel position = spa_type_audio_channel_from_short_name(name); - for (i = 0; spa_type_audio_channel[i].name; i++) { - if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name))) { - position = spa_type_audio_channel[i].type; - break; - } - } for (i = 0; i < SPA_N_ELEMENTS(audio_locations); i++) { if (position == audio_locations[i].position) return audio_locations[i].mask; @@ -313,14 +309,14 @@ static void parse_settings(struct props *props, const struct spa_dict *settings) return; if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.channels"), &v, 0)) - props->channels = SPA_CLAMP(v, 1u, SPA_AUDIO_MAX_CHANNELS); + props->channels = SPA_CLAMP(v, 1u, MAX_CHANNELS); if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.max-bitrate"), &v, 0)) props->max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN); if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.coupled-streams"), &v, 0)) props->coupled_streams = SPA_CLAMP(v, 0u, props->channels / 2); if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.channels"), &v, 0)) - props->bidi_channels = SPA_CLAMP(v, 0u, SPA_AUDIO_MAX_CHANNELS); + props->bidi_channels = SPA_CLAMP(v, 0u, MAX_CHANNELS); if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.max-bitrate"), &v, 0)) props->bidi_max_bitrate = SPA_MAX(v, (uint32_t)BITRATE_MIN); if (spa_atou32(spa_dict_lookup(settings, "bluez5.a2dp.opus.pro.bidi.coupled-streams"), &v, 0)) @@ -497,13 +493,13 @@ static int get_mapping(const struct media_codec *codec, const a2dp_opus_05_direc bool use_surround_encoder, uint8_t *streams_ret, uint8_t *coupled_streams_ret, const uint8_t **surround_mapping, uint32_t *positions) { - const uint8_t channels = conf->channels; + const uint32_t channels = conf->channels; const uint32_t location = OPUS_05_GET_LOCATION(*conf); const uint8_t coupled_streams = conf->coupled_streams; const uint8_t *permutation = NULL; size_t i, j; - if (channels > SPA_AUDIO_MAX_CHANNELS) + if (channels > MAX_CHANNELS) return -EINVAL; if (2 * coupled_streams > channels) return -EINVAL; @@ -542,10 +538,9 @@ static int get_mapping(const struct media_codec *codec, const a2dp_opus_05_direc const struct audio_location loc = audio_locations[i]; if (location & loc.mask) { - if (permutation) - positions[permutation[j++]] = loc.position; - else - positions[j++] = loc.position; + uint32_t idx = permutation ? permutation[j] : j; + positions[idx] = loc.position; + j++; } } for (i = SPA_AUDIO_CHANNEL_START_Aux; j < channels; ++i, ++j) @@ -561,7 +556,7 @@ static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, a2dp_opus_05_t a2dp_opus_05 = { .info = codec->vendor, .main = { - .channels = SPA_AUDIO_MAX_CHANNELS, + .channels = SPA_MIN(255u, MAX_CHANNELS), .frame_duration = (OPUS_05_FRAME_DURATION_25 | OPUS_05_FRAME_DURATION_50 | OPUS_05_FRAME_DURATION_100 | @@ -571,7 +566,7 @@ static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, OPUS_05_INIT_BITRATE(0) }, .bidi = { - .channels = SPA_AUDIO_MAX_CHANNELS, + .channels = SPA_MIN(255u, MAX_CHANNELS), .frame_duration = (OPUS_05_FRAME_DURATION_25 | OPUS_05_FRAME_DURATION_50 | OPUS_05_FRAME_DURATION_100 | @@ -771,7 +766,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, a2dp_opus_05_t conf; a2dp_opus_05_direction_t *dir; struct spa_pod_frame f[1]; - uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t position[MAX_CHANNELS]; if (caps_size < sizeof(conf)) return -EINVAL; @@ -835,7 +830,8 @@ static int codec_validate_config(const struct media_codec *codec, uint32_t flags } info->info.raw.channels = dir1->channels; - if (get_mapping(codec, dir1, surround_encoder, NULL, NULL, NULL, info->info.raw.position) < 0) + if (get_mapping(codec, dir1, surround_encoder, NULL, NULL, NULL, + info->info.raw.position) < 0) return -EINVAL; if (get_mapping(codec, dir2, surround_encoder, NULL, NULL, NULL, NULL) < 0) return -EINVAL; @@ -1188,7 +1184,8 @@ static SPA_UNUSED int codec_start_decode (void *data, const struct rtp_payload *payload = SPA_PTROFF(src, sizeof(struct rtp_header), void); size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); - spa_return_val_if_fail (src_size > header_size, -EINVAL); + if (src_size <= header_size) + return -EINVAL; if (seqnum) *seqnum = ntohs(header->sequence_number); @@ -1349,6 +1346,7 @@ static void codec_set_log(struct spa_log *global_log) } #define OPUS_05_COMMON_DEFS \ + .kind = MEDIA_CODEC_A2DP, \ .codec_id = A2DP_CODEC_VENDOR, \ .vendor = { .vendor_id = OPUS_05_VENDOR_ID, \ .codec_id = OPUS_05_CODEC_ID }, \ diff --git a/spa/plugins/bluez5/a2dp-codec-sbc.c b/spa/plugins/bluez5/a2dp-codec-sbc.c index fc55a031d..a91109e52 100644 --- a/spa/plugins/bluez5/a2dp-codec-sbc.c +++ b/spa/plugins/bluez5/a2dp-codec-sbc.c @@ -346,7 +346,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, struct spa_pod_frame f[2]; struct spa_pod_choice *choice; uint32_t i = 0; - uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t position[2]; if (caps_size < sizeof(conf)) return -EINVAL; @@ -599,7 +599,8 @@ static int codec_start_decode (void *data, const struct rtp_header *header = src; size_t header_size = sizeof(struct rtp_header) + sizeof(struct rtp_payload); - spa_return_val_if_fail (src_size > header_size, -EINVAL); + if (src_size <= header_size) + return -EINVAL; if (seqnum) *seqnum = ntohs(header->sequence_number); @@ -634,6 +635,7 @@ static void codec_get_delay(void *data, uint32_t *encoder, uint32_t *decoder) const struct media_codec a2dp_codec_sbc = { .id = SPA_BLUETOOTH_AUDIO_CODEC_SBC, + .kind = MEDIA_CODEC_A2DP, .codec_id = A2DP_CODEC_SBC, .name = "sbc", .description = "SBC", @@ -657,6 +659,7 @@ const struct media_codec a2dp_codec_sbc = { const struct media_codec a2dp_codec_sbc_xq = { .id = SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ, + .kind = MEDIA_CODEC_A2DP, .codec_id = A2DP_CODEC_SBC, .name = "sbc_xq", .description = "SBC-XQ", diff --git a/spa/plugins/bluez5/asha-codec-g722.c b/spa/plugins/bluez5/asha-codec-g722.c index 1043b4fe8..3aaed84f2 100644 --- a/spa/plugins/bluez5/asha-codec-g722.c +++ b/spa/plugins/bluez5/asha-codec-g722.c @@ -44,10 +44,7 @@ static int codec_get_block_size(void *data) static int codec_start_encode (void *data, void *dst, size_t dst_size, uint16_t seqnum, uint32_t timestamp) { - /* Payload for ASHA must be preceded by 1-byte sequence number */ - *(uint8_t *)dst = seqnum % 256; - - return 1; + return 0; } static int codec_enum_config(const struct media_codec *codec, uint32_t flags, @@ -118,6 +115,7 @@ static int codec_encode(void *data, size_t *dst_out, int *need_flush) { struct impl *this = data; + uint8_t *dest = (uint8_t *)dst; size_t src_sz; int ret; @@ -133,13 +131,17 @@ static int codec_encode(void *data, src_sz = (src_size > this->codesize) ? this->codesize : src_size; - ret = g722_encode(&this->encode, dst, src, src_sz / 2 /* S16LE */); + /* Sequence number will be set in media-sink before flushing */ + *dest = 0; + dest++; + + ret = g722_encode(&this->encode, dest, src, src_sz / 2 /* S16LE */); if (ret < 0) { spa_log_error(spalog, "encode error: %d", ret); return -EIO; } - *dst_out = ret; + *dst_out = ret + ASHA_HEADER_SZ; *need_flush = NEED_FLUSH_ALL; return src_sz; @@ -153,9 +155,9 @@ static void codec_set_log(struct spa_log *global_log) const struct media_codec asha_codec_g722 = { .id = SPA_BLUETOOTH_AUDIO_CODEC_G722, + .kind = MEDIA_CODEC_ASHA, .codec_id = ASHA_CODEC_G722, .name = "g722", - .asha = true, .description = "G722", .fill_caps = NULL, .enum_config = codec_enum_config, diff --git a/spa/plugins/bluez5/backend-hsphfpd.c b/spa/plugins/bluez5/backend-hsphfpd.c index ddbae1c3c..01d81e1a3 100644 --- a/spa/plugins/bluez5/backend-hsphfpd.c +++ b/spa/plugins/bluez5/backend-hsphfpd.c @@ -19,6 +19,8 @@ #include #include "defs.h" +#include "media-codecs.h" +#include "hfp-codec-caps.h" SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.hsphfpd"); #undef SPA_LOG_TOPIC_DEFAULT @@ -659,8 +661,11 @@ static DBusHandlerResult hsphfpd_new_audio_connection(DBusConnection *conn, DBus goto fail; } - if (transport->codec != codec) - spa_log_warn(backend->log, "Expecting codec to be %d, got %d", transport->codec, codec); + if (transport->media_codec->codec_id != codec) { + spa_log_warn(backend->log, "Expecting codec to be %d, got %d", transport->media_codec->codec_id, codec); + r = dbus_message_new_error_printf(m, HSPHFPD_ERROR_REJECTED, "Endpoint %s has wrong codec", endpoint_path); + goto fail; + } if (transport->fd >= 0) { spa_log_error(backend->log, "Endpoint %s has already active transport", endpoint_path); @@ -879,7 +884,7 @@ static int hsphfpd_audio_acquire(void *data, bool optional) if (backend->acquire_in_progress) return -EINPROGRESS; - if (transport->codec == HFP_AUDIO_CODEC_MSBC) { + if (transport->media_codec->codec_id == HFP_AUDIO_CODEC_MSBC) { air_codec = HSPHFP_AIR_CODEC_MSBC; agent_codec = HSPHFP_AGENT_CODEC_MSBC; } @@ -953,6 +958,7 @@ static DBusHandlerResult hsphfpd_parse_endpoint_properties(struct impl *backend, DBusMessageIter element_i; struct spa_bt_device *d; struct spa_bt_transport *t; + const struct media_codec *codec; dbus_message_iter_recurse(i, &element_i); while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { @@ -1046,7 +1052,8 @@ static DBusHandlerResult hsphfpd_parse_endpoint_properties(struct impl *backend, if ((t = spa_bt_transport_find(backend->monitor, endpoint->path)) != NULL) { /* Release transport on disconnection, or when mSBC is supported if there is an update of the remote codecs */ - if (!endpoint->connected || (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC) && t->codec == HFP_AUDIO_CODEC_CVSD)) { + if (!endpoint->connected || (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC) && + t->media_codec->codec_id == HFP_AUDIO_CODEC_CVSD)) { spa_bt_transport_free(t); spa_bt_device_check_profiles(d, false); spa_log_debug(backend->log, "Transport released for %s", endpoint->path); @@ -1059,6 +1066,15 @@ static DBusHandlerResult hsphfpd_parse_endpoint_properties(struct impl *backend, if (!endpoint->valid || !endpoint->connected) return DBUS_HANDLER_RESULT_HANDLED; + if (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC)) + codec = spa_bt_get_hfp_codec(backend->monitor, HFP_AUDIO_CODEC_MSBC); + else + codec = spa_bt_get_hfp_codec(backend->monitor, HFP_AUDIO_CODEC_CVSD); + if (!codec) { + spa_log_error(backend->log, "cannot get codec for %s", endpoint->path); + return DBUS_HANDLER_RESULT_HANDLED; + } + char *t_path = strdup(endpoint->path); t = spa_bt_transport_create(backend->monitor, t_path, sizeof(struct hsphfpd_transport_data)); if (t == NULL) { @@ -1083,11 +1099,8 @@ static DBusHandlerResult hsphfpd_parse_endpoint_properties(struct impl *backend, else if (endpoint->role == HSPHFPD_ROLE_GATEWAY) t->profile = SPA_BT_PROFILE_HFP_AG; } - if (backend->msbc_supported && (endpoint->air_codecs & HFP_AUDIO_CODEC_MSBC)) - t->codec = HFP_AUDIO_CODEC_MSBC; - else - t->codec = HFP_AUDIO_CODEC_CVSD; + t->media_codec = codec; t->n_channels = 1; t->channels[0] = SPA_AUDIO_CHANNEL_MONO; @@ -1420,11 +1433,25 @@ static int backend_hsphfpd_free(void *data) return 0; } +static int backend_hsphfpd_supports_codec(void *data, struct spa_bt_device *device, unsigned int codec) +{ + struct impl *backend = data; + + switch (codec) { + case HFP_AUDIO_CODEC_CVSD: + return 1; + case HFP_AUDIO_CODEC_MSBC: + return backend->msbc_supported; + } + return 0; +} + static const struct spa_bt_backend_implementation backend_impl = { SPA_VERSION_BT_BACKEND_IMPLEMENTATION, .free = backend_hsphfpd_free, .register_profiles = backend_hsphfpd_register, .unregister_profiles = backend_hsphfpd_unregistered, + .supports_codec = backend_hsphfpd_supports_codec, }; static bool is_available(struct impl *backend) @@ -1479,6 +1506,9 @@ struct spa_bt_backend *backend_hsphfpd_new(struct spa_bt_monitor *monitor, else backend->msbc_supported = false; + if (!spa_bt_get_hfp_codec(monitor, HFP_AUDIO_CODEC_MSBC)) + backend->msbc_supported = false; + spa_log_topic_init(backend->log, &log_topic); spa_list_init(&backend->endpoint_list); diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 359696903..0dbe8c9ae 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -32,6 +32,8 @@ #include #include "defs.h" +#include "media-codecs.h" +#include "hfp-codec-caps.h" #ifdef HAVE_LIBUSB #include @@ -48,6 +50,8 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.native"); #define PROP_KEY_ROLES "bluez5.roles" #define PROP_KEY_HEADSET_ROLES "bluez5.headset-roles" #define PROP_KEY_HFP_DISABLE_NREC "bluez5.hfp-hf.disable-nrec" +#define PROP_KEY_HFP_DEFAULT_MIC_VOL "bluez5.hfp-hf.default-mic-volume" +#define PROP_KEY_HFP_DEFAULT_SPEAKER_VOL "bluez5.hfp-hf.default-speaker-volume" #define HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC 5000 #define HFP_CODEC_SWITCH_TIMEOUT_MSEC 20000 @@ -57,6 +61,8 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.native"); #define MAX_HF_INDICATORS 16 +#define RFCOMM_MESSAGE_MAX_LENGTH 256 + enum { HFP_AG_INITIAL_CODEC_SETUP_NONE = 0, HFP_AG_INITIAL_CODEC_SETUP_SEND, @@ -97,9 +103,13 @@ struct impl { struct spa_dbus *dbus; DBusConnection *conn; + const struct media_codec * const * codecs; + #define DEFAULT_ENABLED_PROFILES (SPA_BT_PROFILE_HFP_HF | SPA_BT_PROFILE_HFP_AG) enum spa_bt_profile enabled_profiles; bool hfp_disable_nrec; + int hfp_default_mic_volume; + int hfp_default_speaker_volume; struct spa_source sco; @@ -125,6 +135,7 @@ struct transport_data { }; enum hfp_hf_state { + hfp_hf_idle, hfp_hf_brsf, hfp_hf_bac, hfp_hf_cind1, @@ -135,11 +146,10 @@ enum hfp_hf_state { hfp_hf_ccwa, hfp_hf_cmee, hfp_hf_nrec, - hfp_hf_slc1, - hfp_hf_slc2, + hfp_hf_clcc, hfp_hf_vgs, - hfp_hf_vgm, - hfp_hf_bcs + hfp_hf_clcc_update, + hfp_hf_chld1_hangup }; enum hsp_hs_state { @@ -161,7 +171,19 @@ struct rfcomm_call_data { struct rfcomm_cmd { struct spa_list link; - char* cmd; + int next_state; + DBusMessage *msg; + char cmd[RFCOMM_MESSAGE_MAX_LENGTH + 1]; +}; + +struct codec_item { + struct spa_list link; + const struct media_codec *codec; +}; + +struct updated_call { + struct spa_list link; + int id; }; struct rfcomm { @@ -179,11 +201,20 @@ struct rfcomm { bool has_volume; struct rfcomm_volume volumes[SPA_BT_VOLUME_ID_TERM]; unsigned int broken_mic_hw_volume:1; + + unsigned int hfp_cmd_in_progress:1; + union { + enum hfp_hf_state hf_state; + enum hsp_hs_state hs_state; + int hf_or_hs_state; + }; + struct spa_list cmd_send_queue; // elements: struct rfcomm_cmd + #ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE + struct spa_list available_codec_list; + struct spa_list supported_codec_list; unsigned int slc_configured:1; unsigned int codec_negotiation_supported:1; - unsigned int msbc_supported_by_hfp:1; - unsigned int lc3_supported_by_hfp:1; unsigned int hfp_ag_switching_codec:1; unsigned int hfp_ag_initial_codec_setup:2; unsigned int cind_call_active:1; @@ -194,19 +225,77 @@ struct rfcomm { unsigned int hfp_hf_nrec:1; unsigned int hfp_hf_clcc:1; unsigned int hfp_hf_cme:1; - unsigned int hfp_hf_cmd_in_progress:1; unsigned int hfp_hf_in_progress:1; unsigned int chld_supported:1; - enum hfp_hf_state hf_state; - enum hsp_hs_state hs_state; unsigned int codec; uint32_t cind_enabled_indicators; char *hf_indicators[MAX_HF_INDICATORS]; struct spa_bt_telephony_ag *telephony_ag; - struct spa_list hfp_hf_commands; + struct spa_list updated_call_list; + char *dialing_number; #endif }; +static const struct media_codec *codec_list_get(struct impl *backend, struct spa_list *list, unsigned int codec_id) +{ + struct codec_item *item; + + /* CVSD is always supported: not included in the list */ + if (codec_id == HFP_AUDIO_CODEC_CVSD) + return spa_bt_get_hfp_codec(backend->monitor, codec_id); + + spa_list_for_each(item, list, link) + if (item->codec->codec_id == codec_id) + return item->codec; + + return NULL; +} + +static bool codec_list_add(struct spa_list *list, const struct media_codec *codec) +{ + struct codec_item *item; + + if (codec->codec_id == HFP_AUDIO_CODEC_CVSD) + return true; + + spa_list_for_each(item, list, link) + if (item->codec == codec) + return true; + + item = calloc(1, sizeof(*item)); + if (!item) + return false; + + item->codec = codec; + spa_list_append(list, &item->link); + return true; +} + +static void codec_list_clear(struct spa_list *list) +{ + struct codec_item *item; + + spa_list_consume(item, list, link) { + spa_list_remove(&item->link); + free(item); + } +} + +static const struct media_codec *codec_list_best(struct impl *backend, struct spa_list *list) +{ + size_t i; + + /* Codec list is in 'best' order */ + for (i = 0; backend->codecs[i]; ++i) { + const struct media_codec *c = backend->codecs[i]; + if (c->kind == MEDIA_CODEC_HFP && codec_list_get(backend, list, c->codec_id)) + return c; + } + + spa_assert_not_reached(); + return NULL; +} + static DBusHandlerResult profile_release(DBusConnection *conn, DBusMessage *m, void *userdata) { if (!reply_with_error(conn, m, BLUEZ_PROFILE_INTERFACE ".Error.NotImplemented", "Method not implemented")) @@ -242,9 +331,10 @@ static const struct spa_bt_transport_events transport_events = { static const struct spa_bt_transport_implementation sco_transport_impl; -static int rfcomm_new_transport(struct rfcomm *rfcomm, int codec) +static int rfcomm_new_transport(struct rfcomm *rfcomm, int codec_id) { struct impl *backend = rfcomm->backend; + const struct media_codec *codec; struct spa_bt_transport *t = NULL; struct transport_data *td; char* pathfd; @@ -255,6 +345,12 @@ static int rfcomm_new_transport(struct rfcomm *rfcomm, int codec) rfcomm->transport = NULL; } + codec = spa_bt_get_hfp_codec(backend->monitor, codec_id); + if (!codec) { + spa_log_warn(backend->log, "failed to get HFP codec %d", codec_id); + goto fail; + } + if ((pathfd = spa_aprintf("%s/fd%d", rfcomm->path, rfcomm->source.fd)) == NULL) goto fail; @@ -271,7 +367,7 @@ static int rfcomm_new_transport(struct rfcomm *rfcomm, int codec) t->backend = &backend->this; t->n_channels = 1; t->channels[0] = SPA_AUDIO_CHANNEL_MONO; - t->codec = codec; + t->media_codec = codec; td = t->user_data; td->rfcomm = rfcomm; @@ -295,7 +391,7 @@ static int rfcomm_new_transport(struct rfcomm *rfcomm, int codec) spa_bt_transport_add_listener(t, &rfcomm->transport_listener, &transport_events, rfcomm); if (rfcomm->telephony_ag) { - rfcomm->telephony_ag->transport.codec = codec; + rfcomm->telephony_ag->transport.codec = codec_id; rfcomm->telephony_ag->transport.state = SPA_BT_TRANSPORT_STATE_IDLE; telephony_ag_transport_notify_updated_props(rfcomm->telephony_ag); } @@ -314,6 +410,24 @@ static void volume_sync_stop_timer(struct rfcomm *rfcomm); static void rfcomm_free(struct rfcomm *rfcomm) { + struct updated_call *updated_call; + struct rfcomm_cmd *cmd; + + spa_list_consume(updated_call, &rfcomm->updated_call_list, link) { + spa_list_remove(&updated_call->link); + free(updated_call); + } + + spa_list_consume(cmd, &rfcomm->cmd_send_queue, link) { + if (cmd->msg) { + telephony_send_dbus_method_reply(rfcomm->backend->telephony, cmd->msg, BT_TELEPHONY_ERROR_FAILED, 0); + spa_clear_ptr(cmd->msg, dbus_message_unref); + } + + spa_list_remove(&cmd->link); + free(cmd); + } + codec_switch_stop_timer(rfcomm); if (rfcomm->telephony_ag) { telephony_ag_destroy(rfcomm->telephony_ag); @@ -345,38 +459,63 @@ static void rfcomm_free(struct rfcomm *rfcomm) } if (rfcomm->volume_sync_timer) spa_loop_utils_destroy_source(rfcomm->backend->loop_utils, rfcomm->volume_sync_timer); + codec_list_clear(&rfcomm->available_codec_list); + codec_list_clear(&rfcomm->supported_codec_list); free(rfcomm); } -#define RFCOMM_MESSAGE_MAX_LENGTH 256 - -/* from HF/HS to AG */ -SPA_PRINTF_FUNC(2, 3) -static ssize_t rfcomm_send_cmd(struct rfcomm *rfcomm, const char *format, ...) +static void rfcomm_cmd_done(struct rfcomm *rfcomm, char *reply) { struct impl *backend = rfcomm->backend; - char message[RFCOMM_MESSAGE_MAX_LENGTH + 1]; - ssize_t len; - va_list args; - va_start(args, format); - len = vsnprintf(message, RFCOMM_MESSAGE_MAX_LENGTH + 1, format, args); - va_end(args); + if (SPA_LIKELY (!spa_list_is_empty(&rfcomm->cmd_send_queue))) { + struct rfcomm_cmd *cmd = NULL; + cmd = spa_list_first(&rfcomm->cmd_send_queue, struct rfcomm_cmd, link); - if (len < 0) - return -EINVAL; + spa_log_debug(backend->log, "%s -> %s", cmd->cmd, reply); - if (len > RFCOMM_MESSAGE_MAX_LENGTH) - return -E2BIG; + if (cmd->msg) { + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_NONE; + uint8_t cme_error = 0; - if (rfcomm->hfp_hf_cmd_in_progress) { - spa_log_debug(backend->log, "Command in progress, postponing: %s", message); - struct rfcomm_cmd *cmd = calloc(1, sizeof(struct rfcomm_cmd)); - cmd->cmd = strndup(message, len); - spa_list_append(&rfcomm->hfp_hf_commands, &cmd->link); - return 0; + if (spa_strstartswith(reply, "+CME ERROR:")) { + cme_error = atoi(reply + strlen("+CME ERROR:")); + err = BT_TELEPHONY_ERROR_CME; + } else if (!spa_strstartswith(reply, "OK")) { + err = BT_TELEPHONY_ERROR_FAILED; + } + + telephony_send_dbus_method_reply(backend->telephony, cmd->msg, err, cme_error); + spa_clear_ptr(cmd->msg, dbus_message_unref); + } + + spa_list_remove(&cmd->link); + free(cmd); + } else { + spa_log_warn(backend->log, "received response but no command was sent"); } + rfcomm->hfp_cmd_in_progress = false; +} + +static ssize_t rfcomm_send_next_cmd(struct rfcomm *rfcomm) +{ + struct impl *backend = rfcomm->backend; + struct rfcomm_cmd *cmd = NULL; + char message[RFCOMM_MESSAGE_MAX_LENGTH + 1]; + ssize_t len = 0; + + if (rfcomm->hfp_cmd_in_progress) + return -EINPROGRESS; + + if (spa_list_is_empty(&rfcomm->cmd_send_queue)) + return -ENODATA; + + cmd = spa_list_first(&rfcomm->cmd_send_queue, struct rfcomm_cmd, link); + + strncpy(message, cmd->cmd, RFCOMM_MESSAGE_MAX_LENGTH + 1); + len = strlen(message); + spa_log_debug(backend->log, "RFCOMM >> %s", message); /* @@ -397,11 +536,43 @@ static ssize_t rfcomm_send_cmd(struct rfcomm *rfcomm, const char *format, ...) spa_log_error(backend->log, "RFCOMM write error: %s", strerror(errno)); } - rfcomm->hfp_hf_cmd_in_progress = true; + rfcomm->hfp_cmd_in_progress = true; + rfcomm->hf_or_hs_state = cmd->next_state; return len; } +/* from HF/HS to AG */ +SPA_PRINTF_FUNC(4, 5) +static ssize_t rfcomm_send_cmd(struct rfcomm *rfcomm, int next_state, DBusMessage *m, const char *format, ...) +{ + struct impl *backend = rfcomm->backend; + spa_autofree struct rfcomm_cmd *cmd = NULL; + ssize_t len; + va_list args; + + cmd = calloc(1, sizeof(struct rfcomm_cmd)); + + va_start(args, format); + len = vsnprintf(cmd->cmd, RFCOMM_MESSAGE_MAX_LENGTH + 1, format, args); + va_end(args); + + if (len < 0) + return -EINVAL; + + if (len > RFCOMM_MESSAGE_MAX_LENGTH) + return -E2BIG; + + spa_log_debug(backend->log, "Queueing command: %s", cmd->cmd); + + cmd->next_state = next_state; + cmd->msg = m ? dbus_message_ref(m) : NULL; + spa_list_append(&rfcomm->cmd_send_queue, &cmd->link); + cmd = NULL; + + return rfcomm_send_next_cmd(rfcomm); +} + /* from AG to HF/HS */ SPA_PRINTF_FUNC(2, 3) static ssize_t rfcomm_send_reply(const struct rfcomm *rfcomm, const char *format, ...) @@ -460,7 +631,7 @@ static void rfcomm_send_error(const struct rfcomm *rfcomm, enum cmee_error error rfcomm_send_reply(rfcomm, "ERROR"); } -static bool rfcomm_volume_enabled(struct rfcomm *rfcomm) +static bool rfcomm_hw_volume_enabled(struct rfcomm *rfcomm) { return rfcomm->device != NULL && (rfcomm->device->hw_volume_profiles & rfcomm->profile); @@ -469,28 +640,33 @@ static bool rfcomm_volume_enabled(struct rfcomm *rfcomm) static void rfcomm_emit_volume_changed(struct rfcomm *rfcomm, int id, int hw_volume) { struct spa_bt_transport_volume *t_volume; + bool valid_volume = (id == SPA_BT_VOLUME_ID_RX || id == SPA_BT_VOLUME_ID_TX); - if (!rfcomm_volume_enabled(rfcomm)) - return; - - if ((id == SPA_BT_VOLUME_ID_RX || id == SPA_BT_VOLUME_ID_TX) && hw_volume >= 0) { + if (valid_volume && hw_volume >= 0) { rfcomm->volumes[id].active = true; rfcomm->volumes[id].hw_volume = hw_volume; } spa_log_debug(rfcomm->backend->log, "volume changed %d", hw_volume); - if (rfcomm->transport == NULL || !rfcomm->has_volume) - return; + if (rfcomm_hw_volume_enabled(rfcomm)) { + if (rfcomm->transport == NULL || !rfcomm->has_volume) + return; - for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) { - t_volume = &rfcomm->transport->volumes[i]; - t_volume->active = rfcomm->volumes[i].active; - t_volume->volume = (float) - spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t_volume->hw_volume_max); + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM ; ++i) { + t_volume = &rfcomm->transport->volumes[i]; + t_volume->active = rfcomm->volumes[i].active; + t_volume->volume = (float) + spa_bt_volume_hw_to_linear(rfcomm->volumes[i].hw_volume, t_volume->hw_volume_max); + } + + spa_bt_transport_emit_volume_changed(rfcomm->transport); } - spa_bt_transport_emit_volume_changed(rfcomm->transport); + if (rfcomm->telephony_ag && valid_volume) { + rfcomm->telephony_ag->volume[id] = hw_volume; + telephony_ag_notify_updated_props(rfcomm->telephony_ag); + } } #ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE @@ -530,22 +706,25 @@ static bool rfcomm_hsp_ag(struct rfcomm *rfcomm, char* buf) return true; } -static bool rfcomm_send_volume_cmd(struct rfcomm *rfcomm, int id) +static void rfcomm_send_volume_cmd(struct rfcomm *rfcomm, int next_state, DBusMessage *m, int id) { struct spa_bt_transport_volume *t_volume; const char *format; - int hw_volume; + int hw_volume = rfcomm->volumes[id].hw_volume; - if (!rfcomm_volume_enabled(rfcomm)) - return false; + if (rfcomm_hw_volume_enabled(rfcomm)) { + t_volume = rfcomm->transport ? &rfcomm->transport->volumes[id] : NULL; - t_volume = rfcomm->transport ? &rfcomm->transport->volumes[id] : NULL; + if (t_volume && t_volume->active) { + hw_volume = spa_bt_volume_linear_to_hw(t_volume->volume, t_volume->hw_volume_max); + rfcomm->volumes[id].hw_volume = hw_volume; + } + } - if (!(t_volume && t_volume->active)) - return false; - - hw_volume = spa_bt_volume_linear_to_hw(t_volume->volume, t_volume->hw_volume_max); - rfcomm->volumes[id].hw_volume = hw_volume; + if (rfcomm->telephony_ag) { + rfcomm->telephony_ag->volume[id] = hw_volume; + telephony_ag_notify_updated_props(rfcomm->telephony_ag); + } if (id == SPA_BT_VOLUME_ID_TX) format = "AT+VGM"; @@ -554,9 +733,7 @@ static bool rfcomm_send_volume_cmd(struct rfcomm *rfcomm, int id) else spa_assert_not_reached(); - rfcomm_send_cmd(rfcomm, "%s=%d", format, hw_volume); - - return true; + rfcomm_send_cmd(rfcomm, next_state, m, "%s=%d", format, hw_volume); } static bool rfcomm_hsp_hs(struct rfcomm *rfcomm, char* buf) @@ -583,18 +760,18 @@ static bool rfcomm_hsp_hs(struct rfcomm *rfcomm, char* buf) } else { spa_log_debug(backend->log, "RFCOMM receive unsupported VGM gain: %s", buf); } - } else if (spa_streq(buf, "OK")) { - if (rfcomm->hs_state == hsp_hs_init2) { - if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) - rfcomm->hs_state = hsp_hs_vgs; - else - rfcomm->hs_state = hsp_hs_init1; - } else if (rfcomm->hs_state == hsp_hs_vgs) { - if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX)) - rfcomm->hs_state = hsp_hs_vgm; - else - rfcomm->hs_state = hsp_hs_init1; + } else if (spa_streq(buf, "OK") || spa_streq(buf, "ERROR")) { + rfcomm_cmd_done(rfcomm, buf); + + if (spa_streq(buf, "OK")) { + if (rfcomm->hs_state == hsp_hs_init2) { + rfcomm_send_volume_cmd(rfcomm, hsp_hs_vgs, NULL, SPA_BT_VOLUME_ID_RX); + } else if (rfcomm->hs_state == hsp_hs_vgs) { + rfcomm_send_volume_cmd(rfcomm, hsp_hs_vgm, NULL, SPA_BT_VOLUME_ID_TX); + } } + + rfcomm_send_next_cmd(rfcomm); } return true; @@ -690,7 +867,8 @@ fail: #ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE -static bool device_supports_codec(struct impl *backend, struct spa_bt_device *device, int codec) +static bool device_supports_codec(struct impl *backend, struct spa_bt_device *device, + enum spa_bluetooth_audio_codec codec) { int res; bool alt6_ok = true, alt1_ok = true; @@ -706,14 +884,14 @@ static bool device_supports_codec(struct impl *backend, struct spa_bt_device *de } switch (codec) { - case HFP_AUDIO_CODEC_CVSD: + case SPA_BLUETOOTH_AUDIO_CODEC_CVSD: return true; - case HFP_AUDIO_CODEC_MSBC: + case SPA_BLUETOOTH_AUDIO_CODEC_MSBC: alt1_ok = msbc_alt1_ok; alt6_ok = msbc_alt6_ok; break; - case HFP_AUDIO_CODEC_LC3_SWB: -#ifdef HAVE_LC3 + case SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB: + default: /* LC3-SWB has same transport requirements as msbc. * However, ALT1/ALT5 modes don't appear to work, seem * to lose frame sync so output is garbled. @@ -721,11 +899,6 @@ static bool device_supports_codec(struct impl *backend, struct spa_bt_device *de alt1_ok = false; alt6_ok = msbc_alt6_ok; break; -#else - return false; -#endif - default: - return false; } spa_log_info(backend->log, @@ -773,6 +946,21 @@ static bool device_supports_codec(struct impl *backend, struct spa_bt_device *de return alt6_ok || alt1_ok; } +static void make_available_codec_list(struct impl *backend, struct spa_bt_device *device, struct spa_list *codec_list) +{ + size_t i; + + codec_list_clear(codec_list); + + for (i = 0; backend->codecs[i]; ++i) { + const struct media_codec *codec = backend->codecs[i]; + if (codec->kind != MEDIA_CODEC_HFP) + continue; + if (device_supports_codec(backend, device, codec->id)) + codec_list_add(codec_list, codec); + } +} + static int codec_switch_start_timer(struct rfcomm *rfcomm, int timeout_msec); static void process_xevent_indicator(struct rfcomm *rfcomm, unsigned int level, unsigned int nlevels) @@ -874,8 +1062,6 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) if (sscanf(buf, "AT+BRSF=%u", &features) == 1) { unsigned int ag_features = SPA_BT_HFP_AG_FEATURE_NONE; - bool codecs = device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_MSBC) || - device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_LC3_SWB); /* * Determine device volume control. Some headsets only support control of @@ -886,7 +1072,7 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) /* Decide if we want to signal that the computer supports codec negotiation This should be done when the computers bluetooth adapter supports the necessary transport mode */ - if (codecs) { + if (!spa_list_is_empty(&rfcomm->available_codec_list)) { /* set the feature bit that indicates AG (=computer) supports codec negotiation */ ag_features |= SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION; @@ -897,8 +1083,6 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) features); /* Prepare reply: Audio Gateway (=computer) supports codec negotiation */ rfcomm->codec_negotiation_supported = true; - rfcomm->msbc_supported_by_hfp = false; - rfcomm->lc3_supported_by_hfp = false; } else { /* Codec negotiation not supported */ spa_log_debug(backend->log, @@ -906,8 +1090,6 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) features); rfcomm->codec_negotiation_supported = false; - rfcomm->msbc_supported_by_hfp = false; - rfcomm->lc3_supported_by_hfp = false; } } @@ -924,28 +1106,28 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) char* token; int cntr = 0; + codec_list_clear(&rfcomm->supported_codec_list); + while ((token = strsep(&buf, "=,"))) { unsigned int codec_id; /* skip token 0 i.e. the "AT+BAC=" part */ if (cntr > 0 && sscanf(token, "%u", &codec_id) == 1) { + const struct media_codec *codec; + spa_log_debug(backend->log, "RFCOMM AT+BAC found codec %u", codec_id); - if (codec_id == HFP_AUDIO_CODEC_MSBC) - rfcomm->msbc_supported_by_hfp = - device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_MSBC); - else if (codec_id == HFP_AUDIO_CODEC_LC3_SWB) - rfcomm->lc3_supported_by_hfp = - device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_LC3_SWB); + codec = codec_list_get(backend, &rfcomm->available_codec_list, codec_id); + if (codec) { + spa_log_debug(backend->log, "RFCOMM AT+BAC codec %s supported", codec->description); + codec_list_add(&rfcomm->supported_codec_list, codec); + } else { + spa_log_debug(backend->log, "RFCOMM AT+BAC codec %u not supported", codec_id); + } } cntr++; } - if (rfcomm->msbc_supported_by_hfp) - spa_log_debug(backend->log, "mSBC codec is supported"); - if (rfcomm->lc3_supported_by_hfp) - spa_log_debug(backend->log, "LC3 codec is supported"); - rfcomm_send_reply(rfcomm, "OK"); } else if (spa_strstartswith(buf, "AT+CIND=?")) { rfcomm_send_reply(rfcomm, "+CIND:%s", CIND_INDICATORS); @@ -956,8 +1138,8 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) backend->modem.network_is_roaming, backend->battery_level); rfcomm_send_reply(rfcomm, "OK"); } else if (spa_strstartswith(buf, "AT+CMER")) { + const struct media_codec *best_codec = codec_list_best(backend, &rfcomm->supported_codec_list); int mode, keyp, disp, ind; - bool have_codecs = rfcomm->msbc_supported_by_hfp || rfcomm->lc3_supported_by_hfp; rfcomm->slc_configured = true; rfcomm_send_reply(rfcomm, "OK"); @@ -968,14 +1150,12 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) else rfcomm->cind_call_notify = false; - /* switch codec to mSBC by sending unsolicited +BCS message */ - if (rfcomm->codec_negotiation_supported && have_codecs) { + /* switch to better codec by sending unsolicited +BCS message */ + if (rfcomm->codec_negotiation_supported && best_codec && + best_codec->id != SPA_BLUETOOTH_AUDIO_CODEC_CVSD) { spa_log_debug(backend->log, "RFCOMM initial codec setup"); rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_SEND; - if (rfcomm->lc3_supported_by_hfp) - rfcomm_send_reply(rfcomm, "+BCS: 3"); - else - rfcomm_send_reply(rfcomm, "+BCS: 2"); + rfcomm_send_reply(rfcomm, "+BCS: %u", best_codec->codec_id); codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_INITIAL_TIMEOUT_MSEC); } else { if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0) { @@ -1000,13 +1180,14 @@ static bool rfcomm_hfp_ag(struct rfcomm *rfcomm, char* buf) } else if (sscanf(buf, "AT+BCS=%u", &selected_codec) == 1) { /* parse BCS(=Bluetooth Codec Selection) reply */ bool was_switching_codec = rfcomm->hfp_ag_switching_codec && (rfcomm->device != NULL); + const struct media_codec *codec = codec_list_get(backend, &rfcomm->supported_codec_list, selected_codec); + rfcomm->hfp_ag_switching_codec = false; rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE; codec_switch_stop_timer(rfcomm); volume_sync_stop_timer(rfcomm); - if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC && - selected_codec != HFP_AUDIO_CODEC_LC3_SWB) { + if (!codec) { spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec); rfcomm_send_error(rfcomm, CMEE_AG_FAILURE); if (was_switching_codec) @@ -1213,7 +1394,7 @@ next_indicator: // This command is sent when we activate Apple extensions rfcomm_send_reply(rfcomm, "OK"); } else if (!mm_is_available(backend->modemmanager)) { - spa_log_warn(backend->log, "RFCOMM receive command but modem not available: %s", buf); + spa_log_info(backend->log, "RFCOMM receive command but modem not available: %s", buf); rfcomm_send_error(rfcomm, CMEE_NO_CONNECTION_TO_PHONE); return true; @@ -1281,161 +1462,72 @@ next_indicator: return true; } -static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token); - -static bool hfp_hf_wait_for_reply(struct rfcomm *rfcomm, char *buf, size_t len) -{ - struct impl *backend = rfcomm->backend; - struct pollfd fds[1]; - bool reply_found = false; - - fds[0].fd = rfcomm->source.fd; - fds[0].events = POLLIN; - while (!reply_found) { - int ret; - char tmp_buf[512]; - ssize_t tmp_len; - char *ptr, *token; - - ret = poll(fds, 1, 2000); - if (ret < 0) { - spa_log_error(backend->log, "RFCOMM poll error: %s", strerror(errno)); - goto done; - } else if (ret == 0) { - spa_log_error(backend->log, "RFCOMM poll timeout"); - goto done; - } - - if (fds[0].revents & (POLLHUP | POLLERR)) { - spa_log_info(backend->log, "lost RFCOMM connection."); - rfcomm_free(rfcomm); - return false; - } - - if (fds[0].revents & POLLIN) { - tmp_len = read(rfcomm->source.fd, tmp_buf, sizeof(tmp_buf) - 1); - if (tmp_len < 0) { - spa_log_error(backend->log, "RFCOMM read error: %s", strerror(errno)); - goto done; - } - tmp_buf[tmp_len] = '\0'; - - /* Relaxed parsing of \r\n\r\n */ - ptr = tmp_buf; - while ((token = strsep(&ptr, "\r"))) { - size_t ptr_len; - - /* Skip leading and trailing \n */ - while (*token == '\n') - ++token; - for (ptr_len = strlen(token); ptr_len > 0 && token[ptr_len - 1] == '\n'; --ptr_len) - token[ptr_len - 1] = '\0'; - - /* Skip empty */ - if (*token == '\0' /*&& buf == NULL*/) - continue; - - spa_log_debug(backend->log, "RFCOMM event: %s", token); - if (spa_strstartswith(token, "OK") || spa_strstartswith(token, "ERROR") || - spa_strstartswith(token, "+CME ERROR:")) { - spa_log_debug(backend->log, "RFCOMM reply found: %s", token); - reply_found = true; - strncpy(buf, token, len); - buf[len-1] = '\0'; - } else if (!rfcomm_hfp_hf(rfcomm, token)) { - spa_log_debug(backend->log, "RFCOMM received unsupported event: %s", token); - } - } - } - } - -done: - rfcomm->hfp_hf_cmd_in_progress = false; - if (!spa_list_is_empty(&rfcomm->hfp_hf_commands)) { - struct rfcomm_cmd *cmd; - cmd = spa_list_first(&rfcomm->hfp_hf_commands, struct rfcomm_cmd, link); - spa_list_remove(&cmd->link); - spa_log_debug(backend->log, "Sending postponed command: %s", cmd->cmd); - rfcomm_send_cmd(rfcomm, "%s", cmd->cmd); - free(cmd->cmd); - free(cmd); - } - - return reply_found; -} - -static void hfp_hf_get_error_from_reply(char *reply, enum spa_bt_telephony_error *err, uint8_t *cme_error) -{ - if (spa_strstartswith(reply, "+CME ERROR:")) { - *cme_error = atoi(reply + strlen("+CME ERROR:")); - *err = BT_TELEPHONY_ERROR_CME; - } else { - *err = BT_TELEPHONY_ERROR_FAILED; - } -} - -static void hfp_hf_answer(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_answer(void *data, DBusMessage *m) { struct rfcomm_call_data *call_data = data; struct rfcomm *rfcomm = call_data->rfcomm; struct impl *backend = rfcomm->backend; - char reply[20]; - bool res; if (call_data->call->state != CALL_STATE_INCOMING) { - *err = BT_TELEPHONY_ERROR_INVALID_STATE; + telephony_send_dbus_method_reply(backend->telephony, m, BT_TELEPHONY_ERROR_INVALID_STATE, 0); return; } - rfcomm_send_cmd(rfcomm, "ATA"); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to answer call"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - return; - } - - *err = BT_TELEPHONY_ERROR_NONE; + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "ATA"); } -static void hfp_hf_hangup(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_hangup(void *data, DBusMessage *m) { struct rfcomm_call_data *call_data = data; struct rfcomm *rfcomm = call_data->rfcomm; struct impl *backend = rfcomm->backend; - char reply[20]; - bool res; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_NONE; + struct spa_bt_telephony_call *call; + bool found_held = false; + + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_HELD) + found_held = true; + } switch (call_data->call->state) { case CALL_STATE_ACTIVE: case CALL_STATE_DIALING: case CALL_STATE_ALERTING: case CALL_STATE_INCOMING: - rfcomm_send_cmd(rfcomm, "AT+CHUP"); + if (found_held) { + if (!rfcomm->chld_supported) { + err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + goto error; + } else if (rfcomm->hfp_hf_in_progress) { + err = BT_TELEPHONY_ERROR_IN_PROGRESS; + goto error; + } + + rfcomm_send_cmd(rfcomm, hfp_hf_chld1_hangup, m, "AT+CHLD=1"); + rfcomm->hfp_hf_in_progress = true; + } else { + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+CHUP"); + } break; + case CALL_STATE_HELD: case CALL_STATE_WAITING: - rfcomm_send_cmd(rfcomm, "AT+CHLD=0"); + if (rfcomm->hfp_hf_in_progress) { + err = BT_TELEPHONY_ERROR_IN_PROGRESS; + goto error; + } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+CHLD=0"); + rfcomm->hfp_hf_in_progress = true; break; default: - spa_log_info(backend->log, "Call not incoming, waiting or active: skip hangup"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; + spa_log_warn(backend->log, "Call in invalid state: skip hangup"); + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto error; } - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to hangup call"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - return; - } - - *err = BT_TELEPHONY_ERROR_NONE; + return; +error: + telephony_send_dbus_method_reply(backend->telephony, m, err, 0); } static const struct spa_bt_telephony_call_callbacks telephony_call_callbacks = { @@ -1465,95 +1557,71 @@ static struct spa_bt_telephony_call *hfp_hf_add_call(struct rfcomm *rfcomm, stru return call; } -static void hfp_hf_dial(void *data, const char *number, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_dial(void *data, const char *number, DBusMessage *m) { struct rfcomm *rfcomm = data; struct impl *backend = rfcomm->backend; - char reply[20]; - bool res; + + /* store the number in case we need to create the Call object + via CIND notifications (if CLCC is not supported) */ + free(spa_exchange(rfcomm->dialing_number, strdup(number))); spa_log_info(backend->log, "Dialing: \"%s\"", number); - rfcomm_send_cmd(rfcomm, "ATD%s;", number); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (res && spa_strstartswith(reply, "OK")) { - struct spa_bt_telephony_call *call; - call = hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_DIALING, number); - *err = call ? BT_TELEPHONY_ERROR_NONE : BT_TELEPHONY_ERROR_FAILED; - } else { - spa_log_info(backend->log, "Failed to dial: \"%s\"", number); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "ATD%s;", number); } -static void hfp_hf_swap_calls(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_swap_calls(void *data, DBusMessage *m) { struct rfcomm *rfcomm = data; struct impl *backend = rfcomm->backend; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_NONE; struct spa_bt_telephony_call *call; - bool found_active = false; bool found_held = false; - char reply[20]; - bool res; if (!rfcomm->chld_supported) { - *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; - return; + err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + goto error; } else if (rfcomm->hfp_hf_in_progress) { - *err = BT_TELEPHONY_ERROR_IN_PROGRESS; - return; + err = BT_TELEPHONY_ERROR_IN_PROGRESS; + goto error; } spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { - if (call->state == CALL_STATE_WAITING) { - spa_log_debug(backend->log, "call waiting before swapping"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; - } else if (call->state == CALL_STATE_ACTIVE) - found_active = true; - else if (call->state == CALL_STATE_HELD) + if (call->state == CALL_STATE_HELD) { found_held = true; + break; + } } - if (!found_active || !found_held) { - spa_log_debug(backend->log, "no active and held calls"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; - } - - rfcomm_send_cmd(rfcomm, "AT+CHLD=2"); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to swap calls"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - return; + if (!found_held) { + spa_log_debug(backend->log, "no held calls"); + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto error; } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+CHLD=2"); rfcomm->hfp_hf_in_progress = true; - *err = BT_TELEPHONY_ERROR_NONE; + return; + +error: + telephony_send_dbus_method_reply(backend->telephony, m, err, 0); } -static void hfp_hf_release_and_answer(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_release_and_answer(void *data, DBusMessage *m) { struct rfcomm *rfcomm = data; struct impl *backend = rfcomm->backend; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_NONE; struct spa_bt_telephony_call *call; bool found_active = false; bool found_waiting = false; - char reply[20]; - bool res; if (!rfcomm->chld_supported) { - *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; - return; + err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + goto error; } else if (rfcomm->hfp_hf_in_progress) { - *err = BT_TELEPHONY_ERROR_IN_PROGRESS; - return; + err = BT_TELEPHONY_ERROR_IN_PROGRESS; + goto error; } spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { @@ -1565,48 +1633,40 @@ static void hfp_hf_release_and_answer(void *data, enum spa_bt_telephony_error *e if (!found_active || !found_waiting) { spa_log_debug(backend->log, "no active and waiting calls"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; - } - - rfcomm_send_cmd(rfcomm, "AT+CHLD=1"); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to release and answer calls"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - return; + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto error; } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+CHLD=1"); rfcomm->hfp_hf_in_progress = true; - *err = BT_TELEPHONY_ERROR_NONE; + return; + +error: + telephony_send_dbus_method_reply(backend->telephony, m, err, 0); } -static void hfp_hf_release_and_swap(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_release_and_swap(void *data, DBusMessage *m) { struct rfcomm *rfcomm = data; struct impl *backend = rfcomm->backend; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_NONE; struct spa_bt_telephony_call *call; bool found_active = false; bool found_held = false; - char reply[20]; - bool res; if (!rfcomm->chld_supported) { - *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; - return; + err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + goto error; } else if (rfcomm->hfp_hf_in_progress) { - *err = BT_TELEPHONY_ERROR_IN_PROGRESS; - return; + err = BT_TELEPHONY_ERROR_IN_PROGRESS; + goto error; } spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_WAITING) { spa_log_debug(backend->log, "call waiting before release and swap"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto error; } else if (call->state == CALL_STATE_ACTIVE) found_active = true; else if (call->state == CALL_STATE_HELD) @@ -1615,41 +1675,33 @@ static void hfp_hf_release_and_swap(void *data, enum spa_bt_telephony_error *err if (!found_active || !found_held) { spa_log_debug(backend->log, "no active and held calls"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; - } - - rfcomm_send_cmd(rfcomm, "AT+CHLD=1"); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to release and swap calls"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - return; + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto error; } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+CHLD=1"); rfcomm->hfp_hf_in_progress = true; - *err = BT_TELEPHONY_ERROR_NONE; + return; + +error: + telephony_send_dbus_method_reply(backend->telephony, m, err, 0); } -static void hfp_hf_hold_and_answer(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_hold_and_answer(void *data, DBusMessage *m) { struct rfcomm *rfcomm = data; struct impl *backend = rfcomm->backend; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_NONE; struct spa_bt_telephony_call *call; bool found_active = false; bool found_waiting = false; - char reply[20]; - bool res; if (!rfcomm->chld_supported) { - *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; - return; + err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + goto error; } else if (rfcomm->hfp_hf_in_progress) { - *err = BT_TELEPHONY_ERROR_IN_PROGRESS; - return; + err = BT_TELEPHONY_ERROR_IN_PROGRESS; + goto error; } spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { @@ -1661,34 +1713,24 @@ static void hfp_hf_hold_and_answer(void *data, enum spa_bt_telephony_error *err, if (!found_active || !found_waiting) { spa_log_debug(backend->log, "no active and waiting calls"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; - } - - rfcomm_send_cmd(rfcomm, "AT+CHLD=2"); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to hold and answer calls"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - return; + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto error; } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+CHLD=2"); rfcomm->hfp_hf_in_progress = true; - *err = BT_TELEPHONY_ERROR_NONE; + return; + +error: + telephony_send_dbus_method_reply(backend->telephony, m, err, 0); } -static void hfp_hf_hangup_all(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_hangup_all(void *data, DBusMessage *m) { struct rfcomm *rfcomm = data; - struct impl *backend = rfcomm->backend; struct spa_bt_telephony_call *call; bool found_active = false; bool found_held = false; - char reply[20]; - bool res; spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { switch (call->state) { @@ -1707,58 +1749,39 @@ static void hfp_hf_hangup_all(void *data, enum spa_bt_telephony_error *err, uint } } - *err = BT_TELEPHONY_ERROR_NONE; - /* Hangup held calls */ if (found_held) { - rfcomm_send_cmd(rfcomm, "AT+CHLD=0"); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to hangup held calls"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, found_active ? NULL : m, "AT+CHLD=0"); } /* Hangup active calls */ if (found_active) { - rfcomm_send_cmd(rfcomm, "AT+CHUP"); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to hangup active calls"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+CHUP"); } } -static void hfp_hf_create_multiparty(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_create_multiparty(void *data, DBusMessage *m) { struct rfcomm *rfcomm = data; struct impl *backend = rfcomm->backend; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_NONE; struct spa_bt_telephony_call *call; bool found_active = false; bool found_held = false; - char reply[20]; - bool res; if (!rfcomm->chld_supported) { - *err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; - return; + err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + goto error; } else if (rfcomm->hfp_hf_in_progress) { - *err = BT_TELEPHONY_ERROR_IN_PROGRESS; - return; + err = BT_TELEPHONY_ERROR_IN_PROGRESS; + goto error; } spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_WAITING) { spa_log_debug(backend->log, "call waiting before creating multiparty"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto error; } else if (call->state == CALL_STATE_ACTIVE) found_active = true; else if (call->state == CALL_STATE_HELD) @@ -1767,33 +1790,25 @@ static void hfp_hf_create_multiparty(void *data, enum spa_bt_telephony_error *er if (!found_active || !found_held) { spa_log_debug(backend->log, "no active and held calls"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; - } - - rfcomm_send_cmd(rfcomm, "AT+CHLD=3"); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to create multiparty"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - return; + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto error; } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+CHLD=3"); rfcomm->hfp_hf_in_progress = true; - *err = BT_TELEPHONY_ERROR_NONE; + return; + +error: + telephony_send_dbus_method_reply(backend->telephony, m, err, 0); } -static void hfp_hf_send_tones(void *data, const char *tones, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static void hfp_hf_send_tones(void *data, const char *tones, DBusMessage *m) { struct rfcomm *rfcomm = data; struct impl *backend = rfcomm->backend; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_NONE; struct spa_bt_telephony_call *call; bool found = false; - char reply[20]; - bool res; spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_ACTIVE) { @@ -1804,54 +1819,81 @@ static void hfp_hf_send_tones(void *data, const char *tones, enum spa_bt_telepho if (!found) { spa_log_debug(backend->log, "no active call"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto error; } - rfcomm_send_cmd(rfcomm, "AT+VTS=%s", tones); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to send tones: %s", tones); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; - return; - } + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+VTS=%s", tones); + return; - *err = BT_TELEPHONY_ERROR_NONE; +error: + telephony_send_dbus_method_reply(backend->telephony, m, err, 0); } -static void hfp_hf_transport_activate(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error) +static int sco_do_connect(struct spa_bt_transport *t); + +static void hfp_hf_transport_activate(void *data, DBusMessage *m) { struct rfcomm *rfcomm = data; struct impl *backend = rfcomm->backend; - char reply[20]; - bool res; + enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_NONE; - if (spa_list_is_empty(&rfcomm->telephony_ag->call_list)) { - spa_log_debug(backend->log, "no ongoing call"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; - } - if (rfcomm->transport->fd > 0) { + if (rfcomm->transport && rfcomm->transport->fd > 0) { spa_log_debug(backend->log, "transport is already active; SCO socket exists"); - *err = BT_TELEPHONY_ERROR_INVALID_STATE; - return; + err = BT_TELEPHONY_ERROR_INVALID_STATE; + goto out; } - rfcomm_send_cmd(rfcomm, "AT+BCC"); - res = hfp_hf_wait_for_reply(rfcomm, reply, sizeof(reply)); - if (!res || !spa_strstartswith(reply, "OK")) { - spa_log_info(backend->log, "Failed to send AT+BCC"); - if (res) - hfp_hf_get_error_from_reply(reply, err, cme_error); - else - *err = BT_TELEPHONY_ERROR_FAILED; + if (rfcomm->codec_negotiation_supported) { + rfcomm_send_cmd(rfcomm, hfp_hf_idle, m, "AT+BCC"); return; + } else { + if (!rfcomm->transport || rfcomm->transport->media_codec->id != SPA_BLUETOOTH_AUDIO_CODEC_CVSD) { + err = BT_TELEPHONY_ERROR_NOT_SUPPORTED; + goto out; + } + + sco_do_connect(rfcomm->transport); } - *err = BT_TELEPHONY_ERROR_NONE; +out: + telephony_send_dbus_method_reply(backend->telephony, m, err, 0); +} + +static void hfp_hf_set_speaker_volume(void *data, uint8_t volume, DBusMessage *m) +{ + struct rfcomm *rfcomm = data; + struct spa_bt_transport_volume *t_volume; + + rfcomm->volumes[SPA_BT_VOLUME_ID_RX].hw_volume = volume; + if (rfcomm_hw_volume_enabled(rfcomm)) { + t_volume = rfcomm->transport ? &rfcomm->transport->volumes[SPA_BT_VOLUME_ID_RX] : NULL; + + if (t_volume && t_volume->active) { + t_volume->volume = (float) spa_bt_volume_hw_to_linear(volume, t_volume->hw_volume_max); + spa_bt_transport_emit_volume_changed(rfcomm->transport); + } + } + + rfcomm_send_volume_cmd(rfcomm, hfp_hf_idle, m, SPA_BT_VOLUME_ID_RX); +} + +static void hfp_hf_set_microphone_volume(void *data, uint8_t volume, DBusMessage *m) +{ + struct rfcomm *rfcomm = data; + struct spa_bt_transport_volume *t_volume; + + rfcomm->volumes[SPA_BT_VOLUME_ID_TX].hw_volume = volume; + if (rfcomm_hw_volume_enabled(rfcomm)) { + t_volume = rfcomm->transport ? &rfcomm->transport->volumes[SPA_BT_VOLUME_ID_TX] : NULL; + + if (t_volume && t_volume->active) { + t_volume->volume = (float) spa_bt_volume_hw_to_linear(volume, t_volume->hw_volume_max); + spa_bt_transport_emit_volume_changed(rfcomm->transport); + } + } + + rfcomm_send_volume_cmd(rfcomm, hfp_hf_idle, m, SPA_BT_VOLUME_ID_TX); } static const struct spa_bt_telephony_ag_callbacks telephony_ag_callbacks = { @@ -1865,8 +1907,47 @@ static const struct spa_bt_telephony_ag_callbacks telephony_ag_callbacks = { .create_multiparty = hfp_hf_create_multiparty, .send_tones = hfp_hf_send_tones, .transport_activate = hfp_hf_transport_activate, + .set_speaker_volume = hfp_hf_set_speaker_volume, + .set_microphone_volume = hfp_hf_set_microphone_volume, }; +#define hfp_hf_set_call_state(log, obj, new_state) \ +({ \ + spa_log_debug(log, "call id: %u, %u -> %u", obj->id, obj->state, new_state); \ + obj->state = new_state; \ +}) + +static void hfp_hf_remove_disconnected_calls(struct rfcomm *rfcomm) +{ + struct impl *backend = rfcomm->backend; + struct spa_bt_telephony_call *call, *call_tmp; + struct updated_call *updated_call; + bool found; + + spa_list_for_each_safe(call, call_tmp, &rfcomm->telephony_ag->call_list, link) { + found = false; + spa_list_for_each(updated_call, &rfcomm->updated_call_list, link) { + if (call->id == updated_call->id) { + found = true; + break; + } + } + + spa_log_debug(backend->log, "call %d -> %s", call->id, found ? "updated" : "disconnected"); + + if (!found) { + hfp_hf_set_call_state(backend->log, call, CALL_STATE_DISCONNECTED); + telephony_call_notify_updated_props(call); + telephony_call_destroy(call); + } + } + + spa_list_consume(updated_call, &rfcomm->updated_call_list, link) { + spa_list_remove(&updated_call->link); + free(updated_call); + } +} + static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) { struct impl *backend = rfcomm->backend; @@ -1875,25 +1956,24 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) if (sscanf(token, "+BRSF:%u", &features) == 1) { if (((features & (SPA_BT_HFP_AG_FEATURE_CODEC_NEGOTIATION)) != 0) && - (rfcomm->msbc_supported_by_hfp || rfcomm->lc3_supported_by_hfp)) + !spa_list_is_empty(&rfcomm->available_codec_list)) rfcomm->codec_negotiation_supported = true; rfcomm->hfp_hf_3way = (features & SPA_BT_HFP_AG_FEATURE_3WAY) != 0; rfcomm->hfp_hf_nrec = (features & SPA_BT_HFP_AG_FEATURE_ECNR) != 0; rfcomm->hfp_hf_clcc = (features & SPA_BT_HFP_AG_FEATURE_ENHANCED_CALL_STATUS) != 0; rfcomm->hfp_hf_cme = (features & SPA_BT_HFP_AG_FEATURE_EXTENDED_RES_CODE) != 0; } else if (sscanf(token, "+BCS:%u", &selected_codec) == 1 && rfcomm->codec_negotiation_supported) { - if (selected_codec != HFP_AUDIO_CODEC_CVSD && selected_codec != HFP_AUDIO_CODEC_MSBC && - selected_codec != HFP_AUDIO_CODEC_LC3_SWB) { + const struct media_codec *codec = codec_list_get(backend, &rfcomm->available_codec_list, selected_codec); + + if (!codec) { spa_log_warn(backend->log, "unsupported codec negotiation: %d", selected_codec); } else { spa_log_debug(backend->log, "RFCOMM selected_codec = %i", selected_codec); /* send codec selection to AG */ - rfcomm_send_cmd(rfcomm, "AT+BCS=%u", selected_codec); + rfcomm_send_cmd(rfcomm, hfp_hf_idle, NULL, "AT+BCS=%u", selected_codec); - rfcomm->hf_state = hfp_hf_bcs; - - if (!rfcomm->transport || (rfcomm->transport->codec != selected_codec) ) { + if (!rfcomm->transport || (rfcomm->codec != selected_codec) ) { if (rfcomm_new_transport(rfcomm, selected_codec) < 0) { // TODO: We should manage the missing transport } else { @@ -1973,12 +2053,17 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) if (spa_streq(rfcomm->hf_indicators[indicator], "battchg")) { spa_bt_device_report_battery_level(rfcomm->device, value * 100 / 5); } else if (spa_streq(rfcomm->hf_indicators[indicator], "callsetup")) { + if (rfcomm->hfp_hf_clcc) { + rfcomm_send_cmd(rfcomm, hfp_hf_clcc_update, NULL, "AT+CLCC"); + return true; + } + if (value == CIND_CALLSETUP_NONE) { struct spa_bt_telephony_call *call, *tcall; spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_DIALING || call->state == CALL_STATE_ALERTING || - call->state == CALL_STATE_INCOMING) { - call->state = CALL_STATE_DISCONNECTED; + call->state == CALL_STATE_INCOMING || call->state == CALL_STATE_WAITING) { + hfp_hf_set_call_state(backend->log, call, CALL_STATE_DISCONNECTED); telephony_call_notify_updated_props(call); telephony_call_destroy(call); } @@ -2014,29 +2099,32 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) if (!found && !rfcomm->hfp_hf_clcc) { spa_log_info(backend->log, "Dialing call"); - if (hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_DIALING, NULL) == NULL) + if (hfp_hf_add_call(rfcomm, rfcomm->telephony_ag, CALL_STATE_DIALING, rfcomm->dialing_number) == NULL) spa_log_warn(backend->log, "failed to create dialing call"); + spa_clear_ptr(rfcomm->dialing_number, free); } } else if (value == CIND_CALLSETUP_ALERTING) { struct spa_bt_telephony_call *call; spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_DIALING) { - call->state = CALL_STATE_ALERTING; + hfp_hf_set_call_state(backend->log, call, CALL_STATE_ALERTING); telephony_call_notify_updated_props(call); } } } - if (rfcomm->hfp_hf_clcc) - rfcomm_send_cmd(rfcomm, "AT+CLCC"); - else - rfcomm->hfp_hf_in_progress = false; + rfcomm->hfp_hf_in_progress = false; } else if (spa_streq(rfcomm->hf_indicators[indicator], "call")) { + if (rfcomm->hfp_hf_clcc) { + rfcomm_send_cmd(rfcomm, hfp_hf_clcc_update, NULL, "AT+CLCC"); + return true; + } + if (value == 0) { struct spa_bt_telephony_call *call, *tcall; spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_ACTIVE) { - call->state = CALL_STATE_DISCONNECTED; + hfp_hf_set_call_state(backend->log, call, CALL_STATE_DISCONNECTED); telephony_call_notify_updated_props(call); telephony_call_destroy(call); } @@ -2046,23 +2134,25 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_DIALING || call->state == CALL_STATE_ALERTING || call->state == CALL_STATE_INCOMING) { - call->state = CALL_STATE_ACTIVE; + hfp_hf_set_call_state(backend->log, call, CALL_STATE_ACTIVE); telephony_call_notify_updated_props(call); } } } - if (rfcomm->hfp_hf_clcc) - rfcomm_send_cmd(rfcomm, "AT+CLCC"); - else - rfcomm->hfp_hf_in_progress = false; + rfcomm->hfp_hf_in_progress = false; } else if (spa_streq(rfcomm->hf_indicators[indicator], "callheld")) { + if (rfcomm->hfp_hf_clcc) { + rfcomm_send_cmd(rfcomm, hfp_hf_clcc_update, NULL, "AT+CLCC"); + return true; + } + if (value == 0) { /* Reject waiting call or no held calls */ struct spa_bt_telephony_call *call, *tcall; bool found_waiting = false; spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_WAITING) { - call->state = CALL_STATE_DISCONNECTED; + hfp_hf_set_call_state(backend->log, call, CALL_STATE_DISCONNECTED); telephony_call_notify_updated_props(call); telephony_call_destroy(call); found_waiting = true; @@ -2072,7 +2162,7 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) if (!found_waiting) { spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_HELD) { - call->state = CALL_STATE_DISCONNECTED; + hfp_hf_set_call_state(backend->log, call, CALL_STATE_DISCONNECTED); telephony_call_notify_updated_props(call); telephony_call_destroy(call); } @@ -2083,10 +2173,10 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { bool changed = false; if (call->state == CALL_STATE_ACTIVE) { - call->state = CALL_STATE_HELD; + hfp_hf_set_call_state(backend->log, call, CALL_STATE_HELD); changed = true; } else if (call->state == CALL_STATE_HELD) { - call->state = CALL_STATE_ACTIVE; + hfp_hf_set_call_state(backend->log, call, CALL_STATE_ACTIVE); changed = true; } @@ -2098,7 +2188,7 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { bool changed = false; if (call->state == CALL_STATE_ACTIVE || call->state == CALL_STATE_WAITING) { - call->state = CALL_STATE_HELD; + hfp_hf_set_call_state(backend->log, call, CALL_STATE_HELD); changed = true; } @@ -2107,10 +2197,7 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) } } - if (rfcomm->hfp_hf_clcc) - rfcomm_send_cmd(rfcomm, "AT+CLCC"); - else - rfcomm->hfp_hf_in_progress = false; + rfcomm->hfp_hf_in_progress = false; } } } else if (sscanf(token, "+CLIP: \"%16[^\"]\",%u", number, &type) == 2) { @@ -2193,6 +2280,11 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) } if (SPA_LIKELY (parsed)) { + struct updated_call *updated_call; + updated_call = calloc(1, sizeof(struct updated_call)); + updated_call->id = idx; + spa_list_append(&rfcomm->updated_call_list, &updated_call->link); + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { if (call->id == idx) { bool changed = false; @@ -2226,24 +2318,19 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) spa_log_warn(backend->log, "failed to create call"); else if (call->id != idx) spa_log_warn(backend->log, "wrong call index: %d, expected: %d", call->id, idx); + + if (spa_streq(number, rfcomm->dialing_number)) { + free(rfcomm->dialing_number); + rfcomm->dialing_number = NULL; + } } } else { spa_log_warn(backend->log, "malformed +CLCC command received from AG"); } - - rfcomm->hfp_hf_in_progress = false; } else if (spa_strstartswith(token, "OK") || spa_strstartswith(token, "ERROR") || spa_strstartswith(token, "+CME ERROR:")) { - rfcomm->hfp_hf_cmd_in_progress = false; - if (!spa_list_is_empty(&rfcomm->hfp_hf_commands)) { - struct rfcomm_cmd *cmd; - cmd = spa_list_first(&rfcomm->hfp_hf_commands, struct rfcomm_cmd, link); - spa_list_remove(&cmd->link); - spa_log_debug(backend->log, "Sending postponed command: %s", cmd->cmd); - rfcomm_send_cmd(rfcomm, "%s", cmd->cmd); - free(cmd->cmd); - free(cmd); - } + + rfcomm_cmd_done(rfcomm, token); if (spa_strstartswith(token, "OK")) { switch(rfcomm->hf_state) { @@ -2251,67 +2338,34 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) if (rfcomm->codec_negotiation_supported) { char buf[64]; struct spa_strbuf str; + struct codec_item *item; spa_strbuf_init(&str, buf, sizeof(buf)); spa_strbuf_append(&str, "1"); - if (rfcomm->msbc_supported_by_hfp) - spa_strbuf_append(&str, ",2"); - if (rfcomm->lc3_supported_by_hfp) - spa_strbuf_append(&str, ",3"); + spa_list_for_each(item, &rfcomm->available_codec_list, link) + spa_strbuf_append(&str, ",%u", item->codec->codec_id); - rfcomm_send_cmd(rfcomm, "AT+BAC=%s", buf); - rfcomm->hf_state = hfp_hf_bac; + rfcomm_send_cmd(rfcomm, hfp_hf_bac, NULL, "AT+BAC=%s", buf); } else { - rfcomm_send_cmd(rfcomm, "AT+CIND=?"); - rfcomm->hf_state = hfp_hf_cind1; + rfcomm_send_cmd(rfcomm, hfp_hf_cind1, NULL, "AT+CIND=?"); } break; case hfp_hf_bac: - rfcomm_send_cmd(rfcomm, "AT+CIND=?"); - rfcomm->hf_state = hfp_hf_cind1; + rfcomm_send_cmd(rfcomm, hfp_hf_cind1, NULL, "AT+CIND=?"); break; case hfp_hf_cind1: - rfcomm_send_cmd(rfcomm, "AT+CIND?"); - rfcomm->hf_state = hfp_hf_cind2; + rfcomm_send_cmd(rfcomm, hfp_hf_cind2, NULL, "AT+CIND?"); break; case hfp_hf_cind2: - rfcomm_send_cmd(rfcomm, "AT+CMER=3,0,0,1"); - rfcomm->hf_state = hfp_hf_cmer; + rfcomm_send_cmd(rfcomm, hfp_hf_cmer, NULL, "AT+CMER=3,0,0,1"); break; case hfp_hf_cmer: if (rfcomm->hfp_hf_3way) { - rfcomm_send_cmd(rfcomm, "AT+CHLD=?"); - rfcomm->hf_state = hfp_hf_chld; + rfcomm_send_cmd(rfcomm, hfp_hf_chld, NULL, "AT+CHLD=?"); break; } SPA_FALLTHROUGH; case hfp_hf_chld: - rfcomm_send_cmd(rfcomm, "AT+CLIP=1"); - rfcomm->hf_state = hfp_hf_clip; - break; - case hfp_hf_clip: - if (rfcomm->chld_supported) { - rfcomm_send_cmd(rfcomm, "AT+CCWA=1"); - rfcomm->hf_state = hfp_hf_ccwa; - break; - } - SPA_FALLTHROUGH; - case hfp_hf_ccwa: - if (rfcomm->hfp_hf_cme) { - rfcomm_send_cmd(rfcomm, "AT+CMEE=1"); - rfcomm->hf_state = hfp_hf_cmee; - break; - } - SPA_FALLTHROUGH; - case hfp_hf_cmee: - if (backend->hfp_disable_nrec && rfcomm->hfp_hf_nrec) { - rfcomm_send_cmd(rfcomm, "AT+NREC=0"); - rfcomm->hf_state = hfp_hf_nrec; - break; - } - SPA_FALLTHROUGH; - case hfp_hf_nrec: - rfcomm->hf_state = hfp_hf_slc1; rfcomm->slc_configured = true; if (!rfcomm->codec_negotiation_supported) { @@ -2324,17 +2378,39 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) rfcomm->telephony_ag = telephony_ag_new(backend->telephony, 0); rfcomm->telephony_ag->address = strdup(rfcomm->device->address); + rfcomm->telephony_ag->volume[SPA_BT_VOLUME_ID_RX] = rfcomm->volumes[SPA_BT_VOLUME_ID_RX].hw_volume = backend->hfp_default_speaker_volume; + rfcomm->telephony_ag->volume[SPA_BT_VOLUME_ID_TX] = rfcomm->volumes[SPA_BT_VOLUME_ID_TX].hw_volume = backend->hfp_default_mic_volume; telephony_ag_set_callbacks(rfcomm->telephony_ag, &telephony_ag_callbacks, rfcomm); if (rfcomm->transport) { - rfcomm->telephony_ag->transport.codec = rfcomm->transport->codec; + rfcomm->telephony_ag->transport.codec = rfcomm->transport->media_codec->codec_id; rfcomm->telephony_ag->transport.state = rfcomm->transport->state; } telephony_ag_register(rfcomm->telephony_ag); + rfcomm_send_cmd(rfcomm, hfp_hf_clip, NULL, "AT+CLIP=1"); + break; + case hfp_hf_clip: + if (rfcomm->chld_supported) { + rfcomm_send_cmd(rfcomm, hfp_hf_ccwa, NULL, "AT+CCWA=1"); + break; + } + SPA_FALLTHROUGH; + case hfp_hf_ccwa: + if (rfcomm->hfp_hf_cme) { + rfcomm_send_cmd(rfcomm, hfp_hf_cmee, NULL, "AT+CMEE=1"); + break; + } + SPA_FALLTHROUGH; + case hfp_hf_cmee: + if (backend->hfp_disable_nrec && rfcomm->hfp_hf_nrec) { + rfcomm_send_cmd(rfcomm, hfp_hf_nrec, NULL, "AT+NREC=0"); + break; + } + SPA_FALLTHROUGH; + case hfp_hf_nrec: if (rfcomm->hfp_hf_clcc) { - rfcomm_send_cmd(rfcomm, "AT+CLCC"); - rfcomm->hf_state = hfp_hf_slc2; + rfcomm_send_cmd(rfcomm, hfp_hf_clcc, NULL, "AT+CLCC"); break; } else { // TODO: Create calls if CIND reports one during SLC setup @@ -2342,19 +2418,48 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) /* Report volume on SLC establishment */ SPA_FALLTHROUGH; - case hfp_hf_slc2: - if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) - rfcomm->hf_state = hfp_hf_vgs; + case hfp_hf_clcc: + if (rfcomm->hf_state == hfp_hf_clcc) { + hfp_hf_remove_disconnected_calls(rfcomm); + } + rfcomm_send_volume_cmd(rfcomm, hfp_hf_vgs, NULL, SPA_BT_VOLUME_ID_RX); break; case hfp_hf_vgs: - rfcomm->hf_state = hfp_hf_slc1; - if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_TX)) - rfcomm->hf_state = hfp_hf_vgm; + rfcomm_send_volume_cmd(rfcomm, hfp_hf_idle, NULL, SPA_BT_VOLUME_ID_TX); break; + case hfp_hf_clcc_update: + hfp_hf_remove_disconnected_calls(rfcomm); + rfcomm->hfp_hf_in_progress = false; + break; + case hfp_hf_chld1_hangup: + /* For HFP/HF/TWC/BV-03-C - see 0e92ab9307e05758b3f70b4c0648e29c1d1e50be */ + if (!rfcomm->hfp_hf_clcc) { + struct spa_bt_telephony_call *call, *tcall; + spa_list_for_each_safe(call, tcall, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_ACTIVE) { + hfp_hf_set_call_state(backend->log, call, CALL_STATE_DISCONNECTED); + telephony_call_notify_updated_props(call); + telephony_call_destroy(call); + } + } + spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { + if (call->state == CALL_STATE_HELD) { + hfp_hf_set_call_state(backend->log, call, CALL_STATE_ACTIVE); + telephony_call_notify_updated_props(call); + } + } + } + break; + case hfp_hf_idle: default: break; } + } else { + /* reset state in case of an error reply */ + rfcomm->hfp_hf_in_progress = false; } + + rfcomm_send_next_cmd(rfcomm); } return true; @@ -2487,8 +2592,8 @@ static int sco_do_connect(struct spa_bt_transport *t) struct sockaddr_sco addr; int err; - spa_log_debug(backend->log, "transport %p: enter sco_do_connect, codec=%u", - t, t->codec); + spa_log_debug(backend->log, "transport %p: enter sco_do_connect, codec=%s", + t, t->media_codec->description); td->err = -EIO; @@ -2500,8 +2605,8 @@ static int sco_do_connect(struct spa_bt_transport *t) str2ba(d->address, &addr.sco_bdaddr); for (int retry = 2;;) { - bool transparent = (t->codec == HFP_AUDIO_CODEC_MSBC || t->codec == HFP_AUDIO_CODEC_LC3_SWB); - spa_autoclose int sock = sco_create_socket(backend, d->adapter, transparent); + bool encoded = t->media_codec->id != SPA_BLUETOOTH_AUDIO_CODEC_CVSD; + spa_autoclose int sock = sco_create_socket(backend, d->adapter, encoded); if (sock < 0) return -1; @@ -2514,18 +2619,20 @@ static int sco_do_connect(struct spa_bt_transport *t) } else if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) { spa_log_error(backend->log, "connect(): %s", strerror(errno)); #ifdef HAVE_BLUEZ_5_BACKEND_HFP_NATIVE - if (errno == EOPNOTSUPP && t->codec != HFP_AUDIO_CODEC_CVSD && + if (errno == EOPNOTSUPP && encoded && td->rfcomm->codec_negotiation_supported) { - /* Adapter doesn't support msbc/lc3. Renegotiate. */ + /* Adapter doesn't support msbc/lc3/etc. Renegotiate. */ d->adapter->msbc_probed = true; d->adapter->has_msbc = false; - td->rfcomm->msbc_supported_by_hfp = false; - td->rfcomm->lc3_supported_by_hfp = false; + + codec_list_clear(&td->rfcomm->available_codec_list); + codec_list_clear(&td->rfcomm->supported_codec_list); + if (t->profile == SPA_BT_PROFILE_HFP_HF) { td->rfcomm->hfp_ag_switching_codec = true; rfcomm_send_reply(td->rfcomm, "+BCS: 1"); } else if (t->profile == SPA_BT_PROFILE_HFP_AG) { - rfcomm_send_cmd(td->rfcomm, "AT+BAC=1"); + rfcomm_send_cmd(td->rfcomm, hfp_hf_idle, NULL, "AT+BAC=1"); } } #endif @@ -2717,11 +2824,20 @@ static int sco_release_cb(void *data) static void sco_event(struct spa_source *source) { struct spa_bt_transport *t = source->data; + struct transport_data *td = t->user_data; struct impl *backend = SPA_CONTAINER_OF(t->backend, struct impl, this); if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { - spa_log_debug(backend->log, "transport %p: error on SCO socket: %s", t, strerror(errno)); + /* sco_ready() reads the socket error status in td->err */ sco_ready(t); + + if (td->err < 0) { + spa_log_info(backend->log, "transport %p: SCO socket error: %s (%d)", + t, strerror(-td->err), td->err); + } else { + spa_log_debug(backend->log, "transport %p: SCO socket hangup", t); + } + if (source->loop) spa_loop_remove_source(source->loop, source); if (t->fd >= 0) { @@ -2836,14 +2952,14 @@ static void sco_listen_event(struct spa_source *source) return; } - spa_log_debug(backend->log, "transport %p: codec=%u", t, t->codec); + spa_log_debug(backend->log, "transport %p: codec=%s", t, t->media_codec->description); if (backend->defer_setup_enabled) { /* In BT_DEFER_SETUP mode, when a connection is accepted, the listening socket is unblocked but * the effective connection setup happens only on first receive, allowing to configure the * accepted socket. */ char buff; - if (t->codec == HFP_AUDIO_CODEC_MSBC || t->codec == HFP_AUDIO_CODEC_LC3_SWB) { + if (t->media_codec->id != SPA_BLUETOOTH_AUDIO_CODEC_CVSD) { /* set correct socket options for mSBC/LC3 */ struct bt_voice voice_config; memset(&voice_config, 0, sizeof(voice_config)); @@ -2869,15 +2985,12 @@ static void sco_listen_event(struct spa_source *source) /* Report initial volume to remote */ if (t->profile == SPA_BT_PROFILE_HSP_AG) { - if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) - rfcomm->hs_state = hsp_hs_vgs; - else - rfcomm->hs_state = hsp_hs_init1; - } else if (t->profile == SPA_BT_PROFILE_HFP_AG) { - if (rfcomm_send_volume_cmd(rfcomm, SPA_BT_VOLUME_ID_RX)) - rfcomm->hf_state = hfp_hf_vgs; - else - rfcomm->hf_state = hfp_hf_slc1; + rfcomm_send_volume_cmd(rfcomm, hsp_hs_vgs, NULL, SPA_BT_VOLUME_ID_RX); + } else if (t->profile == SPA_BT_PROFILE_HFP_AG && rfcomm->hf_state > hfp_hf_vgs) { + /* Report volume only if SLC and setup sequence has been completed + * else this could break the sequence. + * The volumes will be reported at the end of the setup sequence. */ + rfcomm_send_volume_cmd(rfcomm, hfp_hf_vgs, NULL, SPA_BT_VOLUME_ID_RX); } spa_bt_transport_set_state(t, SPA_BT_TRANSPORT_STATE_PENDING); @@ -2934,7 +3047,7 @@ static int rfcomm_ag_set_volume(struct spa_bt_transport *t, int id) const char *format; int value; - if (!rfcomm_volume_enabled(rfcomm) + if (!rfcomm_hw_volume_enabled(rfcomm) || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) || !(rfcomm->has_volume && rfcomm->volumes[id].active)) return -ENOTSUP; @@ -2968,7 +3081,7 @@ static int sco_set_volume_cb(void *data, int id, float volume) struct rfcomm *rfcomm = td->rfcomm; int value; - if (!rfcomm_volume_enabled(rfcomm) + if (!rfcomm_hw_volume_enabled(rfcomm) || !(rfcomm->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) || !(rfcomm->has_volume && rfcomm->volumes[id].active)) return -ENOTSUP; @@ -3018,12 +3131,7 @@ static int backend_native_supports_codec(void *data, struct spa_bt_device *devic if (!rfcomm->codec_negotiation_supported) return 0; - if (codec == HFP_AUDIO_CODEC_MSBC) - return rfcomm->msbc_supported_by_hfp; - else if (codec == HFP_AUDIO_CODEC_LC3_SWB) - return rfcomm->lc3_supported_by_hfp; - - return 0; + return codec_list_get(backend, &rfcomm->supported_codec_list, codec) ? 1 : 0; #else return -ENOTSUP; #endif @@ -3106,6 +3214,7 @@ static void codec_switch_timer_event(struct spa_source *source) { struct rfcomm *rfcomm = source->data; struct impl *backend = rfcomm->backend; + const struct media_codec *best_codec; uint64_t exp; if (spa_system_timerfd_read(backend->main_system, source->fd, &exp) < 0) @@ -3118,10 +3227,14 @@ static void codec_switch_timer_event(struct spa_source *source) switch (rfcomm->hfp_ag_initial_codec_setup) { case HFP_AG_INITIAL_CODEC_SETUP_SEND: /* Retry codec selection */ - rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_WAIT; - rfcomm_send_reply(rfcomm, "+BCS: 2"); - codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC); - return; + best_codec = codec_list_best(backend, &rfcomm->supported_codec_list); + if (best_codec && best_codec->id != SPA_BLUETOOTH_AUDIO_CODEC_CVSD) { + rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_WAIT; + rfcomm_send_reply(rfcomm, "+BCS: %u", best_codec->codec_id); + codec_switch_start_timer(rfcomm, HFP_CODEC_SWITCH_TIMEOUT_MSEC); + return; + } + SPA_FALLTHROUGH; case HFP_AG_INITIAL_CODEC_SETUP_WAIT: /* Failure, try falling back to CVSD. */ rfcomm->hfp_ag_initial_codec_setup = HFP_AG_INITIAL_CODEC_SETUP_NONE; @@ -3286,11 +3399,15 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag rfcomm->source.fd = spa_steal_fd(fd); rfcomm->source.mask = SPA_IO_IN; rfcomm->source.rmask = 0; - spa_list_init(&rfcomm->hfp_hf_commands); + spa_list_init(&rfcomm->cmd_send_queue); + spa_list_init(&rfcomm->updated_call_list); /* By default all indicators are enabled */ rfcomm->cind_enabled_indicators = 0xFFFFFFFF; memset(rfcomm->hf_indicators, 0, sizeof rfcomm->hf_indicators); + spa_list_init(&rfcomm->available_codec_list); + spa_list_init(&rfcomm->supported_codec_list); + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) { if (rfcomm->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) rfcomm->volumes[i].active = true; @@ -3305,7 +3422,7 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag if (rfcomm_new_transport(rfcomm, HFP_AUDIO_CODEC_CVSD) < 0) goto fail_need_memory; - rfcomm->has_volume = rfcomm_volume_enabled(rfcomm); + rfcomm->has_volume = rfcomm_hw_volume_enabled(rfcomm); if (profile == SPA_BT_PROFILE_HSP_AG) { rfcomm->hs_state = hsp_hs_init1; @@ -3318,37 +3435,29 @@ static DBusHandlerResult profile_new_connection(DBusConnection *conn, DBusMessag } else if (profile == SPA_BT_PROFILE_HFP_AG) { /* Start SLC connection */ unsigned int hf_features = SPA_BT_HFP_HF_FEATURE_CLIP | SPA_BT_HFP_HF_FEATURE_3WAY | + SPA_BT_HFP_HF_FEATURE_ECNR | SPA_BT_HFP_HF_FEATURE_ENHANCED_CALL_STATUS | SPA_BT_HFP_HF_FEATURE_ESCO_S4; - bool has_msbc = device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_MSBC); - bool has_lc3 = device_supports_codec(backend, rfcomm->device, HFP_AUDIO_CODEC_LC3_SWB); + + make_available_codec_list(backend, rfcomm->device, &rfcomm->available_codec_list); + rfcomm->codec_negotiation_supported = false; /* Decide if we want to signal that the HF supports mSBC/LC3 negotiation This should be done when the bluetooth adapter supports the necessary transport mode */ - if (has_msbc || has_lc3) { - /* set the feature bit that indicates HF supports codec negotiation */ + if (!spa_list_is_empty(&rfcomm->available_codec_list)) hf_features |= SPA_BT_HFP_HF_FEATURE_CODEC_NEGOTIATION; - rfcomm->msbc_supported_by_hfp = has_msbc; - rfcomm->lc3_supported_by_hfp = has_lc3; - rfcomm->codec_negotiation_supported = false; - } else { - rfcomm->msbc_supported_by_hfp = false; - rfcomm->lc3_supported_by_hfp = false; - rfcomm->codec_negotiation_supported = false; - } - if (rfcomm_volume_enabled(rfcomm)) { - rfcomm->has_volume = true; - hf_features |= SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL; - } + rfcomm->has_volume = true; + hf_features |= SPA_BT_HFP_HF_FEATURE_REMOTE_VOLUME_CONTROL; /* send command to AG with the features supported by Hands-Free */ - rfcomm_send_cmd(rfcomm, "AT+BRSF=%u", hf_features); + rfcomm_send_cmd(rfcomm, hfp_hf_brsf, NULL, "AT+BRSF=%u", hf_features); - rfcomm->hf_state = hfp_hf_brsf; + } else if (profile == SPA_BT_PROFILE_HFP_HF) { + make_available_codec_list(backend, rfcomm->device, &rfcomm->available_codec_list); } - if (rfcomm_volume_enabled(rfcomm) && (profile == SPA_BT_PROFILE_HFP_HF || profile == SPA_BT_PROFILE_HSP_HS)) { + if (rfcomm_hw_volume_enabled(rfcomm) && (profile == SPA_BT_PROFILE_HFP_HF || profile == SPA_BT_PROFILE_HSP_HS)) { uint32_t device_features; if (spa_bt_quirks_get_features(backend->quirks, d->adapter, d, &device_features) == 0) { rfcomm->broken_mic_hw_volume = !(device_features & SPA_BT_FEATURE_HW_VOLUME_MIC); @@ -3533,11 +3642,13 @@ static int register_profile(struct impl *backend, const char *profile, const cha } else if (spa_streq(uuid, SPA_BT_UUID_HFP_AG)) { str = "Features"; - /* We announce wideband speech support anyway */ - features = SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH; -#ifdef HAVE_LC3 - features |= SPA_BT_HFP_SDP_AG_FEATURE_SUPER_WIDEBAND_SPEECH; -#endif + features = 0; + + if (spa_bt_get_hfp_codec(backend->monitor, HFP_AUDIO_CODEC_MSBC)) + features |= SPA_BT_HFP_SDP_AG_FEATURE_WIDEBAND_SPEECH; + if (spa_bt_get_hfp_codec(backend->monitor, HFP_AUDIO_CODEC_LC3_SWB)) + features |= SPA_BT_HFP_SDP_AG_FEATURE_SUPER_WIDEBAND_SPEECH; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); @@ -3557,11 +3668,13 @@ static int register_profile(struct impl *backend, const char *profile, const cha } else if (spa_streq(uuid, SPA_BT_UUID_HFP_HF)) { str = "Features"; - /* We announce wideband speech support anyway */ - features = SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH; -#ifdef HAVE_LC3 - features |= SPA_BT_HFP_SDP_HF_FEATURE_SUPER_WIDEBAND_SPEECH; -#endif + features = 0; + + if (spa_bt_get_hfp_codec(backend->monitor, HFP_AUDIO_CODEC_MSBC)) + features |= SPA_BT_HFP_SDP_HF_FEATURE_WIDEBAND_SPEECH; + if (spa_bt_get_hfp_codec(backend->monitor, HFP_AUDIO_CODEC_LC3_SWB)) + features |= SPA_BT_HFP_SDP_HF_FEATURE_SUPER_WIDEBAND_SPEECH; + dbus_message_iter_open_container(&it[1], DBUS_TYPE_DICT_ENTRY, NULL, &it[2]); dbus_message_iter_append_basic(&it[2], DBUS_TYPE_STRING, &str); dbus_message_iter_open_container(&it[2], DBUS_TYPE_VARIANT, "q", &it[3]); @@ -3898,6 +4011,29 @@ static void parse_hfp_disable_nrec(struct impl *backend, const struct spa_dict * backend->hfp_disable_nrec = false; } +static void parse_hfp_default_volumes(struct impl *backend, const struct spa_dict *info) +{ + const char *str; + int vol = -1; + + if ((str = spa_dict_lookup(info, PROP_KEY_HFP_DEFAULT_MIC_VOL)) != NULL) + spa_atoi32(str, &vol, 10); + + if (vol >= 0 && vol <= 15) + backend->hfp_default_mic_volume = vol; + else + backend->hfp_default_mic_volume = SPA_BT_VOLUME_HS_MAX; + + vol = -1; + if ((str = spa_dict_lookup(info, PROP_KEY_HFP_DEFAULT_SPEAKER_VOL)) != NULL) + spa_atoi32(str, &vol, 10); + + if (vol >= 0 && vol <= 15) + backend->hfp_default_speaker_volume = vol; + else + backend->hfp_default_speaker_volume = SPA_BT_VOLUME_HS_MAX; +} + static const struct spa_bt_backend_implementation backend_impl = { SPA_VERSION_BT_BACKEND_IMPLEMENTATION, .free = backend_native_free, @@ -3948,6 +4084,8 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, backend->conn = dbus_connection; backend->sco.fd = -1; + backend->codecs = spa_bt_get_media_codecs(monitor); + spa_log_topic_init(backend->log, &log_topic); spa_list_init(&backend->rfcomm_list); @@ -3956,6 +4094,7 @@ struct spa_bt_backend *backend_native_new(struct spa_bt_monitor *monitor, goto fail; parse_hfp_disable_nrec(backend, info); + parse_hfp_default_volumes(backend, info); #ifdef HAVE_BLUEZ_5_BACKEND_HSP_NATIVE if (!dbus_connection_register_object_path(backend->conn, diff --git a/spa/plugins/bluez5/backend-ofono.c b/spa/plugins/bluez5/backend-ofono.c index 42dd0eceb..d0581ddf4 100644 --- a/spa/plugins/bluez5/backend-ofono.c +++ b/spa/plugins/bluez5/backend-ofono.c @@ -23,6 +23,8 @@ #include #include "defs.h" +#include "media-codecs.h" +#include "hfp-codec-caps.h" #define INITIAL_INTERVAL_NSEC (500 * SPA_NSEC_PER_MSEC) #define ACTION_INTERVAL_NSEC (3000 * SPA_NSEC_PER_MSEC) @@ -53,6 +55,7 @@ struct impl { struct transport_data { struct spa_source sco; + unsigned int codec_id; unsigned int broken:1; unsigned int activated:1; }; @@ -112,17 +115,26 @@ static struct spa_bt_transport *_transport_create(struct impl *backend, const char *path, struct spa_bt_device *device, enum spa_bt_profile profile, - int codec, + int codec_id, struct spa_callbacks *impl) { struct spa_bt_transport *t = NULL; - char *t_path = strdup(path); + const struct media_codec *codec; + struct transport_data *td; + char *t_path; + codec = spa_bt_get_hfp_codec(backend->monitor, codec_id); + if (!codec) { + spa_log_warn(backend->log, "can't create transport: no HFP codec %d", codec_id); + return NULL; + } + + t_path = strdup(path); t = spa_bt_transport_create(backend->monitor, t_path, sizeof(struct transport_data)); if (t == NULL) { spa_log_warn(backend->log, "can't create transport: %m"); free(t_path); - goto finish; + return NULL; } spa_bt_transport_set_implementation(t, impl, t); @@ -130,11 +142,13 @@ static struct spa_bt_transport *_transport_create(struct impl *backend, spa_list_append(&t->device->transport_list, &t->device_link); t->backend = &backend->this; t->profile = profile; - t->codec = codec; + t->media_codec = codec; t->n_channels = 1; t->channels[0] = SPA_AUDIO_CHANNEL_MONO; -finish: + td = t->user_data; + td->codec_id = codec_id; + return t; } @@ -186,7 +200,7 @@ static int ofono_audio_acquire(void *data, bool optional) struct spa_bt_transport *transport = data; struct transport_data *td = transport->user_data; struct impl *backend = SPA_CONTAINER_OF(transport->backend, struct impl, this); - uint8_t codec; + uint8_t codec_id; int ret = 0; if (transport->fd >= 0) @@ -198,17 +212,17 @@ static int ofono_audio_acquire(void *data, bool optional) spa_bt_device_update_last_bluez_action_time(transport->device); - ret = _audio_acquire(backend, transport->path, &codec); + ret = _audio_acquire(backend, transport->path, &codec_id); if (ret < 0) goto finish; transport->fd = ret; - if (transport->codec != codec) { + if (transport->media_codec->codec_id != codec_id) { struct timespec ts; spa_log_info(backend->log, "transport %p: acquired codec (%d) differs from transport one (%d)", - transport, codec, transport->codec); + transport, codec_id, transport->media_codec->codec_id); /* shutdown to make sure connection is dropped immediately */ shutdown(transport->fd, SHUT_RDWR); @@ -216,7 +230,7 @@ static int ofono_audio_acquire(void *data, bool optional) transport->fd = -1; /* schedule immediate profile update, from main loop */ - transport->codec = codec; + td->codec_id = codec_id; td->broken = true; ts.tv_sec = 0; ts.tv_nsec = 1; @@ -229,8 +243,8 @@ static int ofono_audio_acquire(void *data, bool optional) td->broken = false; - spa_log_debug(backend->log, "transport %p: Acquire %s, fd %d codec %d", transport, - transport->path, transport->fd, transport->codec); + spa_log_debug(backend->log, "transport %p: Acquire %s, fd %d codec %s", transport, + transport->path, transport->fd, transport->media_codec->description); ofono_transport_get_mtu(backend, transport); ret = 0; @@ -332,7 +346,7 @@ static bool activate_transport(struct spa_bt_transport *t, const void *data) struct spa_bt_transport *t_copy; t_copy = _transport_create(backend, t->path, t->device, - t->profile, t->codec, (struct spa_callbacks *)&ofono_transport_impl); + t->profile, td->codec_id, (struct spa_callbacks *)&ofono_transport_impl); spa_bt_transport_free(t); if (t_copy) @@ -364,7 +378,7 @@ static DBusHandlerResult ofono_audio_card_found(struct impl *backend, char *path struct spa_bt_transport *t; struct transport_data *td; enum spa_bt_profile profile = SPA_BT_PROFILE_HFP_AG; - uint8_t codec = backend->msbc_supported ? + uint8_t codec_id = backend->msbc_supported ? HFP_AUDIO_CODEC_MSBC : HFP_AUDIO_CODEC_CVSD; spa_assert(backend); @@ -417,7 +431,7 @@ static DBusHandlerResult ofono_audio_card_found(struct impl *backend, char *path } spa_bt_device_add_profile(d, profile); - t = _transport_create(backend, path, d, profile, codec, (struct spa_callbacks *)&ofono_transport_impl); + t = _transport_create(backend, path, d, profile, codec_id, (struct spa_callbacks *)&ofono_transport_impl); if (t == NULL) { spa_log_error(backend->log, "failed to create transport: %s", spa_strerror(-errno)); return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; @@ -444,7 +458,7 @@ static DBusHandlerResult ofono_audio_card_found(struct impl *backend, char *path spa_bt_device_connect_profile(t->device, t->profile); } - spa_log_debug(backend->log, "Transport %s available, codec %d", t->path, t->codec); + spa_log_debug(backend->log, "Transport %s available, codec %s", t->path, t->media_codec->description); return DBUS_HANDLER_RESULT_HANDLED; } @@ -513,7 +527,7 @@ static DBusHandlerResult ofono_new_audio_connection(DBusConnection *conn, DBusMe struct impl *backend = userdata; const char *path; int fd; - uint8_t codec; + uint8_t codec_id; struct spa_bt_transport *t; struct transport_data *td; spa_autoptr(DBusMessage) r = NULL; @@ -521,7 +535,7 @@ static DBusHandlerResult ofono_new_audio_connection(DBusConnection *conn, DBusMe if (dbus_message_get_args(m, NULL, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_UNIX_FD, &fd, - DBUS_TYPE_BYTE, &codec, + DBUS_TYPE_BYTE, &codec_id, DBUS_TYPE_INVALID) == FALSE) { r = dbus_message_new_error(m, OFONO_ERROR_INVALID_ARGUMENTS, "Invalid arguments in method call"); goto fail; @@ -530,6 +544,15 @@ static DBusHandlerResult ofono_new_audio_connection(DBusConnection *conn, DBusMe t = spa_bt_transport_find(backend->monitor, path); if (t && (t->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) { int err; + const struct media_codec *codec; + + codec = spa_bt_get_hfp_codec(backend->monitor, codec_id); + if (!codec) { + spa_log_error(backend->log, "transport %p: Couldn't find HFP codec %d", t, codec_id); + shutdown(fd, SHUT_RDWR); + close(fd); + goto fail; + } err = enable_sco_socket(fd); if (err) { @@ -541,10 +564,10 @@ static DBusHandlerResult ofono_new_audio_connection(DBusConnection *conn, DBusMe } t->fd = fd; - t->codec = codec; + t->media_codec = codec; - spa_log_debug(backend->log, "transport %p: NewConnection %s, fd %d codec %d", - t, t->path, t->fd, t->codec); + spa_log_debug(backend->log, "transport %p: NewConnection %s, fd %d codec %s", + t, t->path, t->fd, t->media_codec->description); td = t->user_data; td->sco.func = sco_event; @@ -817,10 +840,24 @@ static int backend_ofono_free(void *data) return 0; } +static int backend_ofono_supports_codec(void *data, struct spa_bt_device *device, unsigned int codec) +{ + struct impl *backend = data; + + switch (codec) { + case HFP_AUDIO_CODEC_CVSD: + return 1; + case HFP_AUDIO_CODEC_MSBC: + return backend->msbc_supported; + } + return 0; +} + static const struct spa_bt_backend_implementation backend_impl = { SPA_VERSION_BT_BACKEND_IMPLEMENTATION, .free = backend_ofono_free, .register_profiles = backend_ofono_register, + .supports_codec = backend_ofono_supports_codec, }; static bool is_available(struct impl *backend) @@ -874,6 +911,9 @@ struct spa_bt_backend *backend_ofono_new(struct spa_bt_monitor *monitor, else backend->msbc_supported = false; + if (!spa_bt_get_hfp_codec(monitor, HFP_AUDIO_CODEC_MSBC)) + backend->msbc_supported = false; + spa_log_topic_init(backend->log, &log_topic); backend->timer = spa_loop_utils_add_timer(backend->loop_utils, activate_timer_event, backend); diff --git a/spa/plugins/bluez5/bap-codec-caps.h b/spa/plugins/bluez5/bap-codec-caps.h index 9d1da42b4..6a862894c 100644 --- a/spa/plugins/bluez5/bap-codec-caps.h +++ b/spa/plugins/bluez5/bap-codec-caps.h @@ -5,6 +5,8 @@ #ifndef SPA_BLUEZ5_BAP_CODEC_CAPS_H_ #define SPA_BLUEZ5_BAP_CODEC_CAPS_H_ +#include + #define BAP_CODEC_LC3 0x06 #define LC3_TYPE_FREQ 0x01 @@ -175,4 +177,39 @@ struct bap_codec_qos_full { struct bap_codec_qos qos; }; +static const struct { + uint32_t bit; + enum spa_audio_channel channel; +} bap_channel_bits[] = { + { BAP_CHANNEL_MONO, SPA_AUDIO_CHANNEL_MONO }, + { BAP_CHANNEL_FL, SPA_AUDIO_CHANNEL_FL }, + { BAP_CHANNEL_FR, SPA_AUDIO_CHANNEL_FR }, + { BAP_CHANNEL_FC, SPA_AUDIO_CHANNEL_FC }, + { BAP_CHANNEL_LFE, SPA_AUDIO_CHANNEL_LFE }, + { BAP_CHANNEL_BL, SPA_AUDIO_CHANNEL_RL }, + { BAP_CHANNEL_BR, SPA_AUDIO_CHANNEL_RR }, + { BAP_CHANNEL_FLC, SPA_AUDIO_CHANNEL_FLC }, + { BAP_CHANNEL_FRC, SPA_AUDIO_CHANNEL_FRC }, + { BAP_CHANNEL_BC, SPA_AUDIO_CHANNEL_BC }, + { BAP_CHANNEL_LFE2, SPA_AUDIO_CHANNEL_LFE2 }, + { BAP_CHANNEL_SL, SPA_AUDIO_CHANNEL_SL }, + { BAP_CHANNEL_SR, SPA_AUDIO_CHANNEL_SR }, + { BAP_CHANNEL_TFL, SPA_AUDIO_CHANNEL_TFL }, + { BAP_CHANNEL_TFR, SPA_AUDIO_CHANNEL_TFR }, + { BAP_CHANNEL_TFC, SPA_AUDIO_CHANNEL_TFC }, + { BAP_CHANNEL_TC, SPA_AUDIO_CHANNEL_TC }, + { BAP_CHANNEL_TBL, SPA_AUDIO_CHANNEL_TRL }, + { BAP_CHANNEL_TBR, SPA_AUDIO_CHANNEL_TRR }, + { BAP_CHANNEL_TSL, SPA_AUDIO_CHANNEL_TSL }, + { BAP_CHANNEL_TSR, SPA_AUDIO_CHANNEL_TSR }, + { BAP_CHANNEL_TBC, SPA_AUDIO_CHANNEL_TRC }, + { BAP_CHANNEL_BFC, SPA_AUDIO_CHANNEL_BC }, + { BAP_CHANNEL_BFL, SPA_AUDIO_CHANNEL_BLC }, + { BAP_CHANNEL_BFR, SPA_AUDIO_CHANNEL_BRC }, + { BAP_CHANNEL_FLW, SPA_AUDIO_CHANNEL_FLW }, + { BAP_CHANNEL_FRW, SPA_AUDIO_CHANNEL_FRW }, + { BAP_CHANNEL_LS, SPA_AUDIO_CHANNEL_SL }, /* is it the right mapping? */ + { BAP_CHANNEL_RS, SPA_AUDIO_CHANNEL_SR }, /* is it the right mapping? */ +}; + #endif diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c index e9b772325..10d9eecc8 100644 --- a/spa/plugins/bluez5/bap-codec-lc3.c +++ b/spa/plugins/bluez5/bap-codec-lc3.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -31,21 +32,33 @@ struct impl { lc3_decoder_t dec[LC3_MAX_CHANNELS]; int samplerate; + int codec_samplerate; int channels; int frame_dus; int framelen; int samples; unsigned int codesize; + + uint16_t seqnum; +}; + +struct settings { + uint32_t locations; + uint32_t channel_allocation; + bool sink; + bool duplex; + const char *qos_name; + int retransmission; + int latency; + int64_t delay; + int framing; }; struct pac_data { const uint8_t *data; size_t size; int index; - uint32_t locations; - uint32_t channel_allocation; - bool sink; - bool duplex; + const struct settings *settings; }; struct bap_qos { @@ -71,41 +84,6 @@ typedef struct { unsigned int priority; } bap_lc3_t; -static const struct { - uint32_t bit; - enum spa_audio_channel channel; -} channel_bits[] = { - { BAP_CHANNEL_MONO, SPA_AUDIO_CHANNEL_MONO }, - { BAP_CHANNEL_FL, SPA_AUDIO_CHANNEL_FL }, - { BAP_CHANNEL_FR, SPA_AUDIO_CHANNEL_FR }, - { BAP_CHANNEL_FC, SPA_AUDIO_CHANNEL_FC }, - { BAP_CHANNEL_LFE, SPA_AUDIO_CHANNEL_LFE }, - { BAP_CHANNEL_BL, SPA_AUDIO_CHANNEL_RL }, - { BAP_CHANNEL_BR, SPA_AUDIO_CHANNEL_RR }, - { BAP_CHANNEL_FLC, SPA_AUDIO_CHANNEL_FLC }, - { BAP_CHANNEL_FRC, SPA_AUDIO_CHANNEL_FRC }, - { BAP_CHANNEL_BC, SPA_AUDIO_CHANNEL_BC }, - { BAP_CHANNEL_LFE2, SPA_AUDIO_CHANNEL_LFE2 }, - { BAP_CHANNEL_SL, SPA_AUDIO_CHANNEL_SL }, - { BAP_CHANNEL_SR, SPA_AUDIO_CHANNEL_SR }, - { BAP_CHANNEL_TFL, SPA_AUDIO_CHANNEL_TFL }, - { BAP_CHANNEL_TFR, SPA_AUDIO_CHANNEL_TFR }, - { BAP_CHANNEL_TFC, SPA_AUDIO_CHANNEL_TFC }, - { BAP_CHANNEL_TC, SPA_AUDIO_CHANNEL_TC }, - { BAP_CHANNEL_TBL, SPA_AUDIO_CHANNEL_TRL }, - { BAP_CHANNEL_TBR, SPA_AUDIO_CHANNEL_TRR }, - { BAP_CHANNEL_TSL, SPA_AUDIO_CHANNEL_TSL }, - { BAP_CHANNEL_TSR, SPA_AUDIO_CHANNEL_TSR }, - { BAP_CHANNEL_TBC, SPA_AUDIO_CHANNEL_TRC }, - { BAP_CHANNEL_BFC, SPA_AUDIO_CHANNEL_BC }, - { BAP_CHANNEL_BFL, SPA_AUDIO_CHANNEL_BLC }, - { BAP_CHANNEL_BFR, SPA_AUDIO_CHANNEL_BRC }, - { BAP_CHANNEL_FLW, SPA_AUDIO_CHANNEL_FLW }, - { BAP_CHANNEL_FRW, SPA_AUDIO_CHANNEL_FRW }, - { BAP_CHANNEL_LS, SPA_AUDIO_CHANNEL_SL }, /* is it the right mapping? */ - { BAP_CHANNEL_RS, SPA_AUDIO_CHANNEL_SR }, /* is it the right mapping? */ -}; - #define BAP_QOS(name_, rate_, duration_, framing_, framelen_, rtn_, latency_, delay_, priority_) \ ((struct bap_qos){ .name = (name_), .rate = (rate_), .frame_duration = (duration_), .framing = (framing_), \ .framelen = (framelen_), .retransmission = (rtn_), .latency = (latency_), \ @@ -344,7 +322,7 @@ static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, uint8_t *data = caps; const char *str; uint16_t framelen[2]; - uint16_t rate_mask = LC3_FREQ_48KHZ | LC3_FREQ_32KHZ | \ + uint16_t rate_mask = LC3_FREQ_48KHZ | LC3_FREQ_44KHZ | LC3_FREQ_32KHZ | \ LC3_FREQ_24KHZ | LC3_FREQ_16KHZ | LC3_FREQ_8KHZ; uint8_t duration_mask = LC3_DUR_ANY; uint8_t channel_counts = LC3_CHAN_1 | LC3_CHAN_2; @@ -470,28 +448,47 @@ static bool supports_channel_count(uint8_t mask, uint8_t count) return mask & (1u << (count - 1)); } -static const struct bap_qos *select_bap_qos(unsigned int rate_mask, unsigned int duration_mask, uint16_t framelen_min, uint16_t framelen_max) +static bool select_bap_qos(struct bap_qos *conf, + const struct settings *s, unsigned int rate_mask, + unsigned int duration_mask, uint16_t framelen_min, + uint16_t framelen_max) { - const struct bap_qos *best = NULL; - unsigned int best_priority = 0; + bool found = false; + conf->name = NULL; + conf->priority = 0; - SPA_FOR_EACH_ELEMENT_VAR(bap_qos_configs, c) { - if (c->priority < best_priority) - continue; - if (!(get_rate_mask(c->rate) & rate_mask)) - continue; - if (!(get_duration_mask(c->frame_duration) & duration_mask)) - continue; - if (c->framing) - continue; /* XXX: framing not supported */ - if (c->framelen < framelen_min || c->framelen > framelen_max) + SPA_FOR_EACH_ELEMENT_VAR(bap_qos_configs, cur_conf) { + struct bap_qos c = *cur_conf; + + /* Check if custom QoS settings are configured. If so, we check if + * the configured settings are compatible with unicast server + */ + if (spa_streq(c.name, s->qos_name)) + c.priority = UINT_MAX; + else if (c.priority < conf->priority) continue; - best = c; - best_priority = c->priority; + if (s->retransmission >= 0) + c.retransmission = s->retransmission; + if (s->latency >= 0) + c.latency = s->latency; + if (s->delay >= 0) + c.delay = s->delay; + if (s->framing >= 0) + c.framing = s->framing; + + if (!(get_rate_mask(c.rate) & rate_mask)) + continue; + if (!(get_duration_mask(c.frame_duration) & duration_mask)) + continue; + if (c.framelen < framelen_min || c.framelen > framelen_max) + continue; + + *conf = c; + found = true; } - return best; + return found; } static int select_channels(uint8_t channel_counts, uint32_t locations, uint32_t channel_allocation, @@ -538,9 +535,9 @@ static int select_channels(uint8_t channel_counts, uint32_t locations, uint32_t return -1; *allocation = 0; - for (i = 0; i < SPA_N_ELEMENTS(channel_bits); ++i) { - if (locations & channel_bits[i].bit) { - *allocation |= channel_bits[i].bit; + for (i = 0; i < SPA_N_ELEMENTS(bap_channel_bits); ++i) { + if (locations & bap_channel_bits[i].bit) { + *allocation |= bap_channel_bits[i].bit; --num; if (num == 0) break; @@ -560,15 +557,16 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac, struct sp uint8_t max_channels = 0; uint8_t duration_mask = 0; uint16_t rate_mask = 0; - const struct bap_qos *bap_qos = NULL; + struct bap_qos bap_qos; unsigned int i; + bool found = false; if (!data_size) return false; memset(conf, 0, sizeof(*conf)); - conf->sink = pac->sink; - conf->duplex = pac->duplex; + conf->sink = pac->settings->sink; + conf->duplex = pac->settings->duplex; /* XXX: we always use one frame block */ conf->n_blks = 1; @@ -630,7 +628,7 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac, struct sp max_frames = max_channels; } - if (select_channels(channel_counts, pac->locations, pac->channel_allocation, &conf->channels) < 0) { + if (select_channels(channel_counts, pac->settings->locations, pac->settings->channel_allocation, &conf->channels) < 0) { spa_debugc(debug_ctx, "invalid channel configuration: 0x%02x %u", channel_counts, max_frames); return false; @@ -647,27 +645,31 @@ static bool select_config(bap_lc3_t *conf, const struct pac_data *pac, struct sp * Frame length is not limited by ISO MTU, as kernel will fragment * and reassemble SDUs as needed. */ - if (pac->sink && pac->duplex) { + if (pac->settings->duplex) { /* 16KHz input is mandatory in BAP v1.0.1 Table 3.5, so prefer - * it for now for input rate in duplex configuration. + * it or 32kHz for now for input rate in duplex configuration. + * + * It appears few devices support 48kHz out + input, so in duplex mode + * try 32 kHz or 16 kHz also for output direction. * * Devices may list other values but not certain they will work properly. */ - bap_qos = select_bap_qos(rate_mask & LC3_FREQ_16KHZ, duration_mask, framelen_min, framelen_max); + found = select_bap_qos(&bap_qos, pac->settings, rate_mask & (LC3_FREQ_16KHZ | LC3_FREQ_32KHZ), + duration_mask, framelen_min, framelen_max); } - if (!bap_qos) - bap_qos = select_bap_qos(rate_mask, duration_mask, framelen_min, framelen_max); + if (!found) + found = select_bap_qos(&bap_qos, pac->settings, rate_mask, duration_mask, framelen_min, framelen_max); - if (!bap_qos) { + if (!found) { spa_debugc(debug_ctx, "no compatible configuration found, rate:0x%08x, duration:0x%08x frame:%u-%u", rate_mask, duration_mask, framelen_min, framelen_max); return false; } - conf->rate = bap_qos->rate; - conf->frame_duration = bap_qos->frame_duration; - conf->framelen = bap_qos->framelen; - conf->priority = bap_qos->priority; + conf->rate = bap_qos.rate; + conf->frame_duration = bap_qos.frame_duration; + conf->framelen = bap_qos.framelen; + conf->priority = bap_qos.priority; return true; } @@ -749,11 +751,13 @@ static int conf_cmp(const bap_lc3_t *conf1, int res1, const bap_lc3_t *conf2, in if (!a || !b) return b - a; + PREFER_EXPR(conf->priority == UINT_MAX); + PREFER_BOOL(conf->channels & LC3_CHAN_2); PREFER_BOOL(conf->channels & LC3_CHAN_1); - if (conf->sink && conf->duplex) - PREFER_BOOL(conf->rate & LC3_CONFIG_FREQ_16KHZ); + if (conf->duplex) + PREFER_BOOL(conf->rate & (LC3_CONFIG_FREQ_16KHZ | LC3_CONFIG_FREQ_32KHZ)); PREFER_EXPR(conf->priority); @@ -777,6 +781,62 @@ static int pac_cmp(const void *p1, const void *p2) return conf_cmp(&conf1, res1, &conf2, res2); } +static void parse_settings(struct settings *s, const struct spa_dict *settings, + struct spa_debug_log_ctx *debug_ctx) +{ + const char *str; + uint32_t value; + + spa_zero(*s); + s->retransmission = -1; + s->latency = -1; + s->delay = -1; + s->framing = -1; + + if (!settings) + return; + + if ((str = spa_dict_lookup(settings, "bluez5.bap.preset"))) + s->qos_name = str; + + if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.rtn"), &value, 0)) + s->retransmission = value; + + if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.latency"), &value, 0)) + s->latency = value; + + if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.delay"), &value, 0)) + s->delay = value; + + if ((str = spa_dict_lookup(settings, "bluez5.bap.framing"))) + s->framing = spa_atob(str); + + if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.locations"), &value, 0)) + s->locations = value; + + if (spa_atou32(spa_dict_lookup(settings, "bluez5.bap.channel-allocation"), &value, 0)) + s->channel_allocation = value; + + if (spa_atob(spa_dict_lookup(settings, "bluez5.bap.debug"))) + *debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_DEBUG); + else + *debug_ctx = SPA_LOG_DEBUG_INIT(NULL, SPA_LOG_LEVEL_TRACE); + + /* Is remote endpoint sink or source */ + s->sink = spa_atob(spa_dict_lookup(settings, "bluez5.bap.sink")); + + /* Is remote endpoint duplex */ + s->duplex = spa_atob(spa_dict_lookup(settings, "bluez5.bap.duplex")); + + spa_debugc(&debug_ctx->ctx, + "BAP LC3 settings: preset:%s rtn:%d latency:%d delay:%d framing:%d " + "locations:%x chnalloc:%x sink:%d duplex:%d", + s->qos_name ? s->qos_name : "auto", + s->retransmission, s->latency, (int)s->delay, s->framing, + (unsigned int)s->locations, (unsigned int)s->channel_allocation, + (int)s->sink, (int)s->duplex); +} + static int codec_select_config(const struct media_codec *codec, uint32_t flags, const void *caps, size_t caps_size, const struct media_codec_audio_info *info, @@ -786,32 +846,14 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags, int npacs; bap_lc3_t conf; uint8_t *data = config; - uint32_t locations = 0; - uint32_t channel_allocation = 0; - bool sink = false, duplex = false; - struct spa_debug_log_ctx debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_TRACE); + struct spa_debug_log_ctx debug_ctx; + struct settings s; int i; if (caps == NULL) return -EINVAL; - if (settings) { - for (i = 0; i < (int)settings->n_items; ++i) { - if (spa_streq(settings->items[i].key, "bluez5.bap.locations")) - sscanf(settings->items[i].value, "%"PRIu32, &locations); - if (spa_streq(settings->items[i].key, "bluez5.bap.channel-allocation")) - sscanf(settings->items[i].value, "%"PRIu32, &channel_allocation); - } - - if (spa_atob(spa_dict_lookup(settings, "bluez5.bap.debug"))) - debug_ctx = SPA_LOG_DEBUG_INIT(log_, SPA_LOG_LEVEL_DEBUG); - - /* Is remote endpoint sink or source */ - sink = spa_atob(spa_dict_lookup(settings, "bluez5.bap.sink")); - - /* Is remote endpoint duplex */ - duplex = spa_atob(spa_dict_lookup(settings, "bluez5.bap.duplex")); - } + parse_settings(&s, settings, &debug_ctx); /* Select best conf from those possible */ npacs = parse_bluez_pacs(caps, caps_size, pacs, &debug_ctx.ctx); @@ -823,12 +865,8 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags, return -EINVAL; } - for (i = 0; i < npacs; ++i) { - pacs[i].locations = locations; - pacs[i].channel_allocation = channel_allocation; - pacs[i].sink = sink; - pacs[i].duplex = duplex; - } + for (i = 0; i < npacs; ++i) + pacs[i].settings = &s; qsort(pacs, npacs, sizeof(struct pac_data), pac_cmp); @@ -863,12 +901,12 @@ static int codec_caps_preference_cmp(const struct media_codec *codec, uint32_t f return conf_cmp(&conf1, res1, &conf2, res2); } -static uint8_t channels_to_positions(uint32_t channels, uint32_t *position) +static uint8_t channels_to_positions(uint32_t channels, uint32_t *position, uint32_t max_position) { - uint8_t n_channels = get_channel_count(channels); + uint32_t n_channels = get_channel_count(channels); uint8_t n_positions = 0; - spa_assert(n_channels <= SPA_AUDIO_MAX_CHANNELS); + spa_assert(n_channels <= max_position); if (channels == 0) { position[0] = SPA_AUDIO_CHANNEL_MONO; @@ -876,9 +914,9 @@ static uint8_t channels_to_positions(uint32_t channels, uint32_t *position) } else { unsigned int i; - for (i = 0; i < SPA_N_ELEMENTS(channel_bits); ++i) - if (channels & channel_bits[i].bit) - position[n_positions++] = channel_bits[i].channel; + for (i = 0; i < SPA_N_ELEMENTS(bap_channel_bits); ++i) + if (channels & bap_channel_bits[i].bit) + position[n_positions++] = bap_channel_bits[i].channel; } if (n_positions != n_channels) @@ -894,7 +932,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, bap_lc3_t conf; struct spa_pod_frame f[2]; struct spa_pod_choice *choice; - uint32_t position[SPA_AUDIO_MAX_CHANNELS]; + uint32_t position[LC3_MAX_CHANNELS]; uint32_t i = 0; uint8_t res; @@ -920,6 +958,11 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, spa_pod_builder_int(b, 48000); spa_pod_builder_int(b, 48000); } + if (conf.rate == LC3_CONFIG_FREQ_44KHZ) { + if (i++ == 0) + spa_pod_builder_int(b, 44100); + spa_pod_builder_int(b, 44100); + } if (conf.rate == LC3_CONFIG_FREQ_32KHZ) { if (i++ == 0) spa_pod_builder_int(b, 32000); @@ -947,7 +990,7 @@ static int codec_enum_config(const struct media_codec *codec, uint32_t flags, if (i == 0) return -EINVAL; - res = channels_to_positions(conf.channels, position); + res = channels_to_positions(conf.channels, position, SPA_N_ELEMENTS(position)); if (res == 0) return -EINVAL; spa_pod_builder_add(b, @@ -982,6 +1025,9 @@ static int codec_validate_config(const struct media_codec *codec, uint32_t flags case LC3_CONFIG_FREQ_48KHZ: info->info.raw.rate = 48000U; break; + case LC3_CONFIG_FREQ_44KHZ: + info->info.raw.rate = 44100U; + break; case LC3_CONFIG_FREQ_32KHZ: info->info.raw.rate = 32000U; break; @@ -998,7 +1044,8 @@ static int codec_validate_config(const struct media_codec *codec, uint32_t flags return -EINVAL; } - res = channels_to_positions(conf.channels, info->info.raw.position); + res = channels_to_positions(conf.channels, info->info.raw.position, + SPA_N_ELEMENTS(info->info.raw.position)); if (res == 0) return -EINVAL; info->info.raw.channels = res; @@ -1017,25 +1064,34 @@ static int codec_validate_config(const struct media_codec *codec, uint32_t flags static int codec_get_qos(const struct media_codec *codec, const void *config, size_t config_size, const struct bap_endpoint_qos *endpoint_qos, - struct bap_codec_qos *qos) + struct bap_codec_qos *qos, const struct spa_dict *settings) { - const struct bap_qos *bap_qos; + struct bap_qos bap_qos; bap_lc3_t conf; + bool found = false; + struct settings s; + struct spa_debug_log_ctx debug_ctx; spa_zero(*qos); if (!parse_conf(&conf, config, config_size)) return -EINVAL; - bap_qos = select_bap_qos(get_rate_mask(conf.rate), get_duration_mask(conf.frame_duration), + parse_settings(&s, settings, &debug_ctx); + + found = select_bap_qos(&bap_qos, &s, get_rate_mask(conf.rate), get_duration_mask(conf.frame_duration), conf.framelen, conf.framelen); - if (!bap_qos) { + if (!found) { /* shouldn't happen: select_config should pick existing one */ spa_log_error(log_, "no QoS settings found"); return -EINVAL; } - qos->framing = false; + if (endpoint_qos->framing == 0x01) + qos->framing = true; + else + qos->framing = bap_qos.framing; + if (endpoint_qos->phy & 0x2) qos->phy = 0x2; else if (endpoint_qos->phy & 0x1) @@ -1047,9 +1103,9 @@ static int codec_get_qos(const struct media_codec *codec, qos->interval = (conf.frame_duration == LC3_CONFIG_DURATION_7_5 ? 7500 : 10000); qos->target_latency = BT_ISO_QOS_TARGET_LATENCY_BALANCED; - qos->delay = bap_qos->delay; - qos->latency = bap_qos->latency; - qos->retransmission = bap_qos->retransmission; + qos->delay = bap_qos.delay; + qos->latency = bap_qos.latency; + qos->retransmission = bap_qos.retransmission; /* Clamp to ASE values (if known) */ if (endpoint_qos->delay_min) @@ -1098,6 +1154,10 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, this->channels = config_info.info.raw.channels; this->framelen = conf.framelen; + /* Google liblc3 doesn't have direct support for encoding to 44.1kHz; instead + * lc3.h suggests using a nearby samplerate, so we do just that */ + this->codec_samplerate = (this->samplerate == 44100) ? 48000 : this->samplerate; + switch (conf.frame_duration) { case LC3_CONFIG_DURATION_10: this->frame_dus = 10000; @@ -1113,7 +1173,7 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, spa_log_info(log_, "LC3 rate:%d frame_duration:%d channels:%d framelen:%d nblks:%d", this->samplerate, this->frame_dus, this->channels, this->framelen, conf.n_blks); - res = lc3_frame_samples(this->frame_dus, this->samplerate); + res = lc3_frame_samples(this->frame_dus, this->codec_samplerate); if (res < 0) { spa_log_error(log_, "invalid LC3 frame samples"); res = -EINVAL; @@ -1124,7 +1184,8 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, if (!(flags & MEDIA_CODEC_FLAG_SINK)) { for (ich = 0; ich < this->channels; ich++) { - this->enc[ich] = lc3_setup_encoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_encoder_size(this->frame_dus, this->samplerate))); + this->enc[ich] = lc3_setup_encoder(this->frame_dus, this->codec_samplerate, 0, + calloc(1, lc3_encoder_size(this->frame_dus, this->codec_samplerate))); if (this->enc[ich] == NULL) { res = -EINVAL; goto error; @@ -1132,7 +1193,8 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, } } else { for (ich = 0; ich < this->channels; ich++) { - this->dec[ich] = lc3_setup_decoder(this->frame_dus, this->samplerate, 0, calloc(1, lc3_decoder_size(this->frame_dus, this->samplerate))); + this->dec[ich] = lc3_setup_decoder(this->frame_dus, this->codec_samplerate, 0, + calloc(1, lc3_decoder_size(this->frame_dus, this->codec_samplerate))); if (this->dec[ich] == NULL) { res = -EINVAL; goto error; @@ -1184,7 +1246,7 @@ static uint64_t codec_get_interval(void *data) { struct impl *this = data; - return (uint64_t)this->frame_dus * 1000; + return (uint64_t)this->samples * SPA_NSEC_PER_SEC / this->samplerate; } static int codec_abr_process (void *data, size_t unsent) @@ -1232,13 +1294,23 @@ static int codec_encode(void *data, return processed; } -static SPA_UNUSED int codec_start_decode (void *data, +static int codec_start_decode (void *data, const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) { + struct impl *this = data; + + /* packets come from controller, so also invalid ones bump seqnum */ + this->seqnum++; + + if (!src_size) + return -EINVAL; + + if (*seqnum) + *seqnum = this->seqnum; return 0; } -static SPA_UNUSED int codec_decode(void *data, +static int codec_decode(void *data, const void *src, size_t src_size, void *dst, size_t dst_size, size_t *dst_out) @@ -1268,6 +1340,24 @@ static SPA_UNUSED int codec_decode(void *data, return consumed; } +static int codec_produce_plc(void *data, void *dst, size_t dst_size) +{ + struct impl *this = data; + int ich, res; + + if (dst_size < this->codesize) + return -EINVAL; + + for (ich = 0; ich < this->channels; ich++) { + uint8_t *out = (uint8_t *)dst + (ich * 4); + res = lc3_decode(this->dec[ich], NULL, 0, LC3_PCM_FORMAT_S24, out, this->channels); + if (SPA_UNLIKELY(res < 0)) + return -EINVAL; + } + + return this->codesize; +} + static int codec_reduce_bitpool(void *data) { return -ENOTSUP; @@ -1323,6 +1413,9 @@ static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps, case LC3_CONFIG_FREQ_48KHZ: data += write_ltv_uint8(data, LC3_TYPE_FREQ, LC3_CONFIG_FREQ_48KHZ); break; + case LC3_CONFIG_FREQ_44KHZ: + data += write_ltv_uint8(data, LC3_TYPE_FREQ, LC3_CONFIG_FREQ_44KHZ); + break; case LC3_CONFIG_FREQ_32KHZ: data += write_ltv_uint8(data, LC3_TYPE_FREQ, LC3_CONFIG_FREQ_32KHZ); break; @@ -1363,9 +1456,9 @@ static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps, const struct media_codec bap_codec_lc3 = { .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3, + .kind = MEDIA_CODEC_BAP, .name = "lc3", .codec_id = BAP_CODEC_LC3, - .bap = true, .description = "LC3", .fill_caps = codec_fill_caps, .select_config = codec_select_config, @@ -1382,6 +1475,7 @@ const struct media_codec bap_codec_lc3 = { .encode = codec_encode, .start_decode = codec_start_decode, .decode = codec_decode, + .produce_plc = codec_produce_plc, .reduce_bitpool = codec_reduce_bitpool, .increase_bitpool = codec_increase_bitpool, .set_log = codec_set_log, diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index e4495f0e8..ac91c6cff 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -33,8 +35,9 @@ #include #include #include +#include +#include -#include "config.h" #include "codec-loader.h" #include "player.h" #include "iso-io.h" @@ -67,8 +70,6 @@ enum backend_selection { */ #define BLUEZ_ACTION_RATE_MSEC 3000 -#define CODEC_SWITCH_RETRIES 1 - /* How many times to retry acquire on errors, and how long delay to require before we can * try again. */ @@ -83,6 +84,7 @@ struct spa_bt_monitor { struct spa_log *log; struct spa_loop *main_loop; struct spa_loop *data_loop; + struct spa_loop_utils *loop_utils; struct spa_system *main_system; struct spa_system *data_system; struct spa_plugin_loader *plugin_loader; @@ -122,6 +124,13 @@ struct spa_bt_monitor { struct spa_list bcast_source_config_list; + uint32_t bap_sink_locations; + uint32_t bap_sink_contexts; + uint32_t bap_sink_supported_contexts; + uint32_t bap_source_locations; + uint32_t bap_source_contexts; + uint32_t bap_source_supported_contexts; + struct spa_bt_quirks *quirks; #define MAX_SETTINGS 128 @@ -143,10 +152,15 @@ struct spa_bt_remote_endpoint { char *uuid; unsigned int codec; struct spa_bt_device *device; - uint8_t *capabilities; + uint8_t capabilities[A2DP_MAX_CAPS_SIZE]; int capabilities_len; bool delay_reporting; bool acceptor; + + struct bap_endpoint_qos qos; + + bool asha_right_side; + uint64_t hisyncid; }; #define METADATA_MAX_LEN 255 @@ -178,7 +192,6 @@ struct spa_bt_big { struct spa_list link; char broadcast_code[BROADCAST_CODE_LEN]; bool encryption; - int presentation_delay; struct spa_list bis_list; int big_id; int sync_factor; @@ -191,33 +204,43 @@ struct spa_bt_big { * with the desired capabilities. * The codec switch struct tracks candidates still to be tried. */ -struct spa_bt_media_codec_switch { + +#define SPA_TYPE_BT_WORK_CODEC_SWITCH SPA_TYPE_INFO_BT_WORK_BASE "CodecSwitch" +#define SPA_TYPE_BT_WORK_RATE_LIMIT SPA_TYPE_INFO_BT_WORK_BASE "RateLimit" + +struct spa_bt_codec_switch_path { + char *path; + bool clear; +}; + +struct spa_bt_codec_switch { + struct spa_list link; + + bool canceled; + bool failed; + bool waiting; + + uint32_t profiles; + struct spa_bt_device *device; - struct spa_list device_link; - /* - * Codec switch may be waiting for either DBus reply from BlueZ - * or a timeout (but not both). - */ - struct spa_source timer; + struct spa_source *timer; DBusPendingCall *pending; - uint32_t profile; - /* * Called asynchronously, so endpoint paths instead of pointers (which may be * invalidated in the meantime). */ - const struct media_codec **codecs; - char **paths; - - const struct media_codec **codec_iter; /**< outer iterator over codecs */ - char **path_iter; /**< inner iterator over endpoint paths */ - - uint16_t retries; - size_t num_paths; + const struct media_codec *codec; + struct spa_bt_codec_switch_path *paths; + unsigned int path_idx; }; +static struct spa_bt_codec_switch *codec_switch_cmp_sw; /* global for qsort */ + +static void codec_switch_list_process(struct spa_list *codec_switch_list); +static void codec_switch_destroy(struct spa_bt_codec_switch *sw); + #define DEFAULT_RECONNECT_PROFILES SPA_BT_PROFILE_NULL #define DEFAULT_HW_VOLUME_PROFILES (SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY | SPA_BT_PROFILE_HEADSET_HEAD_UNIT | \ SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_A2DP_SINK | \ @@ -250,8 +273,6 @@ static void spa_bt_transport_commit_release_timer(struct spa_bt_transport *trans static int device_start_timer(struct spa_bt_device *device); static int device_stop_timer(struct spa_bt_device *device); -static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw); - // Working with BlueZ Battery Provider. // Developed using https://github.com/dgreid/adhd/commit/655b58f as an example of DBus calls. @@ -439,6 +460,11 @@ static void register_battery_provider(struct spa_bt_device *device) } } +const struct media_codec * const * spa_bt_get_media_codecs(struct spa_bt_monitor *monitor) +{ + return monitor->media_codecs; +} + static int media_codec_to_endpoint(const struct media_codec *codec, enum spa_bt_media_direction direction, char** object_path) @@ -446,9 +472,9 @@ static int media_codec_to_endpoint(const struct media_codec *codec, const char * endpoint; if (direction == SPA_BT_MEDIA_SOURCE) - endpoint = codec->bap ? BAP_SOURCE_ENDPOINT : A2DP_SOURCE_ENDPOINT; + endpoint = codec->kind == MEDIA_CODEC_BAP ? BAP_SOURCE_ENDPOINT : A2DP_SOURCE_ENDPOINT; else if (direction == SPA_BT_MEDIA_SINK) - endpoint = codec->bap ? BAP_SINK_ENDPOINT : A2DP_SINK_ENDPOINT; + endpoint = codec->kind == MEDIA_CODEC_BAP ? BAP_SINK_ENDPOINT : A2DP_SINK_ENDPOINT; else if (direction == SPA_BT_MEDIA_SOURCE_BROADCAST) endpoint = BAP_BROADCAST_SOURCE_ENDPOINT; else if (direction == SPA_BT_MEDIA_SINK_BROADCAST) @@ -534,20 +560,14 @@ static int media_endpoint_to_profile(const char *endpoint) static bool is_media_codec_enabled(struct spa_bt_monitor *monitor, const struct media_codec *codec) { - return spa_dict_lookup(&monitor->enabled_codecs, codec->name) != NULL; -} - -static bool codec_has_direction(const struct media_codec *codec, enum spa_bt_media_direction direction) -{ - switch (direction) { - case SPA_BT_MEDIA_SOURCE: - case SPA_BT_MEDIA_SOURCE_BROADCAST: - return codec->encode; - case SPA_BT_MEDIA_SINK: - case SPA_BT_MEDIA_SINK_BROADCAST: - return codec->decode; + /* Mandatory codecs are always enabled */ + switch (codec->id) { + case SPA_BLUETOOTH_AUDIO_CODEC_SBC: + case SPA_BLUETOOTH_AUDIO_CODEC_CVSD: + case SPA_BLUETOOTH_AUDIO_CODEC_LC3: + return true; default: - spa_assert_not_reached(); + return spa_dict_lookup(&monitor->enabled_codecs, codec->name) != NULL; } } @@ -556,11 +576,14 @@ static enum spa_bt_profile get_codec_profile(const struct media_codec *codec, { switch (direction) { case SPA_BT_MEDIA_SOURCE: - return codec->bap ? SPA_BT_PROFILE_BAP_SOURCE : SPA_BT_PROFILE_A2DP_SOURCE; + return codec->kind == MEDIA_CODEC_BAP ? SPA_BT_PROFILE_BAP_SOURCE : SPA_BT_PROFILE_A2DP_SOURCE; case SPA_BT_MEDIA_SINK: - if (codec->asha) + if (codec->kind == MEDIA_CODEC_ASHA) return SPA_BT_PROFILE_ASHA_SINK; - return codec->bap ? SPA_BT_PROFILE_BAP_SINK : SPA_BT_PROFILE_A2DP_SINK; + else if (codec->kind == MEDIA_CODEC_BAP) + return SPA_BT_PROFILE_BAP_SINK; + else + return SPA_BT_PROFILE_A2DP_SINK; case SPA_BT_MEDIA_SOURCE_BROADCAST: return SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; case SPA_BT_MEDIA_SINK_BROADCAST: @@ -570,6 +593,25 @@ static enum spa_bt_profile get_codec_profile(const struct media_codec *codec, } } +static bool codec_has_direction(struct spa_bt_monitor *monitor, const struct media_codec *codec, enum spa_bt_media_direction direction) +{ + if (!is_media_codec_enabled(monitor, codec)) + return false; + if (!(get_codec_profile(codec, direction) & monitor->enabled_profiles)) + return false; + + switch (direction) { + case SPA_BT_MEDIA_SOURCE: + case SPA_BT_MEDIA_SOURCE_BROADCAST: + return codec->encode; + case SPA_BT_MEDIA_SINK: + case SPA_BT_MEDIA_SINK_BROADCAST: + return codec->decode; + default: + spa_assert_not_reached(); + } +} + static enum spa_bt_profile swap_profile(enum spa_bt_profile profile) { switch (profile) { @@ -590,6 +632,19 @@ static enum spa_bt_profile swap_profile(enum spa_bt_profile profile) } } +static uint32_t get_codec_target_profile(struct spa_bt_monitor *monitor, + const struct media_codec *codec) +{ + enum spa_bt_profile profile = 0; + int i; + + for (i = 0; i < SPA_BT_MEDIA_DIRECTION_LAST; ++i) + if (codec_has_direction(monitor, codec, i)) + profile |= swap_profile(get_codec_profile(codec, i)); + + return profile; +} + static bool endpoint_should_be_registered(struct spa_bt_monitor *monitor, const struct media_codec *codec, enum spa_bt_media_direction direction) @@ -597,10 +652,8 @@ static bool endpoint_should_be_registered(struct spa_bt_monitor *monitor, /* Codecs with fill_caps == NULL share endpoint with another codec, * and don't have their own endpoint */ - return is_media_codec_enabled(monitor, codec) && - codec_has_direction(codec, direction) && - codec->fill_caps && - (get_codec_profile(codec, direction) & monitor->enabled_profiles); + return codec_has_direction(monitor, codec, direction) && + codec->fill_caps; } static DBusHandlerResult endpoint_select_configuration(DBusConnection *conn, DBusMessage *m, void *userdata) @@ -832,10 +885,6 @@ static int parse_endpoint_props(struct spa_bt_monitor *monitor, DBusMessageIter const char *key = NULL; int type = 0; - memset(caps, 0, A2DP_MAX_CAPS_SIZE); - *endpoint_path = NULL; - memset(qos, 0, sizeof(*qos)); - if (!check_iter_signature(&dict_iter, "{sv}")) { spa_log_warn(monitor->log, "Invalid BAP Endpoint QoS in DBus"); return -EINVAL; @@ -854,6 +903,9 @@ static int parse_endpoint_props(struct spa_bt_monitor *monitor, DBusMessageIter if (spa_streq(key, "Capabilities")) { uint8_t *buf; + if (!caps) + goto next; + if (type != DBUS_TYPE_ARRAY) goto bad_property; @@ -872,6 +924,9 @@ static int parse_endpoint_props(struct spa_bt_monitor *monitor, DBusMessageIter spa_log_info(monitor->log, "%p: %s size:%d", monitor, key, *caps_size); spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, ' ', caps, (size_t)*caps_size); } else if (spa_streq(key, "Endpoint")) { + if (!endpoint_path) + goto next; + if (type != DBUS_TYPE_OBJECT_PATH) goto bad_property; @@ -879,6 +934,9 @@ static int parse_endpoint_props(struct spa_bt_monitor *monitor, DBusMessageIter spa_log_info(monitor->log, "%p: %s %s", monitor, key, *endpoint_path); } else if (spa_streq(key, "QoS")) { + if (!qos) + goto next; + if (!check_iter_signature(&it[1], "a{sv}")) goto bad_property; @@ -887,6 +945,9 @@ static int parse_endpoint_props(struct spa_bt_monitor *monitor, DBusMessageIter } else if (spa_streq(key, "Locations") || spa_streq(key, "Location")) { dbus_uint32_t value; + if (!qos) + goto next; + if (type != DBUS_TYPE_UINT32) goto bad_property; @@ -896,14 +957,34 @@ static int parse_endpoint_props(struct spa_bt_monitor *monitor, DBusMessageIter } else if (spa_streq(key, "ChannelAllocation")) { dbus_uint32_t value; + if (!qos) + goto next; + if (type != DBUS_TYPE_UINT32) goto bad_property; dbus_message_iter_get_basic(&it[1], &value); spa_log_debug(monitor->log, "ep qos: %s=%d", key, (int)value); qos->channel_allocation = value; + } else if (spa_streq(key, "Context") || spa_streq(key, "SupportedContext")) { + dbus_uint16_t value; + + if (!qos) + goto next; + + if (type != DBUS_TYPE_UINT16) + goto bad_property; + + dbus_message_iter_get_basic(&it[1], &value); + spa_log_debug(monitor->log, "ep qos: %s=%d", key, (int)value); + + if (spa_streq(key, "Context")) + qos->context = value; + else if (spa_streq(key, "SupportedContext")) + qos->supported_context = value; } +next: dbus_message_iter_next(&dict_iter); } @@ -926,20 +1007,15 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe bool sink, duplex; const char *err_msg = "Unknown error"; struct spa_dict settings; - struct spa_dict_item setting_items[SPA_N_ELEMENTS(monitor->global_setting_items) + 5]; - int i; + struct spa_dict_item setting_items[128]; + unsigned int i, j; const char *endpoint_path = NULL; - uint8_t caps[A2DP_MAX_CAPS_SIZE]; uint8_t config[A2DP_MAX_CAPS_SIZE]; char locations[64] = {0}; char channel_allocation[64] = {0}; - int caps_size = 0; int conf_size; DBusMessageIter dict; - struct bap_endpoint_qos endpoint_qos; - - spa_zero(endpoint_qos); if (!dbus_message_iter_init(m, &args) || !spa_streq(dbus_message_get_signature(m), "a{sv}")) { spa_log_error(monitor->log, "Invalid signature for method SelectProperties()"); @@ -959,44 +1035,56 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe */ codec = media_endpoint_to_codec(monitor, path, &sink, NULL); spa_log_debug(monitor->log, "%p: %s codec:%s", monitor, path, codec ? codec->name : ""); - if (!codec || !codec->bap || !codec->get_qos) { + if (!codec || codec->kind != MEDIA_CODEC_BAP || !codec->get_qos) { spa_log_error(monitor->log, "Unsupported codec"); err_msg = "Unsupported codec"; goto error; } - /* Parse endpoint properties */ - if (parse_endpoint_props(monitor, &props, caps, &caps_size, &endpoint_path, &endpoint_qos) < 0) + /* Find endpoint */ + iter = props; + if (parse_endpoint_props(monitor, &iter, NULL, NULL, &endpoint_path, NULL) < 0) goto error_invalid; - if (endpoint_qos.locations) - spa_scnprintf(locations, sizeof(locations), "%"PRIu32, endpoint_qos.locations); - if (endpoint_qos.channel_allocation) - spa_scnprintf(channel_allocation, sizeof(channel_allocation), "%"PRIu32, endpoint_qos.channel_allocation); ep = remote_endpoint_find(monitor, endpoint_path); - if (!ep || !ep->device) { + if (!ep || !ep->device || !ep->uuid) { spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", endpoint_path); goto error_invalid; } - duplex = SPA_FLAG_IS_SET(ep->device->profiles, SPA_BT_PROFILE_BAP_DUPLEX); - - /* Call of SelectProperties means that local device acts as an initiator - * and therefore remote endpoint is an acceptor + /* Call of SelectProperties means that local device is BAP Client + * and therefore remote endpoint is BAP Server / acceptor */ ep->acceptor = true; - for (i = 0; i < (int)monitor->global_settings.n_items; ++i) - setting_items[i] = monitor->global_settings.items[i]; + /* Parse endpoint properties */ + iter = props; + if (parse_endpoint_props(monitor, &iter, ep->capabilities, &ep->capabilities_len, NULL, &ep->qos) < 0) + goto error_invalid; + + if (ep->qos.locations) + spa_scnprintf(locations, sizeof(locations), "%"PRIu32, ep->qos.locations); + if (ep->qos.channel_allocation) + spa_scnprintf(channel_allocation, sizeof(channel_allocation), "%"PRIu32, ep->qos.channel_allocation); + + if (!ep->device->preferred_profiles) + ep->device->preferred_profiles = ep->device->profiles; + + duplex = SPA_FLAG_IS_SET(ep->device->preferred_profiles, SPA_BT_PROFILE_BAP_DUPLEX); + + i = 0; setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.locations", locations); setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.channel-allocation", channel_allocation); setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.sink", sink ? "true" : "false"); setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.duplex", duplex ? "true" : "false"); setting_items[i++] = SPA_DICT_ITEM_INIT("bluez5.bap.debug", "true"); + if (ep->device->settings) + for (j = 0; j < ep->device->settings->n_items && i < SPA_N_ELEMENTS(setting_items); ++i, ++j) + setting_items[i] = ep->device->settings->items[j]; settings = SPA_DICT_INIT(setting_items, i); - spa_assert((size_t)i <= SPA_N_ELEMENTS(setting_items)); - conf_size = codec->select_config(codec, 0, caps, caps_size, &monitor->default_audio_info, &settings, config); + conf_size = codec->select_config(codec, 0, ep->capabilities, ep->capabilities_len, + &monitor->default_audio_info, &settings, config); if (conf_size < 0) { spa_log_error(monitor->log, "can't select config: %d (%s)", conf_size, spa_strerror(conf_size)); @@ -1025,7 +1113,7 @@ static DBusHandlerResult endpoint_select_properties(DBusConnection *conn, DBusMe spa_zero(qos); - res = codec->get_qos(codec, config, conf_size, &endpoint_qos, &qos); + res = codec->get_qos(codec, config, conf_size, &ep->qos, &qos, &settings); if (res < 0) { spa_log_error(monitor->log, "can't select QOS config: %d (%s)", res, spa_strerror(res)); @@ -1285,8 +1373,28 @@ static int adapter_media_update_props(struct spa_bt_adapter *adapter, dbus_message_iter_next(&iter); } - } - else + } else if (spa_streq(key, "SupportedFeatures")) { + DBusMessageIter iter; + + if (!check_iter_signature(&it[1], "as")) + goto next; + + dbus_message_iter_recurse(&it[1], &iter); + + while (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_INVALID) { + const char *feature; + + dbus_message_iter_get_basic(&iter, &feature); + + if (spa_streq(feature, "tx-timestamping")) { + adapter->tx_timestamping_supported = true; + spa_log_info(monitor->log, "Adapter %s: TX timestamping supported", + adapter->path); + } + + dbus_message_iter_next(&iter); + } + } else spa_log_debug(monitor->log, "media: unhandled key %s", key); next: @@ -1549,12 +1657,13 @@ static void device_clear_sub(struct spa_bt_device *device) battery_remove(device); spa_bt_device_release_transports(device); device->preferred_codec = NULL; + device->preferred_profiles = 0; } static void device_free(struct spa_bt_device *device) { struct spa_bt_remote_endpoint *ep, *tep; - struct spa_bt_media_codec_switch *sw; + struct spa_bt_codec_switch *sw; struct spa_bt_transport *t, *tt; struct spa_bt_monitor *monitor = device->monitor; struct spa_bt_set_membership *s; @@ -1584,8 +1693,8 @@ static void device_free(struct spa_bt_device *device) } } - spa_list_consume(sw, &device->codec_switch_list, device_link) - media_codec_switch_free(sw); + spa_list_consume(sw, &device->codec_switch_list, link) + codec_switch_destroy(sw); spa_list_consume(s, &device->set_membership_list, link) { spa_list_remove(&s->link); @@ -1709,8 +1818,9 @@ static void emit_device_info(struct spa_bt_monitor *monitor, { struct spa_device_object_info info; char dev[32], name[128], class[16], vendor_id[64], product_id[64], product_id_tot[67]; - struct spa_dict_item items[23]; + struct spa_dict_item items[24]; uint32_t n_items = 0; + enum spa_bt_form_factor ff; info = SPA_DEVICE_OBJECT_INFO_INIT(); info.type = SPA_TYPE_INTERFACE_Device; @@ -1719,6 +1829,8 @@ static void emit_device_info(struct spa_bt_monitor *monitor, SPA_DEVICE_OBJECT_CHANGE_MASK_PROPS; info.flags = 0; + ff = spa_bt_form_factor_from_class(device->bluetooth_class); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_API, "bluez5"); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_BUS, "bluetooth"); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Audio/Device"); @@ -1733,9 +1845,8 @@ static void emit_device_info(struct spa_bt_monitor *monitor, items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_VENDOR_ID, vendor_id); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PRODUCT_ID, product_id_tot); } - items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_FORM_FACTOR, - spa_bt_form_factor_name( - spa_bt_form_factor_from_class(device->bluetooth_class))); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_FORM_FACTOR, spa_bt_form_factor_name(ff)); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_ICON_NAME, spa_bt_form_factor_icon_name(ff)); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_STRING, device->address); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_ICON, device->icon); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_BLUEZ5_PATH, device->path); @@ -2017,6 +2128,11 @@ static int device_stop_timer(struct spa_bt_device *device) return 0; } +static bool has_codec_switch(struct spa_bt_device *device) +{ + return !spa_list_is_empty(&device->codec_switch_list); +} + int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force) { struct spa_bt_monitor *monitor = device->monitor; @@ -2058,7 +2174,9 @@ int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force) device, device->profiles, connected_profiles, connectable_profiles, device->added, all_connected, direction_connected, set_connected); - if (connected_profiles == 0 && spa_list_is_empty(&device->codec_switch_list)) { + if (has_codec_switch(device)) { + /* noop */ + } else if (connected_profiles == 0) { device_stop_timer(device); device_connected(monitor, device, BT_DEVICE_DISCONNECTED); } else if (force || ((direction_connected || all_connected) && set_connected && connected_profiles)) { @@ -2085,10 +2203,10 @@ static void device_set_connected(struct spa_bt_device *device, int connected) spa_bt_quirks_log_features(monitor->quirks, device->adapter, device); spa_bt_device_check_profiles(device, false); } else { - /* Stop codec switch on disconnect */ - struct spa_bt_media_codec_switch *sw; - spa_list_consume(sw, &device->codec_switch_list, device_link) - media_codec_switch_free(sw); + /* Stop works on disconnect */ + struct spa_bt_codec_switch *sw; + spa_list_consume(sw, &device->codec_switch_list, link) + codec_switch_destroy(sw); if (device->reconnect_state != BT_DEVICE_RECONNECT_INIT) device_stop_timer(device); @@ -2101,7 +2219,8 @@ static void device_update_set_status(struct spa_bt_device *device, bool force, c int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile) { device->connected_profiles |= profile; - if (profile & SPA_BT_PROFILE_BAP_DUPLEX) + if (profile & SPA_BT_PROFILE_BAP_DUPLEX || + profile & SPA_BT_PROFILE_ASHA_SINK) device_update_set_status(device, true, NULL); spa_bt_device_check_profiles(device, false); spa_bt_device_emit_profiles_changed(device, profile); @@ -2133,7 +2252,10 @@ static bool device_set_update_leader(struct spa_bt_set_membership *set) * appear under a specific device. */ spa_bt_for_each_set_member(s, set) { - if (!(s->device->connected_profiles & SPA_BT_PROFILE_BAP_DUPLEX)) + bool bap_duplex = s->device->connected_profiles & SPA_BT_PROFILE_BAP_DUPLEX; + bool is_asha = s->device->connected_profiles & SPA_BT_PROFILE_ASHA_SINK; + + if (!bap_duplex && !is_asha) continue; if (leader == NULL || s->rank < leader->rank || @@ -2498,18 +2620,25 @@ bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const stru { SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX }, { SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, SPA_BT_FEATURE_A2DP_DUPLEX }, }; - bool is_a2dp = !codec->bap && !codec->asha; + bool is_a2dp = codec->kind == MEDIA_CODEC_A2DP; size_t i; - if (!is_media_codec_enabled(device->monitor, codec)) + codec_target_profile = get_codec_target_profile(monitor, codec); + if (!codec_target_profile) return false; + if (codec->kind == MEDIA_CODEC_HFP) { + if (!(profile & SPA_BT_PROFILE_HEADSET_AUDIO)) + return false; + return spa_bt_backend_supports_codec(monitor->backend, device, codec->codec_id) == 1; + } + if (!device->adapter->a2dp_application_registered && is_a2dp) { /* Codec switching not supported: only plain SBC allowed */ return (codec->codec_id == A2DP_CODEC_SBC && spa_streq(codec->name, "sbc") && device->adapter->legacy_endpoints_registered); } - if (!device->adapter->bap_application_registered && codec->bap) + if (!device->adapter->bap_application_registered && codec->kind == MEDIA_CODEC_BAP) return false; /* Check codec quirks */ @@ -2526,10 +2655,6 @@ bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const stru return false; } - for (i = 0, codec_target_profile = 0; i < (size_t)SPA_BT_MEDIA_DIRECTION_LAST; ++i) - if (codec_has_direction(codec, i)) - codec_target_profile |= swap_profile(get_codec_profile(codec, i)); - spa_list_for_each(ep, &device->remote_endpoint_list, device_link) { enum spa_bt_profile ep_profile = spa_bt_profile_from_uuid(ep->uuid); @@ -2600,6 +2725,25 @@ const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_b return spa_steal_ptr(supported_codecs); } +const struct media_codec *spa_bt_get_hfp_codec(struct spa_bt_monitor *monitor, unsigned int hfp_codec_id) +{ + const struct media_codec * const * const media_codecs = monitor->media_codecs; + size_t i; + + for (i = 0; media_codecs[i] != NULL; ++i) { + const struct media_codec *codec = media_codecs[i]; + + if (codec->kind != MEDIA_CODEC_HFP) + continue; + if (!is_media_codec_enabled(monitor, codec)) + continue; + if (codec->codec_id == hfp_codec_id) + return codec; + } + + return NULL; +} + static struct spa_bt_remote_endpoint *device_remote_endpoint_find(struct spa_bt_device *device, const char *path) { struct spa_bt_remote_endpoint *ep; @@ -2658,6 +2802,11 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en DBusMessageIter *invalidated_iter) { struct spa_bt_monitor *monitor = remote_endpoint->monitor; + DBusMessageIter copy_iter = *props_iter; + + parse_endpoint_props(monitor, ©_iter, + remote_endpoint->capabilities, &remote_endpoint->capabilities_len, NULL, + &remote_endpoint->qos); while (dbus_message_iter_get_arg_type(props_iter) != DBUS_TYPE_INVALID) { DBusMessageIter it[2]; @@ -2671,7 +2820,12 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en type = dbus_message_iter_get_arg_type(&it[1]); - if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) { + if (spa_streq(key, "Capabilities") || spa_streq(key, "Locations") || + spa_streq(key, "QoS") || spa_streq(key, "Context") || + spa_streq(key, "SupportedContext")) { + /* parsed by parse_endpoint_props */ + } + else if (type == DBUS_TYPE_STRING || type == DBUS_TYPE_OBJECT_PATH) { const char *value; dbus_message_iter_get_basic(&it[1], &value); @@ -2692,9 +2846,8 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en struct spa_bt_device *device; device = spa_bt_device_find(monitor, value); - if (device == NULL) { + if (device == NULL) goto next; - } spa_log_debug(monitor->log, "remote_endpoint %p: device -> %p", remote_endpoint, device); @@ -2705,11 +2858,17 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en if (device != NULL) spa_list_append(&device->remote_endpoint_list, &remote_endpoint->device_link); } - } - /* For ASHA */ - else if (spa_streq(key, "Transport")) { + } else if (spa_streq(key, "Transport")) { + /* For ASHA */ free(remote_endpoint->transport_path); remote_endpoint->transport_path = strdup(value); + } else if (spa_streq(key, "Side")) { + if (spa_streq(value, "right")) + remote_endpoint->asha_right_side = true; + else + remote_endpoint->asha_right_side = false; + } else { + goto unhandled; } } else if (type == DBUS_TYPE_BOOLEAN) { @@ -2721,6 +2880,8 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en if (spa_streq(key, "DelayReporting")) { remote_endpoint->delay_reporting = value; + } else { + goto unhandled; } } else if (type == DBUS_TYPE_BYTE) { @@ -2732,46 +2893,27 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en if (spa_streq(key, "Codec")) { remote_endpoint->codec = value; + } else { + goto unhandled; } } - /* Codecs property is present for ASHA */ else if (type == DBUS_TYPE_UINT16) { + /* Codecs property is present for ASHA */ uint16_t value; dbus_message_iter_get_basic(&it[1], &value); if (spa_streq(key, "Codecs")) { spa_log_debug(monitor->log, "remote_endpoint %p: %s=%02x", remote_endpoint, key, value); + } else { + goto unhandled; } } - else if (spa_streq(key, "Capabilities")) { - DBusMessageIter iter; - uint8_t *value; - int len; - - if (!check_iter_signature(&it[1], "ay")) - goto next; - - dbus_message_iter_recurse(&it[1], &iter); - dbus_message_iter_get_fixed_array(&iter, &value, &len); - - spa_log_debug(monitor->log, "remote_endpoint %p: %s=%d", remote_endpoint, key, len); - spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 2, value, (size_t)len); - - free(remote_endpoint->capabilities); - remote_endpoint->capabilities_len = 0; - - remote_endpoint->capabilities = malloc(len); - if (remote_endpoint->capabilities) { - memcpy(remote_endpoint->capabilities, value, len); - remote_endpoint->capabilities_len = len; - } - } - /* HiSyncId property is present for ASHA */ + /* + * HiSyncId property is present for ASHA. An ASHA "left" and + * "right" device pair will always have the same "HiSyncId". + */ else if (spa_streq(key, "HiSyncId")) { - /* - * TODO: Required for Stereo support in ASHA, for now just log. - */ DBusMessageIter iter; uint8_t *value; int len; @@ -2782,10 +2924,17 @@ static int remote_endpoint_update_props(struct spa_bt_remote_endpoint *remote_en dbus_message_iter_recurse(&it[1], &iter); dbus_message_iter_get_fixed_array(&iter, &value, &len); - spa_log_debug(monitor->log, "remote_endpoint %p: %s=%d", remote_endpoint, key, len); + if (len != 8 /* HiSyncId will always be 8 bytes */) + goto next; + + remote_endpoint->hisyncid = *(uint64_t *)value; + + spa_log_debug(monitor->log, "remote_endpoint %p: %s=%"PRIu64, remote_endpoint, key, remote_endpoint->hisyncid); } - else + else { +unhandled: spa_log_debug(monitor->log, "remote_endpoint %p: unhandled key %s", remote_endpoint, key); + } next: dbus_message_iter_next(props_iter); @@ -2841,7 +2990,6 @@ static void remote_endpoint_free(struct spa_bt_remote_endpoint *remote_endpoint) free(remote_endpoint->path); free(remote_endpoint->transport_path); free(remote_endpoint->uuid); - free(remote_endpoint->capabilities); free(remote_endpoint); } @@ -2949,6 +3097,7 @@ void spa_bt_transport_free(struct spa_bt_transport *transport) { struct spa_bt_monitor *monitor = transport->monitor; struct spa_bt_device *device = transport->device; + char hisyncid[32] = { 0 }; spa_log_debug(monitor->log, "transport %p: free %s", transport, transport->path); @@ -2997,6 +3146,12 @@ void spa_bt_transport_free(struct spa_bt_transport *transport) if (transport->profile & SPA_BT_PROFILE_BAP_DUPLEX) device_update_set_status(device, true, NULL); + if (transport->profile & SPA_BT_PROFILE_ASHA_SINK) { + spa_scnprintf(hisyncid, sizeof(hisyncid), "/asha/%" PRIu64, transport->hisyncid); + device_update_set_status(device, true, hisyncid); + device_remove_device_set(device, hisyncid); + } + spa_bt_device_emit_profiles_changed(device, transport->profile); } @@ -3004,6 +3159,7 @@ void spa_bt_transport_free(struct spa_bt_transport *transport) free(transport->configuration); free(transport->endpoint_path); + free(transport->remote_endpoint_path); free(transport->path); free(transport); } @@ -3260,10 +3416,10 @@ static int spa_bt_transport_stop_volume_timer(struct spa_bt_transport *transport } -int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop) +int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop, struct spa_system *data_system) { if (t->sco_io == NULL) { - t->sco_io = spa_bt_sco_io_create(t, data_loop, t->monitor->log); + t->sco_io = spa_bt_sco_io_create(t, data_loop, data_system, t->monitor->log); if (t->sco_io == NULL) return -ENOMEM; } @@ -3286,9 +3442,6 @@ int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *t) /* Fallback values when device does not provide information */ - if (t->media_codec == NULL) - return 20 * SPA_NSEC_PER_MSEC; - switch (t->media_codec->id) { case SPA_BLUETOOTH_AUDIO_CODEC_SBC: case SPA_BLUETOOTH_AUDIO_CODEC_SBC_XQ: @@ -3305,6 +3458,10 @@ int64_t spa_bt_transport_get_delay_nsec(struct spa_bt_transport *t) case SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX: case SPA_BLUETOOTH_AUDIO_CODEC_LC3: return 40 * SPA_NSEC_PER_MSEC; + case SPA_BLUETOOTH_AUDIO_CODEC_CVSD: + case SPA_BLUETOOTH_AUDIO_CODEC_MSBC: + case SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB: + return 20 * SPA_NSEC_PER_MSEC; default: break; }; @@ -3400,6 +3557,10 @@ static int transport_update_props(struct spa_bt_transport *transport, } else if (spa_streq(key, "Endpoint")) { struct spa_bt_remote_endpoint *ep = remote_endpoint_find(monitor, value); + + free(transport->remote_endpoint_path); + transport->remote_endpoint_path = strdup(value); + if (!ep) { spa_log_warn(monitor->log, "Unable to find remote endpoint for %s", value); goto next; @@ -3409,17 +3570,6 @@ static int transport_update_props(struct spa_bt_transport *transport, transport->bap_initiator = ep->acceptor; } } - else if (spa_streq(key, "Codec")) { - uint8_t value; - - if (type != DBUS_TYPE_BYTE) - goto next; - dbus_message_iter_get_basic(&it[1], &value); - - spa_log_debug(monitor->log, "transport %p: %s=%02x", transport, key, value); - - transport->codec = value; - } else if (spa_streq(key, "Configuration")) { DBusMessageIter iter; uint8_t *value; @@ -3608,9 +3758,13 @@ fail: static int transport_set_volume(void *data, int id, float volume) { struct spa_bt_transport *transport = data; - struct spa_bt_transport_volume *t_volume = &transport->volumes[id]; + struct spa_bt_transport_volume *t_volume; uint16_t value; + spa_assert(id >= 0 && id < (int)SPA_N_ELEMENTS(transport->volumes)); + + t_volume = &transport->volumes[id]; + if (!t_volume->active || !spa_bt_transport_volume_enabled(transport)) return -ENOTSUP; @@ -3715,6 +3869,21 @@ static void transport_acquire_reply(DBusPendingCall *pending, void *user_data) spa_log_error(monitor->log, "Acquire %s returned error: %s", transport->path, dbus_message_get_error_name(r)); + + /* If no reply, BlueZ may consider operation still active, so release to + * try to get to a known state. + */ + if (spa_streq(dbus_message_get_error_name(r), DBUS_ERROR_NO_REPLY)) { + spa_autoptr(DBusMessage) m = NULL; + + spa_log_info(monitor->log, "Releasing transport %s (clean up NoReply)", + transport->path); + m = dbus_message_new_method_call(BLUEZ_SERVICE, transport->path, + BLUEZ_MEDIA_TRANSPORT_INTERFACE, "Release"); + if (m) + dbus_connection_send(monitor->conn, m, NULL); + } + ret = -EIO; goto finish; } @@ -3990,6 +4159,11 @@ release: */ spa_log_debug(monitor->log, "Failed to release idle transport %s: %s", transport->path, err.message); + } else if (spa_streq(err.name, DBUS_ERROR_UNKNOWN_METHOD) || + spa_streq(err.name, DBUS_ERROR_UNKNOWN_OBJECT)) { + /* Transport disappeared */ + spa_log_debug(monitor->log, "Failed to release (gone) transport %s: %s", + transport->path, err.message); } else { spa_log_error(monitor->log, "Failed to release transport %s: %s", transport->path, err.message); @@ -4050,7 +4224,7 @@ static int transport_set_delay(void *data, int64_t delay_nsec) if (!(transport->profile & SPA_BT_PROFILE_A2DP_DUPLEX)) return -ENOTSUP; - value = SPA_CLAMP(delay_nsec / (100 * SPA_NSEC_PER_USEC), 0, 10 * UINT16_MAX); + value = SPA_CLAMP(delay_nsec / (100 * SPA_NSEC_PER_USEC), 0, UINT16_MAX); if (transport->delay_us == 100 * value) return 0; @@ -4087,6 +4261,7 @@ static int setup_asha_transport(struct spa_bt_remote_endpoint *remote_endpoint, const struct media_codec * const * const media_codecs = monitor->media_codecs; const struct media_codec *codec = NULL; struct spa_bt_transport *transport; + char hisyncid[32] = { 0 }; char *tpath; if (!remote_endpoint->transport_path) { @@ -4116,7 +4291,7 @@ static int setup_asha_transport(struct spa_bt_remote_endpoint *remote_endpoint, for (int i = 0; media_codecs[i]; i++) { const struct media_codec *mcodec = media_codecs[i]; - if (!mcodec->asha) + if (mcodec->kind != MEDIA_CODEC_ASHA) continue; if (!spa_streq(mcodec->name, "g722")) continue; @@ -4124,11 +4299,15 @@ static int setup_asha_transport(struct spa_bt_remote_endpoint *remote_endpoint, spa_log_debug(monitor->log, "Setting ASHA codec: %s", mcodec->name); } + free(transport->remote_endpoint_path); free(transport->endpoint_path); + transport->remote_endpoint_path = strdup(remote_endpoint->path); transport->endpoint_path = strdup(remote_endpoint->path); transport->profile = SPA_BT_PROFILE_ASHA_SINK; transport->media_codec = codec; transport->device = remote_endpoint->device; + transport->hisyncid = remote_endpoint->hisyncid; + transport->asha_right_side = remote_endpoint->asha_right_side; spa_list_append(&remote_endpoint->device->transport_list, &transport->device_link); @@ -4137,160 +4316,195 @@ static int setup_asha_transport(struct spa_bt_remote_endpoint *remote_endpoint, transport->volumes[SPA_BT_VOLUME_ID_TX].active = true; transport->volumes[SPA_BT_VOLUME_ID_TX].volume = DEFAULT_TX_VOLUME; transport->n_channels = 1; - transport->channels[0] = SPA_AUDIO_CHANNEL_MONO; + transport->channels[0] = transport->asha_right_side ? SPA_AUDIO_CHANNEL_FR : SPA_AUDIO_CHANNEL_FL; spa_bt_device_add_profile(transport->device, transport->profile); spa_bt_device_connect_profile(transport->device, transport->profile); transport_sync_volume(transport); - spa_log_debug(monitor->log, "ASHA transport setup complete"); + spa_scnprintf(hisyncid, sizeof(hisyncid), "/asha/%" PRIu64, transport->hisyncid); + device_add_device_set(transport->device, hisyncid, transport->asha_right_side ? 1 : 0); + device_update_set_status(transport->device, true, hisyncid); + + const char *side = transport->asha_right_side ? "right" : "left"; + spa_log_debug(monitor->log, "ASHA transport setup complete for %s side", side); return 0; } -static void media_codec_switch_reply(DBusPendingCall *pending, void *userdata); - -static int media_codec_switch_cmp(const void *a, const void *b); - -static struct spa_bt_media_codec_switch *media_codec_switch_cmp_sw; /* global for qsort */ - -static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout); - -static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw); - -static void media_codec_switch_free(struct spa_bt_media_codec_switch *sw) +static void codec_switch_resume(struct spa_bt_codec_switch *sw) { - char **p; - - media_codec_switch_stop_timer(sw); - - cancel_and_unref(&sw->pending); - - if (sw->device != NULL) - spa_list_remove(&sw->device_link); - - if (sw->paths != NULL) - for (p = sw->paths; *p != NULL; ++p) - free(*p); - - free(sw->paths); - free(sw->codecs); - free(sw); + spa_assert(sw->waiting); + sw->waiting = false; + codec_switch_list_process(&sw->device->codec_switch_list); } -static void media_codec_switch_next(struct spa_bt_media_codec_switch *sw) +static void codec_switch_rate_limit_event(void *data, uint64_t exp) { - spa_assert(*sw->codec_iter != NULL && *sw->path_iter != NULL); + codec_switch_resume(data); +} - ++sw->path_iter; - if (*sw->path_iter == NULL) { - ++sw->codec_iter; - sw->path_iter = sw->paths; +static bool codec_switch_rate_limit(struct spa_bt_codec_switch *sw) +{ + struct spa_bt_device *device = sw->device; + struct spa_bt_monitor *monitor = device->monitor; + uint64_t now, wakeup; + struct timespec ts; + + now = get_time_now(monitor); + wakeup = device->last_bluez_action_time + BLUEZ_ACTION_RATE_MSEC * SPA_NSEC_PER_MSEC; + if (now >= wakeup) + return false; + + if (!sw->timer) + sw->timer = spa_loop_utils_add_timer(monitor->loop_utils, + codec_switch_rate_limit_event, sw); + if (!sw->timer) + return false; + + ts.tv_sec = wakeup / SPA_NSEC_PER_SEC; + ts.tv_nsec = wakeup % SPA_NSEC_PER_SEC; + if (spa_loop_utils_update_timer(monitor->loop_utils, sw->timer, &ts, NULL, true) < 0) { + spa_loop_utils_destroy_source(monitor->loop_utils, sw->timer); + sw->timer = NULL; + return false; } - sw->retries = CODEC_SWITCH_RETRIES; + return true; } -static bool media_codec_switch_process_current(struct spa_bt_media_codec_switch *sw) +static bool codec_switch_check_endpoint(struct spa_bt_remote_endpoint *ep, + const struct media_codec *codec, + bool *sink, char **local_endpoint) { + enum spa_bt_media_direction direction; + spa_autofree char *path = NULL; + uint32_t ep_profile; + + if (!ep || !ep->uuid || !ep->device) + return false; + + ep_profile = spa_bt_profile_from_uuid(ep->uuid); + if (!(ep_profile & get_codec_target_profile(ep->device->monitor, codec))) + return false; + + if (!media_codec_check_caps(codec, ep->codec, ep->capabilities, ep->capabilities_len, + &ep->device->monitor->default_audio_info, + &ep->device->monitor->global_settings)) + return false; + + if (ep_profile & (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_BAP_SINK)) { + direction = SPA_BT_MEDIA_SOURCE; + if (sink) + *sink = false; + } else if (ep_profile & (SPA_BT_PROFILE_A2DP_SOURCE | SPA_BT_PROFILE_BAP_SOURCE)) { + direction = SPA_BT_MEDIA_SINK; + if (sink) + *sink = true; + } else { + return false; + } + + if (!(get_codec_profile(codec, direction) & ep->device->monitor->enabled_profiles)) + return false; + + if (media_codec_to_endpoint(codec, direction, &path) < 0) + return false; + + if (local_endpoint) + *local_endpoint = spa_steal_ptr(path); + + return true; +} + +static void codec_switch_reply(DBusPendingCall *pending, void *user_data) +{ + struct spa_bt_codec_switch *sw = user_data; + struct spa_bt_device *device = sw->device; + struct spa_bt_monitor *monitor = device->monitor; + + spa_assert(sw->pending == pending); + spa_autoptr(DBusMessage) r = steal_reply_and_unref(&sw->pending); + + spa_bt_device_update_last_bluez_action_time(device); + + if (r == NULL) { + spa_log_error(monitor->log, + "media codec switch %p: empty reply from dbus", + sw); + sw->failed = true; + } else if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { + spa_log_error(monitor->log, + "media codec switch %p: failed (%s)", + sw, dbus_message_get_error_name(r)); + sw->failed = true; + } + + codec_switch_resume(sw); +} + +static bool codec_switch_configure_a2dp(struct spa_bt_codec_switch *sw, const char *path) +{ + struct spa_bt_device *device = sw->device; + struct spa_bt_monitor *monitor = device->monitor; struct spa_bt_remote_endpoint *ep; struct spa_bt_transport *t; const struct media_codec *codec; uint8_t config[A2DP_MAX_CAPS_SIZE]; - enum spa_bt_media_direction direction; - spa_autofree char *local_endpoint = NULL; int res, config_size; + spa_autofree char *local_endpoint = NULL; spa_autoptr(DBusMessage) m = NULL; DBusMessageIter iter, d; - int i; bool sink; - /* Try setting configuration for current codec on current endpoint in list */ + codec = sw->codec; + ep = device_remote_endpoint_find(device, path); - codec = *sw->codec_iter; - - spa_log_debug(sw->device->monitor->log, "media codec switch %p: consider codec %s for remote endpoint %s", - sw, (*sw->codec_iter)->name, *sw->path_iter); - - ep = device_remote_endpoint_find(sw->device, *sw->path_iter); - - if (ep == NULL || ep->capabilities == NULL || ep->uuid == NULL) { - spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s not valid, try next", - sw, *sw->path_iter); + if (!codec_switch_check_endpoint(ep, codec, &sink, &local_endpoint)) { + spa_log_error(monitor->log, "media codec switch %p: endpoint %s not valid", + sw, path); return false; } - /* Setup and check compatible configuration */ - if (ep->codec != codec->codec_id) { - spa_log_debug(sw->device->monitor->log, "media codec switch %p: different codec, try next", sw); - return false; - } - - if (!(sw->profile & spa_bt_profile_from_uuid(ep->uuid))) { - spa_log_debug(sw->device->monitor->log, "media codec switch %p: wrong uuid (%s) for profile, try next", - sw, ep->uuid); - return false; - } - - if ((sw->profile & SPA_BT_PROFILE_A2DP_SINK) || (sw->profile & SPA_BT_PROFILE_BAP_SINK) ) { - direction = SPA_BT_MEDIA_SOURCE; - sink = false; - } else if ((sw->profile & SPA_BT_PROFILE_A2DP_SOURCE) || (sw->profile & SPA_BT_PROFILE_BAP_SOURCE) ) { - direction = SPA_BT_MEDIA_SINK; - sink = true; - } else { - spa_log_debug(sw->device->monitor->log, "media codec switch %p: bad profile (%d), try next", - sw, sw->profile); - return false; - } - - if (media_codec_to_endpoint(codec, direction, &local_endpoint) < 0) { - spa_log_debug(sw->device->monitor->log, "media codec switch %p: no endpoint for codec %s, try next", - sw, codec->name); - return false; - } - - /* Each endpoint can be used by only one device at a time (on each adapter) */ - spa_list_for_each(t, &sw->device->monitor->transport_list, link) { - if (t->device == sw->device) + /* Each A2DP endpoint can be used by only one device at a time (on each adapter) */ + spa_list_for_each(t, &monitor->transport_list, link) { + if (t->device == device) continue; - if (t->device->adapter != sw->device->adapter) + if (t->device->adapter != device->adapter) continue; if (spa_streq(t->endpoint_path, local_endpoint)) { - spa_log_debug(sw->device->monitor->log, "media codec switch %p: endpoint %s in use, try next", + spa_log_error(monitor->log, "media codec switch %p: endpoint %s in use", sw, local_endpoint); return false; } } res = codec->select_config(codec, sink ? MEDIA_CODEC_FLAG_SINK : 0, ep->capabilities, ep->capabilities_len, - &sw->device->monitor->default_audio_info, - &sw->device->monitor->global_settings, config); + &monitor->default_audio_info, &monitor->global_settings, config); if (res < 0) { - spa_log_debug(sw->device->monitor->log, "media codec switch %p: incompatible capabilities (%d), try next", + spa_log_error(monitor->log, "media codec switch %p: incompatible capabilities (%d)", sw, res); return false; } config_size = res; - spa_log_debug(sw->device->monitor->log, "media codec switch %p: configuration %d", sw, config_size); - for (i = 0; i < config_size; i++) - spa_log_debug(sw->device->monitor->log, "media codec switch %p: %d: %02x", sw, i, config[i]); + spa_log_debug(monitor->log, "media codec switch %p: configuration %d", sw, config_size); + spa_debug_log_mem(monitor->log, SPA_LOG_LEVEL_DEBUG, 4, config, config_size); /* Codecs may share the same endpoint, so indicate which one we are using */ - sw->device->preferred_codec = codec; + device->preferred_codec = codec; /* org.bluez.MediaEndpoint1.SetConfiguration on remote endpoint */ m = dbus_message_new_method_call(BLUEZ_SERVICE, ep->path, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "SetConfiguration"); if (m == NULL) { - spa_log_debug(sw->device->monitor->log, "media codec switch %p: dbus allocation failure, try next", sw); + spa_log_error(monitor->log, "media codec switch %p: dbus allocation failure", sw); return false; } - spa_bt_device_update_last_bluez_action_time(sw->device); + spa_bt_device_update_last_bluez_action_time(device); - spa_log_info(sw->device->monitor->log, "media codec switch %p: trying codec %s for endpoint %s, local endpoint %s", + spa_log_info(monitor->log, "media codec switch %p: set codec %s for endpoint %s, local endpoint %s", sw, codec->name, ep->path, local_endpoint); dbus_message_iter_init_append(m, &iter); @@ -4300,201 +4514,233 @@ static bool media_codec_switch_process_current(struct spa_bt_media_codec_switch dbus_message_iter_close_container(&iter, &d); spa_assert(sw->pending == NULL); - sw->pending = send_with_reply(sw->device->monitor->conn, m, media_codec_switch_reply, sw); + sw->pending = send_with_reply(monitor->conn, m, codec_switch_reply, sw); if (!sw->pending) { - spa_log_error(sw->device->monitor->log, "media codec switch %p: dbus call failure, try next", sw); + spa_log_error(monitor->log, "media codec switch %p: dbus call failure", sw); return false; } return true; } -static void media_codec_switch_process(struct spa_bt_media_codec_switch *sw) +static bool codec_switch_configure_bap(struct spa_bt_codec_switch *sw, const char *path, bool last) { - while (*sw->codec_iter != NULL && *sw->path_iter != NULL) { - uint64_t now, threshold; - - /* Rate limit BlueZ calls */ - now = get_time_now(sw->device->monitor); - threshold = sw->device->last_bluez_action_time + BLUEZ_ACTION_RATE_MSEC * SPA_NSEC_PER_MSEC; - if (now < threshold) { - /* Wait for timeout */ - media_codec_switch_start_timer(sw, threshold - now); - return; - } - - if (sw->path_iter == sw->paths && (*sw->codec_iter)->caps_preference_cmp) { - /* Sort endpoints according to codec preference, when at a new codec. */ - media_codec_switch_cmp_sw = sw; - qsort(sw->paths, sw->num_paths, sizeof(char *), media_codec_switch_cmp); - } - - if (media_codec_switch_process_current(sw)) { - /* Wait for dbus reply */ - return; - } - - media_codec_switch_next(sw); - }; - - /* Didn't find any suitable endpoint. Report failure. */ - spa_log_info(sw->device->monitor->log, "media codec switch %p: failed to get an endpoint", sw); - spa_bt_device_emit_codec_switched(sw->device, -ENODEV); - spa_bt_device_check_profiles(sw->device, false); - media_codec_switch_free(sw); -} - -static bool media_codec_switch_goto_active(struct spa_bt_media_codec_switch *sw) -{ - struct spa_bt_device *device = sw->device; - struct spa_bt_media_codec_switch *active_sw; - - active_sw = spa_list_first(&device->codec_switch_list, struct spa_bt_media_codec_switch, device_link); - - if (active_sw != sw) { - struct spa_bt_media_codec_switch *t; - - /* This codec switch has been canceled. Switch to the newest one. */ - spa_log_debug(sw->device->monitor->log, - "media codec switch %p: canceled, go to new switch", sw); - - spa_list_for_each_safe(sw, t, &device->codec_switch_list, device_link) { - if (sw != active_sw) - media_codec_switch_free(sw); - } - - media_codec_switch_process(active_sw); - return false; - } - - return true; -} - -static void media_codec_switch_timer_event(struct spa_source *source) -{ - struct spa_bt_media_codec_switch *sw = source->data; struct spa_bt_device *device = sw->device; struct spa_bt_monitor *monitor = device->monitor; - uint64_t exp; + struct spa_bt_remote_endpoint *ep; + spa_autoptr(DBusMessage) m = NULL; + DBusMessageIter iter, d; + dbus_bool_t defer = !last; - if (spa_system_timerfd_read(monitor->main_system, source->fd, &exp) < 0) - spa_log_warn(monitor->log, "error reading timerfd: %s", strerror(errno)); + ep = device_remote_endpoint_find(device, path); + if (!ep) { + spa_log_error(monitor->log, "media codec switch %p: no endpoint %s", sw, path); + return false; + } - spa_log_debug(monitor->log, "media codec switch %p: rate limit timer event", sw); + device->preferred_codec = sw->codec; + device->preferred_profiles = sw->profiles; - media_codec_switch_stop_timer(sw); - - if (!media_codec_switch_goto_active(sw)) - return; - - media_codec_switch_process(sw); -} - -static void media_codec_switch_reply(DBusPendingCall *pending, void *user_data) -{ - struct spa_bt_media_codec_switch *sw = user_data; - struct spa_bt_device *device = sw->device; - - spa_assert(sw->pending == pending); - spa_autoptr(DBusMessage) r = steal_reply_and_unref(&sw->pending); + m = dbus_message_new_method_call(BLUEZ_SERVICE, ep->path, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "Reconfigure"); + if (m == NULL) { + spa_log_error(monitor->log, "media codec switch %p: dbus allocation failure", sw); + return false; + } spa_bt_device_update_last_bluez_action_time(device); - if (!media_codec_switch_goto_active(sw)) - return; + spa_log_info(monitor->log, "media codec switch %p: reconfigure endpoint %s, defer:%d", + sw, ep->path, (int)defer); - if (r == NULL) { - spa_log_error(sw->device->monitor->log, - "media codec switch %p: empty reply from dbus, trying next", - sw); - goto next; + dbus_message_iter_init_append(m, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &d); + append_basic_variant_dict_entry(&d, "Defer", DBUS_TYPE_BOOLEAN, "b", &defer); + dbus_message_iter_close_container(&iter, &d); + + spa_assert(sw->pending == NULL); + sw->pending = send_with_reply(monitor->conn, m, codec_switch_reply, sw); + if (!sw->pending) { + spa_log_error(monitor->log, "media codec switch %p: dbus call failure", sw); + return false; } - if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { - spa_log_debug(sw->device->monitor->log, - "media codec switch %p: failed (%s), trying next", - sw, dbus_message_get_error_name(r)); - goto next; + return true; +} + +static bool codec_switch_clear_bap(struct spa_bt_codec_switch *sw, const char *path) +{ + struct spa_bt_device *device = sw->device; + struct spa_bt_monitor *monitor = device->monitor; + struct spa_bt_remote_endpoint *ep; + spa_autoptr(DBusMessage) m = NULL; + DBusMessageIter iter; + + ep = device_remote_endpoint_find(device, path); + if (!ep) + return true; + + m = dbus_message_new_method_call(BLUEZ_SERVICE, ep->path, BLUEZ_MEDIA_ENDPOINT_INTERFACE, "ClearConfiguration"); + if (m == NULL) { + spa_log_error(monitor->log, "media codec switch %p: dbus allocation failure", sw); + return false; } - /* Success */ - spa_log_info(sw->device->monitor->log, "media codec switch %p: success", sw); - spa_bt_device_emit_codec_switched(sw->device, 0); + spa_bt_device_update_last_bluez_action_time(device); + + spa_log_info(monitor->log, "media codec switch %p: clear endpoint %s", sw, ep->path); + + dbus_message_iter_init_append(m, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path); + + spa_assert(sw->pending == NULL); + sw->pending = send_with_reply(monitor->conn, m, codec_switch_reply, sw); + if (!sw->pending) { + spa_log_error(monitor->log, "media codec switch %p: dbus call failure", sw); + return false; + } + + return true; +} + +static void codec_switch_emit_switching(struct spa_bt_monitor *monitor) +{ + struct spa_bt_device *d; + struct spa_bt_codec_switch *sw; + bool found = false; + + spa_list_for_each(d, &monitor->device_list, link) { + spa_list_for_each(sw, &d->codec_switch_list, link) { + if (sw->profiles & SPA_BT_PROFILE_BAP_AUDIO) { + found = true; + goto done; + } + } + } + +done: + spa_list_for_each(d, &monitor->device_list, link) + spa_bt_device_emit_codec_switch_other(d, found); +} + +static bool codec_switch_process(struct spa_bt_codec_switch *sw) +{ + if (sw->waiting) + return false; + if (sw->canceled) + return true; + if (sw->failed) + goto fail; + + if (sw->paths[sw->path_idx].path == NULL) { + /* Success */ + spa_log_info(sw->device->monitor->log, "media codec switch %p: success", sw); + spa_bt_device_emit_codec_switched(sw->device, 0); + spa_bt_device_check_profiles(sw->device, false); + + sw->profiles = 0; + codec_switch_emit_switching(sw->device->monitor); + return true; + } + + if (sw->profiles & SPA_BT_PROFILE_A2DP_DUPLEX) { + /* Rate limit BlueZ calls */ + if (codec_switch_rate_limit(sw)) + return false; + + if (!codec_switch_configure_a2dp(sw, sw->paths[sw->path_idx].path)) + goto fail; + } else { + if (sw->path_idx == 0 && codec_switch_rate_limit(sw)) + return false; + + if (sw->path_idx == 0) + codec_switch_emit_switching(sw->device->monitor); + + if (sw->paths[sw->path_idx].clear) { + if (!codec_switch_clear_bap(sw, sw->paths[sw->path_idx].path)) + goto fail; + } else { + bool last = (sw->paths[sw->path_idx + 1].path == NULL); + + if (!codec_switch_configure_bap(sw, sw->paths[sw->path_idx].path, last)) + goto fail; + } + } + + /* Configure another endpoint next */ + sw->path_idx++; + + /* Wait for dbus reply */ + return false; + +fail: + /* Report failure. */ + spa_log_info(sw->device->monitor->log, "media codec switch %p: failed", sw); + spa_bt_device_emit_codec_switched(sw->device, -ENODEV); spa_bt_device_check_profiles(sw->device, false); - media_codec_switch_free(sw); - return; -next: - if (sw->retries > 0) - --sw->retries; - else - media_codec_switch_next(sw); - - media_codec_switch_process(sw); - return; + sw->profiles = 0; + codec_switch_emit_switching(sw->device->monitor); + return true; } -static int media_codec_switch_start_timer(struct spa_bt_media_codec_switch *sw, uint64_t timeout) +static void codec_switch_cancel(struct spa_bt_codec_switch *sw) { - struct spa_bt_monitor *monitor = sw->device->monitor; - struct itimerspec ts; + /* BlueZ does not appear to allow calling dbus_pending_call_cancel on an + * active request, so we have to wait for the reply to arrive. + */ + sw->canceled = true; +} - spa_assert(sw->timer.data == NULL); +static void codec_switch_destroy(struct spa_bt_codec_switch *sw) +{ + unsigned int i; - spa_log_debug(monitor->log, "media codec switch %p: starting rate limit timer", sw); + spa_list_remove(&sw->link); - if (sw->timer.data == NULL) { - sw->timer.data = sw; - sw->timer.func = media_codec_switch_timer_event; - sw->timer.fd = spa_system_timerfd_create(monitor->main_system, - CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); - sw->timer.mask = SPA_IO_IN; - sw->timer.rmask = 0; - spa_loop_add_source(monitor->main_loop, &sw->timer); + cancel_and_unref(&sw->pending); + + if (sw->paths != NULL) + for (i = 0; sw->paths[i].path; ++i) + free(sw->paths[i].path); + + if (sw->timer) + spa_loop_utils_destroy_source(sw->device->monitor->loop_utils, sw->timer); + + free(sw->paths); + free(sw); +} + +static void codec_switch_list_process(struct spa_list *list) +{ + struct spa_bt_codec_switch *sw; + + spa_list_consume(sw, list, link) { + if (codec_switch_process(sw)) { + codec_switch_destroy(sw); + } else { + sw->waiting = true; + break; + } } - ts.it_value.tv_sec = timeout / SPA_NSEC_PER_SEC; - ts.it_value.tv_nsec = timeout % SPA_NSEC_PER_SEC; - ts.it_interval.tv_sec = 0; - ts.it_interval.tv_nsec = 0; - spa_system_timerfd_settime(monitor->main_system, sw->timer.fd, 0, &ts, NULL); - return 0; } -static int media_codec_switch_stop_timer(struct spa_bt_media_codec_switch *sw) +static int codec_switch_cmp(const void *a, const void *b) { + struct spa_bt_codec_switch *sw = codec_switch_cmp_sw; + const struct media_codec *codec = sw->codec; struct spa_bt_monitor *monitor = sw->device->monitor; - struct itimerspec ts; - - if (sw->timer.data == NULL) - return 0; - - spa_log_debug(monitor->log, "media codec switch %p: stopping rate limit timer", sw); - - spa_loop_remove_source(monitor->main_loop, &sw->timer); - ts.it_value.tv_sec = 0; - ts.it_value.tv_nsec = 0; - ts.it_interval.tv_sec = 0; - ts.it_interval.tv_nsec = 0; - spa_system_timerfd_settime(monitor->main_system, sw->timer.fd, 0, &ts, NULL); - spa_system_close(monitor->main_system, sw->timer.fd); - sw->timer.data = NULL; - return 0; -} - -static int media_codec_switch_cmp(const void *a, const void *b) -{ - struct spa_bt_media_codec_switch *sw = media_codec_switch_cmp_sw; - const struct media_codec *codec = *sw->codec_iter; - const char *path1 = *(char **)a, *path2 = *(char **)b; + const struct spa_bt_codec_switch_path *path1 = a; + const struct spa_bt_codec_switch_path *path2 = b; struct spa_bt_remote_endpoint *ep1, *ep2; uint32_t flags; - ep1 = device_remote_endpoint_find(sw->device, path1); - ep2 = device_remote_endpoint_find(sw->device, path2); + ep1 = device_remote_endpoint_find(sw->device, path1->path); + ep2 = device_remote_endpoint_find(sw->device, path2->path); - if (ep1 != NULL && (ep1->uuid == NULL || ep1->codec != codec->codec_id || ep1->capabilities == NULL)) + if (ep1 != NULL && (ep1->uuid == NULL || ep1->codec != codec->codec_id)) ep1 = NULL; - if (ep2 != NULL && (ep2->uuid == NULL || ep2->codec != codec->codec_id || ep2->capabilities == NULL)) + if (ep2 != NULL && (ep2->uuid == NULL || ep2->codec != codec->codec_id)) ep2 = NULL; if (ep1 && ep2 && !spa_streq(ep1->uuid, ep2->uuid)) { ep1 = NULL; @@ -4508,24 +4754,27 @@ static int media_codec_switch_cmp(const void *a, const void *b) else if (ep2 == NULL) return -1; - if (codec->bap) + if (codec->kind == MEDIA_CODEC_BAP) flags = spa_streq(ep1->uuid, SPA_BT_UUID_BAP_SOURCE) ? MEDIA_CODEC_FLAG_SINK : 0; else flags = spa_streq(ep1->uuid, SPA_BT_UUID_A2DP_SOURCE) ? MEDIA_CODEC_FLAG_SINK : 0; return codec->caps_preference_cmp(codec, flags, ep1->capabilities, ep1->capabilities_len, - ep2->capabilities, ep2->capabilities_len, &sw->device->monitor->default_audio_info, - &sw->device->monitor->global_settings); + ep2->capabilities, ep2->capabilities_len, &monitor->default_audio_info, + &monitor->global_settings); } /* Ensure there's a transport for at least one of the listed codecs */ -int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs) +int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs, + uint32_t profiles) { - struct spa_bt_media_codec_switch *sw; + struct spa_bt_monitor *monitor = device->monitor; + struct spa_bt_codec_switch *sw, *sw2; struct spa_bt_remote_endpoint *ep; struct spa_bt_transport *t; - const struct media_codec *preferred_codec = NULL; - size_t i, j, num_codecs, num_eps; + const struct media_codec *codec = NULL; + size_t i, j, num_eps, res; + uint32_t remaining = 0; if (!device->adapter->a2dp_application_registered && !device->adapter->bap_application_registered) { @@ -4534,112 +4783,164 @@ int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct } for (i = 0; codecs[i] != NULL; ++i) { + if (codecs[i]->kind != MEDIA_CODEC_BAP && codecs[i]->kind != MEDIA_CODEC_A2DP) + continue; + if (spa_bt_device_supports_media_codec(device, codecs[i], device->connected_profiles)) { - preferred_codec = codecs[i]; + codec = codecs[i]; break; } } - /* Check if we already have an enabled transport for the most preferred codec. + if (!profiles) + profiles = device->connected_profiles & (SPA_BT_PROFILE_MEDIA_SOURCE | + SPA_BT_PROFILE_MEDIA_SINK); + + if (!codec) + return -EINVAL; + + /* Check if we already have an enabled transports for the profiles. * However, if there already was a codec switch running, these transports may * disappear soon. In that case, we have to do the full thing. */ - if (spa_list_is_empty(&device->codec_switch_list) && preferred_codec != NULL) { + if (!has_codec_switch(device)) { + uint32_t found_profiles = 0; + spa_list_for_each(t, &device->transport_list, device_link) { - if (t->media_codec != preferred_codec) + if (t->media_codec != codec) continue; - if ((device->connected_profiles & t->profile) != t->profile) - continue; + found_profiles |= t->profile; + } + if (found_profiles == profiles) { spa_bt_device_emit_codec_switched(device, 0); return 0; } } - /* Setup and start iteration */ - - sw = calloc(1, sizeof(struct spa_bt_media_codec_switch)); + /* Setup */ + sw = calloc(1, sizeof(struct spa_bt_codec_switch)); if (sw == NULL) - return -ENOMEM; + goto error_errno; + sw->codec = codec; + sw->device = device; + sw->profiles = profiles; + + spa_list_append(&device->codec_switch_list, &sw->link); + + /* Find endpoints */ num_eps = 0; spa_list_for_each(ep, &device->remote_endpoint_list, device_link) ++num_eps; - num_codecs = 0; - while (codecs[num_codecs] != NULL) - ++num_codecs; + sw->paths = calloc(num_eps + 1, sizeof(*sw->paths)); + if (!sw->paths) + goto error_errno; - sw->codecs = calloc(num_codecs + 1, sizeof(const struct media_codec *)); - sw->paths = calloc(num_eps + 1, sizeof(char *)); - sw->num_paths = num_eps; - - if (sw->codecs == NULL || sw->paths == NULL) { - media_codec_switch_free(sw); - return -ENOMEM; - } - - for (i = 0, j = 0; i < num_codecs; ++i) { - if (is_media_codec_enabled(device->monitor, codecs[i])) { - sw->codecs[j] = codecs[i]; - ++j; - } - } - sw->codecs[j] = NULL; + sw->path_idx = 0; i = 0; spa_list_for_each(ep, &device->remote_endpoint_list, device_link) { - sw->paths[i] = strdup(ep->path); - if (sw->paths[i] == NULL) { - media_codec_switch_free(sw); - return -ENOMEM; - } + sw->paths[i].path = strdup(ep->path); + if (sw->paths[i].path == NULL) + goto error_errno; ++i; } - sw->paths[i] = NULL; - sw->codec_iter = sw->codecs; - sw->path_iter = sw->paths; - sw->retries = CODEC_SWITCH_RETRIES; - - sw->profile = device->connected_profiles; - - sw->device = device; - - if (!spa_list_is_empty(&device->codec_switch_list)) { - /* - * There's a codec switch already running, either waiting for timeout or - * BlueZ reply. - * - * BlueZ does not appear to allow calling dbus_pending_call_cancel on an - * active request, so we have to wait for the reply to arrive first, and - * only then start processing this request. The timeout we would also have - * to wait to pass in any case, so we don't cancel it either. - */ - spa_log_debug(sw->device->monitor->log, - "media codec switch %p: already in progress, canceling previous", - sw); - - spa_list_prepend(&device->codec_switch_list, &sw->device_link); - } else { - spa_list_prepend(&device->codec_switch_list, &sw->device_link); - media_codec_switch_process(sw); + /* Sort in codec preference order */ + if (codec->caps_preference_cmp) { + codec_switch_cmp_sw = sw; + qsort(sw->paths, num_eps, sizeof(*sw->paths), codec_switch_cmp); } + /* Pick at most one source and one sink endpoint, if corresponding profiles are + * set */ + remaining = profiles; + for (i = 0, j = 0; i < num_eps; ++i) { + struct spa_bt_remote_endpoint *ep; + bool sink; + uint32_t mask; + + ep = remote_endpoint_find(monitor, sw->paths[i].path); + if (!codec_switch_check_endpoint(ep, codec, &sink, NULL)) + continue; + + mask = sink ? SPA_BT_PROFILE_MEDIA_SOURCE : SPA_BT_PROFILE_MEDIA_SINK; + if (!(remaining & mask)) + continue; + SPA_FLAG_CLEAR(remaining, mask); + + spa_log_debug(monitor->log, + "media codec switch %p: select endpoint %s for codec %s", + sw, sw->paths[i].path, codec->name); + + SPA_SWAP(sw->paths[j], sw->paths[i]); + ++j; + } + if (profiles & SPA_BT_PROFILE_BAP_AUDIO) { + /* Active unselected endpoints must be cleared */ + for (i = j; i < num_eps; ++i) { + bool active_ep = false; + + spa_list_for_each(t, &device->transport_list, device_link) { + if (spa_streq(t->remote_endpoint_path, sw->paths[i].path)) { + active_ep = true; + break; + } + } + if (!active_ep) + continue; + + spa_log_debug(monitor->log, + "media codec switch %p: select endpoint %s to be cleared", + sw, sw->paths[i].path); + SPA_SWAP(sw->paths[j], sw->paths[i]); + sw->paths[j].clear = true; + ++j; + } + + /* Reverse order so that clears come first */ + for (i = 0; i < j/2; ++i) + SPA_SWAP(sw->paths[i], sw->paths[j - 1 - i]); + } + for (; j < num_eps; ++j) { + free(sw->paths[j].path); + spa_zero(sw->paths[j]); + } + + if (!sw->paths[0].path || remaining) { + spa_log_error(monitor->log, + "media codec switch %p: no valid profile 0x%x endpoints for codec %s", + sw, profiles, codec->name); + errno = EINVAL; + goto error_errno; + } + + /* Cancel other codec switches */ + spa_list_for_each(sw2, &device->codec_switch_list, link) + if (sw2 != sw) + codec_switch_cancel(sw2); + + codec_switch_list_process(&device->codec_switch_list); return 0; + +error_errno: + res = -errno; + if (sw) + codec_switch_destroy(sw); + return res; } -int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec) +int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, const struct media_codec *codec) { struct spa_bt_monitor *monitor = device->monitor; - return spa_bt_backend_ensure_codec(monitor->backend, device, codec); -} -int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec) -{ - struct spa_bt_monitor *monitor = device->monitor; - return spa_bt_backend_supports_codec(monitor->backend, device, codec); + if (!codec || codec->kind != MEDIA_CODEC_HFP) + return -EINVAL; + + return spa_bt_backend_ensure_codec(monitor->backend, device, codec->codec_id); } static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn, @@ -4737,6 +5038,10 @@ static DBusHandlerResult endpoint_set_configuration(DBusConnection *conn, spa_log_error(monitor->log, "invalid transport configuration"); return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } + if (info.info.raw.channels > MAX_CHANNELS) { + spa_log_error(monitor->log, "too many channels in transport"); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } transport->n_channels = info.info.raw.channels; memcpy(transport->channels, info.info.raw.position, transport->n_channels * sizeof(uint32_t)); @@ -5001,7 +5306,7 @@ out: return err; } -static void append_media_object(DBusMessageIter *iter, const char *endpoint, +static void append_media_object(struct spa_bt_monitor *monitor, DBusMessageIter *iter, const char *endpoint, const char *uuid, uint8_t codec_id, uint8_t *caps, size_t caps_size) { const char *interface_name = BLUEZ_MEDIA_ENDPOINT_INTERFACE; @@ -5028,19 +5333,24 @@ static void append_media_object(DBusMessageIter *iter, const char *endpoint, } if (spa_bt_profile_from_uuid(uuid) & (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE)) { dbus_uint32_t locations; - dbus_uint16_t supported_context, context; + dbus_uint16_t supported_contexts, contexts; - locations = BAP_CHANNEL_ALL; if (spa_bt_profile_from_uuid(uuid) & SPA_BT_PROFILE_BAP_SINK) { - supported_context = context = BAP_CONTEXT_ALL; + locations = monitor->bap_sink_locations; + contexts = monitor->bap_sink_contexts; + supported_contexts = monitor->bap_sink_supported_contexts; } else { - supported_context = context = (BAP_CONTEXT_UNSPECIFIED | BAP_CONTEXT_CONVERSATIONAL | - BAP_CONTEXT_MEDIA | BAP_CONTEXT_GAME); + locations = monitor->bap_source_locations; + contexts = monitor->bap_source_contexts; + supported_contexts = monitor->bap_source_supported_contexts; } + spa_log_debug(monitor->log, "BAP endpoint %s locations:0x%x contexts:0x%x supported-contexs:0x%x", + endpoint, locations, contexts, supported_contexts); + append_basic_variant_dict_entry(&dict, "Locations", DBUS_TYPE_UINT32, "u", &locations); - append_basic_variant_dict_entry(&dict, "Context", DBUS_TYPE_UINT16, "q", &context); - append_basic_variant_dict_entry(&dict, "SupportedContext", DBUS_TYPE_UINT16, "q", &supported_context); + append_basic_variant_dict_entry(&dict, "Context", DBUS_TYPE_UINT16, "q", &contexts); + append_basic_variant_dict_entry(&dict, "SupportedContext", DBUS_TYPE_UINT16, "q", &supported_contexts); } dbus_message_iter_close_container(&entry, &dict); @@ -5104,10 +5414,9 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage * uint8_t caps[A2DP_MAX_CAPS_SIZE]; int caps_size, ret; uint16_t codec_id = codec->codec_id; + enum media_codec_kind kind = is_bap ? MEDIA_CODEC_BAP : MEDIA_CODEC_A2DP; - if (codec->bap != is_bap) - continue; - if (codec->asha) + if (codec->kind != kind) continue; if (!is_media_codec_enabled(monitor, codec)) @@ -5122,8 +5431,8 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage * ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SINK, &endpoint); if (ret == 0) { spa_log_info(monitor->log, "register media sink codec %s: %s", media_codecs[i]->name, endpoint); - append_media_object(&array, endpoint, - codec->bap ? SPA_BT_UUID_BAP_SINK : SPA_BT_UUID_A2DP_SINK, + append_media_object(monitor, &array, endpoint, + is_bap ? SPA_BT_UUID_BAP_SINK : SPA_BT_UUID_A2DP_SINK, codec_id, caps, caps_size); } } @@ -5137,13 +5446,13 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage * ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SOURCE, &endpoint); if (ret == 0) { spa_log_info(monitor->log, "register media source codec %s: %s", media_codecs[i]->name, endpoint); - append_media_object(&array, endpoint, - codec->bap ? SPA_BT_UUID_BAP_SOURCE : SPA_BT_UUID_A2DP_SOURCE, + append_media_object(monitor, &array, endpoint, + is_bap ? SPA_BT_UUID_BAP_SOURCE : SPA_BT_UUID_A2DP_SOURCE, codec_id, caps, caps_size); } } - if (codec->bap && register_bcast) { + if (is_bap && register_bcast) { if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST)) { caps_size = codec->fill_caps(codec, 0, &monitor->global_settings, caps); if (caps_size < 0) @@ -5153,7 +5462,7 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage * ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SOURCE_BROADCAST, &endpoint); if (ret == 0) { spa_log_info(monitor->log, "register media source codec %s: %s", media_codecs[i]->name, endpoint); - append_media_object(&array, endpoint, + append_media_object(monitor, &array, endpoint, SPA_BT_UUID_BAP_BROADCAST_SOURCE, codec_id, caps, caps_size); } @@ -5168,7 +5477,7 @@ static DBusHandlerResult object_manager_handler(DBusConnection *c, DBusMessage * ret = media_codec_to_endpoint(codec, SPA_BT_MEDIA_SINK_BROADCAST, &endpoint); if (ret == 0) { spa_log_info(monitor->log, "register broadcast media sink codec %s: %s", media_codecs[i]->name, endpoint); - append_media_object(&array, endpoint, + append_media_object(monitor, &array, endpoint, SPA_BT_UUID_BAP_BROADCAST_SINK, codec_id, caps, caps_size); } @@ -5301,7 +5610,7 @@ static int register_media_application(struct spa_bt_monitor * monitor) register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE); register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK); - if (codec->bap) { + if (codec->kind == MEDIA_CODEC_BAP) { register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST); register_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK_BROADCAST); } @@ -5337,7 +5646,7 @@ static void unregister_media_application(struct spa_bt_monitor * monitor) unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE); unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK); - if (codec->bap) { + if (codec->kind == MEDIA_CODEC_BAP) { unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SOURCE_BROADCAST); unregister_media_endpoint(monitor, codec, SPA_BT_MEDIA_SINK_BROADCAST); } @@ -5354,8 +5663,9 @@ static bool have_codec_endpoints(struct spa_bt_monitor *monitor, bool bap) for (i = 0; media_codecs[i]; i++) { const struct media_codec *codec = media_codecs[i]; + enum media_codec_kind kind = bap ? MEDIA_CODEC_BAP : MEDIA_CODEC_A2DP; - if (codec->bap != bap) + if (codec->kind != kind) continue; if (endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SINK) || endpoint_should_be_registered(monitor, codec, SPA_BT_MEDIA_SOURCE) || @@ -5495,7 +5805,6 @@ static void configure_bis(struct spa_bt_monitor *monitor, uint8_t metadata [METADATA_MAX_LEN]; uint8_t caps_size, metadata_size = 0; struct bap_codec_qos qos; - int presentation_delay; struct spa_bt_metadata *metadata_entry; struct spa_dict settings; struct spa_dict_item setting_items[2]; @@ -5577,7 +5886,7 @@ static void configure_bis(struct spa_bt_monitor *monitor, append_basic_variant_dict_entry(&qos_dict, "SDU", DBUS_TYPE_UINT16, "q", &qos.sdu); append_basic_variant_dict_entry(&qos_dict, "Retransmissions", DBUS_TYPE_BYTE, "y", &qos.retransmission); append_basic_variant_dict_entry(&qos_dict, "Latency", DBUS_TYPE_UINT16, "q", &qos.latency); - append_basic_variant_dict_entry(&qos_dict, "PresentationDelay", DBUS_TYPE_UINT32, "u", &presentation_delay); + append_basic_variant_dict_entry(&qos_dict, "PresentationDelay", DBUS_TYPE_UINT32, "u", &qos.delay); dbus_message_iter_close_container(&variant, &qos_dict); dbus_message_iter_close_container(&entry, &variant); @@ -5705,7 +6014,7 @@ static void interface_added(struct spa_bt_monitor *monitor, /* get local endpoint */ for (i = 0; monitor->media_codecs[i]; i++) { - if (!monitor->media_codecs[i]->bap) + if (monitor->media_codecs[i]->kind != MEDIA_CODEC_BAP) continue; if (!is_media_codec_enabled(monitor, monitor->media_codecs[i])) continue; @@ -6511,8 +6820,6 @@ static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict if (spa_dict_lookup_item(&this->enabled_codecs, codec->name) != NULL) continue; - spa_log_debug(this->log, "enabling codec %s", codec->name); - spa_assert(this->enabled_codecs.n_items < num_codecs); codecs[this->enabled_codecs.n_items].key = codec->name; @@ -6527,8 +6834,8 @@ static int parse_codec_array(struct spa_bt_monitor *this, const struct spa_dict for (i = 0; media_codecs[i]; ++i) { const struct media_codec *codec = media_codecs[i]; - if (!is_media_codec_enabled(this, codec)) - spa_log_debug(this->log, "disabling codec %s", codec->name); + spa_log_debug(this->log, "codec %s: %s", codec->name, + is_media_codec_enabled(this, codec) ? "enabled" : "disabled"); } return 0; @@ -6544,6 +6851,56 @@ fallback: return 0; } +static void parse_bap_locations(struct spa_bt_monitor *this, const struct spa_dict *info, + const char *key, uint32_t *value) +{ + const char *str; + uint32_t position[MAX_CHANNELS]; + uint32_t n_channels; + uint32_t locations; + unsigned int i, j; + + if (!info || !(str = spa_dict_lookup(info, key))) + return; + + if (spa_atou32(str, value, 0)) + return; + + if (!spa_audio_parse_position_n(str, strlen(str), position, + SPA_N_ELEMENTS(position), &n_channels)) { + spa_log_error(this->log, "property %s '%s' is not valid position array", key, str); + return; + } + + locations = 0; + for (i = 0; i < n_channels; ++i) + for (j = 0; j < SPA_N_ELEMENTS(bap_channel_bits); ++j) + if (bap_channel_bits[j].channel == position[i]) + locations |= bap_channel_bits[j].bit; + + *value = locations; +} + +static void parse_bap_server(struct spa_bt_monitor *this, const struct spa_dict *info) +{ + this->bap_sink_locations = BAP_CHANNEL_ALL; + this->bap_source_locations = BAP_CHANNEL_ALL; + this->bap_sink_contexts = this->bap_sink_supported_contexts = BAP_CONTEXT_ALL; + this->bap_source_contexts = this->bap_source_supported_contexts = (BAP_CONTEXT_UNSPECIFIED | BAP_CONTEXT_CONVERSATIONAL | + BAP_CONTEXT_MEDIA | BAP_CONTEXT_GAME); + + if (!info) + return; + + parse_bap_locations(this, info, "bluez5.bap-server-capabilities.sink.locations", &this->bap_sink_locations); + spa_atou32(spa_dict_lookup(info, "bluez5.bap-server-capabilities.sink.contexts"), &this->bap_sink_contexts, 0); + spa_atou32(spa_dict_lookup(info, "bluez5.bap-server-capabilities.sink.supported-contexts"), &this->bap_sink_supported_contexts, 0); + + parse_bap_locations(this, info, "bluez5.bap-server-capabilities.source.locations", &this->bap_source_locations); + spa_atou32(spa_dict_lookup(info, "bluez5.bap-server-capabilities.source.contexts"), &this->bap_source_contexts, 0); + spa_atou32(spa_dict_lookup(info, "bluez5.bap-server-capabilities.source.supported-contexts"), &this->bap_source_supported_contexts, 0); +} + static void get_global_settings(struct spa_bt_monitor *this, const struct spa_dict *dict) { uint32_t n_items = 0; @@ -6586,6 +6943,7 @@ impl_init(const struct spa_handle_factory *factory, this->dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus); this->main_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop); this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); + this->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils); this->main_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System); this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); this->plugin_loader = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_PluginLoader); @@ -6602,6 +6960,11 @@ impl_init(const struct spa_handle_factory *factory, return -EINVAL; } + if (this->loop_utils == NULL) { + spa_log_error(this->log, "loop utils is needed"); + return -EINVAL; + } + this->media_codecs = NULL; this->quirks = NULL; this->conn = NULL; @@ -6657,6 +7020,7 @@ impl_init(const struct spa_handle_factory *factory, parse_roles(this, info); parse_broadcast_source_config(this, info); + parse_bap_server(this, info); this->default_audio_info.rate = A2DP_CODEC_DEFAULT_RATE; this->default_audio_info.channels = A2DP_CODEC_DEFAULT_CHANNELS; diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c index 2095ba82c..6546abf4f 100644 --- a/spa/plugins/bluez5/bluez5-device.c +++ b/spa/plugins/bluez5/bluez5-device.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -38,7 +39,7 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.device"); #undef SPA_LOG_TOPIC_DEFAULT #define SPA_LOG_TOPIC_DEFAULT &log_topic -#define MAX_NODES (2*SPA_AUDIO_MAX_CHANNELS) +#define MAX_NODES (2*MAX_CHANNELS) #define DEVICE_ID_SOURCE 0 #define DEVICE_ID_SINK 1 @@ -53,13 +54,15 @@ static struct spa_i18n *_i18n; #define _(_str) spa_i18n_text(_i18n,(_str)) #define N_(_str) (_str) -enum { +enum device_profile { DEVICE_PROFILE_OFF = 0, - DEVICE_PROFILE_AG = 1, - DEVICE_PROFILE_A2DP = 2, - DEVICE_PROFILE_HSP_HFP = 3, - DEVICE_PROFILE_BAP = 4, - DEVICE_PROFILE_ASHA = 5, + DEVICE_PROFILE_AG, + DEVICE_PROFILE_A2DP, + DEVICE_PROFILE_HSP_HFP, + DEVICE_PROFILE_BAP, + DEVICE_PROFILE_BAP_SINK, + DEVICE_PROFILE_BAP_SOURCE, + DEVICE_PROFILE_ASHA, DEVICE_PROFILE_LAST, }; @@ -97,9 +100,9 @@ struct node { unsigned int offload_acquired:1; uint32_t n_channels; int64_t latency_offset; - uint32_t channels[SPA_AUDIO_MAX_CHANNELS]; - float volumes[SPA_AUDIO_MAX_CHANNELS]; - float soft_volumes[SPA_AUDIO_MAX_CHANNELS]; + uint32_t channels[MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; + float soft_volumes[MAX_CHANNELS]; }; struct dynamic_node @@ -127,8 +130,8 @@ struct device_set { bool leader; uint32_t sinks; uint32_t sources; - struct device_set_member sink[SPA_AUDIO_MAX_CHANNELS]; - struct device_set_member source[SPA_AUDIO_MAX_CHANNELS]; + struct device_set_member sink[MAX_CHANNELS]; + struct device_set_member source[MAX_CHANNELS]; }; struct impl { @@ -156,6 +159,7 @@ struct impl { uint32_t profile; unsigned int switching_codec:1; + unsigned int switching_codec_other:1; unsigned int save_profile:1; uint32_t prev_bt_connected_profiles; @@ -179,12 +183,25 @@ static void init_node(struct impl *this, struct node *node, uint32_t id) spa_zero(*node); node->id = id; - for (i = 0; i < SPA_AUDIO_MAX_CHANNELS; i++) { + for (i = 0; i < MAX_CHANNELS; i++) { node->volumes[i] = 1.0f; node->soft_volumes[i] = 1.0f; } } +static bool profile_is_bap(enum device_profile profile) +{ + switch (profile) { + case DEVICE_PROFILE_BAP: + case DEVICE_PROFILE_BAP_SINK: + case DEVICE_PROFILE_BAP_SOURCE: + return true; + default: + break; + } + return false; +} + static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec id, const struct media_codec **codecs, size_t size) { const struct media_codec * const *c; @@ -193,6 +210,9 @@ static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec i spa_assert(this->supported_codecs); for (c = this->supported_codecs; *c && size > 1; ++c) { + if ((*c)->kind == MEDIA_CODEC_HFP) + continue; + if ((*c)->id == id || id == 0) { *codecs++ = *c; --size; @@ -203,7 +223,7 @@ static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec i } static const struct media_codec *get_supported_media_codec(struct impl *this, enum spa_bluetooth_audio_codec id, - size_t *idx, enum spa_bt_profile profile) + int *priority, enum spa_bt_profile profile) { const struct media_codec *media_codec = NULL; size_t i; @@ -211,8 +231,7 @@ static const struct media_codec *get_supported_media_codec(struct impl *this, en for (i = 0; i < this->supported_codec_count; ++i) { if (this->supported_codecs[i]->id == id) { media_codec = this->supported_codecs[i]; - if (idx) - *idx = i; + break; } } @@ -222,6 +241,16 @@ static const struct media_codec *get_supported_media_codec(struct impl *this, en if (!spa_bt_device_supports_media_codec(this->bt_dev, media_codec, profile)) return NULL; + if (priority) { + *priority = 0; + for (i = 0; i < this->supported_codec_count; ++i) { + if (this->supported_codecs[i] == media_codec) + break; + if (this->supported_codecs[i]->kind == media_codec->kind) + ++(*priority); + } + } + return media_codec; } @@ -238,81 +267,11 @@ static bool is_bap_client(struct impl *this) return false; } -static bool can_bap_codec_switch(struct impl *this) -{ - if (!is_bap_client(this)) - return false; - - /* XXX: codec switching for source/duplex is not currently - * XXX: implemented properly. TODO: fix this - */ - if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_BAP_SOURCE) - return false; - - return true; -} - -static unsigned int get_hfp_codec(enum spa_bluetooth_audio_codec id) -{ - switch (id) { - case SPA_BLUETOOTH_AUDIO_CODEC_CVSD: - return HFP_AUDIO_CODEC_CVSD; - case SPA_BLUETOOTH_AUDIO_CODEC_MSBC: - return HFP_AUDIO_CODEC_MSBC; - case SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB: - return HFP_AUDIO_CODEC_LC3_SWB; - default: - return 0; - } -} - -static enum spa_bluetooth_audio_codec get_hfp_codec_id(unsigned int codec) -{ - switch (codec) { - case HFP_AUDIO_CODEC_MSBC: - return SPA_BLUETOOTH_AUDIO_CODEC_MSBC; - case HFP_AUDIO_CODEC_LC3_SWB: - return SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB; - case HFP_AUDIO_CODEC_CVSD: - return SPA_BLUETOOTH_AUDIO_CODEC_CVSD; - } - return SPA_ID_INVALID; -} - -static const char *get_hfp_codec_description(unsigned int codec) -{ - switch (codec) { - case HFP_AUDIO_CODEC_MSBC: - return "mSBC"; - case HFP_AUDIO_CODEC_LC3_SWB: - return "LC3-SWB"; - case HFP_AUDIO_CODEC_CVSD: - return "CVSD"; - } - return "unknown"; -} - -static const char *get_hfp_codec_name(unsigned int codec) -{ - switch (codec) { - case HFP_AUDIO_CODEC_MSBC: - return "msbc"; - case HFP_AUDIO_CODEC_LC3_SWB: - return "lc3_swb"; - case HFP_AUDIO_CODEC_CVSD: - return "cvsd"; - } - return "unknown"; -} - static const char *get_codec_name(struct spa_bt_transport *t, bool a2dp_duplex) { - if (t->media_codec != NULL) { - if (a2dp_duplex && t->media_codec->duplex_codec) - return t->media_codec->duplex_codec->name; - return t->media_codec->name; - } - return get_hfp_codec_name(t->codec); + if (a2dp_duplex && t->media_codec->duplex_codec) + return t->media_codec->duplex_codec->name; + return t->media_codec->name; } static void transport_destroy(void *userdata) @@ -402,9 +361,15 @@ static void node_update_soft_volumes(struct node *node, float hw_volume) } } +static int get_volume_id(int node_id) +{ + return (node_id & SINK_ID_FLAG) ? SPA_BT_VOLUME_ID_TX : SPA_BT_VOLUME_ID_RX; +} + static bool node_update_volume_from_transport(struct node *node, bool reset) { struct impl *impl = node->impl; + int volume_id = get_volume_id(node->id); struct spa_bt_transport_volume *t_volume; float prev_hw_volume; @@ -414,10 +379,12 @@ static bool node_update_volume_from_transport(struct node *node, bool reset) /* PW is the controller for remote device. */ if (impl->profile != DEVICE_PROFILE_A2DP && impl->profile != DEVICE_PROFILE_BAP + && impl->profile != DEVICE_PROFILE_BAP_SINK + && impl->profile != DEVICE_PROFILE_BAP_SOURCE && impl->profile != DEVICE_PROFILE_HSP_HFP) return false; - t_volume = &node->transport->volumes[node->id]; + t_volume = &node->transport->volumes[volume_id]; if (!t_volume->active) return false; @@ -510,6 +477,7 @@ static void get_channels(struct spa_bt_transport *t, bool a2dp_duplex, uint32_t *n_channels = info.info.raw.channels; memcpy(channels, info.info.raw.position, info.info.raw.channels * sizeof(uint32_t)); + } static const char *get_channel_name(uint32_t channel) @@ -528,7 +496,7 @@ static void emit_device_set_node(struct impl *this, uint32_t id) struct spa_bt_device *device = this->bt_dev; struct node *node = &this->nodes[id]; struct spa_device_object_info info; - struct spa_dict_item items[8]; + struct spa_dict_item items[9]; char str_id[32], members_json[8192], channels_json[512]; struct device_set_member *members; uint32_t n_members; @@ -580,7 +548,7 @@ static void emit_device_set_node(struct impl *this, uint32_t id) if (node->channels[k] == t->channels[j]) break; } - if (k == node->n_channels && node->n_channels < SPA_AUDIO_MAX_CHANNELS) + if (k == node->n_channels && node->n_channels < MAX_CHANNELS) node->channels[node->n_channels++] = t->channels[j]; } } @@ -645,7 +613,7 @@ static void emit_node(struct impl *this, struct spa_bt_transport *t, { struct spa_bt_device *device = this->bt_dev; struct spa_device_object_info info; - struct spa_dict_item items[11]; + struct spa_dict_item items[13]; uint32_t n_items = 0; char transport[32], str_id[32], object_path[512]; bool is_dyn_node = SPA_FLAG_IS_SET(id, DYNAMIC_NODE_ID_FLAG); @@ -960,7 +928,77 @@ static const struct spa_bt_transport_events device_set_transport_events = { .destroy = device_set_transport_destroy, }; -static void device_set_update(struct impl *this, struct device_set *dset) +static void device_set_update_asha(struct impl *this, struct device_set *dset) +{ + struct spa_bt_device *device = this->bt_dev; + struct spa_bt_set_membership *set; + struct spa_bt_set_membership tmp_set = { + .device = device, + .rank = 0, + .leader = true, + .path = device->path, + .others = SPA_LIST_INIT(&tmp_set.others), + }; + struct spa_list tmp_set_list = SPA_LIST_INIT(&tmp_set_list); + struct spa_list *membership_list = &device->set_membership_list; + + /* + * If no device set, use a dummy one, so that we can handle also those devices + * here (they may have multiple transports regardless). + */ + if (spa_list_is_empty(membership_list)) { + spa_list_append(&tmp_set_list, &tmp_set.link); + membership_list = &tmp_set_list; + } + + spa_list_for_each(set, membership_list, link) { + struct spa_bt_set_membership *s; + int num_devices = 0; + + device_set_clear(this, dset); + + spa_bt_for_each_set_member(s, set) { + struct spa_bt_transport *t; + bool active = false; + uint32_t sink_id = DEVICE_ID_SINK; + + if (!(s->device->connected_profiles & SPA_BT_PROFILE_ASHA_SINK)) + continue; + + spa_list_for_each(t, &s->device->transport_list, device_link) { + if (!transport_enabled(t, SPA_BT_PROFILE_ASHA_SINK)) + continue; + if (dset->sinks >= SPA_N_ELEMENTS(dset->sink)) + break; + + active = true; + dset->leader = set->leader = t->asha_right_side; + dset->path = strdup(set->path); + dset->sink[dset->sinks].impl = this; + dset->sink[dset->sinks].transport = t; + dset->sink[dset->sinks].id = sink_id; + sink_id += 2; + spa_bt_transport_add_listener(t, &dset->sink[dset->sinks].listener, + &device_set_transport_events, &dset->sink[dset->sinks]); + ++dset->sinks; + } + + if (active) + ++num_devices; + } + + if (dset == &this->device_set) + spa_log_debug(this->log, "%p: %s belongs to ASHA set %s leader:%d", this, + device->path, set->path, set->leader); + + if (num_devices > 1) + break; + } + + dset->sink_enabled = dset->path && (dset->sinks > 1); +} + +static void device_set_update_bap(struct impl *this, struct device_set *dset) { struct spa_bt_device *device = this->bt_dev; struct spa_bt_set_membership *set; @@ -1038,8 +1076,9 @@ static void device_set_update(struct impl *this, struct device_set *dset) ++num_devices; } - spa_log_debug(this->log, "%p: %s belongs to set %s leader:%d", this, - device->path, set->path, set->leader); + if (dset == &this->device_set) + spa_log_debug(this->log, "%p: %s belongs to set %s leader:%d", this, + device->path, set->path, set->leader); if (is_bap_client(this)) { dset->path = strdup(set->path); @@ -1060,6 +1099,50 @@ static void device_set_update(struct impl *this, struct device_set *dset) dset->source_enabled = dset->path && (dset->sources > 1); } +static void device_set_update(struct impl *this, struct device_set *dset, int profile) +{ + if (profile_is_bap(this->profile)) + device_set_update_bap(this, dset); + else if (profile == DEVICE_PROFILE_ASHA) + device_set_update_asha(this, dset); + else + device_set_clear(this, dset); +} + +static void device_set_get_dset_info(const struct device_set *dset, + int *n_set_sink, int *n_set_source) +{ + if (dset->sink_enabled) + *n_set_sink = dset->leader ? 1 : 0; + if (dset->source_enabled) + *n_set_source = dset->leader ? 1 : 0; +} + +static void device_set_get_info(struct impl *this, uint32_t profile, + int *n_set_sink, int *n_set_source) +{ + struct device_set dset = { .impl = this }; + + *n_set_sink = -1; + *n_set_source = -1; + + if (profile == this->profile) { + device_set_get_dset_info(&this->device_set, n_set_sink, n_set_source); + } else if (profile != SPA_ID_INVALID) { + device_set_update(this, &dset, profile); + device_set_get_dset_info(&dset, n_set_sink, n_set_source); + device_set_clear(this, &dset); + } else { + device_set_update(this, &dset, DEVICE_PROFILE_BAP); + device_set_get_dset_info(&dset, n_set_sink, n_set_source); + device_set_clear(this, &dset); + + device_set_update(this, &dset, DEVICE_PROFILE_ASHA); + device_set_get_dset_info(&dset, n_set_sink, n_set_source); + device_set_clear(this, &dset); + } +} + static bool device_set_equal(struct device_set *a, struct device_set *b) { unsigned int i; @@ -1081,9 +1164,18 @@ static int emit_nodes(struct impl *this) { struct spa_bt_transport *t; + switch (this->profile) { + case DEVICE_PROFILE_BAP: + case DEVICE_PROFILE_BAP_SINK: + case DEVICE_PROFILE_BAP_SOURCE: + if (this->switching_codec_other) + return -EBUSY; + break; + } + this->props.codec = 0; - device_set_update(this, &this->device_set); + device_set_update(this, &this->device_set, this->profile); switch (this->profile) { case DEVICE_PROFILE_OFF: @@ -1094,7 +1186,7 @@ static int emit_nodes(struct impl *this) if (!t) t = find_transport(this, SPA_BT_PROFILE_HSP_AG); if (t) { - this->props.codec = get_hfp_codec_id(t->codec); + this->props.codec = t->media_codec->id; emit_dynamic_node(this, t, 0, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false); emit_dynamic_node(this, t, 1, SPA_NAME_API_BLUEZ5_SCO_SINK, false); } @@ -1112,10 +1204,13 @@ static int emit_nodes(struct impl *this) break; case DEVICE_PROFILE_ASHA: if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_ASHA_SINK) { + struct device_set *set = &this->device_set; t = find_transport(this, SPA_BT_PROFILE_ASHA_SINK); if (t) { this->props.codec = t->media_codec->id; emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_MEDIA_SINK, false); + if (set->sink_enabled && set->leader) + emit_device_set_node(this, DEVICE_ID_SINK_SET); } else { spa_log_warn(this->log, "Unable to find transport for ASHA"); } @@ -1156,7 +1251,10 @@ static int emit_nodes(struct impl *this) if (!this->props.codec) this->props.codec = SPA_BLUETOOTH_AUDIO_CODEC_SBC; break; - case DEVICE_PROFILE_BAP: { + case DEVICE_PROFILE_BAP: + case DEVICE_PROFILE_BAP_SINK: + case DEVICE_PROFILE_BAP_SOURCE: + { struct device_set *set = &this->device_set; unsigned int i; @@ -1224,7 +1322,7 @@ static int emit_nodes(struct impl *this) if (!t) t = find_transport(this, SPA_BT_PROFILE_HSP_HS); if (t) { - this->props.codec = get_hfp_codec_id(t->codec); + this->props.codec = t->media_codec->id; emit_node(this, t, DEVICE_ID_SOURCE, SPA_NAME_API_BLUEZ5_SCO_SOURCE, false); emit_node(this, t, DEVICE_ID_SINK, SPA_NAME_API_BLUEZ5_SCO_SINK, false); } @@ -1299,7 +1397,7 @@ static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_a if (this->profile == profile && (this->profile != DEVICE_PROFILE_ASHA || codec == this->props.codec) && (this->profile != DEVICE_PROFILE_A2DP || codec == this->props.codec) && - (this->profile != DEVICE_PROFILE_BAP || codec == this->props.codec) && + (!profile_is_bap(this->profile) || codec == this->props.codec) && (this->profile != DEVICE_PROFILE_HSP_HFP || codec == this->props.codec)) return 0; @@ -1321,28 +1419,49 @@ static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_a * XXX: source-only case, as it will only switch the sink, and we only * XXX: list the sink codecs here. TODO: fix this */ - if ((profile == DEVICE_PROFILE_A2DP || (profile == DEVICE_PROFILE_BAP && can_bap_codec_switch(this))) + if ((profile == DEVICE_PROFILE_A2DP || (profile_is_bap(profile) && is_bap_client(this))) && !(this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE)) { int ret; const struct media_codec *codecs[64]; + uint32_t profiles; get_media_codecs(this, codec, codecs, SPA_N_ELEMENTS(codecs)); this->switching_codec = true; - ret = spa_bt_device_ensure_media_codec(this->bt_dev, codecs); + switch (profile) { + case DEVICE_PROFILE_BAP_SINK: + profiles = SPA_BT_PROFILE_BAP_SINK; + break; + case DEVICE_PROFILE_BAP_SOURCE: + profiles = SPA_BT_PROFILE_BAP_SOURCE; + break; + case DEVICE_PROFILE_BAP: + profiles = this->bt_dev->profiles & SPA_BT_PROFILE_BAP_DUPLEX; + break; + case DEVICE_PROFILE_A2DP: + profiles = this->bt_dev->profiles & SPA_BT_PROFILE_A2DP_DUPLEX; + break; + default: + profiles = 0; + break; + } + + ret = spa_bt_device_ensure_media_codec(this->bt_dev, codecs, profiles); if (ret < 0) { if (ret != -ENOTSUP) spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret); } else { return 0; } - } else if (profile == DEVICE_PROFILE_HSP_HFP && get_hfp_codec(codec)) { + } else if (profile == DEVICE_PROFILE_HSP_HFP) { int ret; + const struct media_codec *media_codec = get_supported_media_codec(this, codec, NULL, + SPA_BT_PROFILE_HEADSET_AUDIO); this->switching_codec = true; - ret = spa_bt_device_ensure_hfp_codec(this->bt_dev, get_hfp_codec(codec)); + ret = spa_bt_device_ensure_hfp_codec(this->bt_dev, media_codec); if (ret < 0) { if (ret != -ENOTSUP) spa_log_error(this->log, "failed to switch codec (%d), setting basic profile", ret); @@ -1352,6 +1471,7 @@ static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_a } this->switching_codec = false; + emit_nodes(this); this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; @@ -1380,8 +1500,13 @@ static void codec_switched(void *userdata, int status) emit_nodes(this); this->info.change_mask |= SPA_DEVICE_CHANGE_MASK_PARAMS; - if (this->prev_bt_connected_profiles != this->bt_dev->connected_profiles) + if ((this->prev_bt_connected_profiles ^ this->bt_dev->connected_profiles) + & ~SPA_BT_PROFILE_BAP_DUPLEX) { + spa_log_debug(this->log, "profiles changed %x -> %x", + this->prev_bt_connected_profiles, + this->bt_dev->connected_profiles); this->params[IDX_EnumProfile].flags ^= SPA_PARAM_INFO_SERIAL; + } this->params[IDX_Profile].flags ^= SPA_PARAM_INFO_SERIAL; this->params[IDX_Route].flags ^= SPA_PARAM_INFO_SERIAL; this->params[IDX_EnumRoute].flags ^= SPA_PARAM_INFO_SERIAL; @@ -1390,15 +1515,53 @@ static void codec_switched(void *userdata, int status) emit_info(this, false); } +static void codec_switch_other(void *userdata, bool switching) +{ + struct impl *this = userdata; + + this->switching_codec_other = switching; + + switch (this->profile) { + case DEVICE_PROFILE_BAP: + case DEVICE_PROFILE_BAP_SINK: + case DEVICE_PROFILE_BAP_SOURCE: + break; + default: + return; + } + + spa_log_debug(this->log, "%p: BAP codec switching by another device, switching:%d", + this, (int)switching); + + /* + * In unicast BAP, output/input must be halted when another device is + * switching codec, because CIG must be torn down before it can be + * reconfigured. Easiest way to do this and to suspend output/input is to + * remove the nodes. + */ + if (!find_device_transport(this->bt_dev, SPA_BT_PROFILE_BAP_SINK) && + !find_device_transport(this->bt_dev, SPA_BT_PROFILE_BAP_SOURCE)) + return; + + if (switching) { + emit_remove_nodes(this); + spa_bt_device_release_transports(this->bt_dev); + } else { + emit_remove_nodes(this); + emit_nodes(this); + } +} + static bool device_set_needs_update(struct impl *this) { struct device_set dset = { .impl = this }; bool changed; - if (this->profile != DEVICE_PROFILE_BAP) + if (!profile_is_bap(this->profile) && + this->profile != DEVICE_PROFILE_ASHA) return false; - device_set_update(this, &dset); + device_set_update(this, &dset, this->profile); changed = !device_set_equal(&dset, &this->device_set); device_set_clear(this, &dset); return changed; @@ -1443,6 +1606,8 @@ static void profiles_changed(void *userdata, uint32_t connected_change) nodes_changed); break; case DEVICE_PROFILE_BAP: + case DEVICE_PROFILE_BAP_SINK: + case DEVICE_PROFILE_BAP_SOURCE: nodes_changed = ((connected_change & SPA_BT_PROFILE_BAP_DUPLEX) && device_set_needs_update(this)) || (connected_change & (SPA_BT_PROFILE_BAP_BROADCAST_SINK | @@ -1476,7 +1641,11 @@ static void device_set_changed(void *userdata) { struct impl *this = userdata; - if (this->profile != DEVICE_PROFILE_BAP) + if (!profile_is_bap(this->profile) && + this->profile != DEVICE_PROFILE_ASHA) + return; + + if (this->switching_codec) return; if (!device_set_needs_update(this)) { @@ -1536,6 +1705,7 @@ static const struct spa_bt_device_events bt_dev_events = { SPA_VERSION_BT_DEVICE_EVENTS, .connected = device_connected, .codec_switched = codec_switched, + .codec_switch_other = codec_switch_other, .profiles_changed = profiles_changed, .device_set_changed = device_set_changed, .switch_profile = device_switch_profile, @@ -1596,11 +1766,17 @@ static uint32_t profile_direction_mask(struct impl *this, uint32_t index, enum s have_input = true; break; case DEVICE_PROFILE_BAP: - if (device->connected_profiles & SPA_BT_PROFILE_BAP_SINK) + if (device->profiles & SPA_BT_PROFILE_BAP_SINK) have_output = true; - if (device->connected_profiles & SPA_BT_PROFILE_BAP_SOURCE) + if (device->profiles & SPA_BT_PROFILE_BAP_SOURCE) have_input = true; break; + case DEVICE_PROFILE_BAP_SINK: + have_output = true; + break; + case DEVICE_PROFILE_BAP_SOURCE: + have_input = true; + break; case DEVICE_PROFILE_HSP_HFP: if (device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) have_output = have_input = true; @@ -1639,6 +1815,8 @@ static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32 case DEVICE_PROFILE_A2DP: case DEVICE_PROFILE_HSP_HFP: case DEVICE_PROFILE_BAP: + case DEVICE_PROFILE_BAP_SINK: + case DEVICE_PROFILE_BAP_SOURCE: *codec = (index & 0xffff); *next = (profile + 1) << 16; @@ -1668,6 +1846,8 @@ static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum case DEVICE_PROFILE_A2DP: case DEVICE_PROFILE_BAP: + case DEVICE_PROFILE_BAP_SINK: + case DEVICE_PROFILE_BAP_SOURCE: case DEVICE_PROFILE_HSP_HFP: if (!codec) return SPA_ID_INVALID; @@ -1709,7 +1889,7 @@ static bool set_initial_hsp_hfp_profile(struct impl *this) if (t) { this->profile = (i & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) ? DEVICE_PROFILE_AG : DEVICE_PROFILE_HSP_HFP; - this->props.codec = get_hfp_codec_id(t->codec); + this->props.codec = t->media_codec->id; spa_log_debug(this->log, "initial profile HSP/HFP profile:%d codec:%d", this->profile, this->props.codec); @@ -1814,7 +1994,7 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * */ if ((device->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) && (device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)) - priority = 15; + priority = 127; else priority = 256; break; @@ -1822,6 +2002,7 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * case DEVICE_PROFILE_ASHA: { uint32_t profile = device->connected_profiles & SPA_BT_PROFILE_ASHA_SINK; + int n_set_sink, n_set_source; if (codec == 0) return NULL; @@ -1837,6 +2018,9 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * n_sink++; priority = 1; + device_set_get_info(this, DEVICE_PROFILE_ASHA, &n_set_sink, &n_set_source); + if (n_set_sink >= 0) + n_sink = n_set_sink; break; } case DEVICE_PROFILE_A2DP: @@ -1855,8 +2039,8 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * name = spa_bt_profile_name(profile); n_sink++; if (codec) { - size_t idx; - const struct media_codec *media_codec = get_supported_media_codec(this, codec, &idx, profile); + int prio; + const struct media_codec *media_codec = get_supported_media_codec(this, codec, &prio, profile); if (media_codec == NULL) { errno = EINVAL; return NULL; @@ -1868,7 +2052,7 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * * selected at command line with out knowing which codecs are actually * supported */ - if (idx != 0) + if (prio != 0) name = name_and_codec; if (profile == SPA_BT_PROFILE_A2DP_SINK && !media_codec->duplex_codec) { @@ -1880,30 +2064,52 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * } desc = desc_and_codec; - priority = 16 + this->supported_codec_count - idx; /* order as in codec list */ + priority = 128 + this->supported_codec_count - prio; /* order as in codec list */ } else { if (profile == SPA_BT_PROFILE_A2DP_SINK) { desc = _("High Fidelity Playback (A2DP Sink)"); } else { desc = _("High Fidelity Duplex (A2DP Source/Sink)"); } - priority = 16; + priority = 128; } break; } + case DEVICE_PROFILE_BAP_SINK: + case DEVICE_PROFILE_BAP_SOURCE: + /* These are client-only */ + if (!is_bap_client(this)) + return NULL; + SPA_FALLTHROUGH; case DEVICE_PROFILE_BAP: { - uint32_t profile = device->connected_profiles & - (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE - | SPA_BT_PROFILE_BAP_BROADCAST_SOURCE - | SPA_BT_PROFILE_BAP_BROADCAST_SINK); - size_t idx; + uint32_t profile; const struct media_codec *media_codec; + int n_set_sink, n_set_source; /* BAP will only enlist codec profiles */ if (codec == 0) return NULL; + switch (profile_index) { + case DEVICE_PROFILE_BAP: + profile = device->profiles & + (SPA_BT_PROFILE_BAP_SINK | SPA_BT_PROFILE_BAP_SOURCE + | SPA_BT_PROFILE_BAP_BROADCAST_SOURCE + | SPA_BT_PROFILE_BAP_BROADCAST_SINK); + break; + case DEVICE_PROFILE_BAP_SINK: + if (!(device->profiles & SPA_BT_PROFILE_BAP_SOURCE)) + return NULL; + profile = device->profiles & SPA_BT_PROFILE_BAP_SINK; + break; + case DEVICE_PROFILE_BAP_SOURCE: + if (!(device->profiles & SPA_BT_PROFILE_BAP_SINK)) + return NULL; + profile = device->profiles & SPA_BT_PROFILE_BAP_SOURCE; + break; + } + if (profile == 0) return NULL; @@ -1917,6 +2123,8 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * name = spa_bt_profile_name(profile); if (codec) { + int idx; + media_codec = get_supported_media_codec(this, codec, &idx, profile); if (media_codec == NULL) { errno = EINVAL; @@ -1948,7 +2156,7 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * media_codec->description); } desc = desc_and_codec; - priority = 128 + this->supported_codec_count - idx; /* order as in codec list */ + priority = 512 + this->supported_codec_count - idx; /* order as in codec list */ } else { switch (profile) { case SPA_BT_PROFILE_BAP_SINK: @@ -1962,54 +2170,48 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * default: desc = _("High Fidelity Duplex (BAP Source/Sink)"); } - priority = 128; + priority = 512; } - if (this->device_set.sink_enabled) - n_sink = this->device_set.leader ? 1 : 0; - if (this->device_set.source_enabled) - n_source = this->device_set.leader ? 1 : 0; + device_set_get_info(this, DEVICE_PROFILE_BAP, &n_set_sink, &n_set_source); + if (n_set_sink >= 0) + n_sink = n_set_sink; + if (n_set_source >= 0) + n_source = n_set_source; break; } case DEVICE_PROFILE_HSP_HFP: { - /* make this device profile visible only if there is a head unit */ uint32_t profile = device->connected_profiles & - SPA_BT_PROFILE_HEADSET_HEAD_UNIT; - unsigned int hfp_codec = get_hfp_codec(codec); - unsigned int idx; + SPA_BT_PROFILE_HEADSET_HEAD_UNIT; + int prio; + const struct media_codec *media_codec = get_supported_media_codec(this, codec, &prio, profile); - if (profile == 0) + if (!profile) return NULL; - /* HFP will only enlist codec profiles */ - if (codec == 0) - return NULL; - if (codec != SPA_BLUETOOTH_AUDIO_CODEC_CVSD && - spa_bt_device_supports_hfp_codec(this->bt_dev, hfp_codec) != 1) + /* Only list codec profiles */ + if (!codec || !media_codec) return NULL; name = spa_bt_profile_name(profile); n_source++; n_sink++; - name_and_codec = spa_aprintf("%s-%s", name, get_hfp_codec_name(hfp_codec)); + name_and_codec = spa_aprintf("%s-%s", name, media_codec->name); /* * Give base name to highest priority profile, so that best codec can be * selected at command line with out knowing which codecs are actually * supported */ - for (idx = HFP_AUDIO_CODEC_LC3_SWB; idx > 0; --idx) - if (spa_bt_device_supports_hfp_codec(this->bt_dev, idx) == 1) - break; - if (hfp_codec < idx) + if (prio != 0) name = name_and_codec; desc_and_codec = spa_aprintf(_("Headset Head Unit (HSP/HFP, codec %s)"), - get_hfp_codec_description(hfp_codec)); + media_codec->description); desc = desc_and_codec; - priority = 1 + hfp_codec; /* prefer lc3_swb > msbc > cvsd */ + priority = 1 + this->supported_codec_count - prio; break; } default: @@ -2096,6 +2298,20 @@ static bool profile_has_route(uint32_t profile, uint32_t route) return true; } break; + case DEVICE_PROFILE_BAP_SINK: + switch (route) { + case ROUTE_OUTPUT: + case ROUTE_SET_OUTPUT: + return true; + } + break; + case DEVICE_PROFILE_BAP_SOURCE: + switch (route) { + case ROUTE_INPUT: + case ROUTE_SET_INPUT: + return true; + } + break; case DEVICE_PROFILE_ASHA: switch (route) { case ROUTE_OUTPUT: @@ -2106,6 +2322,22 @@ static bool profile_has_route(uint32_t profile, uint32_t route) return false; } +static bool device_has_route(struct impl *this, uint32_t route) +{ + bool found = false; + + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_DUPLEX) + found = found || profile_has_route(DEVICE_PROFILE_A2DP, route); + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_BAP_AUDIO) + found = found || profile_has_route(DEVICE_PROFILE_BAP, route); + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + found = found || profile_has_route(DEVICE_PROFILE_HSP_HFP, route); + if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_ASHA_SINK) + found = found || profile_has_route(DEVICE_PROFILE_ASHA, route); + + return found; +} + static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, uint32_t id, uint32_t route, uint32_t profile) { @@ -2113,12 +2345,14 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, struct spa_pod_frame f[2]; enum spa_direction direction; const char *name_prefix, *description, *hfp_description, *port_type; + const char *port_icon_name = NULL; enum spa_bt_form_factor ff; enum spa_bluetooth_audio_codec codec; enum spa_param_availability available; char name[128]; uint32_t i, j, mask, next; uint32_t dev; + int n_set_sink, n_set_source; ff = spa_bt_form_factor_from_class(device->bluetooth_class); @@ -2149,7 +2383,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, break; case SPA_BT_FORM_FACTOR_HEADPHONE: name_prefix = "headphone"; - description = _("Headphone"); + description = _("Headphones"); hfp_description = _("Handsfree"); port_type = "headphones"; break; @@ -2186,12 +2420,14 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, break; } + device_set_get_info(this, profile, &n_set_sink, &n_set_source); + switch (route) { case ROUTE_INPUT: direction = SPA_DIRECTION_INPUT; snprintf(name, sizeof(name), "%s-input", name_prefix); dev = DEVICE_ID_SOURCE; - available = this->device_set.source_enabled ? + available = (n_set_source >= 0) ? SPA_PARAM_AVAILABILITY_no : SPA_PARAM_AVAILABILITY_yes; if ((this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) && @@ -2204,8 +2440,22 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, direction = SPA_DIRECTION_OUTPUT; snprintf(name, sizeof(name), "%s-output", name_prefix); dev = DEVICE_ID_SINK; - available = this->device_set.sink_enabled ? + available = (n_set_sink >= 0) ? SPA_PARAM_AVAILABILITY_no : SPA_PARAM_AVAILABILITY_yes; + + if (device_has_route(this, ROUTE_HF_OUTPUT)) { + /* Distinguish A2DP vs. HFP output routes */ + switch (ff) { + case SPA_BT_FORM_FACTOR_HEADSET: + case SPA_BT_FORM_FACTOR_HANDSFREE: + port_icon_name = spa_bt_form_factor_icon_name(SPA_BT_FORM_FACTOR_HEADPHONE); + /* Don't call it "headset", the HF one has the mic */ + description = _("Headphones"); + break; + default: + break; + } + } break; case ROUTE_HF_OUTPUT: direction = SPA_DIRECTION_OUTPUT; @@ -2213,9 +2463,11 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, description = hfp_description; dev = DEVICE_ID_SINK; available = SPA_PARAM_AVAILABILITY_yes; + if (device_has_route(this, ROUTE_OUTPUT)) + port_icon_name = spa_bt_form_factor_icon_name(SPA_BT_FORM_FACTOR_HEADSET); break; case ROUTE_SET_INPUT: - if (!(this->device_set.source_enabled && this->device_set.leader)) + if (n_set_source < 1) return NULL; direction = SPA_DIRECTION_INPUT; snprintf(name, sizeof(name), "%s-set-input", name_prefix); @@ -2223,7 +2475,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, available = SPA_PARAM_AVAILABILITY_yes; break; case ROUTE_SET_OUTPUT: - if (!(this->device_set.sink_enabled && this->device_set.leader)) + if (n_set_sink < 1) return NULL; direction = SPA_DIRECTION_OUTPUT; snprintf(name, sizeof(name), "%s-set-output", name_prefix); @@ -2248,11 +2500,16 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, 0); spa_pod_builder_prop(b, SPA_PARAM_ROUTE_info, 0); spa_pod_builder_push_struct(b, &f[1]); - spa_pod_builder_int(b, 1); + spa_pod_builder_int(b, port_icon_name ? 2 : 1); spa_pod_builder_add(b, SPA_POD_String("port.type"), SPA_POD_String(port_type), NULL); + if (port_icon_name) + spa_pod_builder_add(b, + SPA_POD_String("device.icon-name"), + SPA_POD_String(port_icon_name), + NULL); spa_pod_builder_pop(b, &f[1]); spa_pod_builder_prop(b, SPA_PARAM_ROUTE_profiles, 0); spa_pod_builder_push_array(b, &f[1]); @@ -2317,7 +2574,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, spa_pod_builder_array(b, sizeof(uint32_t), SPA_TYPE_Id, node->n_channels, node->channels); - if ((this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) && + if ((this->profile == DEVICE_PROFILE_A2DP || profile_is_bap(this->profile)) && (dev & SINK_ID_FLAG)) { spa_pod_builder_prop(b, SPA_PROP_latencyOffsetNsec, 0); spa_pod_builder_long(b, node->latency_offset); @@ -2343,6 +2600,7 @@ static struct spa_pod *build_route(struct impl *this, struct spa_pod_builder *b, static bool iterate_supported_media_codecs(struct impl *this, int *j, const struct media_codec **codec) { int i; + const struct media_codec *c; next: *j = *j + 1; @@ -2350,11 +2608,20 @@ next: if ((size_t)*j >= this->supported_codec_count) return false; - for (i = 0; i < *j; ++i) - if (this->supported_codecs[i]->id == this->supported_codecs[*j]->id) - goto next; + c = this->supported_codecs[*j]; - *codec = this->supported_codecs[*j]; + if (!(this->profile == DEVICE_PROFILE_A2DP && c->kind == MEDIA_CODEC_A2DP) && + !(profile_is_bap(this->profile) && c->kind == MEDIA_CODEC_BAP) && + !(this->profile == DEVICE_PROFILE_HSP_HFP && c->kind == MEDIA_CODEC_HFP) && + !(this->profile == DEVICE_PROFILE_ASHA && c->kind == MEDIA_CODEC_ASHA)) + goto next; + + /* skip endpoint aliases */ + for (i = 0; i < *j; ++i) + if (this->supported_codecs[i]->id == c->id) + goto next; + + *codec = c; return true; } @@ -2368,9 +2635,6 @@ static struct spa_pod *build_prop_info_codec(struct impl *this, struct spa_pod_b #define FOR_EACH_MEDIA_CODEC(j, codec) \ for (j = -1; iterate_supported_media_codecs(this, &j, &codec);) -#define FOR_EACH_HFP_CODEC(j) \ - for (j = HFP_AUDIO_CODEC_LC3_SWB; j >= HFP_AUDIO_CODEC_CVSD; --j) \ - if (spa_bt_device_supports_hfp_codec(this->bt_dev, j) == 1) spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); @@ -2388,42 +2652,25 @@ static struct spa_pod *build_prop_info_codec(struct impl *this, struct spa_pod_b spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); choice = (struct spa_pod_choice *)spa_pod_builder_frame(b, &f[1]); n = 0; - if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP) { - FOR_EACH_MEDIA_CODEC(j, codec) { - if (n == 0) - spa_pod_builder_int(b, codec->id); + FOR_EACH_MEDIA_CODEC(j, codec) { + if (n == 0) spa_pod_builder_int(b, codec->id); - ++n; - } - } else if (this->profile == DEVICE_PROFILE_HSP_HFP) { - FOR_EACH_HFP_CODEC(j) { - if (n == 0) - spa_pod_builder_int(b, get_hfp_codec_id(j)); - spa_pod_builder_int(b, get_hfp_codec_id(j)); - ++n; - } + spa_pod_builder_int(b, codec->id); + ++n; } if (n == 0) choice->body.type = SPA_CHOICE_None; spa_pod_builder_pop(b, &f[1]); spa_pod_builder_prop(b, SPA_PROP_INFO_labels, 0); spa_pod_builder_push_struct(b, &f[1]); - if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP || this->profile == DEVICE_PROFILE_ASHA) { - FOR_EACH_MEDIA_CODEC(j, codec) { - spa_pod_builder_int(b, codec->id); - spa_pod_builder_string(b, codec->description); - } - } else if (this->profile == DEVICE_PROFILE_HSP_HFP) { - FOR_EACH_HFP_CODEC(j) { - spa_pod_builder_int(b, get_hfp_codec_id(j)); - spa_pod_builder_string(b, get_hfp_codec_description(j)); - } + FOR_EACH_MEDIA_CODEC(j, codec) { + spa_pod_builder_int(b, codec->id); + spa_pod_builder_string(b, codec->description); } spa_pod_builder_pop(b, &f[1]); return spa_pod_builder_pop(b, &f[0]); #undef FOR_EACH_MEDIA_CODEC -#undef FOR_EACH_HFP_CODEC } static struct spa_pod *build_props(struct impl *this, struct spa_pod_builder *b, uint32_t id) @@ -2562,6 +2809,7 @@ static void device_set_update_volumes(struct node *node) struct device_set *dset = &impl->device_set; float hw_volume = node_get_hw_volume(node); bool sink = (node->id == DEVICE_ID_SINK_SET); + int volume_id = get_volume_id(node->id); struct device_set_member *members = sink ? dset->sink : dset->source; uint32_t n_members = sink ? dset->sinks : dset->sources; uint32_t i; @@ -2572,7 +2820,7 @@ static void device_set_update_volumes(struct node *node) for (i = 0; i < n_members; ++i) { struct spa_bt_transport *t = members[i].transport; - struct spa_bt_transport_volume *t_volume = t ? &t->volumes[members[i].id] : NULL; + struct spa_bt_transport_volume *t_volume = t ? &t->volumes[volume_id] : NULL; if (!t_volume || !t_volume->active) goto soft_volume; @@ -2580,13 +2828,13 @@ static void device_set_update_volumes(struct node *node) node_update_soft_volumes(node, hw_volume); for (i = 0; i < n_members; ++i) - spa_bt_transport_set_volume(members[i].transport, members[i].id, hw_volume); + spa_bt_transport_set_volume(members[i].transport, volume_id, hw_volume); return; soft_volume: /* Soft volume fallback */ for (i = 0; i < n_members; ++i) - spa_bt_transport_set_volume(members[i].transport, members[i].id, 1.0f); + spa_bt_transport_set_volume(members[i].transport, volume_id, 1.0f); node_update_soft_volumes(node, 1.0f); return; } @@ -2596,6 +2844,7 @@ static int node_set_volume(struct impl *this, struct node *node, float volumes[] uint32_t i; int changed = 0; struct spa_bt_transport_volume *t_volume; + int volume_id = get_volume_id(node->id); if (n_volumes == 0) return -EINVAL; @@ -2609,7 +2858,7 @@ static int node_set_volume(struct impl *this, struct node *node, float volumes[] node->volumes[i] = volumes[i % n_volumes]; } - t_volume = node->transport ? &node->transport->volumes[node->id]: NULL; + t_volume = node->transport ? &node->transport->volumes[volume_id]: NULL; if (t_volume && t_volume->active && spa_bt_transport_volume_enabled(node->transport)) { @@ -2617,7 +2866,7 @@ static int node_set_volume(struct impl *this, struct node *node, float volumes[] spa_log_debug(this->log, "node %d hardware volume %f", node->id, hw_volume); node_update_soft_volumes(node, hw_volume); - spa_bt_transport_set_volume(node->transport, node->id, hw_volume); + spa_bt_transport_set_volume(node->transport, volume_id, hw_volume); } else if (node->id == DEVICE_ID_SOURCE_SET || node->id == DEVICE_ID_SINK_SET) { device_set_update_volumes(node); } else { @@ -2699,8 +2948,8 @@ static int apply_device_props(struct impl *this, struct node *node, struct spa_p struct spa_pod_prop *prop; struct spa_pod_object *obj = (struct spa_pod_object *) props; int changed = 0; - float volumes[SPA_AUDIO_MAX_CHANNELS]; - uint32_t channels[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; + uint32_t channels[MAX_CHANNELS]; uint32_t n_volumes = 0, SPA_UNUSED n_channels = 0; int64_t latency_offset = 0; @@ -2725,11 +2974,11 @@ static int apply_device_props(struct impl *this, struct node *node, struct spa_p break; case SPA_PROP_channelVolumes: n_volumes = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - volumes, SPA_AUDIO_MAX_CHANNELS); + volumes, SPA_N_ELEMENTS(volumes)); break; case SPA_PROP_channelMap: n_channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, - channels, SPA_AUDIO_MAX_CHANNELS); + channels, SPA_N_ELEMENTS(channels)); break; case SPA_PROP_latencyOffsetNsec: if (spa_pod_get_long(&prop->value, &latency_offset) == 0) { @@ -2864,24 +3113,14 @@ static int impl_set_param(void *object, if (codec_id == SPA_ID_INVALID) return 0; - if (this->profile == DEVICE_PROFILE_A2DP || this->profile == DEVICE_PROFILE_BAP || this->profile == DEVICE_PROFILE_ASHA) { + if (this->profile == DEVICE_PROFILE_A2DP || profile_is_bap(this->profile) || + this->profile == DEVICE_PROFILE_ASHA || this->profile == DEVICE_PROFILE_HSP_HFP) { size_t j; for (j = 0; j < this->supported_codec_count; ++j) { if (this->supported_codecs[j]->id == codec_id) { return set_profile(this, this->profile, codec_id, true); } } - } else if (this->profile == DEVICE_PROFILE_HSP_HFP) { - if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_CVSD && - spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_CVSD) == 1) { - return set_profile(this, this->profile, codec_id, true); - } else if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_MSBC && - spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_MSBC) == 1) { - return set_profile(this, this->profile, codec_id, true); - } else if (codec_id == SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB && - spa_bt_device_supports_hfp_codec(this->bt_dev, HFP_AUDIO_CODEC_LC3_SWB) == 1) { - return set_profile(this, this->profile, codec_id, true); - } } return -EINVAL; } diff --git a/spa/plugins/bluez5/bt-latency.h b/spa/plugins/bluez5/bt-latency.h index 6fba869aa..d8ed24571 100644 --- a/spa/plugins/bluez5/bt-latency.h +++ b/spa/plugins/bluez5/bt-latency.h @@ -5,6 +5,7 @@ #ifndef SPA_BLUEZ5_BT_LATENCY_H #define SPA_BLUEZ5_BT_LATENCY_H +#include #include #include #include @@ -13,15 +14,16 @@ #include #include +#include "defs.h" #include "rate-control.h" /* New kernel API */ #ifndef BT_SCM_ERROR #define BT_SCM_ERROR 0x04 #endif -#ifndef BT_POLL_ERRQUEUE -#define BT_POLL_ERRQUEUE 21 -#endif + +#define NEW_SOF_TIMESTAMPING_TX_COMPLETION (1 << 18) +#define NEW_SCM_TSTAMP_COMPLETION (SCM_TSTAMP_ACK + 1) /** * Bluetooth latency tracking. @@ -31,46 +33,45 @@ struct spa_bt_latency uint64_t value; struct spa_bt_ptp ptp; bool valid; - bool disabled; + bool enabled; + uint32_t queue; + uint32_t kernel_queue; + size_t unsent; struct { int64_t send[64]; + size_t size[64]; uint32_t pos; int64_t prev_tx; } impl; }; -static inline void spa_bt_latency_init(struct spa_bt_latency *lat, int fd, +static inline void spa_bt_latency_init(struct spa_bt_latency *lat, struct spa_bt_transport *transport, uint32_t period, struct spa_log *log) { - int so_timestamping = (SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_SOFTWARE | - SOF_TIMESTAMPING_OPT_ID | SOF_TIMESTAMPING_OPT_TSONLY); - uint32_t flag; + int so_timestamping = (NEW_SOF_TIMESTAMPING_TX_COMPLETION | SOF_TIMESTAMPING_TX_SOFTWARE | + SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_OPT_ID | SOF_TIMESTAMPING_OPT_TSONLY); int res; spa_zero(*lat); - flag = 0; - res = setsockopt(fd, SOL_BLUETOOTH, BT_POLL_ERRQUEUE, &flag, sizeof(flag)); - if (res < 0) { - spa_log_warn(log, "setsockopt(BT_POLL_ERRQUEUE) failed (kernel feature not enabled?): %d (%m)", errno); - lat->disabled = true; + if (!transport->device->adapter->tx_timestamping_supported) return; - } - res = setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &so_timestamping, sizeof(so_timestamping)); + res = setsockopt(transport->fd, SOL_SOCKET, SO_TIMESTAMPING, &so_timestamping, sizeof(so_timestamping)); if (res < 0) { - spa_log_warn(log, "setsockopt(SO_TIMESTAMPING) failed (kernel feature not enabled?): %d (%m)", errno); - lat->disabled = true; + spa_log_info(log, "setsockopt(SO_TIMESTAMPING) failed (kernel feature not enabled?): %d (%m)", errno); return; } /* Flush errqueue on start */ do { - res = recv(fd, NULL, 0, MSG_ERRQUEUE | MSG_DONTWAIT | MSG_TRUNC); + res = recv(transport->fd, NULL, 0, MSG_ERRQUEUE | MSG_DONTWAIT | MSG_TRUNC); } while (res == 0); spa_bt_ptp_init(&lat->ptp, period, period / 2); + + lat->enabled = true; } static inline void spa_bt_latency_reset(struct spa_bt_latency *lat) @@ -80,26 +81,37 @@ static inline void spa_bt_latency_reset(struct spa_bt_latency *lat) spa_bt_ptp_init(&lat->ptp, lat->ptp.period, lat->ptp.period / 2); } -static inline void spa_bt_latency_sent(struct spa_bt_latency *lat, uint64_t now) +static inline ssize_t spa_bt_send(int fd, const void *buf, size_t size, + struct spa_bt_latency *lat, uint64_t now) { - const unsigned int n = SPA_N_ELEMENTS(lat->impl.send); + ssize_t res = send(fd, buf, size, MSG_DONTWAIT | MSG_NOSIGNAL); - if (lat->disabled) - return; + if (!lat || !lat->enabled) + return res; - lat->impl.send[lat->impl.pos++] = now; - if (lat->impl.pos >= n) - lat->impl.pos = 0; + if (res >= 0) { + lat->impl.send[lat->impl.pos] = now; + lat->impl.size[lat->impl.pos] = size; + lat->impl.pos++; + if (lat->impl.pos >= SPA_N_ELEMENTS(lat->impl.send)) + lat->impl.pos = 0; + + lat->queue++; + lat->kernel_queue++; + lat->unsent += size; + } + + return res; } static inline int spa_bt_latency_recv_errqueue(struct spa_bt_latency *lat, int fd, struct spa_log *log) { - struct { - struct cmsghdr cm; - char control[512]; + union { + char buf[CMSG_SPACE(32 * sizeof(struct scm_timestamping))]; + struct cmsghdr align; } control; - if (lat->disabled) + if (!lat->enabled) return -EOPNOTSUPP; do { @@ -110,8 +122,8 @@ static inline int spa_bt_latency_recv_errqueue(struct spa_bt_latency *lat, int f struct msghdr msg = { .msg_iov = &data, .msg_iovlen = 1, - .msg_control = &control, - .msg_controllen = sizeof(control), + .msg_control = control.buf, + .msg_controllen = sizeof(control.buf), }; struct cmsghdr *cmsg; struct scm_timestamping *tss = NULL; @@ -136,20 +148,38 @@ static inline int spa_bt_latency_recv_errqueue(struct spa_bt_latency *lat, int f if (!tss || !serr || serr->ee_errno != ENOMSG || serr->ee_origin != SO_EE_ORIGIN_TIMESTAMPING) return -EINVAL; - if (serr->ee_info != SCM_TSTAMP_SND) + + switch (serr->ee_info) { + case SCM_TSTAMP_SND: + if (lat->kernel_queue) + lat->kernel_queue--; continue; + case NEW_SCM_TSTAMP_COMPLETION: + break; + default: + continue; + } struct timespec *ts = &tss->ts[0]; int64_t tx_time = SPA_TIMESPEC_TO_NSEC(ts); uint32_t tx_pos = serr->ee_data % SPA_N_ELEMENTS(lat->impl.send); lat->value = tx_time - lat->impl.send[tx_pos]; + if (lat->unsent > lat->impl.size[tx_pos]) + lat->unsent -= lat->impl.size[tx_pos]; + else + lat->unsent = 0; if (lat->impl.prev_tx && tx_time > lat->impl.prev_tx) spa_bt_ptp_update(&lat->ptp, lat->value, tx_time - lat->impl.prev_tx); lat->impl.prev_tx = tx_time; + if (lat->queue > 0) + lat->queue--; + if (!lat->queue) + lat->unsent = 0; + spa_log_trace(log, "fd:%d latency[%d] nsec:%"PRIu64" range:%d..%d ms", fd, tx_pos, lat->value, (int)(spa_bt_ptp_valid(&lat->ptp) ? lat->ptp.min / SPA_NSEC_PER_MSEC : -1), @@ -165,11 +195,14 @@ static inline void spa_bt_latency_flush(struct spa_bt_latency *lat, int fd, stru { int so_timestamping = 0; + if (!lat->enabled) + return; + /* Disable timestamping and flush errqueue */ setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &so_timestamping, sizeof(so_timestamping)); spa_bt_latency_recv_errqueue(lat, fd, log); - lat->disabled = true; + lat->enabled = false; } #endif diff --git a/spa/plugins/bluez5/codec-loader.c b/spa/plugins/bluez5/codec-loader.c index 6fd1d0430..68e6af756 100644 --- a/spa/plugins/bluez5/codec-loader.c +++ b/spa/plugins/bluez5/codec-loader.c @@ -52,6 +52,10 @@ static int codec_order(const struct media_codec *c) SPA_BLUETOOTH_AUDIO_CODEC_OPUS_05_PRO, SPA_BLUETOOTH_AUDIO_CODEC_AAC_ELD, SPA_BLUETOOTH_AUDIO_CODEC_G722, + SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB, + SPA_BLUETOOTH_AUDIO_CODEC_LC3_A127, + SPA_BLUETOOTH_AUDIO_CODEC_MSBC, + SPA_BLUETOOTH_AUDIO_CODEC_CVSD, }; size_t i; for (i = 0; i < SPA_N_ELEMENTS(order); ++i) @@ -120,6 +124,18 @@ static int load_media_codecs_from(struct impl *impl, const char *factory_name, c break; } + switch (c->kind) { + case MEDIA_CODEC_A2DP: + case MEDIA_CODEC_BAP: + case MEDIA_CODEC_ASHA: + case MEDIA_CODEC_HFP: + break; + default: + spa_log_warn(impl->log, "codec plugin %s: unknown codec %s kind %d", + factory_name, c->name, c->kind); + continue; + } + /* Don't load duplicate endpoints */ for (j = 0; j < impl->n_codecs; ++j) { const struct media_codec *c2 = impl->codecs[j]; @@ -160,7 +176,6 @@ fail: const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *loader, struct spa_log *log) { struct impl *impl; - bool has_sbc; size_t i; const struct { const char *factory; const char *lib; } plugins[] = { #define MEDIA_CODEC_FACTORY_LIB(basename) \ @@ -174,7 +189,11 @@ const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *lo MEDIA_CODEC_FACTORY_LIB("opus"), MEDIA_CODEC_FACTORY_LIB("opus-g"), MEDIA_CODEC_FACTORY_LIB("lc3"), - MEDIA_CODEC_FACTORY_LIB("g722") + MEDIA_CODEC_FACTORY_LIB("g722"), + MEDIA_CODEC_FACTORY_LIB("hfp-cvsd"), + MEDIA_CODEC_FACTORY_LIB("hfp-msbc"), + MEDIA_CODEC_FACTORY_LIB("hfp-lc3-swb"), + MEDIA_CODEC_FACTORY_LIB("hfp-lc3-a127"), #undef MEDIA_CODEC_FACTORY_LIB }; @@ -190,13 +209,17 @@ const struct media_codec * const *load_media_codecs(struct spa_plugin_loader *lo for (i = 0; i < SPA_N_ELEMENTS(plugins); ++i) load_media_codecs_from(impl, plugins[i].factory, plugins[i].lib); - has_sbc = false; - for (i = 0; i < impl->n_codecs; ++i) - if (impl->codecs[i]->id == SPA_BLUETOOTH_AUDIO_CODEC_SBC) - has_sbc = true; + bool has_sbc = false, has_cvsd = false; + for (i = 0; i < impl->n_codecs; ++i) { + has_sbc |= impl->codecs[i]->id == SPA_BLUETOOTH_AUDIO_CODEC_SBC; + has_cvsd |= impl->codecs[i]->id == SPA_BLUETOOTH_AUDIO_CODEC_CVSD; + } - if (!has_sbc) { - spa_log_error(impl->log, "failed to load A2DP SBC codec from plugins"); + if (!has_sbc || !has_cvsd) { + if (!has_sbc) + spa_log_error(impl->log, "failed to load A2DP SBC codec from plugins"); + if (!has_cvsd) + spa_log_error(impl->log, "failed to load HFP CVSD codec from plugins"); free_media_codecs(impl->codecs); errno = ENOENT; return NULL; diff --git a/spa/plugins/bluez5/decode-buffer.h b/spa/plugins/bluez5/decode-buffer.h index 0441f041c..ab9c0b6a1 100644 --- a/spa/plugins/bluez5/decode-buffer.h +++ b/spa/plugins/bluez5/decode-buffer.h @@ -21,8 +21,9 @@ * The regular timer cycle cannot be aligned with this, so process() * may occur at any time. * - * The buffer level is the position of last received sample, relative to the current - * playback position. If it is larger than duration, there is no underrun. + * The instantaneous buffer level is the time position (in samples) of the last + * received sample, relative to the nominal time position of the last sample of the last + * received packet. If it is always larger than duration, there is no underrun. * * The rate correction aims to maintain the average level at a safety margin. */ @@ -31,6 +32,12 @@ #define SPA_BLUEZ5_DECODE_BUFFER_H #include +#include +#include +#include +#include +#include + #include #include @@ -40,6 +47,12 @@ #define BUFFERING_SHORT_MSEC 1000 #define BUFFERING_RATE_DIFF_MAX 0.005 +#ifndef BT_PKT_SEQNUM +#define BT_PKT_SEQNUM 22 +#endif +#ifndef BT_SCM_PKT_SEQNUM +#define BT_SCM_PKT_SEQNUM 0x05 +#endif struct spa_bt_decode_buffer { @@ -47,6 +60,13 @@ struct spa_bt_decode_buffer uint32_t frame_size; uint32_t rate; + int64_t avg_period; + double rate_diff_max; + + int32_t target; /**< target buffer (0: automatic) */ + int32_t max_extra; + + bool no_overrun_drop; uint8_t *buffer_decoded; uint32_t buffer_size; @@ -60,15 +80,20 @@ struct spa_bt_decode_buffer struct spa_bt_rate_control ctl; double corr; - uint32_t duration; uint32_t pos; - int32_t target; /**< target buffer (0: automatic) */ - int32_t max_extra; - - int32_t level; - uint64_t next_nsec; + int64_t duration_ns; + int64_t next_nsec; double rate_diff; + int32_t delay; + int32_t delay_frac; + + double level; + + struct { + int64_t nsec; + int64_t position; + } rx; uint8_t buffering:1; }; @@ -87,6 +112,8 @@ static inline int spa_bt_decode_buffer_init(struct spa_bt_decode_buffer *this, s this->target = 0; this->buffering = true; this->max_extra = INT32_MAX; + this->avg_period = BUFFERING_SHORT_MSEC * SPA_NSEC_PER_MSEC; + this->rate_diff_max = BUFFERING_RATE_DIFF_MAX; spa_bt_rate_control_init(&this->ctl, 0); @@ -163,35 +190,35 @@ static inline void *spa_bt_decode_buffer_get_write(struct spa_bt_decode_buffer * return SPA_PTROFF(this->buffer_decoded, this->write_index, void); } +static inline size_t spa_bt_decode_buffer_get_size(struct spa_bt_decode_buffer *this) +{ + return this->write_index - this->read_index; +} + static inline void spa_bt_decode_buffer_write_packet(struct spa_bt_decode_buffer *this, uint32_t size, uint64_t nsec) { - int32_t remain; - uint32_t avail; + const int32_t duration = this->duration_ns * this->rate / SPA_NSEC_PER_SEC; + + if (nsec) { + this->rx.nsec = nsec; + this->rx.position = size / this->frame_size; + } else { + this->rx.position += size / this->frame_size; + } spa_assert(size % this->frame_size == 0); this->write_index += size; spa_bt_ptp_update(&this->packet_size, size / this->frame_size, size / this->frame_size); - if (nsec && this->next_nsec && this->rate_diff != 0.0) { - int64_t dt = (this->next_nsec >= nsec) ? - (int64_t)(this->next_nsec - nsec) : -(int64_t)(nsec - this->next_nsec); - remain = (int32_t)SPA_CLAMP(dt * this->rate_diff * this->rate / SPA_NSEC_PER_SEC, - -(int32_t)this->duration, this->duration); + if (this->rx.nsec && this->next_nsec) { + uint32_t avail = spa_bt_decode_buffer_get_size(this) / this->frame_size; + int64_t dt = this->next_nsec - this->rx.nsec; + + this->level = dt * this->rate_diff * this->rate / SPA_NSEC_PER_SEC + + avail + this->delay + this->delay_frac/1e9 - this->rx.position; } else { - remain = 0; + this->level = spa_bt_decode_buffer_get_size(this) / this->frame_size - duration; } - - spa_bt_decode_buffer_get_read(this, &avail); - this->level = avail / this->frame_size + remain; -} - -static inline void spa_bt_decode_buffer_recover(struct spa_bt_decode_buffer *this) -{ - int32_t size = (this->write_index - this->read_index) / this->frame_size; - - this->level = size; - this->corr = 1.0; - spa_bt_rate_control_init(&this->ctl, size); } static inline void spa_bt_decode_buffer_set_target_latency(struct spa_bt_decode_buffer *this, int32_t samples) @@ -204,39 +231,59 @@ static inline void spa_bt_decode_buffer_set_max_extra_latency(struct spa_bt_deco this->max_extra = samples; } -static inline int32_t spa_bt_decode_buffer_get_target_latency(struct spa_bt_decode_buffer *this) +static inline int32_t spa_bt_decode_buffer_get_auto_latency(struct spa_bt_decode_buffer *this) { - const int32_t duration = this->duration; + const int32_t duration = this->duration_ns * this->rate / SPA_NSEC_PER_SEC; const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8); const int32_t max_buf = (this->buffer_size - this->buffer_reserve) / this->frame_size; const int32_t spike = SPA_CLAMP(this->spike.max, 0, max_buf); int32_t target; - if (this->target) - target = this->target; - else - target = SPA_CLAMP(SPA_ROUND_UP(SPA_MAX(spike * 3/2, duration), - SPA_CLAMP((int)this->rate / 50, 1, INT32_MAX)), - duration, max_buf - 2*packet_size); + target = SPA_CLAMP(SPA_ROUND_UP(SPA_MAX(spike * 3/2, duration), + SPA_CLAMP((int)this->rate / 50, 1, INT32_MAX)), + duration, max_buf - 2*packet_size); return SPA_MIN(target, duration + SPA_CLAMP(this->max_extra, 0, INT32_MAX - duration)); } -static inline void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint32_t samples, uint32_t duration, - double rate_diff, uint64_t next_nsec) +static inline int32_t spa_bt_decode_buffer_get_target_latency(struct spa_bt_decode_buffer *this) +{ + if (this->target) + return this->target; + return spa_bt_decode_buffer_get_auto_latency(this); +} + +static inline void spa_bt_decode_buffer_recover(struct spa_bt_decode_buffer *this) +{ + int32_t target = spa_bt_decode_buffer_get_target_latency(this); + + this->rx.nsec = 0; + this->corr = 1.0; + spa_bt_rate_control_init(&this->ctl, target * SPA_NSEC_PER_SEC / this->rate); + spa_bt_decode_buffer_write_packet(this, 0, 0); +} + +static inline void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *this, uint32_t samples, int64_t duration_ns, + double rate_diff, int64_t next_nsec, int32_t delay, int32_t delay_frac) { const uint32_t data_size = samples * this->frame_size; const int32_t packet_size = SPA_CLAMP(this->packet_size.max, 0, INT32_MAX/8); - const int32_t max_level = SPA_MAX(8 * packet_size, (int32_t)duration); - const uint32_t avg_period = (uint64_t)this->rate * BUFFERING_SHORT_MSEC / 1000; int32_t target; uint32_t avail; + double level; this->rate_diff = rate_diff; this->next_nsec = next_nsec; + this->delay = delay; + this->delay_frac = delay_frac; - if (SPA_UNLIKELY(duration != this->duration)) { - this->duration = duration; + /* The fractional delay is given at the start of current cycle. Make it relative + * to next_nsec used for the level calculations. + */ + this->delay_frac += (int32_t)(1e9 * samples - duration_ns * this->rate * this->rate_diff); + + if (SPA_UNLIKELY(duration_ns != this->duration_ns)) { + this->duration_ns = duration_ns; spa_bt_decode_buffer_recover(this); } @@ -249,63 +296,194 @@ static inline void spa_bt_decode_buffer_process(struct spa_bt_decode_buffer *thi spa_log_trace(this->log, "%p buffering size:%d", this, (int)size); - if (size >= SPA_MAX((int)duration, target)) + if (size >= SPA_MAX((int)samples, target)) this->buffering = false; else return; - spa_bt_ptp_update(&this->spike, packet_size, duration); + spa_bt_ptp_update(&this->spike, packet_size, samples); spa_bt_decode_buffer_recover(this); } spa_bt_decode_buffer_get_read(this, &avail); /* Track buffer level */ - this->level = SPA_MAX(this->level, -max_level); + level = SPA_MAX(this->level, 0); - spa_bt_ptp_update(&this->spike, (int32_t)this->ctl.avg - this->level, duration); + spa_bt_ptp_update(&this->spike, + (int32_t)(this->ctl.avg * this->rate / SPA_NSEC_PER_SEC - level), + samples); - if (this->level > SPA_MAX(4 * target, 3*(int32_t)duration) && - avail > data_size) { + if (!this->no_overrun_drop && + level > SPA_MAX(4 * target, 3*(int32_t)samples) && avail > data_size) { /* Lagging too much: drop data */ uint32_t size = SPA_MIN(avail - data_size, - (this->level - target) * this->frame_size); + ((int32_t)ceil(level) - target) * this->frame_size); spa_bt_decode_buffer_read(this, size); - spa_log_trace(this->log, "%p overrun samples:%d level:%d target:%d", + spa_log_trace(this->log, "%p overrun samples:%d level:%.2f target:%d", this, (int)size/this->frame_size, - (int)this->level, (int)target); + level, (int)target); spa_bt_decode_buffer_recover(this); } - this->pos += duration; - if (this->pos > this->rate) { - spa_log_debug(this->log, - "%p avg:%d target:%d level:%d buffer:%d spike:%d corr:%f", + this->pos += samples; + + enum spa_log_level log_level = (this->pos > this->rate) ? SPA_LOG_LEVEL_DEBUG : SPA_LOG_LEVEL_TRACE; + if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, log_level))) { + spa_log_lev(this->log, log_level, + "%p avg:%.2f target:%d level:%.2f buffer:%d spike:%d corr:%g", this, - (int)this->ctl.avg, + this->ctl.avg * this->rate / SPA_NSEC_PER_SEC, (int)target, - (int)this->level, + level, (int)(avail / this->frame_size), (int)this->spike.max, - (double)this->corr); + (double)this->corr - 1); this->pos = 0; } this->corr = spa_bt_rate_control_update(&this->ctl, - this->level, target, duration, avg_period, - BUFFERING_RATE_DIFF_MAX); - - this->level -= duration; + level * SPA_NSEC_PER_SEC / this->rate, + ((double)target + 0.5/this->rate) * SPA_NSEC_PER_SEC / this->rate, + duration_ns, this->avg_period, this->rate_diff_max); spa_bt_decode_buffer_get_read(this, &avail); if (avail < data_size) { - spa_log_trace(this->log, "%p underrun samples:%d", this, + spa_log_debug(this->log, "%p underrun samples:%d", this, (data_size - avail) / this->frame_size); this->buffering = true; - spa_bt_ptp_update(&this->spike, (int32_t)this->ctl.avg - this->level, duration); + spa_bt_ptp_update(&this->spike, samples, 0); } } +struct spa_bt_recvmsg_data { + struct spa_log *log; + struct spa_system *data_system; + int fd; + int64_t offset; + int64_t err; +}; + +static inline void spa_bt_recvmsg_update_clock(struct spa_bt_recvmsg_data *data, uint64_t *now) +{ + const int64_t max_resync = (50 * SPA_NSEC_PER_USEC); + const int64_t n_avg = 10; + struct timespec ts1, ts2, ts3; + int64_t t1, t2, t3, offset, err; + + spa_system_clock_gettime(data->data_system, CLOCK_MONOTONIC, &ts1); + spa_system_clock_gettime(data->data_system, CLOCK_REALTIME, &ts2); + spa_system_clock_gettime(data->data_system, CLOCK_MONOTONIC, &ts3); + + t1 = SPA_TIMESPEC_TO_NSEC(&ts1); + t2 = SPA_TIMESPEC_TO_NSEC(&ts2); + t3 = SPA_TIMESPEC_TO_NSEC(&ts3); + + if (now) + *now = t1; + + offset = t1 + (t3 - t1) / 2 - t2; + + /* Moving average smoothing, discarding outliers */ + err = offset - data->offset; + + if (SPA_ABS(err) > max_resync) { + /* Clock jump */ + spa_log_debug(data->log, "%p: nsec err %"PRIi64" > max_resync %"PRIi64", resetting", + data, err, max_resync); + data->offset = offset; + data->err = 0; + err = 0; + } else if (SPA_ABS(err) / 2 <= data->err) { + data->offset += err / n_avg; + } + + data->err += (SPA_ABS(err) - data->err) / n_avg; +} + +static inline ssize_t spa_bt_recvmsg(struct spa_bt_recvmsg_data *r, void *buf, size_t max_size, uint64_t *rx_time, + int *seqnum) +{ + union { + char buf[CMSG_SPACE(sizeof(struct scm_timestamping)) + CMSG_SPACE(sizeof(uint16_t))]; + struct cmsghdr align; + } control; + struct iovec data = { + .iov_base = buf, + .iov_len = max_size + }; + struct msghdr msg = { + .msg_iov = &data, + .msg_iovlen = 1, + .msg_control = control.buf, + .msg_controllen = sizeof(control.buf), + }; + struct cmsghdr *cmsg; + uint64_t t = 0, now; + ssize_t res; + + *seqnum = -1; + + res = recvmsg(r->fd, &msg, MSG_DONTWAIT); + if (res < 0 || !rx_time) + return res; + + spa_bt_recvmsg_update_clock(r, &now); + + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + struct scm_timestamping *tss; + + if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPING) { + tss = (struct scm_timestamping *)CMSG_DATA(cmsg); + t = SPA_TIMESPEC_TO_NSEC(&tss->ts[0]); + } else if (cmsg->cmsg_level == SOL_BLUETOOTH && cmsg->cmsg_type == BT_SCM_PKT_SEQNUM) { + *seqnum = *((uint16_t *)CMSG_DATA(cmsg)); + } + } + + if (!t) { + *rx_time = now; + return res; + } + + *rx_time = t + r->offset; + + /* CLOCK_REALTIME may jump, so sanity check */ + if (*rx_time > now || *rx_time + 20 * SPA_NSEC_PER_MSEC < now) + *rx_time = now; + + spa_log_trace(r->log, "%p: rx:%" PRIu64 " now:%" PRIu64 " d:%"PRIu64" off:%"PRIi64" sn:%d", + r, *rx_time, now, now - *rx_time, r->offset, *seqnum); + + return res; +} + + +static inline void spa_bt_recvmsg_init(struct spa_bt_recvmsg_data *data, int fd, + struct spa_system *data_system, struct spa_log *log) +{ + int flags = 0; + socklen_t len = sizeof(flags); + uint32_t opt; + + data->log = log; + data->data_system = data_system; + data->fd = fd; + data->offset = 0; + data->err = 0; + + if (getsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, &len) < 0) + spa_log_info(log, "failed to get SO_TIMESTAMPING"); + + flags |= SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_RX_SOFTWARE; + if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags)) < 0) + spa_log_info(log, "failed to set SO_TIMESTAMPING"); + + opt = 1; + if (setsockopt(fd, SOL_BLUETOOTH, BT_PKT_SEQNUM, &opt, sizeof(opt)) < 0) + spa_log_info(log, "failed to set BT_PKT_SEQNUM"); +} + #endif diff --git a/spa/plugins/bluez5/defs.h b/spa/plugins/bluez5/defs.h index 230945808..0e25592e2 100644 --- a/spa/plugins/bluez5/defs.h +++ b/spa/plugins/bluez5/defs.h @@ -5,9 +5,7 @@ #ifndef SPA_BLUEZ5_DEFS_H #define SPA_BLUEZ5_DEFS_H -#ifdef __cplusplus -extern "C" { -#endif +#include "config.h" #include @@ -17,10 +15,13 @@ extern "C" { #include #include #include +#include #include -#include "config.h" +#ifdef __cplusplus +extern "C" { +#endif #define BLUEZ_SERVICE "org.bluez" #define BLUEZ_PROFILE_MANAGER_INTERFACE BLUEZ_SERVICE ".ProfileManager1" @@ -142,10 +143,6 @@ extern "C" { #define BUS_TYPE_USB 1 #define BUS_TYPE_OTHER 255 -#define HFP_AUDIO_CODEC_CVSD 0x01 -#define HFP_AUDIO_CODEC_MSBC 0x02 -#define HFP_AUDIO_CODEC_LC3_SWB 0x03 - #define A2DP_OBJECT_MANAGER_PATH "/MediaEndpoint" #define A2DP_SINK_ENDPOINT A2DP_OBJECT_MANAGER_PATH "/A2DPSink" #define A2DP_SOURCE_ENDPOINT A2DP_OBJECT_MANAGER_PATH "/A2DPSource" @@ -160,13 +157,7 @@ extern "C" { #define SPA_BT_NO_BATTERY ((uint8_t)255) -/* HFP uses SBC encoding with precisely defined parameters. Hence, the size - * of the input (number of PCM samples) and output is known up front. */ -#define MSBC_DECODED_SIZE 240 -#define MSBC_PAYLOAD_SIZE 57 /* 1 byte padding follows payload */ -#define LC3_SWB_DECODED_SIZE 960 /* 32 kHz mono S24_32 @ 7.5 ms */ -#define LC3_SWB_PAYLOAD_SIZE 58 -#define HFP_CODEC_PACKET_SIZE 60 /* 2 bytes header + payload */ +#define MAX_CHANNELS (SPA_AUDIO_MAX_CHANNELS) enum spa_bt_media_direction { SPA_BT_MEDIA_SOURCE, @@ -206,7 +197,9 @@ enum spa_bt_profile { static inline enum spa_bt_profile spa_bt_profile_from_uuid(const char *uuid) { - if (strcasecmp(uuid, SPA_BT_UUID_A2DP_SOURCE) == 0) + if (!uuid) + return 0; + else if (strcasecmp(uuid, SPA_BT_UUID_A2DP_SOURCE) == 0) return SPA_BT_PROFILE_A2DP_SOURCE; else if (strcasecmp(uuid, SPA_BT_UUID_A2DP_SINK) == 0) return SPA_BT_PROFILE_A2DP_SINK; @@ -384,6 +377,7 @@ struct spa_bt_adapter { unsigned int has_adapter1_interface:1; unsigned int has_media1_interface:1; unsigned int le_audio_bcast_supported:1; + unsigned int tx_timestamping_supported:1; }; enum spa_bt_form_factor { @@ -426,6 +420,31 @@ static inline const char *spa_bt_form_factor_name(enum spa_bt_form_factor ff) } } +static inline const char *spa_bt_form_factor_icon_name(enum spa_bt_form_factor ff) +{ + switch (ff) { + case SPA_BT_FORM_FACTOR_HEADSET: + return "audio-headset-bluetooth"; + case SPA_BT_FORM_FACTOR_HANDSFREE: + return "audio-handsfree-bluetooth"; + case SPA_BT_FORM_FACTOR_MICROPHONE: + return "audio-input-microphone-bluetooth"; + case SPA_BT_FORM_FACTOR_SPEAKER: + return "audio-speakers-bluetooth"; + case SPA_BT_FORM_FACTOR_HEADPHONE: + return "audio-headphones-bluetooth"; + case SPA_BT_FORM_FACTOR_PORTABLE: + return "multimedia-player-bluetooth"; + case SPA_BT_FORM_FACTOR_PHONE: + return "phone-bluetooth"; + case SPA_BT_FORM_FACTOR_CAR: + case SPA_BT_FORM_FACTOR_HIFI: + case SPA_BT_FORM_FACTOR_UNKNOWN: + default: + return "audio-card-bluetooth"; + } +} + static inline enum spa_bt_form_factor spa_bt_form_factor_from_class(uint32_t bluetooth_class) { uint32_t major, minor; @@ -460,7 +479,6 @@ static inline enum spa_bt_form_factor spa_bt_form_factor_from_class(uint32_t blu return SPA_BT_FORM_FACTOR_UNKNOWN; } -struct spa_bt_media_codec_switch; struct spa_bt_transport; struct spa_bt_device_events { @@ -473,6 +491,9 @@ struct spa_bt_device_events { /** Codec switching completed */ void (*codec_switched) (void *data, int status); + /** Codec switching initiated or completed by another device */ + void (*codec_switch_other) (void *data, bool switching); + /** Profile configuration changed */ void (*profiles_changed) (void *data, uint32_t connected_change); @@ -551,6 +572,7 @@ struct spa_bt_device { DBusPendingCall *battery_pending_call; const struct media_codec *preferred_codec; + uint32_t preferred_profiles; }; struct spa_bt_device *spa_bt_device_find(struct spa_bt_monitor *monitor, const char *path); @@ -558,20 +580,22 @@ struct spa_bt_device *spa_bt_device_find_by_address(struct spa_bt_monitor *monit int spa_bt_device_add_profile(struct spa_bt_device *device, enum spa_bt_profile profile); int spa_bt_device_connect_profile(struct spa_bt_device *device, enum spa_bt_profile profile); int spa_bt_device_check_profiles(struct spa_bt_device *device, bool force); -int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs); +int spa_bt_device_ensure_media_codec(struct spa_bt_device *device, const struct media_codec * const *codecs, uint32_t profiles); +int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, const struct media_codec *codec); bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const struct media_codec *codec, enum spa_bt_profile profile); const struct media_codec **spa_bt_device_get_supported_media_codecs(struct spa_bt_device *device, size_t *count); -int spa_bt_device_ensure_hfp_codec(struct spa_bt_device *device, unsigned int codec); -int spa_bt_device_supports_hfp_codec(struct spa_bt_device *device, unsigned int codec); int spa_bt_device_release_transports(struct spa_bt_device *device); int spa_bt_device_report_battery_level(struct spa_bt_device *device, uint8_t percentage); void spa_bt_device_update_last_bluez_action_time(struct spa_bt_device *device); +const struct media_codec * const * spa_bt_get_media_codecs(struct spa_bt_monitor *monitor); +const struct media_codec *spa_bt_get_hfp_codec(struct spa_bt_monitor *monitor, unsigned int hfp_codec_id); #define spa_bt_device_emit(d,m,v,...) spa_hook_list_call(&(d)->listener_list, \ struct spa_bt_device_events, \ m, v, ##__VA_ARGS__) #define spa_bt_device_emit_connected(d,...) spa_bt_device_emit(d, connected, 0, __VA_ARGS__) #define spa_bt_device_emit_codec_switched(d,...) spa_bt_device_emit(d, codec_switched, 0, __VA_ARGS__) +#define spa_bt_device_emit_codec_switch_other(d,...) spa_bt_device_emit(d, codec_switch_other, 0, __VA_ARGS__) #define spa_bt_device_emit_profiles_changed(d,...) spa_bt_device_emit(d, profiles_changed, 0, __VA_ARGS__) #define spa_bt_device_emit_device_set_changed(d) spa_bt_device_emit(d, device_set_changed, 0) #define spa_bt_device_emit_switch_profile(d) spa_bt_device_emit(d, switch_profile, 0) @@ -583,11 +607,12 @@ struct spa_bt_iso_io; struct spa_bt_sco_io; -struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_bt_transport *transport, struct spa_loop *data_loop, struct spa_log *log); +struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_bt_transport *transport, struct spa_loop *data_loop, + struct spa_system *data_system, struct spa_log *log); void spa_bt_sco_io_destroy(struct spa_bt_sco_io *io); -void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *userdata, uint8_t *data, int size), void *userdata); -void spa_bt_sco_io_set_sink_cb(struct spa_bt_sco_io *io, int (*sink_cb)(void *userdata), void *userdata); -int spa_bt_sco_io_write(struct spa_bt_sco_io *io, uint8_t *data, int size); +void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *userdata, uint8_t *data, int size, uint64_t rx_time), void *userdata); +int spa_bt_sco_io_write(struct spa_bt_sco_io *io, const uint8_t *buf, size_t size); +void spa_bt_sco_io_write_start(struct spa_bt_sco_io *io); #define SPA_BT_VOLUME_ID_RX 0 #define SPA_BT_VOLUME_ID_TX 1 @@ -647,15 +672,15 @@ struct spa_bt_transport { enum spa_bt_profile profile; enum spa_bt_transport_state state; const struct media_codec *media_codec; - unsigned int codec; void *configuration; int configuration_len; char *endpoint_path; + char *remote_endpoint_path; bool bap_initiator; struct spa_list bap_transport_linked; uint32_t n_channels; - uint32_t channels[64]; + uint32_t channels[MAX_CHANNELS]; struct spa_bt_transport_volume volumes[SPA_BT_VOLUME_ID_TERM]; @@ -685,6 +710,10 @@ struct spa_bt_transport { struct spa_hook_list listener_list; struct spa_callbacks impl; + /* For ASHA */ + bool asha_right_side; + uint64_t hisyncid; + /* user_data must be the last item in the struct */ void *user_data; }; @@ -702,7 +731,7 @@ bool spa_bt_transport_volume_enabled(struct spa_bt_transport *transport); int spa_bt_transport_acquire(struct spa_bt_transport *t, bool optional); int spa_bt_transport_release(struct spa_bt_transport *t); int spa_bt_transport_keepalive(struct spa_bt_transport *t, bool keepalive); -int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop); +int spa_bt_transport_ensure_sco_io(struct spa_bt_transport *t, struct spa_loop *data_loop, struct spa_system *data_system); #define spa_bt_transport_emit(t,m,v,...) spa_hook_list_call(&(t)->listener_list, \ struct spa_bt_transport_events, \ diff --git a/spa/plugins/bluez5/hfp-codec-caps.h b/spa/plugins/bluez5/hfp-codec-caps.h new file mode 100644 index 000000000..abbd0bbd5 --- /dev/null +++ b/spa/plugins/bluez5/hfp-codec-caps.h @@ -0,0 +1,16 @@ +/* Spa HFP codec API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_HFP_CODEC_CAPS_H_ +#define SPA_BLUEZ5_HFP_CODEC_CAPS_H_ + +#include + +#define HFP_AUDIO_CODEC_CVSD 0x01 +#define HFP_AUDIO_CODEC_MSBC 0x02 +#define HFP_AUDIO_CODEC_LC3_SWB 0x03 + +#define HFP_H2_PACKET_SIZE 60 + +#endif diff --git a/spa/plugins/bluez5/hfp-codec-cvsd.c b/spa/plugins/bluez5/hfp-codec-cvsd.c new file mode 100644 index 000000000..19f908188 --- /dev/null +++ b/spa/plugins/bluez5/hfp-codec-cvsd.c @@ -0,0 +1,203 @@ +/* Spa HFP CVSD Codec */ +/* SPDX-FileCopyrightText: Copyright © 2019 Collabora Ltd. */ +/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "media-codecs.h" +#include "hfp-h2.h" + +static struct spa_log *log; + +struct impl { + size_t block_size; + uint16_t seq; +}; + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + struct spa_pod_frame f[1]; + const uint32_t position[1] = { SPA_AUDIO_CHANNEL_MONO }; + const int channels = 1; + + spa_assert(caps == NULL && caps_size == 0); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16_LE), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_ENUM_Int(1, 8000), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, channels, position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + spa_assert(caps == NULL && caps_size == 0); + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE; + info->info.raw.rate = 8000; + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + return 0; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + struct impl *this = NULL; + + spa_assert(config == NULL && config_len == 0 && props == NULL); + + if (mtu < 2) + return NULL; + + this = calloc(1, sizeof(struct impl)); + if (!this) + return NULL; + + this->block_size = SPA_MIN(2 * (mtu/2), 144u); /* cap to 9 ms */ + return this; +} + +static void codec_deinit(void *data) +{ + free(data); +} + +static int codec_get_block_size(void *data) +{ + struct impl *this = data; + + return this->block_size; +} + +static int codec_start_encode (void *data, void *dst, size_t dst_size, + uint16_t seqnum, uint32_t timestamp) +{ + return 0; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + + if (src_size < this->block_size) + return -EINVAL; + if (dst_size < this->block_size) + return -EINVAL; + + spa_memmove(dst, src, this->block_size); + *dst_out = this->block_size; + *need_flush = NEED_FLUSH_ALL; + return this->block_size; +} + +static int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + struct impl *this = data; + + if (src_size != 48 && is_zero_packet(src, src_size)) { + /* Adapter is returning non-standard CVSD stream. For example + * Intel 8087:0029 at Firmware revision 0.0 build 191 week 21 2021 + * on kernel 5.13.19 produces such data. + */ + return -EINVAL; + } + + if (src_size % 2 != 0) { + /* Unaligned data: reception or adapter problem. + * Consider the whole packet lost and report. + */ + return -EINVAL; + } + + if (seqnum) + *seqnum = this->seq; + + if (timestamp) + *timestamp = 0; + + this->seq++; + return 0; +} + +static int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + uint32_t avail; + + avail = SPA_MIN(src_size, dst_size); + if (avail) + spa_memcpy(dst, src, avail); + + *dst_out = avail; + return avail; +} + +static void codec_set_log(struct spa_log *global_log) +{ + log = global_log; + spa_log_topic_init(log, &codec_plugin_log_topic); +} + +const struct media_codec hfp_codec_cvsd = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_CVSD, + .kind = MEDIA_CODEC_HFP, + .codec_id = 0x01, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .start_encode = codec_start_encode, + .encode = codec_encode, + .set_log = codec_set_log, + .start_decode = codec_start_decode, + .decode = codec_decode, + .name = "cvsd", + .description = "CVSD", +}; + +MEDIA_CODEC_EXPORT_DEF( + "hfp-cvsd", + &hfp_codec_cvsd +); diff --git a/spa/plugins/bluez5/hfp-codec-lc3-a127.c b/spa/plugins/bluez5/hfp-codec-lc3-a127.c new file mode 100644 index 000000000..b10bba874 --- /dev/null +++ b/spa/plugins/bluez5/hfp-codec-lc3-a127.c @@ -0,0 +1,259 @@ +/* Spa HFP LC3-24kHz (Apple) Codec */ +/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "media-codecs.h" +#include "hfp-h2.h" + +#include + +#define LC3_A127_BLOCK_SIZE 720 + +static struct spa_log *log; + +struct impl { + lc3_encoder_t enc; + lc3_decoder_t dec; + + int prev_hwseq; + uint16_t seq; +}; + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + struct spa_pod_frame f[1]; + const uint32_t position[1] = { SPA_AUDIO_CHANNEL_MONO }; + const int channels = 1; + + spa_assert(caps == NULL && caps_size == 0); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_F32), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_ENUM_Int(1, 24000), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, channels, position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + spa_assert(caps == NULL && caps_size == 0); + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_F32; + info->info.raw.rate = 24000; + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + return 0; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + struct impl *this = NULL; + + spa_assert(config == NULL && config_len == 0 && props == NULL); + + this = calloc(1, sizeof(*this)); + if (!this) + goto fail; + + this->enc = lc3_setup_encoder(7500, 24000, 0, + calloc(1, lc3_encoder_size(7500, 24000))); + if (!this->enc) + goto fail; + + this->dec = lc3_setup_decoder(7500, 24000, 0, + calloc(1, lc3_decoder_size(7500, 24000))); + if (!this->dec) + goto fail; + + spa_assert(lc3_frame_samples(7500, 24000) * sizeof(float) == LC3_A127_BLOCK_SIZE); + + this->prev_hwseq = -1; + return this; + +fail: + if (this) { + free(this->enc); + free(this->dec); + free(this); + } + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + + free(this->enc); + free(this->dec); + free(this); +} + +static int codec_get_block_size(void *data) +{ + return LC3_A127_BLOCK_SIZE; +} + +static int codec_start_encode (void *data, void *dst, size_t dst_size, + uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + + this->seq = seqnum; + return 0; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + uint8_t *buf = dst; + int res; + + if (src_size < LC3_A127_BLOCK_SIZE) + return -EINVAL; + if (dst_size < H2_PACKET_SIZE) + return -EINVAL; + + buf[0] = this->seq; + buf[1] = H2_PACKET_SIZE - 2; + this->seq++; + + res = lc3_encode(this->enc, LC3_PCM_FORMAT_FLOAT, src, 1, + H2_PACKET_SIZE - 2, SPA_PTROFF(buf, 2, void)); + if (res != 0) + return -EINVAL; + + *dst_out = H2_PACKET_SIZE; + *need_flush = NEED_FLUSH_ALL; + return LC3_A127_BLOCK_SIZE; +} + +static int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + struct impl *this = data; + const uint8_t *buf = src; + + if (is_zero_packet(src, src_size)) + return -EINVAL; + if (src_size < 2) + return -EINVAL; + if (buf[1] != H2_PACKET_SIZE - 2) + return -EINVAL; + + if (this->prev_hwseq >= 0) + this->seq += (uint8_t)(buf[0] - this->prev_hwseq); + this->prev_hwseq = buf[0]; + + if (seqnum) + *seqnum = this->seq; + if (timestamp) + *timestamp = 0; + + return 2; +} + +static int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + int res; + + *dst_out = 0; + + if (src_size < H2_PACKET_SIZE - 2) + return -EINVAL; + if (dst_size < LC3_A127_BLOCK_SIZE) + return -EINVAL; + + res = lc3_decode(this->dec, src, H2_PACKET_SIZE - 2, LC3_PCM_FORMAT_FLOAT, dst, 1); + if (res) + return -EINVAL; + + *dst_out = LC3_A127_BLOCK_SIZE; + return H2_PACKET_SIZE - 2; +} + +static int codec_produce_plc(void *data, void *dst, size_t dst_size) +{ + struct impl *this = data; + int res; + + if (dst_size < LC3_A127_BLOCK_SIZE) + return -EINVAL; + + res = lc3_decode(this->dec, NULL, 0, LC3_PCM_FORMAT_FLOAT, dst, 1); + if (res != 1) + return -EINVAL; + + return LC3_A127_BLOCK_SIZE; +} + +static void codec_set_log(struct spa_log *global_log) +{ + log = global_log; + spa_log_topic_init(log, &codec_plugin_log_topic); +} + +static const struct media_codec hfp_codec_a127 = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3_A127, + .kind = MEDIA_CODEC_HFP, + .codec_id = 127, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .start_encode = codec_start_encode, + .encode = codec_encode, + .set_log = codec_set_log, + .start_decode = codec_start_decode, + .decode = codec_decode, + .produce_plc = codec_produce_plc, + .name = "lc3_a127", + .description = "LC3-24kHz", +}; + +MEDIA_CODEC_EXPORT_DEF( + "hfp-lc3-a127", + &hfp_codec_a127 +); diff --git a/spa/plugins/bluez5/hfp-codec-lc3-swb.c b/spa/plugins/bluez5/hfp-codec-lc3-swb.c new file mode 100644 index 000000000..1cd679958 --- /dev/null +++ b/spa/plugins/bluez5/hfp-codec-lc3-swb.c @@ -0,0 +1,263 @@ +/* Spa HFP LC3-SWB Codec */ +/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "media-codecs.h" +#include "hfp-h2.h" + +#include + +#define LC3_SWB_BLOCK_SIZE 960 + +static struct spa_log *log; + +struct impl { + lc3_encoder_t enc; + lc3_decoder_t dec; + struct h2_reader h2; + uint16_t seq; + + void *data; + size_t avail; +}; + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + struct spa_pod_frame f[1]; + const uint32_t position[1] = { SPA_AUDIO_CHANNEL_MONO }; + const int channels = 1; + + spa_assert(caps == NULL && caps_size == 0); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_F32), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_ENUM_Int(1, 32000), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, channels, position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + spa_assert(caps == NULL && caps_size == 0); + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_F32; + info->info.raw.rate = 32000; + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + return 0; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + struct impl *this = NULL; + + spa_assert(config == NULL && config_len == 0 && props == NULL); + + this = calloc(1, sizeof(*this)); + if (!this) + goto fail; + + this->enc = lc3_setup_encoder(7500, 32000, 0, + calloc(1, lc3_encoder_size(7500, 32000))); + if (!this->enc) + goto fail; + + this->dec = lc3_setup_decoder(7500, 32000, 0, + calloc(1, lc3_decoder_size(7500, 32000))); + if (!this->dec) + goto fail; + + spa_assert(lc3_frame_samples(7500, 32000) * sizeof(float) == LC3_SWB_BLOCK_SIZE); + + h2_reader_init(&this->h2, false); + + return spa_steal_ptr(this); + +fail: + if (this) { + free(this->enc); + free(this->dec); + free(this); + } + return NULL; +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + + free(this->enc); + free(this->dec); + free(this); +} + +static int codec_get_block_size(void *data) +{ + return LC3_SWB_BLOCK_SIZE; +} + +static int codec_start_encode (void *data, void *dst, size_t dst_size, + uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + + this->seq = seqnum; + return 0; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + int res; + + if (src_size < LC3_SWB_BLOCK_SIZE) + return -EINVAL; + if (dst_size < H2_PACKET_SIZE) + return -EINVAL; + + h2_write(dst, this->seq); + + res = lc3_encode(this->enc, LC3_PCM_FORMAT_FLOAT, src, 1, + H2_PACKET_SIZE - 2, SPA_PTROFF(dst, 2, void)); + if (res != 0) + return -EINVAL; + + *dst_out = H2_PACKET_SIZE; + *need_flush = NEED_FLUSH_ALL; + return LC3_SWB_BLOCK_SIZE; +} + +static int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + struct impl *this = data; + size_t consumed = 0; + + if (is_zero_packet(src, src_size)) + return -EINVAL; + + if (!this->data) + this->data = h2_reader_read(&this->h2, src, src_size, &consumed, &this->avail); + + if (seqnum) + *seqnum = this->h2.seq; + if (timestamp) + *timestamp = 0; + return consumed; +} + +static int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + size_t consumed = 0; + int res; + + *dst_out = 0; + if (dst_size < LC3_SWB_BLOCK_SIZE) + return -EINVAL; + + if (!this->data) + this->data = h2_reader_read(&this->h2, src, src_size, &consumed, &this->avail); + if (!this->data) + return consumed; + + res = lc3_decode(this->dec, this->data, this->avail, LC3_PCM_FORMAT_FLOAT, dst, 1); + this->data = NULL; + + if (res) { + /* fail decoding silently, so remainder of packet is processed */ + spa_log_debug(log, "decoding failed: %d", res); + return consumed; + } + + *dst_out = LC3_SWB_BLOCK_SIZE; + return consumed; +} + +static int codec_produce_plc(void *data, void *dst, size_t dst_size) +{ + struct impl *this = data; + int res; + + if (dst_size < LC3_SWB_BLOCK_SIZE) + return -EINVAL; + + res = lc3_decode(this->dec, NULL, 0, LC3_PCM_FORMAT_FLOAT, dst, 1); + if (res != 1) + return -EINVAL; + + return LC3_SWB_BLOCK_SIZE; +} + +static void codec_set_log(struct spa_log *global_log) +{ + log = global_log; + spa_log_topic_init(log, &codec_plugin_log_topic); +} + +const struct media_codec hfp_codec_msbc = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_LC3_SWB, + .kind = MEDIA_CODEC_HFP, + .codec_id = 0x03, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .start_encode = codec_start_encode, + .encode = codec_encode, + .set_log = codec_set_log, + .start_decode = codec_start_decode, + .decode = codec_decode, + .produce_plc = codec_produce_plc, + .name = "lc3_swb", + .description = "LC3-SWB", + .stream_pkt = true, +}; + +MEDIA_CODEC_EXPORT_DEF( + "hfp-lc3-swb", + &hfp_codec_msbc +); diff --git a/spa/plugins/bluez5/hfp-codec-msbc.c b/spa/plugins/bluez5/hfp-codec-msbc.c new file mode 100644 index 000000000..5175a68d7 --- /dev/null +++ b/spa/plugins/bluez5/hfp-codec-msbc.c @@ -0,0 +1,261 @@ +/* Spa HFP MSBC Codec */ +/* SPDX-FileCopyrightText: Copyright © 2019 Collabora Ltd. */ +/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "media-codecs.h" +#include "hfp-h2.h" +#include "plc.h" + + +#define MSBC_BLOCK_SIZE 240 + +static struct spa_log *log_; + +struct impl { + sbc_t msbc; + struct h2_reader h2; + uint16_t seq; + + void *data; + size_t avail; + + plc_state_t *plc; +}; + +static int codec_enum_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, uint32_t id, uint32_t idx, + struct spa_pod_builder *b, struct spa_pod **param) +{ + struct spa_pod_frame f[1]; + const uint32_t position[1] = { SPA_AUDIO_CHANNEL_MONO }; + const int channels = 1; + + spa_assert(caps == NULL && caps_size == 0); + + if (idx > 0) + return 0; + + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_ENUM_Int(1, 16000), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, channels, position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + +static int codec_validate_config(const struct media_codec *codec, uint32_t flags, + const void *caps, size_t caps_size, + struct spa_audio_info *info) +{ + spa_assert(caps == NULL && caps_size == 0); + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE; + info->info.raw.rate = 16000; + info->info.raw.channels = 1; + info->info.raw.position[0] = SPA_AUDIO_CHANNEL_MONO; + return 0; +} + +static void *codec_init(const struct media_codec *codec, uint32_t flags, + void *config, size_t config_len, const struct spa_audio_info *info, + void *props, size_t mtu) +{ + spa_autofree struct impl *this = NULL; + int res; + + spa_assert(config == NULL && config_len == 0 && props == NULL); + + this = calloc(1, sizeof(*this)); + if (!this) + return NULL; + + res = sbc_init_msbc(&this->msbc, 0); + if (res < 0) + return NULL; + + /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */ + this->msbc.endian = SBC_LE; + + h2_reader_init(&this->h2, true); + + this->plc = plc_init(NULL); + if (!this->plc) + return NULL; + + return spa_steal_ptr(this); +} + +static void codec_deinit(void *data) +{ + struct impl *this = data; + + sbc_finish(&this->msbc); + plc_free(this->plc); + free(this); +} + +static int codec_get_block_size(void *data) +{ + return MSBC_BLOCK_SIZE; +} + +static int codec_start_encode (void *data, void *dst, size_t dst_size, + uint16_t seqnum, uint32_t timestamp) +{ + struct impl *this = data; + + this->seq = seqnum; + return 0; +} + +static int codec_encode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out, int *need_flush) +{ + struct impl *this = data; + ssize_t written = 0; + int res; + + if (src_size < MSBC_BLOCK_SIZE) + return -EINVAL; + if (dst_size < H2_PACKET_SIZE) + return -EINVAL; + + h2_write(dst, this->seq); + + res = sbc_encode(&this->msbc, src, src_size, SPA_PTROFF(dst, 2, void), H2_PACKET_SIZE - 3, &written); + if (res < 0) + return -EINVAL; + + *dst_out = H2_PACKET_SIZE; + *need_flush = NEED_FLUSH_ALL; + return res; +} + +static int codec_start_decode (void *data, + const void *src, size_t src_size, uint16_t *seqnum, uint32_t *timestamp) +{ + struct impl *this = data; + size_t consumed = 0; + + if (is_zero_packet(src, src_size)) + return -EINVAL; + + if (!this->data) + this->data = h2_reader_read(&this->h2, src, src_size, &consumed, &this->avail); + + if (seqnum) + *seqnum = this->h2.seq; + if (timestamp) + *timestamp = 0; + return consumed; +} + +static int codec_decode(void *data, + const void *src, size_t src_size, + void *dst, size_t dst_size, + size_t *dst_out) +{ + struct impl *this = data; + size_t consumed = 0; + int res; + + *dst_out = 0; + + if (!this->data) + this->data = h2_reader_read(&this->h2, src, src_size, &consumed, &this->avail); + if (!this->data) + return consumed; + + res = sbc_decode(&this->msbc, this->data, this->avail, dst, dst_size, dst_out); + this->data = NULL; + + if (res < 0) { + /* fail decoding silently, so remainder of packet is processed */ + spa_log_debug(log_, "decoding failed: %d", res); + return consumed; + } + + plc_rx(this->plc, dst, *dst_out / sizeof(int16_t)); + + return consumed; +} + +static int codec_produce_plc(void *data, void *dst, size_t dst_size) +{ + struct impl *this = data; + int res; + + if (dst_size < MSBC_BLOCK_SIZE) + return -EINVAL; + + res = plc_fillin(this->plc, dst, MSBC_BLOCK_SIZE / sizeof(int16_t)); + if (res < 0) + return res; + + return MSBC_BLOCK_SIZE; +} + +static void codec_set_log(struct spa_log *global_log) +{ + log_ = global_log; + spa_log_topic_init(log_, &codec_plugin_log_topic); +} + +const struct media_codec hfp_codec_msbc = { + .id = SPA_BLUETOOTH_AUDIO_CODEC_MSBC, + .kind = MEDIA_CODEC_HFP, + .codec_id = 0x02, + .enum_config = codec_enum_config, + .validate_config = codec_validate_config, + .init = codec_init, + .deinit = codec_deinit, + .get_block_size = codec_get_block_size, + .start_encode = codec_start_encode, + .encode = codec_encode, + .set_log = codec_set_log, + .start_decode = codec_start_decode, + .decode = codec_decode, + .produce_plc = codec_produce_plc, + .name = "msbc", + .description = "MSBC", + .stream_pkt = true, +}; + +MEDIA_CODEC_EXPORT_DEF( + "hfp-msbc", + &hfp_codec_msbc +); diff --git a/spa/plugins/bluez5/hfp-h2.h b/spa/plugins/bluez5/hfp-h2.h new file mode 100644 index 000000000..064d5249d --- /dev/null +++ b/spa/plugins/bluez5/hfp-h2.h @@ -0,0 +1,128 @@ +/* Spa HFP Codecs */ +/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_HFP_H2_H +#define SPA_BLUEZ5_HFP_H2_H + +#define H2_PACKET_SIZE 60 + +struct h2_reader { + uint8_t buf[H2_PACKET_SIZE]; + uint8_t pos; + bool msbc; + uint16_t seq; + bool started; +}; + +struct h2_writer { + uint8_t seq; +}; + +static inline void h2_reader_init(struct h2_reader *this, bool msbc) +{ + this->pos = 0; + this->msbc = msbc; + this->seq = 0; + this->started = false; +} + +static inline void h2_reader_append_byte(struct h2_reader *this, uint8_t byte) +{ + /* Parse H2 sync header */ + if (this->pos == 0) { + if (byte != 0x01) { + this->pos = 0; + return; + } + } else if (this->pos == 1) { + if (!((byte & 0x0F) == 0x08 && + ((byte >> 4) & 1) == ((byte >> 5) & 1) && + ((byte >> 6) & 1) == ((byte >> 7) & 1))) { + this->pos = 0; + return; + } + } else if (this->msbc) { + /* Beginning of MSBC frame: SYNCWORD + 2 nul bytes */ + if (this->pos == 2) { + if (byte != 0xAD) { + this->pos = 0; + return; + } + } + else if (this->pos == 3) { + if (byte != 0x00) { + this->pos = 0; + return; + } + } + else if (this->pos == 4) { + if (byte != 0x00) { + this->pos = 0; + return; + } + } + } + + if (this->pos >= H2_PACKET_SIZE) { + /* Packet completed. Reset. */ + this->pos = 0; + h2_reader_append_byte(this, byte); + return; + } + + this->buf[this->pos] = byte; + ++this->pos; +} + +static inline void *h2_reader_read(struct h2_reader *this, const uint8_t *src, size_t src_size, size_t *consumed, size_t *avail) +{ + int seq; + size_t i; + + for (i = 0; i < src_size && this->pos < H2_PACKET_SIZE; ++i) + h2_reader_append_byte(this, src[i]); + + *consumed = i; + *avail = 0; + + if (this->pos < H2_PACKET_SIZE) + return NULL; + + this->pos = 0; + + seq = ((this->buf[1] >> 4) & 1) | ((this->buf[1] >> 6) & 2); + if (!this->started) { + this->seq = seq; + this->started = true; + } + + this->seq++; + while (seq != this->seq % 4) + this->seq++; + + *avail = H2_PACKET_SIZE - 2; + return &this->buf[2]; +} + +static inline void h2_write(uint8_t *buf, uint8_t seq) +{ + static const uint8_t sntable[4] = { 0x08, 0x38, 0xc8, 0xf8 }; + + buf[0] = 0x01; + buf[1] = sntable[seq % 4]; + buf[59] = 0; +} + +static inline bool is_zero_packet(const uint8_t *data, size_t size) +{ + size_t i; + + for (i = 0; i < size; ++i) + if (data[i]) + return false; + + return true; +} + +#endif diff --git a/spa/plugins/bluez5/iso-io.c b/spa/plugins/bluez5/iso-io.c index 802961bb6..2f991b8ca 100644 --- a/spa/plugins/bluez5/iso-io.c +++ b/spa/plugins/bluez5/iso-io.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2023 Pauli Virtanen. */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -15,11 +17,11 @@ #include #include -#include "config.h" #include "iso-io.h" #include "media-codecs.h" #include "defs.h" +#include "decode-buffer.h" SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.iso"); #undef SPA_LOG_TOPIC_DEFAULT @@ -30,8 +32,29 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.iso"); #define IDLE_TIME (500 * SPA_NSEC_PER_MSEC) #define EMPTY_BUF_SIZE 65536 -#define LATENCY_PERIOD (200 * SPA_NSEC_PER_MSEC) -#define MAX_PACKET_QUEUE 3 +#define LATENCY_PERIOD (1000 * SPA_NSEC_PER_MSEC) +#define MAX_LATENCY (50 * SPA_NSEC_PER_MSEC) + +#define CLOCK_SYNC_AVG_PERIOD (500 * SPA_NSEC_PER_MSEC) +#define CLOCK_SYNC_RATE_DIFF_MAX 0.005 + +#define ISO_BUFFERING_AVG_PERIOD (50 * SPA_NSEC_PER_MSEC) +#define ISO_BUFFERING_RATE_DIFF_MAX 0.05 + +struct clock_sync { + /** Reference monotonic time for streams in the group */ + int64_t base_time; + + /** Average error for current cycle */ + int64_t avg_err; + unsigned int avg_num; + + /** Log rate limiting */ + uint64_t log_pos; + + /** Rate matching ISO clock to monotonic clock */ + struct spa_bt_rate_control dll; +}; struct group { struct spa_log *log; @@ -41,9 +64,13 @@ struct group { struct spa_list streams; int timerfd; uint8_t id; - uint64_t next; - uint64_t duration; + int64_t next; + int64_t duration_tx; + int64_t duration_rx; + bool flush; bool started; + + struct clock_sync rx_sync; }; struct stream { @@ -60,6 +87,14 @@ struct stream { uint32_t block_size; struct spa_bt_latency tx_latency; + + struct spa_bt_decode_buffer *source_buf; + + /** Stream packet sequence number, relative to group::rx_sync */ + int64_t rx_pos; + + /** Current graph clock position */ + uint64_t position; }; struct modify_info @@ -85,7 +120,7 @@ static void stream_link(struct group *group, struct stream *stream) struct modify_info info = { .stream = stream, .streams = &group->streams }; int res; - res = spa_loop_invoke(group->data_loop, do_modify, 0, NULL, 0, true, &info); + res = spa_loop_locked(group->data_loop, do_modify, 0, NULL, 0, &info); spa_assert_se(res == 0); } @@ -94,7 +129,7 @@ static void stream_unlink(struct stream *stream) struct modify_info info = { .stream = stream, .streams = NULL }; int res; - res = spa_loop_invoke(stream->group->data_loop, do_modify, 0, NULL, 0, true, &info); + res = spa_loop_locked(stream->group->data_loop, do_modify, 0, NULL, 0, &info); spa_assert_se(res == 0); } @@ -146,11 +181,11 @@ static uint64_t get_time_ns(struct spa_system *system, clockid_t clockid) static int set_timers(struct group *group) { - if (group->duration == 0) + if (group->duration_tx == 0) return -EINVAL; - group->next = SPA_ROUND_UP(get_time_ns(group->data_system, CLOCK_MONOTONIC) + group->duration, - group->duration); + group->next = SPA_ROUND_UP(get_time_ns(group->data_system, CLOCK_MONOTONIC) + group->duration_tx, + group->duration_tx); return set_timeout(group, group->next); } @@ -164,6 +199,57 @@ static void drop_rx(int fd) } while (res >= 0); } +static bool group_latency_check(struct group *group) +{ + struct stream *stream; + int32_t min_latency = INT32_MAX, max_latency = INT32_MIN; + unsigned int kernel_queue = UINT_MAX; + + spa_list_for_each(stream, &group->streams, link) { + if (!stream->sink) + continue; + if (!stream->tx_latency.enabled) + return false; + + if (kernel_queue == UINT_MAX) + kernel_queue = stream->tx_latency.kernel_queue; + + if (group->flush && stream->tx_latency.queue) { + spa_log_debug(group->log, "%p: ISO group:%d latency skip: flushing", + group, group->id); + return true; + } + if (stream->tx_latency.kernel_queue != kernel_queue) { + /* Streams out of sync, try to correct if it persists */ + spa_log_debug(group->log, "%p: ISO group:%d latency skip: imbalance", + group, group->id); + group->flush = true; + return true; + } + } + + group->flush = false; + + spa_list_for_each(stream, &group->streams, link) { + if (!stream->sink) + continue; + if (!stream->tx_latency.valid) + return false; + + min_latency = SPA_MIN(min_latency, stream->tx_latency.ptp.min); + max_latency = SPA_MAX(max_latency, stream->tx_latency.ptp.max); + } + + if (max_latency > MAX_LATENCY) { + spa_log_debug(group->log, "%p: ISO group:%d latency skip: latency %d ms", + group, group->id, (int)(max_latency / SPA_NSEC_PER_MSEC)); + group->flush = true; + return true; + } + + return false; +} + static void group_on_timeout(struct spa_source *source) { struct group *group = source->data; @@ -179,6 +265,8 @@ static void group_on_timeout(struct spa_source *source) group, group->id, spa_strerror(res)); return; } + if (!exp) + return; spa_list_for_each(stream, &group->streams, link) { if (!stream->sink) { @@ -200,12 +288,16 @@ static void group_on_timeout(struct spa_source *source) group->started = true; } + if (group_latency_check(group)) { + spa_list_for_each(stream, &group->streams, link) + spa_bt_latency_reset(&stream->tx_latency); + goto done; + } + /* Produce output */ spa_list_for_each(stream, &group->streams, link) { int res = 0; uint64_t now; - int32_t min_latency = INT32_MAX, max_latency = INT32_MIN; - struct stream *other; if (!stream->sink) continue; @@ -223,44 +315,21 @@ static void group_on_timeout(struct spa_source *source) } } - spa_list_for_each(other, &group->streams, link) { - if (!other->sink || stream == other || !other->tx_latency.valid) - continue; - min_latency = SPA_MIN(min_latency, other->tx_latency.ptp.min); - max_latency = SPA_MAX(max_latency, other->tx_latency.ptp.max); - } - - if (stream->tx_latency.valid && min_latency <= max_latency && - stream->tx_latency.ptp.min > min_latency + (int64_t)group->duration/2 && - stream->tx_latency.ptp.max > max_latency + (int64_t)group->duration/2) { - spa_log_debug(group->log, "%p: ISO group:%u latency skip align fd:%d", group, group->id, stream->fd); - spa_bt_latency_reset(&stream->tx_latency); - goto stream_done; - } - - /* TODO: this should use rate match */ - if (stream->tx_latency.valid && - stream->tx_latency.ptp.min > MAX_PACKET_QUEUE * (int64_t)group->duration) { - spa_log_debug(group->log, "%p: ISO group:%u latency skip fd:%d", group, group->id, stream->fd); - spa_bt_latency_reset(&stream->tx_latency); - goto stream_done; - } - now = get_time_ns(group->data_system, CLOCK_REALTIME); - res = send(stream->fd, stream->this.buf, stream->this.size, MSG_DONTWAIT | MSG_NOSIGNAL); + res = spa_bt_send(stream->fd, stream->this.buf, stream->this.size, + &stream->tx_latency, now); if (res < 0) { res = -errno; fail = true; - } else { - spa_bt_latency_sent(&stream->tx_latency, now); + group->flush = true; } - stream_done: - spa_log_trace(group->log, "%p: ISO group:%u sent fd:%d size:%u ts:%u idle:%d res:%d latency:%d..%d us", + spa_log_trace(group->log, "%p: ISO group:%u sent fd:%d size:%u ts:%u idle:%d res:%d latency:%d..%d%sus queue:%u", group, group->id, stream->fd, (unsigned)stream->this.size, (unsigned)stream->this.timestamp, stream->idle, res, - stream->tx_latency.valid ? stream->tx_latency.ptp.min/1000 : -1, - stream->tx_latency.valid ? stream->tx_latency.ptp.max/1000 : -1); + stream->tx_latency.ptp.min/1000, stream->tx_latency.ptp.max/1000, + stream->tx_latency.valid ? " " : "* ", + stream->tx_latency.queue); stream->this.size = 0; } @@ -268,8 +337,9 @@ static void group_on_timeout(struct spa_source *source) if (fail) spa_log_debug(group->log, "%p: ISO group:%d send failure", group, group->id); +done: /* Pull data for the next interval */ - group->next += exp * group->duration; + group->next += exp * group->duration_tx; spa_list_for_each(stream, &group->streams, link) { if (!stream->sink) @@ -315,7 +385,6 @@ static struct group *group_create(struct spa_bt_transport *t, group->log = log; group->data_loop = data_loop; group->data_system = data_system; - group->duration = 0; spa_list_init(&group->streams); @@ -357,7 +426,7 @@ static void group_destroy(struct group *group) spa_assert(spa_list_is_empty(&group->streams)); - res = spa_loop_invoke(group->data_loop, do_remove_source, 0, NULL, 0, true, group); + res = spa_loop_locked(group->data_loop, do_remove_source, 0, NULL, 0, group); spa_assert_se(res == 0); close(group->timerfd); @@ -372,52 +441,56 @@ static struct stream *stream_create(struct spa_bt_transport *t, struct group *gr struct spa_audio_info format = { 0 }; int res; bool sink; + int64_t interval, *duration; if (t->profile == SPA_BT_PROFILE_BAP_SINK || t->profile == SPA_BT_PROFILE_BAP_BROADCAST_SINK) { sink = true; + duration = &group->duration_tx; } else { sink = false; + duration = &group->duration_rx; } - if (!t->media_codec->bap || !t->media_codec->get_interval) { + if (t->media_codec->kind != MEDIA_CODEC_BAP || !t->media_codec->get_interval) { res = -EINVAL; goto fail; } - if (sink) { - uint64_t interval; + res = t->media_codec->validate_config(t->media_codec, 0, t->configuration, t->configuration_len, &format); + if (res < 0) + goto fail; - res = t->media_codec->validate_config(t->media_codec, 0, t->configuration, t->configuration_len, &format); - if (res < 0) - goto fail; + codec_data = t->media_codec->init(t->media_codec, 0, t->configuration, t->configuration_len, + &format, NULL, t->write_mtu); + if (!codec_data) { + res = -EINVAL; + goto fail; + } - codec_data = t->media_codec->init(t->media_codec, 0, t->configuration, t->configuration_len, - &format, NULL, t->write_mtu); - if (!codec_data) { - res = -EINVAL; - goto fail; - } + block_size = t->media_codec->get_block_size(codec_data); + if (block_size < 0 || block_size > EMPTY_BUF_SIZE) { + res = -EINVAL; + goto fail; + } - block_size = t->media_codec->get_block_size(codec_data); - if (block_size < 0 || block_size > EMPTY_BUF_SIZE) { - res = -EINVAL; - goto fail; - } + interval = t->media_codec->get_interval(codec_data); + if (interval <= 5000) { + res = -EINVAL; + goto fail; + } - interval = t->media_codec->get_interval(codec_data); - if (interval <= 5000) { - res = -EINVAL; - goto fail; - } + if (*duration == 0) { + *duration = interval; + } else if (interval != *duration) { + /* SDU_Interval in ISO group must be same for each direction */ + res = -EINVAL; + goto fail; + } - if (group->duration == 0) { - group->duration = interval; - } else if (interval != group->duration) { - /* SDU_Interval in ISO group must be same for each direction */ - res = -EINVAL; - goto fail; - } + if (!sink) { + t->media_codec->deinit(codec_data); + codec_data = NULL; } stream = calloc(1, sizeof(struct stream)); @@ -427,14 +500,14 @@ static struct stream *stream_create(struct spa_bt_transport *t, struct group *gr stream->fd = t->fd; stream->sink = sink; stream->group = group; - stream->this.duration = sink ? group->duration : 0; + stream->this.duration = *duration; stream->codec = t->media_codec; stream->this.codec_data = codec_data; stream->this.format = format; stream->block_size = block_size; - spa_bt_latency_init(&stream->tx_latency, stream->fd, LATENCY_PERIOD, group->log); + spa_bt_latency_init(&stream->tx_latency, t, LATENCY_PERIOD, group->log); if (sink) stream_silence(stream); @@ -540,12 +613,10 @@ void spa_bt_iso_io_set_cb(struct spa_bt_iso_io *this, spa_bt_iso_io_pull_t pull, } stream->idle = true; - stream->this.resync = true; - if (pull == NULL) { - stream->this.size = 0; - return; - } + stream->this.resync = true; + stream->this.size = 0; + stream->this.now = stream->group->next; } /** Must be called from data thread */ @@ -556,3 +627,235 @@ int spa_bt_iso_io_recv_errqueue(struct spa_bt_iso_io *this) return spa_bt_latency_recv_errqueue(&stream->tx_latency, stream->fd, group->log); } + +/** + * Set decode buffer used by a stream when it has packet RX. Set to NULL when stream is + * inactive. + * + * Must be called from data thread. + */ +void spa_bt_iso_io_set_source_buffer(struct spa_bt_iso_io *this, struct spa_bt_decode_buffer *buffer) +{ + struct stream *stream = SPA_CONTAINER_OF(this, struct stream, this); + struct group *group = stream->group; + struct clock_sync *sync = &group->rx_sync; + + spa_zero(sync->dll); + + stream->source_buf = buffer; + if (buffer) { + /* Take over buffer overrun handling */ + buffer->no_overrun_drop = true; + buffer->buffering = false; + buffer->avg_period = ISO_BUFFERING_AVG_PERIOD; + buffer->rate_diff_max = ISO_BUFFERING_RATE_DIFF_MAX; + } +} + +/** + * Get automatic group-wide stream RX target latency. This is useful only for BAP Client. + * BAP Server target latency is determined by the presentation delay. + * + * Must be called from data thread. + */ +int32_t spa_bt_iso_io_get_source_target_latency(struct spa_bt_iso_io *this) +{ + struct stream *stream = SPA_CONTAINER_OF(this, struct stream, this); + struct group *group = stream->group; + struct stream *s; + int32_t latency = 0; + + if (!stream->source_buf) + return 0; + + spa_list_for_each(s, &group->streams, link) + if (s->source_buf) + latency = SPA_MAX(latency, spa_bt_decode_buffer_get_auto_latency(s->source_buf)); + + return latency; +} + +/** + * Called on stream packet RX with packet monotonic timestamp. + * + * Returns the logical SDU reference time, with respect to which decode-buffer should + * target its fill level. This is needed so that all streams converge to same latency + * (with sub-sample accuracy needed for eg. stereo stream alignment). + * + * Determines the ISO group clock rate matching from individual stream packet RX times. + * Packet arrival time is decomposed to + * + * now = group::rx_sync::base_time + stream::rx_pos * group::duration_rx + err + * + * Clock rate matching is done by drifting base_time by the rate difference, so that `err` + * is zero on average across different streams. If stream's rx_pos appears to be out of + * sync, it is resynchronized to a new position. + * + * The logical SDU timestamps for different streams are aligned and occur at equal + * intervals, but the RX timestamp `now` we actually get here is a software timestamp + * indicating when packet was received by kernel. In practice, they are not equally spaced + * but are approximately aligned between different streams. + * + * The Core v6.1 specification does **not** provide any way to synchronize Controller and + * Host clocks, so we can attempt to sync to ISO clock only based on the RX timestamps. + * + * Because the actual packet RX times are not equally spaced, it's ambiguous what the + * logical SDU reference time is. It's then impossible to achieve clock synchronization with + * better accuracy than this jitter (on Intel AX210 it's several ms jitter in a regular + * pattern, plus some random noise). + * + * Aligned playback for different devices cannot be implemented with the tools provided in + * the specification. Some implementation-defined clock synchronization mechanism is + * needed, but kernel (6.17) doesn't have anything and it's not clear such vendor-defined + * mechanisms exist over USB. + * + * The HW timestamps on packets do not help with this, as they are in controller's clock + * domain. They are only useful for aligning packets from different streams. They are also + * optional in the specification and controllers don't necessarily implement them. They + * are not used here. + * + * Must be called from data thread. + */ +int64_t spa_bt_iso_io_recv(struct spa_bt_iso_io *this, int64_t now) +{ + struct stream *stream = SPA_CONTAINER_OF(this, struct stream, this); + struct group *group = stream->group; + struct clock_sync *sync = &group->rx_sync; + struct stream *s; + bool resync = false; + int64_t err, t; + + spa_assert(stream->source_buf); + + if (sync->dll.corr == 0) { + sync->base_time = now; + spa_bt_rate_control_init(&sync->dll, 0); + } + + stream->rx_pos++; + t = sync->base_time + group->duration_rx * stream->rx_pos; + err = now - t; + + if (SPA_ABS(err) > group->duration_rx) { + resync = true; + spa_log_debug(group->log, "%p: ISO rx-resync large group:%u fd:%d", + group, group->id, stream->fd); + } + + spa_list_for_each(s, &group->streams, link) { + if (s == stream || !s->source_buf) + continue; + if (SPA_ABS(now - s->source_buf->rx.nsec) < group->duration_rx / 2 && + stream->rx_pos != s->rx_pos) { + spa_log_debug(group->log, "%p: ISO rx-resync balance group:%u fd:%d fd:%d", + group, group->id, stream->fd, s->fd); + resync = true; + break; + } + } + + if (resync) { + stream->rx_pos = (now - sync->base_time + group->duration_rx/2) / group->duration_rx; + t = sync->base_time + group->duration_rx * stream->rx_pos; + err = now - t; + spa_log_debug(group->log, "%p: ISO rx-resync group:%u fd:%d err:%"PRIi64, + group, group->id, stream->fd, err); + } + + sync->avg_err = (sync->avg_err * sync->avg_num + err) / (sync->avg_num + 1); + sync->avg_num++; + + return t; +} + +/** + * Call at end of stream process(), after consuming data. + * + * Apply ISO clock rate matching. + * + * Realign stream RX to target latency, if it is too far off, so that rate matching + * converges faster to alignment. + * + * Must be called from data thread + */ +void spa_bt_iso_io_check_rx_sync(struct spa_bt_iso_io *this, uint64_t position) +{ + struct stream *stream = SPA_CONTAINER_OF(this, struct stream, this); + struct group *group = stream->group; + struct stream *s; + const int64_t max_err = group->duration_rx; + struct clock_sync *sync = &group->rx_sync; + int32_t target; + bool overrun = false; + double corr; + + if (!stream->source_buf) + return; + + /* Check sync after all input streams have completed process() on same cycle */ + stream->position = position; + spa_list_for_each(s, &group->streams, link) { + if (!s->source_buf) + continue; + if (s->position != stream->position) + return; + } + + target = stream->source_buf->target; + + /* Rate match ISO clock */ + corr = spa_bt_rate_control_update(&sync->dll, sync->avg_err, 0, + group->duration_rx, CLOCK_SYNC_AVG_PERIOD, CLOCK_SYNC_RATE_DIFF_MAX); + sync->base_time += (int64_t)(group->duration_rx * (corr - 1)); + + enum spa_log_level log_level = (sync->log_pos > SPA_NSEC_PER_SEC) ? SPA_LOG_LEVEL_DEBUG + : SPA_LOG_LEVEL_TRACE; + if (SPA_UNLIKELY(spa_log_level_topic_enabled(group->log, SPA_LOG_TOPIC_DEFAULT, log_level))) { + spa_log_lev(group->log, log_level, + "%p: ISO rx-sync group:%u base:%"PRIi64" avg:%g err:%"PRIi64" corr:%g", + group, group->id, sync->base_time, sync->dll.avg, sync->avg_err, corr-1); + sync->log_pos = 0; + } + sync->log_pos += stream->source_buf->duration_ns; + + sync->avg_err = 0; + sync->avg_num = 0; + + /* Handle overrun (e.g. resyncs streams after initial buffering) */ + spa_list_for_each(s, &group->streams, link) { + if (s->source_buf) { + double level = s->source_buf->level; + int max_level = target + max_err * s->source_buf->rate / SPA_NSEC_PER_SEC; + + if (level > max_level) + overrun = true; + } + } + + if (!overrun) + return; + + spa_list_for_each(s, &group->streams, link) { + if (!s->source_buf) + continue; + + int32_t level = (int32_t)s->source_buf->level; + + if (level > target) { + uint32_t drop = (level - target) * s->source_buf->frame_size; + uint32_t avail = spa_bt_decode_buffer_get_size(s->source_buf); + + drop = SPA_MIN(drop, avail); + + spa_log_debug(group->log, "%p: ISO overrun group:%u fd:%d level:%f target:%d drop:%u", + group, group->id, s->fd, + s->source_buf->level, + target, + drop/s->source_buf->frame_size); + + spa_bt_decode_buffer_read(s->source_buf, drop); + } + + spa_bt_decode_buffer_recover(s->source_buf); + } +} diff --git a/spa/plugins/bluez5/iso-io.h b/spa/plugins/bluez5/iso-io.h index ed49c77c1..c59ebc075 100644 --- a/spa/plugins/bluez5/iso-io.h +++ b/spa/plugins/bluez5/iso-io.h @@ -11,6 +11,7 @@ #include #include +struct spa_bt_decode_buffer; struct spa_bt_transport; /** @@ -46,4 +47,9 @@ void spa_bt_iso_io_destroy(struct spa_bt_iso_io *io); void spa_bt_iso_io_set_cb(struct spa_bt_iso_io *io, spa_bt_iso_io_pull_t pull, void *user_data); int spa_bt_iso_io_recv_errqueue(struct spa_bt_iso_io *io); +void spa_bt_iso_io_set_source_buffer(struct spa_bt_iso_io *io, struct spa_bt_decode_buffer *buffer); +int32_t spa_bt_iso_io_get_source_target_latency(struct spa_bt_iso_io *io); +void spa_bt_iso_io_check_rx_sync(struct spa_bt_iso_io *io, uint64_t position); +int64_t spa_bt_iso_io_recv(struct spa_bt_iso_io *io, int64_t now); + #endif diff --git a/spa/plugins/bluez5/media-codecs.c b/spa/plugins/bluez5/media-codecs.c index 718e54ea2..bd24d08f8 100644 --- a/spa/plugins/bluez5/media-codecs.c +++ b/spa/plugins/bluez5/media-codecs.c @@ -81,6 +81,9 @@ bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_ uint8_t config[A2DP_MAX_CAPS_SIZE]; int res; + if (codec->kind == MEDIA_CODEC_HFP) + return true; + if (codec_id != codec->codec_id) return false; @@ -91,7 +94,7 @@ bool media_codec_check_caps(const struct media_codec *codec, unsigned int codec_ if (res < 0) return false; - if (codec->bap) + if (codec->kind == MEDIA_CODEC_BAP) return true; else return ((size_t)res == caps_size); diff --git a/spa/plugins/bluez5/media-codecs.h b/spa/plugins/bluez5/media-codecs.h index c9a00f7bf..4d2827e40 100644 --- a/spa/plugins/bluez5/media-codecs.h +++ b/spa/plugins/bluez5/media-codecs.h @@ -26,7 +26,7 @@ #define SPA_TYPE_INTERFACE_Bluez5CodecMedia SPA_TYPE_INFO_INTERFACE_BASE "Bluez5:Codec:Media:Private" -#define SPA_VERSION_BLUEZ5_CODEC_MEDIA 12 +#define SPA_VERSION_BLUEZ5_CODEC_MEDIA 15 struct spa_bluez5_codec_a2dp { struct spa_interface iface; @@ -60,6 +60,13 @@ enum { NEED_FLUSH_FRAGMENT = 2, }; +enum media_codec_kind { + MEDIA_CODEC_A2DP, + MEDIA_CODEC_BAP, + MEDIA_CODEC_ASHA, + MEDIA_CODEC_HFP, +}; + struct media_codec_audio_info { uint32_t rate; uint32_t channels; @@ -67,12 +74,11 @@ struct media_codec_audio_info { struct media_codec { enum spa_bluetooth_audio_codec id; + enum media_codec_kind kind; + uint8_t codec_id; a2dp_vendor_codec_t vendor; - bool bap; - bool asha; - const char *name; const char *description; const char *endpoint_name; /**< Endpoint name. If NULL, same as name */ @@ -82,6 +88,10 @@ struct media_codec { const struct media_codec *duplex_codec; /**< Codec for non-standard A2DP duplex channel */ + const bool stream_pkt; /**< If true, socket data may contain multiple packets. + * After successful decode, start_decode() should be + * called again to parse the remaining data. */ + int (*get_bis_config)(const struct media_codec *codec, uint8_t *caps, uint8_t *caps_size, struct spa_dict *settings, struct bap_codec_qos *qos); @@ -103,7 +113,7 @@ struct media_codec { int (*get_qos)(const struct media_codec *codec, const void *config, size_t config_size, const struct bap_endpoint_qos *endpoint_qos, - struct bap_codec_qos *qos); + struct bap_codec_qos *qos, const struct spa_dict *settings); /** qsort comparison sorting caps in order of preference for the codec. * Used in codec switching to select best remote endpoints. @@ -196,6 +206,16 @@ struct media_codec { void *dst, size_t dst_size, size_t *dst_out); + /** + * Generate audio data corresponding to one lost packet, using codec internal + * packet loss concealment. + * + * NULL if not available. + * + * \return number of bytes produced, or < 0 for error + */ + int (*produce_plc) (void *data, void *dst, size_t dst_size); + int (*reduce_bitpool) (void *data); int (*increase_bitpool) (void *data); @@ -219,6 +239,21 @@ struct media_codec_config { unsigned int priority; }; +static inline const char *media_codec_kind_str(const struct media_codec *codec) +{ + switch (codec->kind) { + case MEDIA_CODEC_A2DP: + return "A2DP"; + case MEDIA_CODEC_BAP: + return "BAP"; + case MEDIA_CODEC_ASHA: + return "ASHA"; + case MEDIA_CODEC_HFP: + return "HFP"; + } + return "unknown"; +} + int media_codec_select_config(const struct media_codec_config configs[], size_t n, uint32_t cap, int preferred_value); diff --git a/spa/plugins/bluez5/media-sink.c b/spa/plugins/bluez5/media-sink.c index 49dc5de79..bac1e84ab 100644 --- a/spa/plugins/bluez5/media-sink.c +++ b/spa/plugins/bluez5/media-sink.c @@ -58,7 +58,7 @@ struct props { #define MIN_BUFFERS 3 #define MAX_BUFFERS 32 #define BUFFER_SIZE (8192*8) -#define RATE_CTL_DIFF_MAX 0.005 +#define RATE_CTL_DIFF_MAX 0.01 #define LATENCY_PERIOD (200 * SPA_NSEC_PER_MSEC) /* Wait for two cycles before trying to sync ISO. On start/driver reassign, @@ -104,6 +104,23 @@ struct port { struct spa_bt_rate_control ratectl; }; +#define ASHA_ENCODED_PKT_SZ 161 /* 160 bytes encoded + 1 byte sequence number */ +#define ASHA_CONN_INTERVAL (20 * SPA_NSEC_PER_MSEC) + +struct spa_bt_asha { + struct spa_source flush_source; + struct spa_source timer_source; + int timerfd; + + uint8_t buf[512]; + + uint64_t ref_t0; + uint64_t next_time; + + unsigned int flush_pending:1; + unsigned int set_timer:1; +}; + struct impl { struct spa_handle handle; struct spa_node node; @@ -158,6 +175,7 @@ struct impl { uint64_t process_time; uint64_t process_duration; uint64_t process_rate; + double process_rate_diff; uint64_t prev_flush_time; uint64_t next_flush_time; @@ -182,15 +200,43 @@ struct impl { uint32_t header_size; uint32_t block_count; uint16_t seqnum; + uint64_t last_seqnum; uint32_t timestamp; uint64_t sample_count; uint8_t tmp_buffer[BUFFER_SIZE]; uint32_t tmp_buffer_used; uint32_t fd_buffer_size; + uint32_t silence_frames; + + struct spa_bt_asha *asha; + struct spa_list asha_link; + + struct spa_bt_latency tx_latency; }; #define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0) +static struct spa_list asha_sinks = SPA_LIST_INIT(&asha_sinks); + +static void drop_frames(struct impl *this, uint32_t req); +static uint64_t get_reference_time(struct impl *this, uint64_t *duration_ns_ret); + +static struct impl *find_other_asha(struct impl *this) +{ + struct impl *other; + + spa_list_for_each(other, &asha_sinks, asha_link) { + if (this == other) + continue; + + if (this->transport->hisyncid == other->transport->hisyncid) { + return other; + } + } + + return NULL; +} + static void reset_props(struct impl *this, struct props *props) { props->latency_offset = 0; @@ -299,6 +345,52 @@ static int set_timers(struct impl *this) return set_timeout(this, this->following ? 0 : this->next_time); } +static int set_asha_timeout(struct impl *this, uint64_t time) +{ + struct itimerspec ts; + + ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; + ts.it_interval.tv_sec = 0; + ts.it_interval.tv_nsec = 0; + + return spa_system_timerfd_settime(this->data_system, + this->asha->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); +} + +static int set_asha_timer(struct impl *this, struct impl *other) +{ + uint64_t time; + + if (other) { + /* Try to line up our timer with the other side, and drop samples so we're sending + * the same sample position on both sides */ + uint64_t other_samples = (get_reference_time(other, NULL) - other->asha->ref_t0) * + this->port.current_format.info.raw.rate / SPA_NSEC_PER_SEC; + + if (other->asha->next_time < this->process_time) { + /* Other side has not yet been scheduled in this graph cycle, we expect + * there might be one packet left from the previous cycle at most */ + time = other->asha->next_time + ASHA_CONN_INTERVAL; + other_samples += ASHA_CONN_INTERVAL * + this->port.current_format.info.raw.rate / SPA_NSEC_PER_SEC; + } else { + /* Other side has set up its next cycle, catch up */ + time = other->asha->next_time; + } + + /* Since the quantum and packet size aren't correlated, drop any samples from this + * cycle that might have been used to send a packet starting in the previous cycle */ + drop_frames(this, other_samples % this->process_duration); + } else { + time = this->process_time; + } + + this->asha->next_time = time; + + return set_asha_timeout(this, this->asha->next_time); +} + static inline bool is_following(struct impl *this) { return this->position && this->clock && this->position->clock.id != this->clock->id; @@ -362,7 +454,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) } if (this->started) { - spa_loop_invoke(this->data_loop, do_reassign_io, 0, NULL, 0, true, &info); + spa_loop_locked(this->data_loop, do_reassign_io, 0, NULL, 0, &info); } else { this->clock = info.clock; this->position = info.position; @@ -403,7 +495,13 @@ static void set_latency(struct impl *this, bool emit_latency) port->latency.min_ns = port->latency.max_ns = delay; port->latency.min_rate = port->latency.max_rate = 0; - port->latency.min_quantum = port->latency.max_quantum = 0.0f; + + if (this->codec->kind == MEDIA_CODEC_BAP) { + /* ISO has different delay */ + port->latency.min_quantum = port->latency.max_quantum = 1.0f; + } else { + port->latency.min_quantum = port->latency.max_quantum = 0.0f; + } spa_log_info(this->log, "%p: total latency:%d ms", this, (int)(delay / SPA_NSEC_PER_MSEC)); @@ -507,6 +605,8 @@ static uint32_t get_queued_frames(struct impl *this) else bytes = 0; + bytes += this->silence_frames * this->block_size; + /* Count (partially) encoded packet */ bytes += this->tmp_buffer_used; bytes += this->block_count * this->block_size; @@ -517,16 +617,19 @@ static uint32_t get_queued_frames(struct impl *this) static uint64_t get_reference_time(struct impl *this, uint64_t *duration_ns_ret) { struct port *port = &this->port; - uint64_t t, duration_ns; + uint64_t duration_ns; + int64_t t; bool resampling; if (!this->process_rate || !this->process_duration) { if (this->position) { this->process_duration = this->position->clock.duration; this->process_rate = this->position->clock.rate.denom; + this->process_rate_diff = this->position->clock.rate_diff; } else { this->process_duration = 1024; this->process_rate = 48000; + this->process_rate_diff = 1.0; } } @@ -535,7 +638,7 @@ static uint64_t get_reference_time(struct impl *this, uint64_t *duration_ns_ret) *duration_ns_ret = duration_ns; /* Time at the first sample in the current packet. */ - t = this->process_time + duration_ns; + t = duration_ns; t -= ((uint64_t)get_queued_frames(this) * SPA_NSEC_PER_SEC / port->current_format.info.raw.rate); @@ -546,7 +649,13 @@ static uint64_t get_reference_time(struct impl *this, uint64_t *duration_ns_ret) / port->current_format.info.raw.rate; } - return t; + if (this->process_rate_diff > 0) + t = (int64_t)(t / this->process_rate_diff); + + if (this->transport && this->transport->iso_io && this->transport->iso_io->size) + t -= this->transport->iso_io->duration; + + return this->process_time + t; } static int reset_buffer(struct impl *this) @@ -559,8 +668,12 @@ static int reset_buffer(struct impl *this) this->need_flush = 0; this->block_count = 0; this->fragment = false; - this->timestamp = this->codec->bap ? (get_reference_time(this, NULL) / SPA_NSEC_PER_USEC) - : this->sample_count; + + if (this->codec->kind == MEDIA_CODEC_BAP || this->codec->kind == MEDIA_CODEC_ASHA) + this->timestamp = get_reference_time(this, NULL) / SPA_NSEC_PER_USEC; + else + this->timestamp = this->sample_count; + this->buffer_used = this->codec->start_encode(this->codec_data, this->buffer, sizeof(this->buffer), ++this->seqnum, this->timestamp); @@ -578,36 +691,62 @@ static int setup_matching(struct impl *this) if (port->rate_match) { port->rate_match->rate = 1 / port->ratectl.corr; + /* We rate match in the system clock domain. If driver ticks at a + * different rate, we as follower must compensate. + */ + if (this->following && SPA_LIKELY(this->position && + this->position->clock.rate_diff > 0)) + port->rate_match->rate /= this->position->clock.rate_diff; + SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->following); } return 0; } -static int get_transport_unused_size(struct impl *this) +static int get_transport_unsent_size(struct impl *this) { int res, value; - res = ioctl(this->flush_source.fd, TIOCOUTQ, &value); - if (res < 0) { - spa_log_error(this->log, "%p: ioctl fail: %m", this); - return -errno; + + if (this->tx_latency.enabled) { + res = 0; + value = this->tx_latency.unsent; + } else if (this->codec->kind == MEDIA_CODEC_HFP) { + value = 0; + } else { + res = ioctl(this->flush_source.fd, TIOCOUTQ, &value); + if (res < 0) { + spa_log_error(this->log, "%p: ioctl fail: %m", this); + return -errno; + } + if ((unsigned int)value > this->fd_buffer_size) + return -EIO; + value = this->fd_buffer_size - value; } - spa_log_trace(this->log, "%p: fd unused buffer size:%d/%d", this, value, this->fd_buffer_size); + + spa_log_trace(this->log, "%p: fd unsent size:%d/%d", this, value, this->fd_buffer_size); return value; } static int send_buffer(struct impl *this) { int written, unsent; + struct timespec ts_pre; - unsent = get_transport_unused_size(this); - if (unsent >= 0) { - unsent = this->fd_buffer_size - unsent; - this->codec->abr_process(this->codec_data, unsent); + if (this->codec->abr_process) { + unsent = get_transport_unsent_size(this); + if (unsent >= 0) + this->codec->abr_process(this->codec_data, unsent); } - written = send(this->flush_source.fd, this->buffer, - this->buffer_used, MSG_DONTWAIT | MSG_NOSIGNAL); + spa_system_clock_gettime(this->data_system, CLOCK_REALTIME, &ts_pre); + + if (this->codec->kind == MEDIA_CODEC_HFP) { + written = spa_bt_sco_io_write(this->transport->sco_io, this->buffer, this->buffer_used); + } else { + written = spa_bt_send(this->flush_source.fd, this->buffer, this->buffer_used, + &this->tx_latency, SPA_TIMESPEC_TO_NSEC(&ts_pre)); + } if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) { struct timespec ts; @@ -763,17 +902,21 @@ static void enable_flush_timer(struct impl *this, bool enabled) static int flush_data(struct impl *this, uint64_t now_time) { - int written; - uint32_t total_frames; struct port *port = &this->port; - int unused_buffer; + bool is_asha = this->codec->kind == MEDIA_CODEC_ASHA; + bool is_sco = this->codec->kind == MEDIA_CODEC_HFP; + uint32_t total_frames; + int written; + int unsent_buffer; spa_assert(this->transport_started); /* I/O in error state? */ - if (this->transport == NULL || !this->flush_source.loop) + if (this->transport == NULL || (!this->flush_source.loop && !is_asha && !is_sco)) return -EIO; - if (!this->flush_timer_source.loop && !this->transport->iso_io) + if (!this->flush_timer_source.loop && !this->transport->iso_io && !is_asha) + return -EIO; + if (!this->transport->sco_io && is_sco) return -EIO; if (this->transport->iso_io && !this->iso_pending) @@ -791,6 +934,21 @@ again: return res; } } + + while (this->silence_frames && !this->need_flush) { + static const uint8_t empty[1024] = {}; + uint32_t avail = SPA_MIN(this->silence_frames, sizeof(empty) / port->frame_size) + * port->frame_size; + + written = add_data(this, empty, avail); + if (written <= 0) + break; + + this->silence_frames -= written / port->frame_size; + spa_log_trace(this->log, "%p: written %d silence frames", this, + written / port->frame_size); + } + while (!spa_list_is_empty(&port->ready) && !this->need_flush) { uint8_t *src; uint32_t n_bytes, n_frames; @@ -853,6 +1011,7 @@ again: if (this->need_flush) { size_t avail = SPA_MIN(this->buffer_used, sizeof(iso_io->buf)); + uint64_t delay = 0; spa_log_trace(this->log, "%p: ISO put fd:%d size:%u sn:%u ts:%u now:%"PRIu64, this, this->transport->fd, (unsigned)avail, @@ -866,29 +1025,66 @@ again: reset_buffer(this); - update_packet_delay(this, iso_io->duration * 3/2); + if (this->process_rate) { + /* Match target delay in media_iso_pull() */ + delay = this->process_duration * SPA_NSEC_PER_SEC / this->process_rate; + if (delay < iso_io->duration*3/2) + delay = iso_io->duration*3/2 - delay; + else + delay = 0; + } + update_packet_delay(this, delay); } return 0; } + if (is_asha) { + struct spa_bt_asha *asha = this->asha; + + if (this->need_flush && !asha->flush_pending) { + /* + * For ASHA, we cannot send more than one encoded + * packet at a time and can only send them spaced + * 20 ms apart which is the ASHA connection interval. + * All encoded packets will be 160 bytes + 1 byte + * sequence number. + * + * Unlike the A2DP flow below, we cannot delay the + * output by 1 packet. While that might work for the + * mono case, for stereo that make the two sides be + * out of sync with each other and if the two sides + * differ by more than 3 credits, we would have to + * drop packets or the devices themselves might drop + * the connection. + */ + memcpy(asha->buf, this->buffer, this->buffer_used); + asha->flush_pending = true; + reset_buffer(this); + } + + return 0; + } + if (this->flush_pending) { spa_log_trace(this->log, "%p: wait for flush timer", this); return 0; } /* - * Get socket queue size before writing to it. - * This should be the same as buffer size to increase bitpool - * Bitpool shouldn't be increased when data is left over in the buffer + * Get packet queue size before writing to it. This should be zero to increase + * bitpool. Bitpool shouldn't be increased when there is unsent data. */ - unused_buffer = get_transport_unused_size(this); + unsent_buffer = get_transport_unsent_size(this); written = flush_buffer(this); if (written == -EAGAIN) { spa_log_trace(this->log, "%p: fail flush", this); if (now_time - this->last_error > SPA_NSEC_PER_SEC / 2) { - int res = this->codec->reduce_bitpool(this->codec_data); + int res = 0; + + if (this->codec->reduce_bitpool) + res = this->codec->reduce_bitpool(this->codec_data); spa_log_debug(this->log, "%p: reduce bitpool: %i", this, res); this->last_error = now_time; @@ -958,8 +1154,11 @@ again: } if (now_time - this->last_error > SPA_NSEC_PER_SEC) { - if (unused_buffer == (int)this->fd_buffer_size) { - int res = this->codec->increase_bitpool(this->codec_data); + if (unsent_buffer == 0) { + int res = 0; + + if (this->codec->increase_bitpool) + res = this->codec->increase_bitpool(this->codec_data); spa_log_debug(this->log, "%p: increase bitpool: %i", this, res); } @@ -986,6 +1185,14 @@ static void drop_frames(struct impl *this, uint32_t req) { struct port *port = &this->port; + if (this->silence_frames > req) { + this->silence_frames -= req; + req = 0; + } else { + req -= this->silence_frames; + this->silence_frames = 0; + } + while (req > 0 && !spa_list_is_empty(&port->ready)) { struct buffer *b; struct spa_data *d; @@ -1015,36 +1222,46 @@ static void drop_frames(struct impl *this, uint32_t req) } } -static void media_iso_pull(struct spa_bt_iso_io *iso_io) +static void media_iso_rate_match(struct impl *this) { - struct impl *this = iso_io->user_data; + struct spa_bt_iso_io *iso_io = this->transport ? this->transport->iso_io : NULL; struct port *port = &this->port; - const double period = 0.1 * SPA_NSEC_PER_SEC; + const double period = 0.05 * SPA_NSEC_PER_SEC; + uint64_t ref_time; uint64_t duration_ns; double value, target, err, max_err; + if (!iso_io || !this->transport_started) + return; + if (this->resync || !this->position) { spa_bt_rate_control_init(&port->ratectl, 0); - goto done; + setup_matching(this); + return; } /* - * Rate match sample position so that the graph is 3/2 ISO interval + * Rate match sample position so that the graph is max(ISO interval*3/2, quantum) * ahead of the time instant we have to send data. * - * Being 1 ISO interval ahead is unavoidable otherwise we underrun, - * and the 1/2 is safety margin for the graph to deliver data - * in time. + * Being 1 ISO interval ahead is unavoidable otherwise we underrun, and the + * rest is safety margin for the graph to deliver data in time. * * This is then the part of the TX latency on PipeWire side. There is * another part of TX latency on kernel/controller side before the * controller starts processing the packet. */ - value = (int64_t)iso_io->now - (int64_t)get_reference_time(this, &duration_ns); - target = iso_io->duration * 3/2; + ref_time = get_reference_time(this, &duration_ns); + + value = (int64_t)iso_io->now - (int64_t)ref_time; + if (this->process_rate) + target = this->process_duration * SPA_NSEC_PER_SEC / this->process_rate; + else + target = 0; + target = SPA_MAX(target, iso_io->duration*3/2); err = value - target; - max_err = iso_io->duration; + max_err = SPA_MAX(40 * SPA_NSEC_PER_MSEC, target); if (iso_io->resync && err >= 0) { unsigned int req = (unsigned int)(err * port->current_format.info.raw.rate / SPA_NSEC_PER_SEC); @@ -1056,12 +1273,10 @@ static void media_iso_pull(struct spa_bt_iso_io *iso_io) spa_log_debug(this->log, "%p: ISO sync skip frames:%u", this, req); } else if (iso_io->resync && -err >= 0) { unsigned int req = (unsigned int)(-err * port->current_format.info.raw.rate / SPA_NSEC_PER_SEC); - static const uint8_t empty[8192] = {0}; if (req > 0) { spa_bt_rate_control_init(&port->ratectl, 0); - req = SPA_MIN(req, sizeof(empty) / port->frame_size); - add_data(this, empty, req * port->frame_size); + this->silence_frames += req; } spa_log_debug(this->log, "%p: ISO sync pad frames:%u", this, req); } else if (err > max_err || -err > max_err) { @@ -1070,7 +1285,7 @@ static void media_iso_pull(struct spa_bt_iso_io *iso_io) this, err / SPA_NSEC_PER_MSEC); } else { spa_bt_rate_control_update(&port->ratectl, err, 0, - iso_io->duration, period, RATE_CTL_DIFF_MAX); + duration_ns, period, RATE_CTL_DIFF_MAX); spa_log_trace(this->log, "%p: ISO sync err:%+.3g value:%.6f target:%.6f (ms) corr:%g", this, port->ratectl.avg / SPA_NSEC_PER_MSEC, @@ -1080,8 +1295,12 @@ static void media_iso_pull(struct spa_bt_iso_io *iso_io) } iso_io->resync = false; +} + +static void media_iso_pull(struct spa_bt_iso_io *iso_io) +{ + struct impl *this = iso_io->user_data; -done: this->iso_pending = true; flush_data(this, this->current_time); } @@ -1092,9 +1311,13 @@ static void media_on_flush_error(struct spa_source *source) if (source->rmask & SPA_IO_ERR) { /* TX timestamp info? */ - if (this->transport && this->transport->iso_io) + if (this->transport && this->transport->iso_io) { if (spa_bt_iso_io_recv_errqueue(this->transport->iso_io) == 0) return; + } else { + if (spa_bt_latency_recv_errqueue(&this->tx_latency, this->flush_source.fd, this->log) == 0) + return; + } /* Otherwise: actual error */ } @@ -1102,9 +1325,12 @@ static void media_on_flush_error(struct spa_source *source) spa_log_trace(this->log, "%p: flush event", this); if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { - spa_log_warn(this->log, "%p: error %d", this, source->rmask); - if (this->flush_source.loop) + spa_log_warn(this->log, "%p: connection (%s) terminated unexpectedly", + this, this->transport ? this->transport->path : ""); + if (this->flush_source.loop) { + spa_bt_latency_flush(&this->tx_latency, this->flush_source.fd, this->log); spa_loop_remove_source(this->data_loop, &this->flush_source); + } enable_flush_timer(this, false); if (this->flush_timer_source.loop) spa_loop_remove_source(this->data_loop, &this->flush_timer_source); @@ -1196,12 +1422,99 @@ static void media_on_timeout(struct spa_source *source) set_timeout(this, this->next_time); } -static int do_start_iso_io(struct spa_loop *loop, bool async, uint32_t seq, +static uint64_t asha_seqnum(struct impl *this) +{ + uint64_t tn = get_reference_time(this, NULL); + uint64_t dt = tn - this->asha->ref_t0; + uint64_t num_packets = (dt + ASHA_CONN_INTERVAL / 2) / ASHA_CONN_INTERVAL; + + spa_log_trace(this->log, "%" PRIu64 " - %" PRIu64 " / 20ms = %"PRIu64, + tn, this->asha->ref_t0, num_packets); + + if (this->asha->ref_t0 > tn) + return 0; + + return num_packets % 256; +} + +static void media_asha_flush_timeout(struct spa_source *source) +{ + struct impl *this = source->data; + struct port *port = &this->port; + struct spa_bt_asha *asha = this->asha; + const char *address = this->transport->device->address; + struct timespec ts; + int res, written; + uint64_t exp, now; + + if (this->started) { + if ((res = spa_system_timerfd_read(this->data_system, asha->timerfd, &exp)) < 0) { + if (res != -EAGAIN) + spa_log_warn(this->log, "error reading ASHA timerfd: %s", + spa_strerror(res)); + return; + } + } + + spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts); + now = SPA_TIMESPEC_TO_NSEC(&ts); + + asha->next_time += (uint64_t)(ASHA_CONN_INTERVAL * port->ratectl.corr); + + if (asha->flush_pending) { + asha->buf[0] = this->seqnum; + written = send(asha->flush_source.fd, asha->buf, + ASHA_ENCODED_PKT_SZ, MSG_DONTWAIT | MSG_NOSIGNAL); + /* + * For ASHA, when we are out of LE credits and cannot write to + * the socket, return value of `send` will be -EAGAIN. + */ + if (written < 0) { + asha->flush_pending = false; + spa_log_warn(this->log, "%p: ASHA failed to flush %d seqnum on timer for %s, written:%d", + this, this->seqnum, address, -errno); + goto skip_flush; + } + + if (written > 0) { + asha->flush_pending = false; + spa_log_trace(this->log, "%p: ASHA flush %d seqnum for %s, ts:%u", + this, this->seqnum, address, this->timestamp); + } + } + + this->seqnum = asha_seqnum(this); + flush_data(this, now); + +skip_flush: + set_asha_timeout(this, asha->next_time); +} + + +static void media_asha_cb(struct spa_source *source) +{ + struct impl *this = source->data; + struct spa_bt_asha *asha = this->asha; + const char *address = this->transport->device->address; + + if (source->rmask & (SPA_IO_HUP | SPA_IO_ERR)) { + spa_log_error(this->log, "%p: ASHA source error %d on %s", this, source->rmask, address); + + if (asha->flush_source.loop) + spa_loop_remove_source(this->data_loop, &asha->flush_source); + + return; + } +} + +static int do_start_transport(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { struct impl *this = user_data; - spa_bt_iso_io_set_cb(this->transport->iso_io, media_iso_pull, this); + this->transport_started = true; + if (this->transport->iso_io) + spa_bt_iso_io_set_cb(this->transport->iso_io, media_iso_pull, this); return 0; } @@ -1212,6 +1525,8 @@ static int transport_start(struct impl *this) socklen_t len; uint8_t *conf; uint32_t flags; + bool is_asha; + bool is_sco; if (this->transport_started) return 0; @@ -1226,6 +1541,8 @@ static int transport_start(struct impl *this) conf = this->transport->configuration; size = this->transport->configuration_len; + is_asha = this->codec->kind == MEDIA_CODEC_ASHA; + is_sco = this->codec->kind == MEDIA_CODEC_HFP; spa_log_debug(this->log, "Transport configuration:"); spa_debug_log_mem(this->log, SPA_LOG_LEVEL_DEBUG, 2, conf, (size_t)size); @@ -1256,7 +1573,7 @@ static int transport_start(struct impl *this) if (this->codec->get_delay) this->codec->get_delay(this->codec_data, &this->encoder_delay, NULL); - const char *codec_profile = this->codec->asha ? "ASHA" : (this->codec->bap ? "BAP" : "A2DP"); + const char *codec_profile = media_codec_kind_str(this->codec); spa_log_info(this->log, "%p: using %s codec %s, delay:%.2f ms, codec-delay:%.2f ms", this, codec_profile, this->codec->description, (double)spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC, @@ -1268,7 +1585,7 @@ static int transport_start(struct impl *this) if (this->block_size > sizeof(this->tmp_buffer)) { spa_log_error(this->log, "block-size %d > %zu", this->block_size, sizeof(this->tmp_buffer)); - return -EIO; + goto fail; } spa_log_debug(this->log, "%p: block_size %d", this, this->block_size); @@ -1299,36 +1616,82 @@ static int transport_start(struct impl *this) this->update_delay_event = spa_loop_utils_add_event(this->loop_utils, update_delay_event, this); - if (!this->transport->iso_io) { + spa_zero(this->tx_latency); + + if (is_sco) { + int res; + if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop, this->data_system)) < 0) + goto fail; + spa_bt_sco_io_write_start(this->transport->sco_io); + } + + if (!this->transport->iso_io && !is_asha) { this->flush_timer_source.data = this; this->flush_timer_source.fd = this->flush_timerfd; this->flush_timer_source.func = media_on_flush_timeout; this->flush_timer_source.mask = SPA_IO_IN; this->flush_timer_source.rmask = 0; spa_loop_add_source(this->data_loop, &this->flush_timer_source); + + if (!is_sco) + spa_bt_latency_init(&this->tx_latency, this->transport, LATENCY_PERIOD, this->log); } - this->flush_source.data = this; - this->flush_source.fd = this->transport->fd; - this->flush_source.func = media_on_flush_error; - this->flush_source.mask = SPA_IO_ERR | SPA_IO_HUP; - this->flush_source.rmask = 0; - spa_loop_add_source(this->data_loop, &this->flush_source); + if (!is_asha && !is_sco) { + this->flush_source.data = this; + this->flush_source.fd = this->transport->fd; + this->flush_source.func = media_on_flush_error; + this->flush_source.mask = SPA_IO_ERR | SPA_IO_HUP; + this->flush_source.rmask = 0; + spa_loop_add_source(this->data_loop, &this->flush_source); + } - this->resync = RESYNC_CYCLES; + this->resync = 0; this->flush_pending = false; this->iso_pending = false; - this->transport_started = true; + spa_loop_locked(this->data_loop, do_start_transport, 0, NULL, 0, this); - if (this->transport->iso_io) - spa_loop_invoke(this->data_loop, do_start_iso_io, 0, NULL, 0, true, this); + if (is_asha) { + struct spa_bt_asha *asha = this->asha; + + asha->flush_pending = false; + asha->set_timer = false; + + asha->timer_source.data = this; + asha->timer_source.fd = this->asha->timerfd; + asha->timer_source.func = media_asha_flush_timeout; + asha->timer_source.mask = SPA_IO_IN; + asha->timer_source.rmask = 0; + spa_loop_add_source(this->data_loop, &asha->timer_source); + + asha->flush_source.data = this; + asha->flush_source.fd = this->transport->fd; + asha->flush_source.func = media_asha_cb; + asha->flush_source.mask = SPA_IO_ERR | SPA_IO_HUP; + asha->flush_source.rmask = 0; + spa_loop_add_source(this->data_loop, &asha->flush_source); + + spa_list_append(&asha_sinks, &this->asha_link); + } + + set_latency(this, true); return 0; + +fail: + if (this->codec_data) { + if (this->own_codec_data) + this->codec->deinit(this->codec_data); + this->own_codec_data = false; + this->codec_data = NULL; + } + return -EIO; } static int do_start(struct impl *this) { + struct port *port = &this->port; int res; if (this->started) @@ -1342,7 +1705,8 @@ static int do_start(struct impl *this) this->start_ready = true; - if ((res = spa_bt_transport_acquire(this->transport, false)) < 0) { + bool do_accept = this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY; + if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0) { this->start_ready = false; return res; } @@ -1356,6 +1720,7 @@ static int do_start(struct impl *this) this->source.rmask = 0; spa_loop_add_source(this->data_loop, &this->source); + spa_bt_rate_control_init(&port->ratectl, 0); setup_matching(this); set_timers(this); @@ -1397,10 +1762,20 @@ static int do_remove_transport_source(struct spa_loop *loop, this->transport_started = false; - if (this->flush_source.loop) + if (this->flush_source.loop) { + spa_bt_latency_flush(&this->tx_latency, this->flush_source.fd, this->log); spa_loop_remove_source(this->data_loop, &this->flush_source); + } + if (this->flush_timer_source.loop) spa_loop_remove_source(this->data_loop, &this->flush_timer_source); + if (this->codec->kind == MEDIA_CODEC_ASHA) { + if (this->asha->timer_source.loop) + spa_loop_remove_source(this->data_loop, &this->asha->timer_source); + if (this->asha->flush_source.loop) + spa_loop_remove_source(this->data_loop, &this->asha->flush_source); + spa_list_remove(&this->asha_link); + } enable_flush_timer(this, false); if (this->transport->iso_io) @@ -1419,7 +1794,7 @@ static void transport_stop(struct impl *this) spa_log_trace(this->log, "%p: stop transport", this); - spa_loop_invoke(this->data_loop, do_remove_transport_source, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_transport_source, 0, NULL, 0, this); if (this->codec_data && this->own_codec_data) this->codec->deinit(this->codec_data); @@ -1437,7 +1812,7 @@ static int do_stop(struct impl *this) this->start_ready = false; - spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_source, 0, NULL, 0, this); transport_stop(this); @@ -1485,6 +1860,8 @@ static void emit_node_info(struct impl *this, bool full) { char node_group_buf[256]; char *node_group = NULL; + const char *media_role = NULL; + const char *codec_profile = media_codec_kind_str(this->codec); if (this->transport && (this->transport->profile & SPA_BT_PROFILE_BAP_SINK)) { spa_scnprintf(node_group_buf, sizeof(node_group_buf), "[\"bluez-iso-%s-cig-%d\"]", @@ -1496,9 +1873,16 @@ static void emit_node_info(struct impl *this, bool full) this->transport->device->adapter->address, this->transport->bap_big); node_group = node_group_buf; + } else if (this->transport && (this->transport->profile & SPA_BT_PROFILE_ASHA_SINK)) { + spa_scnprintf(node_group_buf, sizeof(node_group_buf), "[\"bluez-asha-%" PRIu64 "d\"]", + this->transport->hisyncid); + node_group = node_group_buf; } - const char *codec_profile = this->codec->asha ? "ASHA" : (this->codec->bap ? "BAP" : "A2DP"); + if (!this->is_output && this->transport && + (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) + media_role = "Communication"; + struct spa_dict_item node_info_items[] = { { SPA_KEY_DEVICE_API, "bluez5" }, { SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Sink/Internal" : @@ -1507,6 +1891,7 @@ static void emit_node_info(struct impl *this, bool full) this->transport->device->name : codec_profile ) }, { SPA_KEY_NODE_DRIVER, this->is_output ? "true" : "false" }, { "node.group", node_group }, + { SPA_KEY_MEDIA_ROLE, media_role }, }; uint64_t old = full ? this->info.change_mask : 0; if (full) @@ -1682,7 +2067,7 @@ impl_node_port_enum_params(void *object, int seq, SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); break; case 1: - if (!this->codec->bap) + if (this->codec->kind != MEDIA_CODEC_BAP) return 0; param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamIO, id, @@ -1754,7 +2139,7 @@ static int port_set_format(struct impl *this, struct port *port, if (info.info.raw.rate == 0 || info.info.raw.channels == 0 || - info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + info.info.raw.channels > MAX_CHANNELS) return -EINVAL; if (this->transport && this->transport->iso_io) { @@ -1768,7 +2153,8 @@ static int port_set_format(struct impl *this, struct port *port, port->frame_size = info.info.raw.channels; switch (info.info.raw.format) { - case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_LE: + case SPA_AUDIO_FORMAT_S16_BE: port->frame_size *= 2; break; case SPA_AUDIO_FORMAT_S24: @@ -1895,7 +2281,7 @@ impl_node_port_set_io(void *object, port->io = data; break; case SPA_IO_RateMatch: - if (!this->codec->bap) + if (this->codec->kind != MEDIA_CODEC_BAP) return -ENOENT; port->rate_match = data; break; @@ -1967,12 +2353,17 @@ static int impl_node_process(void *object) } } + /* Make copies of current position values, so that they can be used later at any + * time without shared memory races + */ if (this->position) { this->process_duration = this->position->clock.duration; this->process_rate = this->position->clock.rate.denom; + this->process_rate_diff = this->position->clock.rate_diff; } else { this->process_duration = 1024; this->process_rate = 48000; + this->process_rate_diff = 1.0; } this->process_time = this->current_time; @@ -1981,6 +2372,23 @@ static int impl_node_process(void *object) setup_matching(this); + media_iso_rate_match(this); + + if (this->codec->kind == MEDIA_CODEC_ASHA && !this->asha->set_timer) { + struct impl *other = find_other_asha(this); + if (other && other->asha->ref_t0 != 0) { + this->asha->ref_t0 = other->asha->ref_t0; + this->seqnum = asha_seqnum(this); + set_asha_timer(this, other); + } else { + this->asha->ref_t0 = get_reference_time(this, NULL); + this->seqnum = 0; + set_asha_timer(this, NULL); + } + + this->asha->set_timer = true; + } + spa_log_trace(this->log, "%p: on process time:%"PRIu64, this, this->process_time); if ((res = flush_data(this, this->current_time)) < 0) { io->status = res; @@ -2032,7 +2440,7 @@ static void transport_destroy(void *data) { struct impl *this = data; spa_log_debug(this->log, "transport %p destroy", this->transport); - spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_transport_destroy, 0, NULL, 0, this); } static void transport_state_changed(void *data, @@ -2112,6 +2520,10 @@ static int impl_clear(struct spa_handle *handle) spa_hook_remove(&this->transport_listener); spa_system_close(this->data_system, this->timerfd); spa_system_close(this->data_system, this->flush_timerfd); + if (this->codec->kind == MEDIA_CODEC_ASHA) { + spa_system_close(this->data_system, this->asha->timerfd); + free(this->asha); + } return 0; } @@ -2237,8 +2649,10 @@ impl_init(const struct spa_handle_factory *factory, this->is_duplex ? MEDIA_CODEC_FLAG_SINK : 0, this->transport->device->settings); - if (this->codec->bap) + if (this->codec->kind == MEDIA_CODEC_BAP) this->is_output = this->transport->bap_initiator; + else if (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY) + this->is_output = false; else this->is_output = true; @@ -2255,6 +2669,15 @@ impl_init(const struct spa_handle_factory *factory, this->flush_timerfd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + if (this->codec->kind == MEDIA_CODEC_ASHA) { + this->asha = calloc(1, sizeof(struct spa_bt_asha)); + if (this->asha == NULL) + return -ENOMEM; + + this->asha->timerfd = spa_system_timerfd_create(this->data_system, + CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + } + return 0; } @@ -2307,3 +2730,13 @@ const struct spa_handle_factory spa_a2dp_sink_factory = { impl_init, impl_enum_interface_info, }; + +/* Retained for backward compatibility: */ +const struct spa_handle_factory spa_sco_sink_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_BLUEZ5_SCO_SINK, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/bluez5/media-source.c b/spa/plugins/bluez5/media-source.c index 005597490..dc52a09b4 100644 --- a/spa/plugins/bluez5/media-source.c +++ b/spa/plugins/bluez5/media-source.c @@ -47,10 +47,16 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.source.media"); struct props { char clock_name[64]; + char latency[64]; + bool has_latency; + char rate[64]; + bool has_rate; }; #define MAX_BUFFERS 32 +#define MAX_PLC_PACKETS 16 + struct buffer { uint32_t id; unsigned int outstanding:1; @@ -133,11 +139,14 @@ struct impl { unsigned int following:1; unsigned int matching:1; unsigned int resampling:1; + unsigned int io_error:1; unsigned int is_input:1; unsigned int is_duplex:1; unsigned int is_internal:1; + unsigned int decode_buffer_target; + unsigned int node_latency; int fd; @@ -159,14 +168,19 @@ struct impl { struct spa_audio_info codec_format; uint8_t buffer_read[4096]; - struct timespec now; + uint64_t now; uint64_t sample_count; + int seqnum; + uint32_t plc_packets; + uint32_t errqueue_count; struct delay_info delay; int64_t delay_sink; struct spa_source *update_delay_event; + + struct spa_bt_recvmsg_data recv; }; #define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) @@ -174,6 +188,10 @@ struct impl { static void reset_props(struct props *props) { strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); + spa_zero(props->latency); + props->has_latency = false; + spa_zero(props->rate); + props->has_rate = false; } static int impl_node_enum_params(void *object, int seq, @@ -313,7 +331,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) if (this->started && following != this->following) { spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); this->following = following; - spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_reassign_follower, 0, NULL, 0, this); } return 0; } @@ -322,7 +340,7 @@ static void emit_node_info(struct impl *this, bool full); static void set_latency(struct impl *this, bool emit_latency) { - if (this->codec->bap && !this->is_input && this->transport && + if (this->codec->kind == MEDIA_CODEC_BAP && !this->is_input && this->transport && this->transport->delay_us != SPA_BT_UNKNOWN_DELAY) { struct port *port = &this->port; unsigned int node_latency = 2048; @@ -420,13 +438,14 @@ static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer } } -static int32_t read_data(struct impl *this) { +static int32_t read_data(struct impl *this, uint64_t *rx_time, int *seqnum) +{ const ssize_t b_size = sizeof(this->buffer_read); int32_t size_read = 0; again: /* read data from socket */ - size_read = recv(this->fd, this->buffer_read, b_size, MSG_DONTWAIT); + size_read = spa_bt_recvmsg(&this->recv, this->buffer_read, b_size, rx_time, seqnum); if (size_read == 0) return 0; @@ -447,34 +466,147 @@ again: return size_read; } +static int produce_plc_data(struct impl *this) +{ + struct port *port = &this->port; + uint32_t avail; + int res; + void *buf; + + if (!this->codec->produce_plc) + return -ENOTSUP; + + buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail); + res = this->codec->produce_plc(this->codec_data, buf, avail); + if (res <= 0) + return res; + + spa_bt_decode_buffer_write_packet(&port->buffer, res, 0); + + spa_log_debug(this->log, "%p: produced PLC audio, frames:%u", + this, (unsigned int)(res / port->frame_size)); + + this->plc_packets++; + return res; +} + static int32_t decode_data(struct impl *this, uint8_t *src, uint32_t src_size, - uint8_t *dst, uint32_t dst_size) + uint8_t *dst, uint32_t dst_size, uint32_t *dst_out, int pkt_seqnum) { ssize_t processed; size_t written, avail; + size_t src_avail = src_size; + uint16_t seqnum = this->seqnum + 1; + + *dst_out = 0; if ((processed = this->codec->start_decode(this->codec_data, - src, src_size, NULL, NULL)) < 0) + src, src_avail, &seqnum, NULL)) < 0) return processed; + if (pkt_seqnum >= 0) + seqnum = pkt_seqnum; + src += processed; - src_size -= processed; + src_avail -= processed; + + if (this->seqnum < 0) { + /* first packet */ + } else if (this->codec->stream_pkt && this->seqnum == seqnum) { + /* previous packet continues */ + } else { + uint16_t lost = seqnum - (uint16_t)(this->seqnum + 1); + if (lost) + spa_log_debug(this->log, "%p: lost packets:%u (%u -> %u)", + this, (unsigned int)lost, this->seqnum + 1, seqnum); + + if (this->plc_packets > MAX_PLC_PACKETS || lost > MAX_PLC_PACKETS) { + /* Don't try to compensate for too big skips */ + this->plc_packets = 0; + lost = 0; + } + + if (lost >= this->plc_packets) { + lost -= this->plc_packets; + } else { + /* We already produced PLC audio for this packet. However, this + * only occurs if we are underflowing, so we should retain this + * packet regardless and let rate matching take care of it. + */ + lost = 0; + } + + /* Pad with PLC audio for any missing packets */ + while (lost > 0 && produce_plc_data(this) > 0) + --lost; + + this->plc_packets = 0; + } /* decode */ avail = dst_size; - while (src_size > 0) { + do { + written = 0; if ((processed = this->codec->decode(this->codec_data, - src, src_size, dst, avail, &written)) <= 0) + src, src_avail, dst, avail, &written)) < 0) return processed; /* update source and dest pointers */ spa_return_val_if_fail (avail > written, -ENOSPC); - src_size -= processed; + src_avail -= processed; src += processed; avail -= written; dst += written; - } - return dst_size - avail; + } while (src_avail && (processed || written) && !this->codec->stream_pkt); + + this->seqnum = seqnum; + + *dst_out = dst_size - avail; + return src_size - src_avail; +} + +static void add_data(struct impl *this, uint8_t *src, uint32_t src_size, uint64_t now, int pkt_seqnum) +{ + struct port *port = &this->port; + uint32_t decoded; + + spa_log_trace(this->log, "%p: read socket data size:%d", this, src_size); + + if (this->transport->iso_io) + now = spa_bt_iso_io_recv(this->transport->iso_io, now); + + do { + int32_t consumed; + uint32_t avail; + void *buf; + uint64_t dt; + + buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail); + + consumed = decode_data(this, src, src_size, buf, avail, &decoded, pkt_seqnum); + if (consumed < 0) { + spa_log_debug(this->log, "%p: failed to decode data: %d", this, consumed); + return; + } + + src = SPA_PTROFF(src, consumed, void); + src_size -= consumed; + + /* discard when not started */ + if (this->started) + spa_bt_decode_buffer_write_packet(&port->buffer, decoded, now); + + if (decoded) { + dt = now - this->now; + this->now = now; + spa_log_trace(this->log, "%p: decoded socket data seq:%u size:%d frames:%d dt:%d dms", + this, + (unsigned int)this->seqnum, (int)decoded, (int)decoded/port->frame_size, + (int)(dt / 100000)); + } else { + spa_log_trace(this->log, "no decoded socket data"); + } + } while (this->codec->stream_pkt && src_size && decoded); } static void handle_errqueue(struct impl *this) @@ -491,19 +623,20 @@ static void handle_errqueue(struct impl *this) } this->errqueue_count = 0; - res = recv(this->fd, NULL, 0, MSG_ERRQUEUE | MSG_TRUNC); - spa_log_trace(this->log, "%p: ignoring errqueue data (%d)", this, res); + do { + char buf[512]; + + res = recv(this->fd, buf, sizeof(buf), MSG_ERRQUEUE | MSG_TRUNC | MSG_DONTWAIT); + spa_log_trace(this->log, "%p: ignoring errqueue data (%d)", this, res); + } while (res > 0); } static void media_on_ready_read(struct spa_source *source) { struct impl *this = source->data; - struct port *port = &this->port; - struct timespec now; - void *buf; - int32_t size_read, decoded; - uint32_t avail; - uint64_t dt; + int32_t size_read; + uint64_t now = 0; + int pkt_seqnum = -1; /* make sure the source is an input */ if ((source->rmask & SPA_IO_IN) == 0) { @@ -524,13 +657,8 @@ static void media_on_ready_read(struct spa_source *source) spa_log_trace(this->log, "socket poll"); - /* update the current pts */ - spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); - /* read */ - size_read = read_data (this); - if (size_read == 0) - return; + size_read = read_data (this, &now, &pkt_seqnum); if (size_read < 0) { spa_log_error(this->log, "failed to read data: %s", spa_strerror(size_read)); goto stop; @@ -542,40 +670,39 @@ static void media_on_ready_read(struct spa_source *source) this->codec_props_changed = false; } - /* decode to buffer */ - buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail); - spa_log_trace(this->log, "read socket data size:%d, avail:%d", size_read, avail); - decoded = decode_data(this, this->buffer_read, size_read, buf, avail); - if (decoded < 0) { - spa_log_debug(this->log, "failed to decode data: %d", decoded); - return; - } - if (decoded == 0) { - spa_log_trace(this->log, "no decoded socket data"); - return; - } - - /* discard when not started */ - if (!this->started) - return; - - spa_bt_decode_buffer_write_packet(&port->buffer, decoded, SPA_TIMESPEC_TO_NSEC(&now)); - - dt = SPA_TIMESPEC_TO_NSEC(&this->now); - this->now = now; - dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt; - - spa_log_trace(this->log, "decoded socket data size:%d frames:%d dt:%d dms", - (int)decoded, (int)decoded/port->frame_size, - (int)(dt / 100000)); - + add_data(this, this->buffer_read, size_read, now, pkt_seqnum); return; stop: + this->io_error = true; if (this->source.loop) spa_loop_remove_source(this->data_loop, &this->source); - if (this->transport && this->transport->iso_io) + if (this->transport && this->transport->iso_io) { spa_bt_iso_io_set_cb(this->transport->iso_io, NULL, NULL); + spa_bt_iso_io_set_source_buffer(this->transport->iso_io, NULL); + } +} + +static int media_sco_pull(void *userdata, uint8_t *buffer_read, int size_read, uint64_t now) +{ + struct impl *this = userdata; + + if (this->transport == NULL) { + spa_log_debug(this->log, "no transport, stop reading"); + goto stop; + } + + if (size_read == 0) + return 0; + + add_data(this, buffer_read, size_read, now, -1); + return 0; + +stop: + this->io_error = true; + if (this->transport && this->transport->sco_io) + spa_bt_sco_io_set_source_cb(this->transport->sco_io, NULL, NULL); + return 1; } static int setup_matching(struct impl *this) @@ -591,6 +718,10 @@ static int setup_matching(struct impl *this) this->matching = this->following; this->resampling = this->matching || (port->current_format.info.raw.rate != this->position->clock.target_rate.denom); + + /* Rate match in system clock domain also when follower */ + if (this->matching && this->position->clock.rate_diff > 0) + port->rate_match->rate *= this->position->clock.rate_diff; } else { this->matching = false; this->resampling = false; @@ -714,12 +845,17 @@ static void update_delay_event(void *data, uint64_t count) update_transport_delay(data); } -static int do_start_iso_io(struct spa_loop *loop, bool async, uint32_t seq, +static int do_start_sco_iso_io(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { struct impl *this = user_data; - spa_bt_iso_io_set_cb(this->transport->iso_io, media_iso_pull, this); + if (this->transport->sco_io) + spa_bt_sco_io_set_source_cb(this->transport->sco_io, media_sco_pull, this); + if (this->transport->iso_io) { + spa_bt_iso_io_set_cb(this->transport->iso_io, media_iso_pull, this); + spa_bt_iso_io_set_source_buffer(this->transport->iso_io, &this->port.buffer); + } return 0; } @@ -752,7 +888,7 @@ static int transport_start(struct impl *this) return -EIO; spa_log_info(this->log, "%p: using %s codec %s", this, - this->codec->bap ? "BAP" : "A2DP", this->codec->description); + media_codec_kind_str(this->codec), this->codec->description); /* * If the link is bidirectional, media-sink may also be polling the same FD, @@ -774,7 +910,13 @@ static int transport_start(struct impl *this) this->quantum_limit, this->quantum_limit)) < 0) return res; - if (this->is_duplex) { + spa_bt_decode_buffer_set_target_latency(&port->buffer, (int32_t) this->decode_buffer_target); + + if (this->codec->kind == MEDIA_CODEC_HFP || this->codec->kind == MEDIA_CODEC_BAP) { + /* 40 ms max buffer (on top of duration) */ + spa_bt_decode_buffer_set_max_extra_latency(&port->buffer, + port->current_format.info.raw.rate * 40 / 1000); + } else if (this->is_duplex) { /* 80 ms max extra buffer */ spa_bt_decode_buffer_set_max_extra_latency(&port->buffer, port->current_format.info.raw.rate * 80 / 1000); @@ -787,22 +929,41 @@ static int transport_start(struct impl *this) this->sample_count = 0; this->errqueue_count = 0; - this->source.data = this; + this->seqnum = -1; - this->source.fd = this->fd; - this->source.func = media_on_ready_read; - this->source.mask = SPA_IO_IN; - this->source.rmask = 0; - if ((res = spa_loop_add_source(this->data_loop, &this->source)) < 0) - spa_log_error(this->log, "%p: failed to add poll source: %s", this, - spa_strerror(res)); + this->io_error = false; - if (this->transport->iso_io) - spa_loop_invoke(this->data_loop, do_start_iso_io, 0, NULL, 0, true, this); + if (this->codec->kind != MEDIA_CODEC_HFP) { + spa_bt_recvmsg_init(&this->recv, this->fd, this->data_system, this->log); + + spa_loop_locked(this->data_loop, do_start_sco_iso_io, 0, NULL, 0, this); + + this->source.data = this; + + this->source.fd = this->fd; + this->source.func = media_on_ready_read; + this->source.mask = SPA_IO_IN; + this->source.rmask = 0; + if ((res = spa_loop_add_source(this->data_loop, &this->source)) < 0) + spa_log_error(this->log, "%p: failed to add poll source: %s", this, + spa_strerror(res)); + } else { + spa_zero(this->source); + if (spa_bt_transport_ensure_sco_io(this->transport, this->data_loop, this->data_system) < 0) + goto fail; + spa_loop_locked(this->data_loop, do_start_sco_iso_io, 0, NULL, 0, this); + } this->transport_started = true; return 0; + +fail: + if (this->codec_data) { + this->codec->deinit(this->codec_data); + this->codec_data = NULL; + } + return -EIO; } static int do_start(struct impl *this) @@ -822,7 +983,9 @@ static int do_start(struct impl *this) spa_log_debug(this->log, "%p: transport %p acquire", this, this->transport); - if ((res = spa_bt_transport_acquire(this->transport, false)) < 0) { + + bool do_accept = (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); + if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0) { this->start_ready = false; return res; } @@ -856,8 +1019,12 @@ static int do_remove_source(struct spa_loop *loop, if (this->timer_source.loop) spa_loop_remove_source(this->data_loop, &this->timer_source); - if (this->transport && this->transport->iso_io) + if (this->transport && this->transport->iso_io) { spa_bt_iso_io_set_cb(this->transport->iso_io, NULL, NULL); + spa_bt_iso_io_set_source_buffer(this->transport->iso_io, NULL); + } + if (this->transport && this->transport->sco_io) + spa_bt_sco_io_set_source_cb(this->transport->sco_io, NULL, NULL); set_timeout(this, 0); if (this->update_delay_event) { @@ -883,8 +1050,12 @@ static int do_remove_transport_source(struct spa_loop *loop, if (this->source.loop) spa_loop_remove_source(this->data_loop, &this->source); - if (this->transport->iso_io) + if (this->transport->iso_io) { spa_bt_iso_io_set_cb(this->transport->iso_io, NULL, NULL); + spa_bt_iso_io_set_source_buffer(this->transport->iso_io, NULL); + } + if (this->transport->sco_io) + spa_bt_sco_io_set_source_cb(this->transport->sco_io, NULL, NULL); return 0; } @@ -898,7 +1069,7 @@ static void transport_stop(struct impl *this) spa_log_debug(this->log, "%p: transport stop", this); - spa_loop_invoke(this->data_loop, do_remove_transport_source, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_transport_source, 0, NULL, 0, this); if (this->fd >= 0) { close(this->fd); @@ -923,7 +1094,7 @@ static int do_stop(struct impl *this) this->start_ready = false; - spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_source, 0, NULL, 0, this); transport_stop(this); @@ -975,6 +1146,7 @@ static void emit_node_info(struct impl *this, bool full) char latency[64]; char rate[64]; char media_name[256]; + const char *media_role = NULL; struct port *port = &this->port; spa_scnprintf( @@ -982,27 +1154,52 @@ static void emit_node_info(struct impl *this, bool full) sizeof(media_name), "%s (codec %s)", ((this->transport && this->transport->device->name) ? - this->transport->device->name : this->codec->bap ? "BAP" : "A2DP"), + this->transport->device->name : media_codec_kind_str(this->codec)), this->codec->description ); - struct spa_dict_item node_info_items[] = { + if (!this->is_input && this->transport && + (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY)) + media_role = "Communication"; + + struct spa_dict_item node_info_items[7] = { { SPA_KEY_DEVICE_API, "bluez5" }, { SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Source/Internal" : this->is_input ? "Audio/Source" : "Stream/Output/Audio" }, - { SPA_KEY_NODE_LATENCY, this->is_input ? "" : latency }, { "media.name", media_name }, - { "node.rate", this->is_input ? "" : rate }, { SPA_KEY_NODE_DRIVER, this->is_input ? "true" : "false" }, + { SPA_KEY_MEDIA_ROLE, media_role }, }; + size_t n_items = 5; - spa_scnprintf(latency, sizeof(latency), "%u/%u", this->node_latency, port->current_format.info.raw.rate); - spa_scnprintf(rate, sizeof(rate), "1/%u", port->current_format.info.raw.rate); + spa_assert(n_items + 2 <= SPA_N_ELEMENTS(node_info_items)); + + if (this->props.has_latency) { + node_info_items[n_items].key = SPA_KEY_NODE_LATENCY; + node_info_items[n_items].value = this->props.latency; + n_items++; + } else if (!this->is_input && this->node_latency != 0) { + spa_scnprintf(latency, sizeof(latency), "%u/%u", this->node_latency, port->current_format.info.raw.rate); + node_info_items[n_items].key = SPA_KEY_NODE_LATENCY; + node_info_items[n_items].value = latency; + n_items++; + } + + if (this->props.has_rate) { + node_info_items[n_items].key = "node.rate"; + node_info_items[n_items].value = this->props.rate; + n_items++; + } else if (!this->is_input && this->node_latency != 0) { + spa_scnprintf(rate, sizeof(rate), "1/%u", port->current_format.info.raw.rate); + node_info_items[n_items].key = "node.rate"; + node_info_items[n_items].value = rate; + n_items++; + } if (full) this->info.change_mask = this->info_all; if (this->info.change_mask) { - this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); + this->info.props = &SPA_DICT_INIT(node_info_items, n_items); spa_node_emit_info(&this->hooks, &this->info); this->info.change_mask = old; } @@ -1242,13 +1439,14 @@ static int port_set_format(struct impl *this, struct port *port, if (info.info.raw.rate == 0 || info.info.raw.channels == 0 || - info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + info.info.raw.channels > MAX_CHANNELS) return -EINVAL; port->frame_size = info.info.raw.channels; switch (info.info.raw.format) { - case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_LE: + case SPA_AUDIO_FORMAT_S16_BE: port->frame_size *= 2; break; case SPA_AUDIO_FORMAT_S24: @@ -1431,7 +1629,7 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t return 0; } -static uint32_t get_samples(struct impl *this, uint32_t *result_duration) +static uint32_t get_samples(struct impl *this, int64_t *duration_ns) { struct port *port = &this->port; uint32_t samples, rate_denom; @@ -1445,12 +1643,12 @@ static uint32_t get_samples(struct impl *this, uint32_t *result_duration) rate_denom = port->current_format.info.raw.rate; } - *result_duration = duration * port->current_format.info.raw.rate / rate_denom; + *duration_ns = duration * SPA_NSEC_PER_SEC / rate_denom; if (SPA_LIKELY(port->rate_match) && this->resampling) { samples = port->rate_match->size; } else { - samples = *result_duration; + samples = duration; } return samples; } @@ -1458,17 +1656,28 @@ static uint32_t get_samples(struct impl *this, uint32_t *result_duration) static void update_target_latency(struct impl *this) { struct port *port = &this->port; - uint32_t samples, duration, latency; + uint32_t samples, latency; int64_t delay_sink; if (this->transport == NULL || !port->have_format) return; - - if (!this->codec->bap || this->is_input || - this->transport->delay_us == SPA_BT_UNKNOWN_DELAY) + if (this->codec->kind != MEDIA_CODEC_BAP) return; - get_samples(this, &duration); + if (this->is_input) { + /* BAP Client. Should use same buffer size for all streams in the same + * group, so that capture is in sync. + */ + if (this->transport->iso_io) { + int32_t target = spa_bt_iso_io_get_source_target_latency(this->transport->iso_io); + + spa_bt_decode_buffer_set_target_latency(&port->buffer, target); + } + return; + } + + if (this->transport->delay_us == SPA_BT_UNKNOWN_DELAY) + return; /* Presentation delay for BAP server * @@ -1507,49 +1716,55 @@ static void update_target_latency(struct impl *this) static void process_buffering(struct impl *this) { struct port *port = &this->port; - uint32_t duration; - const uint32_t samples = get_samples(this, &duration); + int64_t duration_ns; + const uint32_t samples = get_samples(this, &duration_ns); + uint32_t data_size = samples * port->frame_size; uint32_t avail; - void *buf; update_target_latency(this); - spa_bt_decode_buffer_process(&port->buffer, samples, duration, - this->position ? this->position->clock.rate_diff : 1.0, - this->position ? this->position->clock.next_nsec : 0); + if (samples > this->quantum_limit) + return; + + /* Produce PLC data if possible to avoid underrun */ + while (spa_bt_decode_buffer_get_size(&port->buffer) < data_size) { + if (produce_plc_data(this) <= 0) + break; + } setup_matching(this); - buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail); + spa_bt_decode_buffer_process(&port->buffer, samples, duration_ns, + this->position ? this->position->clock.rate_diff : 1.0, + this->position ? this->position->clock.next_nsec : 0, + this->resampling ? this->port.rate_match->delay : 0, + this->resampling ? this->port.rate_match->delay_frac : 0); /* copy data to buffers */ if (!spa_list_is_empty(&port->free)) { struct buffer *buffer; struct spa_data *datas; - uint32_t data_size; + void *buf; buffer = spa_list_first(&port->free, struct buffer, link); datas = buffer->buf->datas; - data_size = samples * port->frame_size; - WARN_ONCE(datas[0].maxsize < data_size && !this->following, this->log, "source buffer too small (%u < %u)", datas[0].maxsize, data_size); data_size = SPA_MIN(data_size, SPA_ROUND_DOWN(datas[0].maxsize, port->frame_size)); + buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail); avail = SPA_MIN(avail, data_size); - spa_bt_decode_buffer_read(&port->buffer, avail); - spa_list_remove(&buffer->link); spa_log_trace(this->log, "dequeue %d", buffer->id); if (buffer->h) { buffer->h->seq = this->sample_count; - buffer->h->pts = SPA_TIMESPEC_TO_NSEC(&this->now); + buffer->h->pts = this->now; buffer->h->dts_offset = 0; } @@ -1559,7 +1774,9 @@ static void process_buffering(struct impl *this) memcpy(datas[0].data, buf, avail); - /* pad with silence */ + spa_bt_decode_buffer_read(&port->buffer, avail); + + /* Pad with silence, if PLC failed to produce enough */ if (avail < data_size) memset(SPA_PTROFF(datas[0].data, avail, void), 0, data_size - avail); @@ -1570,9 +1787,13 @@ static void process_buffering(struct impl *this) spa_list_append(&port->ready, &buffer->link); } + if (this->transport->iso_io && this->position) + spa_bt_iso_io_check_rx_sync(this->transport->iso_io, this->position->clock.position); + if (this->update_delay_event) { int32_t target = spa_bt_decode_buffer_get_target_latency(&port->buffer); uint32_t decoder_delay = 0; + uint32_t duration = this->position ? this->position->clock.duration : 1024; if (this->codec->get_delay) this->codec->get_delay(this->codec_data, NULL, &decoder_delay); @@ -1608,7 +1829,7 @@ static int produce_buffer(struct impl *this) io->buffer_id = SPA_ID_INVALID; } - if (this->transport_started && !this->source.loop) { + if (this->io_error) { io->status = -EIO; return SPA_STATUS_STOPPED; } @@ -1735,7 +1956,7 @@ static void transport_destroy(void *data) { struct impl *this = data; spa_log_debug(this->log, "transport %p destroy", this->transport); - spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_transport_destroy, 0, NULL, 0, this); } static const struct spa_bt_transport_events transport_events = { @@ -1872,19 +2093,8 @@ impl_init(const struct spa_handle_factory *factory, this->quantum_limit = 8192; - if (info != NULL) { - if (info && (str = spa_dict_lookup(info, "clock.quantum-limit"))) - spa_atou32(str, &this->quantum_limit, 0); - if ((str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)) != NULL) - sscanf(str, "pointer:%p", &this->transport); - if ((str = spa_dict_lookup(info, "bluez5.media-source-role")) != NULL) - this->is_input = spa_streq(str, "input"); - if ((str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL) - this->is_duplex = spa_atob(str); - if ((str = spa_dict_lookup(info, "api.bluez5.internal")) != NULL) - this->is_internal = spa_atob(str); - } - + if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT)) != NULL) + sscanf(str, "pointer:%p", &this->transport); if (this->transport == NULL) { spa_log_error(this->log, "a transport is needed"); return -EINVAL; @@ -1895,6 +2105,30 @@ impl_init(const struct spa_handle_factory *factory, } this->codec = this->transport->media_codec; + if (this->transport->profile & SPA_BT_PROFILE_HEADSET_HEAD_UNIT) + this->is_input = true; + + if (info) { + if ((str = spa_dict_lookup(info, "clock.quantum-limit"))) + spa_atou32(str, &this->quantum_limit, 0); + if ((str = spa_dict_lookup(info, "bluez5.media-source-role")) != NULL) + this->is_input = spa_streq(str, "input"); + if ((str = spa_dict_lookup(info, "api.bluez5.a2dp-duplex")) != NULL) + this->is_duplex = spa_atob(str); + if ((str = spa_dict_lookup(info, "api.bluez5.internal")) != NULL) + this->is_internal = spa_atob(str); + if ((str = spa_dict_lookup(info, "bluez5.decode-buffer.latency")) != NULL) + spa_atou32(str, &this->decode_buffer_target, 0); + if ((str = spa_dict_lookup(info, SPA_KEY_NODE_LATENCY)) != NULL) { + spa_scnprintf(this->props.latency, sizeof(this->props.latency), "%s", str); + this->props.has_latency = true; + } + if ((str = spa_dict_lookup(info, "node.rate")) != NULL) { + spa_scnprintf(this->props.rate, sizeof(this->props.rate), "%s", str); + this->props.has_rate = true; + } + } + if (this->is_duplex) { if (!this->codec->duplex_codec) { spa_log_error(this->log, "transport codec doesn't support duplex"); @@ -1904,7 +2138,7 @@ impl_init(const struct spa_handle_factory *factory, this->is_input = true; } - if (this->codec->bap) + if (this->codec->kind == MEDIA_CODEC_BAP) this->is_input = this->transport->bap_initiator; if (this->codec->init_props != NULL) @@ -1976,3 +2210,13 @@ const struct spa_handle_factory spa_a2dp_source_factory = { impl_init, impl_enum_interface_info, }; + +/* Retained for backward compatibility: */ +const struct spa_handle_factory spa_sco_source_factory = { + SPA_VERSION_HANDLE_FACTORY, + SPA_NAME_API_BLUEZ5_SCO_SOURCE, + &info, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; diff --git a/spa/plugins/bluez5/meson.build b/spa/plugins/bluez5/meson.build index b0990eaf6..01c5f3ac1 100644 --- a/spa/plugins/bluez5/meson.build +++ b/spa/plugins/bluez5/meson.build @@ -16,8 +16,6 @@ bluez5_sources = [ 'media-codecs.c', 'media-sink.c', 'media-source.c', - 'sco-sink.c', - 'sco-source.c', 'sco-io.c', 'iso-io.c', 'quirks.c', @@ -100,6 +98,22 @@ bluez_codec_faststream = shared_library('spa-codec-bluez5-faststream', install : true, install_dir : spa_plugindir / 'bluez5') +bluez_codec_hfp_cvsd = shared_library('spa-codec-bluez5-hfp-cvsd', + [ 'hfp-codec-cvsd.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep ], + install : true, + install_dir : spa_plugindir / 'bluez5') + +bluez_codec_hfp_msbc = shared_library('spa-codec-bluez5-hfp-msbc', + [ 'hfp-codec-msbc.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, sbc_dep, spandsp_dep ], + install : true, + install_dir : spa_plugindir / 'bluez5') + if fdk_aac_dep.found() bluez_codec_aac = shared_library('spa-codec-bluez5-aac', [ 'a2dp-codec-aac.c', 'media-codecs.c' ], @@ -125,6 +139,10 @@ if ldac_dep.found() if ldac_abr_dep.found() ldac_args += [ '-DENABLE_LDAC_ABR' ] endif + if get_option('bluez5-codec-ldac-dec').allowed() and ldac_dec_dep.found() + ldac_args += [ '-DENABLE_LDAC_DEC' ] + ldac_dep = [ldac_dep, ldac_dec_dep] + endif bluez_codec_ldac = shared_library('spa-codec-bluez5-ldac', [ 'a2dp-codec-ldac.c', 'media-codecs.c' ], include_directories : [ configinc ], @@ -171,6 +189,22 @@ if get_option('bluez5-codec-lc3').allowed() and lc3_dep.found() dependencies : [ spa_dep, lc3_dep, mathlib ], install : true, install_dir : spa_plugindir / 'bluez5') + + bluez_codec_hfp_lc3_swb = shared_library('spa-codec-bluez5-hfp-lc3-swb', + [ 'hfp-codec-lc3-swb.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, lc3_dep, mathlib ], + install : true, + install_dir : spa_plugindir / 'bluez5') + + bluez_codec_hfp_lc3_a127 = shared_library('spa-codec-bluez5-hfp-lc3-a127', + [ 'hfp-codec-lc3-a127.c', 'media-codecs.c' ], + include_directories : [ configinc ], + c_args : codec_args, + dependencies : [ spa_dep, lc3_dep, mathlib ], + install : true, + install_dir : spa_plugindir / 'bluez5') endif if get_option('bluez5-codec-g722').allowed() diff --git a/spa/plugins/bluez5/midi-enum.c b/spa/plugins/bluez5/midi-enum.c index 9ef7d2bf5..662228032 100644 --- a/spa/plugins/bluez5/midi-enum.c +++ b/spa/plugins/bluez5/midi-enum.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Pauli Virtanen */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include @@ -21,7 +23,6 @@ #include #include "midi.h" -#include "config.h" #include "bluez5-interface-gen.h" #include "dbus-monitor.h" diff --git a/spa/plugins/bluez5/midi-node.c b/spa/plugins/bluez5/midi-node.c index b5fd179ea..7146d6f8a 100644 --- a/spa/plugins/bluez5/midi-node.c +++ b/spa/plugins/bluez5/midi-node.c @@ -766,49 +766,57 @@ static int flush_packet(struct impl *this) static int write_data(struct impl *this, struct spa_data *d) { struct port *port = &this->ports[PORT_IN]; - struct spa_pod_sequence *pod; - struct spa_pod_control *c; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + const void *seq_body, *c_body; + struct spa_pod_control c; uint64_t time; int res; - pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size); - if (pod == NULL) { + spa_pod_parser_init_from_data(&parser, d->data, d->maxsize, d->chunk->offset, d->chunk->size); + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) { spa_log_warn(this->log, "%p: invalid sequence in buffer max:%u offset:%u size:%u", this, d->maxsize, d->chunk->offset, d->chunk->size); return -EINVAL; } + spa_bt_midi_writer_init(&this->writer, port->mtu); time = 0; - SPA_POD_SEQUENCE_FOREACH(pod, c) { + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { int size; uint8_t event[32]; + const uint32_t *ump = c_body; + size_t ump_size = c.value.size; + uint64_t state = 0; - if (c->type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_UMP) continue; - time = SPA_MAX(time, this->current_time + c->offset * SPA_NSEC_PER_SEC / this->rate); + time = SPA_MAX(time, this->current_time + c.offset * SPA_NSEC_PER_SEC / this->rate); - size = spa_ump_to_midi(SPA_POD_BODY(&c->value), - SPA_POD_BODY_SIZE(&c->value), event, sizeof(event)); - if (size <= 0) - continue; + while (ump_size > 0) { + size = spa_ump_to_midi(&ump, &ump_size, event, sizeof(event), &state); + if (size <= 0) + break; - spa_log_trace(this->log, "%p: output event:0x%x time:%"PRIu64, this, - (size > 0) ? event[0] : 0, time); + spa_log_trace(this->log, "%p: output event:0x%x time:%"PRIu64, this, + (size > 0) ? event[0] : 0, time); - do { - res = spa_bt_midi_writer_write(&this->writer, - time, event, size); - if (res < 0) { - return res; - } else if (res) { - int res2; - if ((res2 = flush_packet(this)) < 0) - return res2; - } - } while (res); + do { + res = spa_bt_midi_writer_write(&this->writer, + time, event, size); + if (res < 0) { + return res; + } else if (res) { + int res2; + if ((res2 = flush_packet(this)) < 0) + return res2; + } + } while (res); + } } if ((res = flush_packet(this)) < 0) @@ -1135,7 +1143,7 @@ static int do_stop(struct impl *this) spa_log_debug(this->log, "%p: stop", this); - spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_source, 0, NULL, 0, this); this->started = false; @@ -1149,7 +1157,7 @@ static int do_release(struct impl *this) spa_log_debug(this->log, "%p: release", this); - spa_loop_invoke(this->data_loop, do_remove_port_source, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_port_source, 0, NULL, 0, this); for (i = 0; i < N_PORTS; ++i) { struct port *port = &this->ports[i]; @@ -1260,7 +1268,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) if (this->started && following != this->following) { spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); this->following = following; - spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_reassign_follower, 0, NULL, 0, this); } return 0; @@ -1567,8 +1575,7 @@ next: param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), - SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int(1u<ports[i]; static const struct spa_dict_item in_port_items[] = { - SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"), + SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "in"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "in"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, "group.0"), }; static const struct spa_dict_item out_port_items[] = { - SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"), + SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "out"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_ALIAS, "out"), SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, "group.0"), diff --git a/spa/plugins/bluez5/midi-server.c b/spa/plugins/bluez5/midi-server.c index a3a5e9270..fffe5cb27 100644 --- a/spa/plugins/bluez5/midi-server.c +++ b/spa/plugins/bluez5/midi-server.c @@ -177,7 +177,6 @@ static gboolean chr_handle_acquire(Bluez5GattCharacteristic1 *object, int res; GUnixFDList *fd_list = NULL; GVariant *fd_handle = NULL; - GError *err = NULL; if ((write && (impl->cb->acquire_write == NULL)) || (!write && (impl->cb->acquire_notify == NULL))) { @@ -230,8 +229,6 @@ fail: if (fds[1] >= 0) close(fds[1]); - if (err) - g_error_free(err); g_clear_pointer(&fd_handle, g_variant_unref); g_clear_object(&fd_list); g_dbus_method_invocation_return_dbus_error(invocation, diff --git a/spa/plugins/bluez5/plc.h b/spa/plugins/bluez5/plc.h new file mode 100644 index 000000000..7ad23697d --- /dev/null +++ b/spa/plugins/bluez5/plc.h @@ -0,0 +1,26 @@ +/* Spa PLC */ +/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_BLUEZ5_PLC_H +#define SPA_BLUEZ5_PLC_H + +#include +#include +#include + +#ifdef HAVE_SPANDSP +#include +#else +typedef struct { char dummy; } plc_state_t; +static inline int plc_rx(plc_state_t *s, int16_t *data, int len) { return -ENOTSUP; } +static inline int plc_fillin(plc_state_t *s, int16_t *data, int len) { return -ENOTSUP; } +static inline plc_state_t *plc_init(plc_state_t *s) +{ + static plc_state_t state; + return &state; +} +static inline int plc_free(plc_state_t *s) { return 0; } +#endif + +#endif diff --git a/spa/plugins/bluez5/sco-io.c b/spa/plugins/bluez5/sco-io.c index 4b4b8f96d..b18a52a61 100644 --- a/spa/plugins/bluez5/sco-io.c +++ b/spa/plugins/bluez5/sco-io.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -29,11 +30,15 @@ #include #include "defs.h" +#include "media-codecs.h" +#include "hfp-codec-caps.h" SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.sco-io"); #undef SPA_LOG_TOPIC_DEFAULT #define SPA_LOG_TOPIC_DEFAULT &log_topic +#include "decode-buffer.h" + /* We'll use the read rx data size to find the correct packet size for writing, * since kernel might not report it as the socket MTU, see @@ -47,12 +52,15 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.sco-io"); */ #define MAX_MTU 1024 +#define KEEPALIVE_NSEC (500 * SPA_NSEC_PER_MSEC) + struct spa_bt_sco_io { - bool started; - uint8_t read_buffer[MAX_MTU]; - uint32_t read_size; + size_t read_size; + + uint8_t write_buffer[MAX_MTU]; + size_t write_size; int fd; uint16_t read_mtu; @@ -60,30 +68,63 @@ struct spa_bt_sco_io { struct spa_log *log; struct spa_loop *data_loop; + struct spa_system *data_system; struct spa_source source; - int (*source_cb)(void *userdata, uint8_t *data, int size); + struct spa_bt_recvmsg_data recv; + + int (*source_cb)(void *userdata, uint8_t *data, int size, uint64_t rx_time); void *source_userdata; - int (*sink_cb)(void *userdata); - void *sink_userdata; + const struct media_codec *codec; + void *codec_data; + + uint64_t last_tx_time; + uint64_t last_rx_time; + uint16_t keepalive_seqnum; + bool keepalive; }; - -static void update_source(struct spa_bt_sco_io *io) +static void keepalive_send(struct spa_bt_sco_io *io) { - int enabled; - int changed = 0; + static const uint8_t zeros[2048]; + uint8_t buf[MAX_MTU]; + int res, need_flush = 0; + size_t size; - enabled = io->sink_cb != NULL; - if (SPA_FLAG_IS_SET(io->source.mask, SPA_IO_OUT) != enabled) { - SPA_FLAG_UPDATE(io->source.mask, SPA_IO_OUT, enabled); - changed = 1; + if (io->codec->id == SPA_BLUETOOTH_AUDIO_CODEC_CVSD) { + /* Doesn't have fixed block size; TX same amount as RX instead */ + size = SPA_MIN(sizeof(buf), io->read_size); + memset(buf, 0, size); + goto send; } - if (changed) { - spa_loop_update_source(io->data_loop, &io->source); - } + size = res = io->codec->start_encode(io->codec_data, buf, sizeof(buf), ++io->keepalive_seqnum, + io->last_rx_time / SPA_NSEC_PER_USEC); + if (res < 0) + return; + + do { + size_t encoded; + + res = io->codec->encode(io->codec_data, zeros, sizeof(zeros), + SPA_PTROFF(buf, size, void), sizeof(buf) - size, + &encoded, &need_flush); + if (res < 0) + return; + + size += encoded; + if (size >= sizeof(buf)) + return; + } while (!need_flush); + +send: + if (!io->keepalive) + spa_bt_sco_io_write_start(io); + + io->keepalive = false; + spa_bt_sco_io_write(io, buf, size); + io->keepalive = true; } static void sco_io_on_ready(struct spa_source *source) @@ -92,9 +133,11 @@ static void sco_io_on_ready(struct spa_source *source) if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_IN)) { int res; + int dummy; + uint64_t rx_time = 0; read_again: - res = recv(io->fd, io->read_buffer, SPA_MIN(io->read_mtu, MAX_MTU), MSG_DONTWAIT); + res = spa_bt_recvmsg(&io->recv, io->read_buffer, SPA_MIN(io->read_mtu, MAX_MTU), &rx_time, &dummy); if (res <= 0) { if (errno == EINTR) { /* retry if interrupted */ @@ -108,94 +151,140 @@ static void sco_io_on_ready(struct spa_source *source) goto stop; } - if (res != (int)io->read_size) + if (res != (int)io->read_size) { spa_log_trace(io->log, "%p: packet size:%d", io, res); + /* drop buffer when packet size changes */ + io->write_size = 0; + } + io->read_size = res; + io->last_rx_time = rx_time; + if (!io->last_tx_time) + io->last_tx_time = rx_time; if (io->source_cb) { int res; - res = io->source_cb(io->source_userdata, io->read_buffer, io->read_size); + res = io->source_cb(io->source_userdata, io->read_buffer, io->read_size, rx_time); if (res) { io->source_cb = NULL; } } + + /* If sink has not supplied packets for some time, for each RX packet send + * same amount of silence to keep the connection alive. Some devices (with + * LC3-24kHZ) require this and it doesn't hurt for others. + */ + if (io->last_tx_time + KEEPALIVE_NSEC < io->last_rx_time || io->keepalive) + keepalive_send(io); } read_done: - if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_OUT)) { - if (io->sink_cb) { - int res; - res = io->sink_cb(io->sink_userdata); - if (res) { - io->sink_cb = NULL; - } - } - } - if (SPA_FLAG_IS_SET(source->rmask, SPA_IO_ERR) || SPA_FLAG_IS_SET(source->rmask, SPA_IO_HUP)) { goto stop; } - - /* Poll socket in/out only if necessary */ - update_source(io); - return; stop: - if (io->source.loop) { + if (io->source.loop) spa_loop_remove_source(io->data_loop, &io->source); - io->started = false; +} + +static int write_packets(struct spa_bt_sco_io *io, const uint8_t **buf, size_t *size, size_t packet_size) +{ + while (*size >= packet_size) { + ssize_t written; + + written = send(io->fd, *buf, packet_size, MSG_DONTWAIT | MSG_NOSIGNAL); + if (written < 0) { + if (errno == EINTR) + continue; + return -errno; + } + + *buf += written; + *size -= written; } + return 0; } /* * Write data to socket in correctly sized blocks. - * Returns the number of bytes written, 0 when data cannot be written now or - * there is too little of it to write, and <0 on write error. + * Returns the number of bytes written or buffered, and <0 on write error. */ -int spa_bt_sco_io_write(struct spa_bt_sco_io *io, uint8_t *buf, int size) +int spa_bt_sco_io_write(struct spa_bt_sco_io *io, const uint8_t *buf, size_t size) { - uint16_t packet_size; - uint8_t *buf_start = buf; + const size_t orig_size = size; + const uint8_t *pos; + size_t packet_size; + int res; + + io->last_tx_time = io->last_rx_time; if (io->read_size == 0) { /* The proper write packet size is not known yet */ return 0; } - packet_size = SPA_MIN(io->write_mtu, io->read_size); - - if (size < packet_size) { - return 0; + if (io->keepalive) { + /* Transition from keepalive to sink-fed data */ + io->write_size = 0; + io->keepalive = false; } - do { - int written; + packet_size = SPA_MIN(SPA_MIN(io->write_mtu, io->read_size), sizeof(io->write_buffer)); - written = send(io->fd, buf, packet_size, MSG_DONTWAIT | MSG_NOSIGNAL); - if (written < 0) { - if (errno == EINTR) { - /* retry if interrupted */ - continue; - } else if (errno == EAGAIN || errno == EWOULDBLOCK) { - /* Don't continue writing */ - break; - } - return -errno; - } + if (io->write_size >= packet_size) { + /* packet size changed, drop data */ + io->write_size = 0; + } else if (io->write_size) { + /* write fragment */ + size_t need = SPA_MIN(packet_size - io->write_size, size); - buf += written; - size -= written; - } while (size >= packet_size); + memcpy(io->write_buffer + io->write_size, buf, need); + buf += need; + size -= need; + io->write_size += need; - return buf - buf_start; + if (io->write_size < packet_size) + return orig_size; + + pos = io->write_buffer; + if ((res = write_packets(io, &pos, &io->write_size, packet_size)) < 0) + goto fail; + if (io->write_size) + goto fail; + } + + /* write */ + if ((res = write_packets(io, &buf, &size, packet_size)) < 0) + goto fail; + + spa_assert(size < packet_size); + + /* store fragment */ + io->write_size = size; + if (size) + memcpy(io->write_buffer, buf, size); + + return orig_size; + +fail: + io->write_size = 0; + return res; } - -struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_bt_transport *transport, struct spa_loop *data_loop, struct spa_log *log) +void spa_bt_sco_io_write_start(struct spa_bt_sco_io *io) { - struct spa_bt_sco_io *io; + /* drop fragment */ + io->write_size = 0; +} + +struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_bt_transport *transport, struct spa_loop *data_loop, + struct spa_system *data_system, struct spa_log *log) +{ + spa_autofree struct spa_bt_sco_io *io = NULL; + struct spa_audio_info format = { 0 }; spa_log_topic_init(log, &log_topic); @@ -207,6 +296,7 @@ struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_bt_transport *transport, s io->read_mtu = transport->read_mtu; io->write_mtu = transport->write_mtu; io->data_loop = data_loop; + io->data_system = data_system; io->log = log; if (transport->device->adapter->bus_type == BUS_TYPE_USB) { @@ -216,31 +306,38 @@ struct spa_bt_sco_io *spa_bt_sco_io_create(struct spa_bt_transport *transport, s io->read_size = 0; } else { /* Set some sensible initial packet size */ - switch (transport->codec) { - case HFP_AUDIO_CODEC_CVSD: + switch (transport->media_codec->id) { + case SPA_BLUETOOTH_AUDIO_CODEC_CVSD: io->read_size = 48; /* 3ms S16_LE 8000 Hz */ break; - case HFP_AUDIO_CODEC_MSBC: - case HFP_AUDIO_CODEC_LC3_SWB: default: - io->read_size = HFP_CODEC_PACKET_SIZE; + io->read_size = HFP_H2_PACKET_SIZE; break; } } - spa_log_debug(io->log, "%p: initial packet size:%d", io, io->read_size); + io->codec = transport->media_codec; + + if (io->codec->validate_config(io->codec, 0, NULL, 0, &format) < 0) + return NULL; + + io->codec_data = io->codec->init(io->codec, 0, NULL, 0, &format, NULL, transport->write_mtu); + if (!io->codec_data) + return NULL; + + spa_log_debug(io->log, "%p: initial packet size:%d", io, (int)io->read_size); + + spa_bt_recvmsg_init(&io->recv, io->fd, io->data_system, io->log); /* Add the ready callback */ io->source.data = io; io->source.fd = io->fd; io->source.func = sco_io_on_ready; - io->source.mask = SPA_IO_IN | SPA_IO_OUT | SPA_IO_ERR | SPA_IO_HUP; + io->source.mask = SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP; io->source.rmask = 0; spa_loop_add_source(io->data_loop, &io->source); - io->started = true; - - return io; + return spa_steal_ptr(io); } static int do_remove_source(struct spa_loop *loop, @@ -260,10 +357,9 @@ static int do_remove_source(struct spa_loop *loop, void spa_bt_sco_io_destroy(struct spa_bt_sco_io *io) { - if (io->started) - spa_loop_invoke(io->data_loop, do_remove_source, 0, NULL, 0, true, io); - - io->started = false; + spa_log_debug(io->log, "%p: destroy", io); + spa_loop_locked(io->data_loop, do_remove_source, 0, NULL, 0, io); + io->codec->deinit(io->codec_data); free(io); } @@ -271,26 +367,10 @@ void spa_bt_sco_io_destroy(struct spa_bt_sco_io *io) * This function should only be called from the data thread. * Callback is called (in data loop) with data just read from the socket. */ -void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *, uint8_t *, int), void *userdata) +void spa_bt_sco_io_set_source_cb(struct spa_bt_sco_io *io, int (*source_cb)(void *, uint8_t *, int, uint64_t), void *userdata) { io->source_cb = source_cb; io->source_userdata = userdata; - - if (io->started) { - update_source(io); - } -} - -/* Set sink callback. - * This function should only be called from the data thread. - * Callback is called (in data loop) when socket can be written to. - */ -void spa_bt_sco_io_set_sink_cb(struct spa_bt_sco_io *io, int (*sink_cb)(void *), void *userdata) -{ - io->sink_cb = sink_cb; - io->sink_userdata = userdata; - - if (io->started) { - update_source(io); - } + io->last_rx_time = 0; + io->last_tx_time = 0; } diff --git a/spa/plugins/bluez5/sco-sink.c b/spa/plugins/bluez5/sco-sink.c deleted file mode 100644 index fc377ae10..000000000 --- a/spa/plugins/bluez5/sco-sink.c +++ /dev/null @@ -1,1763 +0,0 @@ -/* Spa SCO Sink */ -/* SPDX-FileCopyrightText: Copyright © 2019 Collabora Ltd. */ -/* SPDX-License-Identifier: MIT */ - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "defs.h" - -#ifdef HAVE_LC3 -#include -#endif - -SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.sink.sco"); -#undef SPA_LOG_TOPIC_DEFAULT -#define SPA_LOG_TOPIC_DEFAULT &log_topic - -#define DEFAULT_CLOCK_NAME "clock.system.monotonic" - -struct props { - int64_t latency_offset; - char clock_name[64]; -}; - -#define MAX_BUFFERS 32 - -#define ALT1_PACKET_SIZE 24 -#define ALT6_PACKET_SIZE 60 - -struct buffer { - uint32_t id; - unsigned int outstanding:1; - struct spa_buffer *buf; - struct spa_meta_header *h; - struct spa_list link; -}; - -struct port { - struct spa_audio_info current_format; - int frame_size; - unsigned int have_format:1; - - uint64_t info_all; - struct spa_port_info info; - struct spa_io_buffers *io; - struct spa_io_rate_match *rate_match; - struct spa_latency_info latency; -#define IDX_EnumFormat 0 -#define IDX_Meta 1 -#define IDX_IO 2 -#define IDX_Format 3 -#define IDX_Buffers 4 -#define IDX_Latency 5 -#define N_PORT_PARAMS 6 - struct spa_param_info params[N_PORT_PARAMS]; - - struct buffer buffers[MAX_BUFFERS]; - uint32_t n_buffers; - - struct spa_list ready; - - struct buffer *current_buffer; - uint32_t ready_offset; - uint8_t write_buffer[4096]; - uint32_t write_buffer_size; -}; - -struct impl { - struct spa_handle handle; - struct spa_node node; - - /* Support */ - struct spa_log *log; - struct spa_loop *data_loop; - struct spa_system *data_system; - - /* Hooks and callbacks */ - struct spa_hook_list hooks; - struct spa_callbacks callbacks; - - /* Info */ - uint64_t info_all; - struct spa_node_info info; -#define IDX_PropInfo 0 -#define IDX_Props 1 -#define N_NODE_PARAMS 2 - struct spa_param_info params[N_NODE_PARAMS]; - struct props props; - - uint32_t quantum_limit; - - /* Transport */ - struct spa_bt_transport *transport; - struct spa_hook transport_listener; - - /* Port */ - struct port port; - - /* Flags */ - unsigned int started:1; - unsigned int start_ready:1; - unsigned int transport_started:1; - unsigned int following:1; - unsigned int flush_pending:1; - - unsigned int is_internal:1; - - /* Sources */ - struct spa_source source; - struct spa_source flush_timer_source; - - /* Timer */ - int timerfd; - int flush_timerfd; - struct spa_io_clock *clock; - struct spa_io_position *position; - - uint64_t current_time; - uint64_t next_time; - uint64_t process_time; - - uint64_t prev_flush_time; - uint64_t next_flush_time; - - /* Codecs */ - uint8_t *buffer; - uint8_t *buffer_head; - uint8_t *buffer_next; - int buffer_size; - int h2_seq; - - /* mSBC */ - sbc_t msbc; - - /* LC3 */ -#ifdef HAVE_LC3 - lc3_encoder_t lc3; -#else - void *lc3; -#endif -}; - -#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == 0) - -static const char sntable[4] = { 0x08, 0x38, 0xC8, 0xF8 }; - -static void reset_props(struct props *props) -{ - props->latency_offset = 0; - strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); -} - -static int impl_node_enum_params(void *object, int seq, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) -{ - struct impl *this = object; - struct spa_pod *param; - struct spa_pod_builder b = { 0 }; - uint8_t buffer[1024]; - struct spa_result_node_params result; - uint32_t count = 0; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(num != 0, -EINVAL); - - result.id = id; - result.next = start; - next: - result.index = result.next++; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - - switch (id) { - case SPA_PARAM_PropInfo: - { - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_latencyOffsetNsec), - SPA_PROP_INFO_description, SPA_POD_String("Latency offset (ns)"), - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Long(0LL, INT64_MIN, INT64_MAX)); - break; - default: - return 0; - } - break; - } - case SPA_PARAM_Props: - { - struct props *p = &this->props; - - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_Props, id, - SPA_PROP_latencyOffsetNsec, SPA_POD_Long(p->latency_offset)); - break; - default: - return 0; - } - break; - } - default: - return -ENOENT; - } - - if (spa_pod_filter(&b, &result.param, param, filter) < 0) - goto next; - - spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); - - if (++count != num) - goto next; - - return 0; -} - -static int set_timeout(struct impl *this, uint64_t time) -{ - struct itimerspec ts; - ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; - ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; - ts.it_interval.tv_sec = 0; - ts.it_interval.tv_nsec = 0; - return spa_system_timerfd_settime(this->data_system, - this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); -} - -static int set_timers(struct impl *this) -{ - struct timespec now; - - spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); - this->next_time = SPA_TIMESPEC_TO_NSEC(&now); - - return set_timeout(this, this->following ? 0 : this->next_time); -} - -static int do_reassign_follower(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *this = user_data; - set_timers(this); - return 0; -} - -static inline bool is_following(struct impl *this) -{ - return this->position && this->clock && this->position->clock.id != this->clock->id; -} - -static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) -{ - struct impl *this = object; - bool following; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - switch (id) { - case SPA_IO_Clock: - this->clock = data; - if (this->clock != NULL) { - spa_scnprintf(this->clock->name, - sizeof(this->clock->name), - "%s", this->props.clock_name); - } - break; - case SPA_IO_Position: - this->position = data; - break; - default: - return -ENOENT; - } - - following = is_following(this); - if (this->started && following != this->following) { - spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); - this->following = following; - spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); - } - return 0; -} - -static void emit_node_info(struct impl *this, bool full); - -static void emit_port_info(struct impl *this, struct port *port, bool full); - -static void set_latency(struct impl *this, bool emit_latency) -{ - struct port *port = &this->port; - int64_t delay; - - if (this->transport == NULL) - return; - - /* - * We start flushing data immediately, so the delay is: - * - * (transport delay) + (packet delay) + (codec internal delay) + (latency offset) - * - * and doesn't depend on the quantum. The codec internal delay is neglected. - * Kernel knows the latency due to socket/controller queue, but doesn't - * tell us, so not included but hopefully in < 20 ms range. - */ - - switch (this->transport->codec) { - case HFP_AUDIO_CODEC_MSBC: - case HFP_AUDIO_CODEC_LC3_SWB: - delay = 7500 * SPA_NSEC_PER_USEC; - break; - default: - delay = this->transport->write_mtu / (2 * 8000); - break; - } - - delay += spa_bt_transport_get_delay_nsec(this->transport); - delay += SPA_CLAMP(this->props.latency_offset, -delay, INT64_MAX / 2); - delay = SPA_MAX(delay, 0); - - spa_log_info(this->log, "%p: total latency:%d ms", this, (int)(delay / SPA_NSEC_PER_MSEC)); - - port->latency.min_ns = port->latency.max_ns = delay; - port->latency.min_rate = port->latency.max_rate = 0; - port->latency.min_quantum = port->latency.max_quantum = 0.0f; - - if (emit_latency) { - port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; - port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; - emit_port_info(this, port, false); - } -} - -static int apply_props(struct impl *this, const struct spa_pod *param) -{ - struct props new_props = this->props; - int changed = 0; - - if (param == NULL) { - reset_props(&new_props); - } else { - spa_pod_parse_object(param, - SPA_TYPE_OBJECT_Props, NULL, - SPA_PROP_latencyOffsetNsec, SPA_POD_OPT_Long(&new_props.latency_offset)); - } - - changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0); - this->props = new_props; - - if (changed) - set_latency(this, true); - - return changed; -} - -static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, - const struct spa_pod *param) -{ - struct impl *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - switch (id) { - case SPA_PARAM_Props: - { - if (apply_props(this, param) > 0) { - this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; - this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; - emit_node_info(this, false); - } - break; - } - default: - return -ENOENT; - } - - return 0; -} - -static void enable_flush_timer(struct impl *this, bool enabled) -{ - struct itimerspec ts; - - if (!enabled) - this->next_flush_time = 0; - - ts.it_value.tv_sec = this->next_flush_time / SPA_NSEC_PER_SEC; - ts.it_value.tv_nsec = this->next_flush_time % SPA_NSEC_PER_SEC; - ts.it_interval.tv_sec = 0; - ts.it_interval.tv_nsec = 0; - spa_system_timerfd_settime(this->data_system, - this->flush_timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); - - this->flush_pending = enabled; -} - -static uint32_t get_queued_frames(struct impl *this) -{ - struct port *port = &this->port; - uint32_t bytes = 0; - struct buffer *b; - - spa_list_for_each(b, &port->ready, link) { - struct spa_data *d = b->buf->datas; - - bytes += d[0].chunk->size; - } - - if (bytes > port->ready_offset) - bytes -= port->ready_offset; - else - bytes = 0; - - return bytes / port->frame_size; -} - -static int lc3_encode_frame(struct impl *this, const void *src, size_t src_size, void *dst, size_t dst_size, - ssize_t *dst_out) -{ -#ifdef HAVE_LC3 - int res; - - if (src_size < LC3_SWB_DECODED_SIZE) - return -EINVAL; - if (dst_size < LC3_SWB_PAYLOAD_SIZE) - return -EINVAL; - - res = lc3_encode(this->lc3, LC3_PCM_FORMAT_S24, src, 1, LC3_SWB_PAYLOAD_SIZE, dst); - if (res != 0) - return -EINVAL; - - *dst_out = LC3_SWB_PAYLOAD_SIZE; - return LC3_SWB_DECODED_SIZE; -#else - return -EOPNOTSUPP; -#endif -} - -static int flush_data(struct impl *this) -{ - struct port *port = &this->port; - int processed = 0; - int written; - - spa_assert(this->transport_started); - - if (this->transport == NULL || this->transport->sco_io == NULL || !this->flush_timer_source.loop) - return -EIO; - - const uint32_t min_in_size = (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ? MSBC_DECODED_SIZE : - (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) ? LC3_SWB_DECODED_SIZE : - this->transport->write_mtu; - const uint32_t packet_samples = min_in_size / port->frame_size; - const uint64_t packet_time = (uint64_t)packet_samples * SPA_NSEC_PER_SEC - / port->current_format.info.raw.rate; - - while (!spa_list_is_empty(&port->ready) && port->write_buffer_size < min_in_size) { - struct spa_data *datas; - - /* get buffer */ - if (!port->current_buffer) { - spa_return_val_if_fail(!spa_list_is_empty(&port->ready), -EIO); - port->current_buffer = spa_list_first(&port->ready, struct buffer, link); - port->ready_offset = 0; - } - datas = port->current_buffer->buf->datas; - - /* if buffer has data, copy it into the write buffer */ - if (datas[0].chunk->size - port->ready_offset > 0) { - const uint32_t avail = - SPA_MIN(min_in_size, datas[0].chunk->size - port->ready_offset); - const uint32_t size = - (avail + port->write_buffer_size) > min_in_size ? - min_in_size - port->write_buffer_size : avail; - memcpy(port->write_buffer + port->write_buffer_size, - (uint8_t *)datas[0].data + port->ready_offset, - size); - port->write_buffer_size += size; - port->ready_offset += size; - } else { - struct buffer *b; - - b = port->current_buffer; - port->current_buffer = NULL; - - /* reuse buffer */ - spa_list_remove(&b->link); - b->outstanding = true; - spa_log_trace(this->log, "sco-sink %p: reuse buffer %u", this, b->id); - port->io->buffer_id = b->id; - spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); - } - } - - if (this->flush_pending) { - spa_log_trace(this->log, "%p: wait for flush timer", this); - return 0; - } - - if (port->write_buffer_size < min_in_size) { - /* wait for more data */ - spa_log_trace(this->log, "%p: skip flush", this); - enable_flush_timer(this, false); - return 0; - } - - if (this->transport->codec == HFP_AUDIO_CODEC_MSBC || - this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) { - ssize_t out_encoded; - /* Encode */ - if (this->buffer_next + HFP_CODEC_PACKET_SIZE > this->buffer + this->buffer_size) { - /* Buffer overrun; shouldn't usually happen. Drop data and reset. */ - this->buffer_head = this->buffer_next = this->buffer; - spa_log_warn(this->log, "sco-sink: mSBC/LC3 buffer overrun, dropping data"); - } - - /* H2 sync header */ - this->buffer_next[0] = 0x01; - this->buffer_next[1] = sntable[this->h2_seq % 4]; - this->buffer_next[59] = 0x00; - this->h2_seq = (this->h2_seq + 1) % 4; - - if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { - processed = sbc_encode(&this->msbc, port->write_buffer, port->write_buffer_size, - this->buffer_next + 2, HFP_CODEC_PACKET_SIZE - 3, &out_encoded); - out_encoded += 1; /* pad */ - } else { - processed = lc3_encode_frame(this, port->write_buffer, port->write_buffer_size, - this->buffer_next + 2, HFP_CODEC_PACKET_SIZE - 2, &out_encoded); - } - - if (processed < 0) { - spa_log_warn(this->log, "encode failed: %d", processed); - return -EINVAL; - } - this->buffer_next += out_encoded + 2; - port->write_buffer_size = 0; - - /* Write */ - written = spa_bt_sco_io_write(this->transport->sco_io, this->buffer_head, - this->buffer_next - this->buffer_head); - if (written < 0) { - spa_log_warn(this->log, "failed to write data: %d (%s)", - written, spa_strerror(written)); - goto stop; - } - - this->buffer_head += written; - - if (this->buffer_head == this->buffer_next) - this->buffer_head = this->buffer_next = this->buffer; - else if (this->buffer_next + HFP_CODEC_PACKET_SIZE > this->buffer + this->buffer_size) { - /* Written bytes is not necessarily commensurate - * with HFP_CODEC_PACKET_SIZE. If this occurs, copy data. - */ - int size = this->buffer_next - this->buffer_head; - spa_memmove(this->buffer, this->buffer_head, size); - this->buffer_next = this->buffer + size; - this->buffer_head = this->buffer; - } - } else { - written = spa_bt_sco_io_write(this->transport->sco_io, port->write_buffer, - port->write_buffer_size); - if (written < 0) { - spa_log_warn(this->log, "sco-sink: write failure: %d (%s)", - written, spa_strerror(written)); - goto stop; - } else if (written == 0) { - /* EAGAIN or similar, just skip ahead */ - written = SPA_MIN(port->write_buffer_size, (uint32_t)48); - } - - processed = written; - port->write_buffer_size -= written; - - if (port->write_buffer_size > 0 && written > 0) { - spa_memmove(port->write_buffer, port->write_buffer + written, port->write_buffer_size); - } - } - - if (SPA_UNLIKELY(spa_log_level_topic_enabled(this->log, SPA_LOG_TOPIC_DEFAULT, SPA_LOG_LEVEL_TRACE))) { - struct timespec ts; - uint64_t now; - uint64_t dt; - - spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &ts); - now = SPA_TIMESPEC_TO_NSEC(&ts); - dt = now - this->prev_flush_time; - this->prev_flush_time = now; - - spa_log_trace(this->log, - "%p: send wrote:%d dt:%"PRIu64, - this, written, dt); - } - - spa_log_trace(this->log, "write socket data %d", written); - - if (SPA_LIKELY(this->position)) { - uint32_t frames = get_queued_frames(this); - uint64_t duration_ns; - - /* - * Flush at the time position of the next buffered sample. - */ - duration_ns = ((uint64_t)this->position->clock.duration * SPA_NSEC_PER_SEC - / this->position->clock.rate.denom); - this->next_flush_time = this->process_time + duration_ns - - ((uint64_t)frames * SPA_NSEC_PER_SEC - / port->current_format.info.raw.rate); - - /* - * We could delay the output by one packet to avoid waiting - * for the next buffer and so make send intervals more regular. - * However, this appears not needed in practice, and it's better - * to not add latency if not needed. - */ -#if 0 - this->next_flush_time += SPA_MIN(packet_time, - duration_ns * (port->n_buffers - 1)); -#endif - } else { - if (this->next_flush_time == 0) - this->next_flush_time = this->process_time; - this->next_flush_time += packet_time; - } - - enable_flush_timer(this, true); - return 0; - -stop: - enable_flush_timer(this, false); - if (this->flush_timer_source.loop) - spa_loop_remove_source(this->data_loop, &this->flush_timer_source); - return -EIO; -} - -static void sco_on_flush_timeout(struct spa_source *source) -{ - struct impl *this = source->data; - uint64_t exp = 0; - int res; - - spa_log_trace(this->log, "%p: flush on timeout", this); - - if ((res = spa_system_timerfd_read(this->data_system, this->flush_timerfd, &exp)) < 0) { - if (res != -EAGAIN) - spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res)); - return; - } - - if (this->transport == NULL) { - enable_flush_timer(this, false); - return; - } - - while (exp-- > 0) { - this->flush_pending = false; - flush_data(this); - } -} - -static void sco_on_timeout(struct spa_source *source) -{ - struct impl *this = source->data; - struct port *port = &this->port; - uint64_t exp, duration; - uint32_t rate; - struct spa_io_buffers *io = port->io; - uint64_t prev_time, now_time; - int status, res; - - if (this->started) { - if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) { - if (res != -EAGAIN) - spa_log_warn(this->log, "error reading timerfd: %s", spa_strerror(res)); - return; - } - } - - prev_time = this->current_time; - now_time = this->current_time = this->next_time; - - spa_log_debug(this->log, "%p: timer %"PRIu64" %"PRIu64"", this, - now_time, now_time - prev_time); - - if (SPA_LIKELY(this->position)) { - duration = this->position->clock.target_duration; - rate = this->position->clock.target_rate.denom; - } else { - duration = 1024; - rate = 48000; - } - - this->next_time = now_time + duration * SPA_NSEC_PER_SEC / rate; - - if (SPA_LIKELY(this->clock)) { - this->clock->nsec = now_time; - this->clock->rate = this->clock->target_rate; - this->clock->position += this->clock->duration; - this->clock->duration = duration; - this->clock->rate_diff = 1.0f; - this->clock->next_nsec = this->next_time; - this->clock->delay = 0; - } - - status = this->transport_started ? SPA_STATUS_NEED_DATA : SPA_STATUS_HAVE_DATA; - - spa_log_trace(this->log, "%p: %d -> %d", this, io->status, status); - io->status = status; - io->buffer_id = SPA_ID_INVALID; - spa_node_call_ready(&this->callbacks, status); - - set_timeout(this, this->next_time); -} - -/* greater common divider */ -static int gcd(int a, int b) { - while(b) { - int c = b; - b = a % b; - a = c; - } - return a; -} -/* least common multiple */ -static int lcm(int a, int b) { - return (a*b)/gcd(a,b); -} - -static int transport_start(struct impl *this) -{ - int res; - - /* Don't do anything if the node has already started */ - if (this->transport_started) - return 0; - if (!this->start_ready) - return -EIO; - - /* Make sure the transport is valid */ - spa_return_val_if_fail(this->transport != NULL, -EIO); - - this->following = is_following(this); - - spa_log_debug(this->log, "%p: start transport", this); - - /* Init mSBC/LC3 if needed */ - if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { - res = sbc_init_msbc(&this->msbc, 0); - if (res < 0) - return res; - /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */ - this->msbc.endian = SBC_LE; - - /* write_mtu might not be correct at this point, so we'll throw - * in some common ones, at the cost of a potentially larger - * allocation (size <= 120 * write_mtu). If it still fails to be - * commensurate, we may end up doing memmoves, but nothing worse - * is going to happen. - */ - this->buffer_size = lcm(ALT1_PACKET_SIZE, lcm(ALT6_PACKET_SIZE, lcm(this->transport->write_mtu, 2 * HFP_CODEC_PACKET_SIZE))); - } else if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) { -#ifdef HAVE_LC3 - this->lc3 = lc3_setup_encoder(7500, 32000, 0, - calloc(1, lc3_encoder_size(7500, 32000))); - if (!this->lc3) - return -EINVAL; - - spa_assert(lc3_frame_samples(7500, 32000) * this->port.frame_size == LC3_SWB_DECODED_SIZE); - - this->buffer_size = lcm(ALT1_PACKET_SIZE, lcm(ALT6_PACKET_SIZE, lcm(this->transport->write_mtu, 2 * HFP_CODEC_PACKET_SIZE))); -#else - res = -EOPNOTSUPP; - goto fail; -#endif - } else { - this->buffer_size = 0; - } - - if (this->buffer_size) { - this->buffer = calloc(this->buffer_size, sizeof(uint8_t)); - this->buffer_head = this->buffer_next = this->buffer; - if (this->buffer == NULL) { - res = -errno; - goto fail; - } - } - - spa_return_val_if_fail(this->transport->write_mtu <= sizeof(this->port.write_buffer), -EINVAL); - - spa_log_info(this->log, "%p: using codec %d, delay:%"PRIi64" ms", this, this->transport->codec, - (int64_t)(spa_bt_transport_get_delay_nsec(this->transport) / SPA_NSEC_PER_MSEC)); - - /* start socket i/o */ - if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop)) < 0) - goto fail; - - this->flush_timer_source.data = this; - this->flush_timer_source.fd = this->flush_timerfd; - this->flush_timer_source.func = sco_on_flush_timeout; - this->flush_timer_source.mask = SPA_IO_IN; - this->flush_timer_source.rmask = 0; - spa_loop_add_source(this->data_loop, &this->flush_timer_source); - - /* start processing */ - this->flush_pending = false; - - /* Set the started flag */ - this->transport_started = true; - - return 0; - -fail: - free(this->buffer); - this->buffer = NULL; - sbc_finish(&this->msbc); - free(this->lc3); - this->lc3 = NULL; - return res; -} - -static int do_start(struct impl *this) -{ - bool do_accept; - int res; - - if (this->started) - return 0; - - spa_return_val_if_fail(this->transport, -EIO); - - this->following = is_following(this); - - this->start_ready = true; - - spa_log_debug(this->log, "%p: start following:%d", this, this->following); - - /* Do accept if Gateway; otherwise do connect for Head Unit */ - do_accept = this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY; - - /* acquire the socket fd (false -> connect | true -> accept) */ - if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0) { - this->start_ready = false; - return res; - } - - /* Add the timeout callback */ - this->source.data = this; - this->source.fd = this->timerfd; - this->source.func = sco_on_timeout; - this->source.mask = SPA_IO_IN; - this->source.rmask = 0; - spa_loop_add_source(this->data_loop, &this->source); - - set_timers(this); - - this->started = true; - - return 0; -} - -/* Drop any buffered data remaining in the port */ -static void drop_port_output(struct impl *this) -{ - struct port *port = &this->port; - - port->write_buffer_size = 0; - port->current_buffer = NULL; - port->ready_offset = 0; - - while (!spa_list_is_empty(&port->ready)) { - struct buffer *b; - b = spa_list_first(&port->ready, struct buffer, link); - - spa_list_remove(&b->link); - b->outstanding = true; - port->io->buffer_id = b->id; - spa_node_call_reuse_buffer(&this->callbacks, 0, b->id); - } -} - -static int do_remove_source(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *this = user_data; - - if (this->source.loop) - spa_loop_remove_source(this->data_loop, &this->source); - set_timeout(this, 0); - - return 0; -} - -static int do_remove_transport_source(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *this = user_data; - - this->transport_started = false; - - if (this->flush_timer_source.loop) - spa_loop_remove_source(this->data_loop, &this->flush_timer_source); - enable_flush_timer(this, false); - - /* Drop queued data */ - drop_port_output(this); - - return 0; -} - -static void transport_stop(struct impl *this) -{ - if (!this->transport_started) - return; - - spa_log_trace(this->log, "sco-sink %p: transport stop", this); - - spa_loop_invoke(this->data_loop, do_remove_transport_source, 0, NULL, 0, true, this); - - if (this->buffer) { - free(this->buffer); - this->buffer = NULL; - this->buffer_head = this->buffer_next = this->buffer; - } - - sbc_finish(&this->msbc); - free(this->lc3); - this->lc3 = NULL; -} - -static int do_stop(struct impl *this) -{ - int res; - - if (!this->started) - return 0; - - spa_log_debug(this->log, "%p: stop", this); - - this->start_ready = false; - - spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); - - transport_stop(this); - - if (this->transport) - res = spa_bt_transport_release(this->transport); - else - res = 0; - - this->started = false; - - return res; -} - -static int impl_node_send_command(void *object, const struct spa_command *command) -{ - struct impl *this = object; - struct port *port; - int res; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(command != NULL, -EINVAL); - - port = &this->port; - - switch (SPA_NODE_COMMAND_ID(command)) { - case SPA_NODE_COMMAND_Start: - if (!port->have_format) - return -EIO; - if (port->n_buffers == 0) - return -EIO; - if ((res = do_start(this)) < 0) - return res; - break; - case SPA_NODE_COMMAND_Pause: - case SPA_NODE_COMMAND_Suspend: - if ((res = do_stop(this)) < 0) - return res; - break; - default: - return -ENOTSUP; - } - return 0; -} - -static void emit_node_info(struct impl *this, bool full) -{ - const struct spa_dict_item hu_node_info_items[] = { - { SPA_KEY_DEVICE_API, "bluez5" }, - { SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Sink/Internal" : "Audio/Sink" }, - { SPA_KEY_NODE_DRIVER, "true" }, - }; - - const struct spa_dict_item ag_node_info_items[] = { - { SPA_KEY_DEVICE_API, "bluez5" }, - { SPA_KEY_MEDIA_CLASS, "Stream/Input/Audio" }, - { "media.name", ((this->transport && this->transport->device->name) ? - this->transport->device->name : "HSP/HFP") }, - { SPA_KEY_MEDIA_ROLE, "Communication" }, - }; - bool is_ag = this->transport && - (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); - uint64_t old = full ? this->info.change_mask : 0; - - if (full) - this->info.change_mask = this->info_all; - if (this->info.change_mask) { - this->info.props = is_ag ? - &SPA_DICT_INIT_ARRAY(ag_node_info_items) : - &SPA_DICT_INIT_ARRAY(hu_node_info_items); - spa_node_emit_info(&this->hooks, &this->info); - this->info.change_mask = old; - } -} - -static void emit_port_info(struct impl *this, struct port *port, bool full) -{ - uint64_t old = full ? port->info.change_mask : 0; - if (full) - port->info.change_mask = port->info_all; - if (port->info.change_mask) { - spa_node_emit_port_info(&this->hooks, - SPA_DIRECTION_INPUT, 0, &port->info); - port->info.change_mask = old; - } -} - -static int -impl_node_add_listener(void *object, - struct spa_hook *listener, - const struct spa_node_events *events, - void *data) -{ - struct impl *this = object; - struct spa_hook_list save; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_hook_list_isolate(&this->hooks, &save, listener, events, data); - - emit_node_info(this, true); - emit_port_info(this, &this->port, true); - - spa_hook_list_join(&this->hooks, &save); - - return 0; -} - -static int -impl_node_set_callbacks(void *object, - const struct spa_node_callbacks *callbacks, - void *data) -{ - struct impl *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); - - return 0; -} - -static int impl_node_sync(void *object, int seq) -{ - struct impl *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); - - return 0; -} - -static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, - const struct spa_dict *props) -{ - return -ENOTSUP; -} - -static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) -{ - return -ENOTSUP; -} - -static int -impl_node_port_enum_params(void *object, int seq, - enum spa_direction direction, uint32_t port_id, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) -{ - - struct impl *this = object; - struct port *port; - struct spa_pod *param; - struct spa_pod_builder b = { 0 }; - uint8_t buffer[1024]; - struct spa_result_node_params result; - uint32_t count = 0; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(num != 0, -EINVAL); - - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - port = &this->port; - - result.id = id; - result.next = start; - next: - result.index = result.next++; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - - switch (id) { - case SPA_PARAM_EnumFormat: - if (result.index > 0) - return 0; - if (this->transport == NULL) - return -EIO; - - /* set the info structure */ - struct spa_audio_info_raw info = { 0, }; - - if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) - info.format = SPA_AUDIO_FORMAT_S24_32_LE; - else - info.format = SPA_AUDIO_FORMAT_S16_LE; - info.channels = 1; - info.position[0] = SPA_AUDIO_CHANNEL_MONO; - - /* CVSD format has a rate of 8kHz - * MSBC format has a rate of 16kHz - * LC3-SWB format has a rate of 32kHz - */ - if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) - info.rate = 32000; - else if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) - info.rate = 16000; - else - info.rate = 8000; - - /* build the param */ - param = spa_format_audio_raw_build(&b, id, &info); - - break; - - case SPA_PARAM_Format: - if (!port->have_format) - return -EIO; - if (result.index > 0) - return 0; - - param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw); - break; - - case SPA_PARAM_Buffers: - if (!port->have_format) - return -EIO; - if (result.index > 0) - return 0; - - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamBuffers, id, - SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), - SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), - SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( - this->quantum_limit * port->frame_size, - 16 * port->frame_size, - INT32_MAX), - SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size)); - break; - - case SPA_PARAM_Meta: - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamMeta, id, - SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), - SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); - break; - default: - return 0; - } - break; - - case SPA_PARAM_IO: - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamIO, id, - SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), - SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); - break; - case 1: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamIO, id, - SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch), - SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match))); - break; - default: - return 0; - } - break; - - case SPA_PARAM_Latency: - switch (result.index) { - case 0: - param = spa_latency_build(&b, id, &port->latency); - break; - default: - return 0; - } - break; - - default: - return -ENOENT; - } - - if (spa_pod_filter(&b, &result.param, param, filter) < 0) - goto next; - - spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); - - if (++count != num) - goto next; - - return 0; -} - -static int clear_buffers(struct impl *this, struct port *port) -{ - do_stop(this); - if (port->n_buffers > 0) { - spa_list_init(&port->ready); - port->n_buffers = 0; - } - return 0; -} - -static int port_set_format(struct impl *this, struct port *port, - uint32_t flags, - const struct spa_pod *format) -{ - int err; - - if (format == NULL) { - spa_log_debug(this->log, "clear format"); - clear_buffers(this, port); - port->have_format = false; - } else { - struct spa_audio_info info = { 0 }; - - if (!this->transport) - return -EIO; - - if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) - return err; - - if (info.media_type != SPA_MEDIA_TYPE_audio || - info.media_subtype != SPA_MEDIA_SUBTYPE_raw) - return -EINVAL; - - if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) - return -EINVAL; - - if (info.info.raw.rate == 0 || - info.info.raw.channels != 1) - return -EINVAL; - - switch (info.info.raw.format) { - case SPA_AUDIO_FORMAT_S16_LE: - if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) - return -EINVAL; - port->frame_size = info.info.raw.channels * 2; - break; - case SPA_AUDIO_FORMAT_S24_32_LE: - if (this->transport->codec != HFP_AUDIO_CODEC_LC3_SWB) - return -EINVAL; - port->frame_size = info.info.raw.channels * 4; - break; - default: - return -EINVAL; - } - - port->current_format = info; - port->have_format = true; - } - - set_latency(this, false); - - port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; - if (port->have_format) { - port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; - port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate); - port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); - port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); - port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; - } else { - port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); - port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); - } - emit_port_info(this, port, false); - - return 0; -} - -static int -impl_node_port_set_param(void *object, - enum spa_direction direction, uint32_t port_id, - uint32_t id, uint32_t flags, - const struct spa_pod *param) -{ - struct impl *this = object; - struct port *port; - int res; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); - port = &this->port; - - switch (id) { - case SPA_PARAM_Format: - res = port_set_format(this, port, flags, param); - break; - case SPA_PARAM_Latency: - res = 0; - break; - default: - res = -ENOENT; - break; - } - return res; -} - -static int -impl_node_port_use_buffers(void *object, - enum spa_direction direction, uint32_t port_id, - uint32_t flags, - struct spa_buffer **buffers, uint32_t n_buffers) -{ - struct impl *this = object; - struct port *port; - uint32_t i; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - port = &this->port; - - spa_log_debug(this->log, "use buffers %d", n_buffers); - - clear_buffers(this, port); - - if (n_buffers > 0 && !port->have_format) - return -EIO; - if (n_buffers > MAX_BUFFERS) - return -ENOSPC; - - for (i = 0; i < n_buffers; i++) { - struct buffer *b = &port->buffers[i]; - - b->buf = buffers[i]; - b->id = i; - b->outstanding = true; - - b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); - - if (buffers[i]->datas[0].data == NULL) { - spa_log_error(this->log, "%p: need mapped memory", this); - return -EINVAL; - } - } - port->n_buffers = n_buffers; - - return 0; -} - -static int -impl_node_port_set_io(void *object, - enum spa_direction direction, - uint32_t port_id, - uint32_t id, - void *data, size_t size) -{ - struct impl *this = object; - struct port *port; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - port = &this->port; - - switch (id) { - case SPA_IO_Buffers: - port->io = data; - break; - case SPA_IO_RateMatch: - port->rate_match = data; - break; - default: - return -ENOENT; - } - return 0; -} - -static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) -{ - return -ENOTSUP; -} - -static int impl_node_process(void *object) -{ - struct impl *this = object; - struct port *port; - struct spa_io_buffers *io; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - port = &this->port; - if ((io = port->io) == NULL) - return -EIO; - - if (this->position && this->position->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) { - io->status = SPA_STATUS_NEED_DATA; - return SPA_STATUS_HAVE_DATA; - } - - if (!this->started || !this->transport_started) { - if (io->status != SPA_STATUS_HAVE_DATA) { - io->status = SPA_STATUS_HAVE_DATA; - io->buffer_id = SPA_ID_INVALID; - } - return SPA_STATUS_HAVE_DATA; - } - - if (io->status == SPA_STATUS_HAVE_DATA && io->buffer_id < port->n_buffers) { - struct buffer *b = &port->buffers[io->buffer_id]; - - if (!b->outstanding) { - spa_log_warn(this->log, "%p: buffer %u in use", this, io->buffer_id); - io->status = -EINVAL; - return -EINVAL; - } - - spa_log_trace(this->log, "%p: queue buffer %u", this, io->buffer_id); - - spa_list_append(&port->ready, &b->link); - b->outstanding = false; - io->buffer_id = SPA_ID_INVALID; - io->status = SPA_STATUS_OK; - } - - if (this->following) { - if (this->position) { - this->current_time = this->position->clock.nsec; - } else { - struct timespec now; - spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); - this->current_time = SPA_TIMESPEC_TO_NSEC(&now); - } - } - - this->process_time = this->current_time; - - if (!spa_list_is_empty(&port->ready)) { - int res; - spa_log_trace(this->log, "%p: flush on process", this); - if ((res = flush_data(this)) < 0) { - io->status = res; - return SPA_STATUS_STOPPED; - } - } - - return SPA_STATUS_HAVE_DATA; -} - -static const struct spa_node_methods impl_node = { - SPA_VERSION_NODE_METHODS, - .add_listener = impl_node_add_listener, - .set_callbacks = impl_node_set_callbacks, - .sync = impl_node_sync, - .enum_params = impl_node_enum_params, - .set_param = impl_node_set_param, - .set_io = impl_node_set_io, - .send_command = impl_node_send_command, - .add_port = impl_node_add_port, - .remove_port = impl_node_remove_port, - .port_enum_params = impl_node_port_enum_params, - .port_set_param = impl_node_port_set_param, - .port_use_buffers = impl_node_port_use_buffers, - .port_set_io = impl_node_port_set_io, - .port_reuse_buffer = impl_node_port_reuse_buffer, - .process = impl_node_process, -}; - -static void transport_state_changed(void *data, - enum spa_bt_transport_state old, - enum spa_bt_transport_state state) -{ - struct impl *this = data; - - spa_log_debug(this->log, "%p: transport %p state %d->%d", this, this->transport, old, state); - - if (state == SPA_BT_TRANSPORT_STATE_ACTIVE) - transport_start(this); - else if (state < SPA_BT_TRANSPORT_STATE_ACTIVE) - transport_stop(this); - - if (state == SPA_BT_TRANSPORT_STATE_ERROR) { - uint8_t buffer[1024]; - struct spa_pod_builder b = { 0 }; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - spa_node_emit_event(&this->hooks, - spa_pod_builder_add_object(&b, - SPA_TYPE_EVENT_Node, SPA_NODE_EVENT_Error)); - } -} - -static int do_transport_destroy(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *this = user_data; - this->transport = NULL; - return 0; -} - -static void transport_destroy(void *data) -{ - struct impl *this = data; - spa_log_debug(this->log, "transport %p destroy", this->transport); - spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this); -} - -static const struct spa_bt_transport_events transport_events = { - SPA_VERSION_BT_TRANSPORT_EVENTS, - .state_changed = transport_state_changed, - .destroy = transport_destroy, -}; - -static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) -{ - struct impl *this; - - spa_return_val_if_fail(handle != NULL, -EINVAL); - spa_return_val_if_fail(interface != NULL, -EINVAL); - - this = (struct impl *) handle; - - if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) - *interface = &this->node; - else - return -ENOENT; - - return 0; -} - -static int impl_clear(struct spa_handle *handle) -{ - struct impl *this = (struct impl *) handle; - - do_stop(this); - if (this->transport) - spa_hook_remove(&this->transport_listener); - spa_system_close(this->data_system, this->timerfd); - spa_system_close(this->data_system, this->flush_timerfd); - return 0; -} - -static size_t -impl_get_size(const struct spa_handle_factory *factory, - const struct spa_dict *params) -{ - return sizeof(struct impl); -} - -static int -impl_init(const struct spa_handle_factory *factory, - struct spa_handle *handle, - const struct spa_dict *info, - const struct spa_support *support, - uint32_t n_support) -{ - struct impl *this; - struct port *port; - const char *str; - - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(handle != NULL, -EINVAL); - - handle->get_interface = impl_get_interface; - handle->clear = impl_clear; - - this = (struct impl *) handle; - - this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); - this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); - this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); - - spa_log_topic_init(this->log, &log_topic); - - if (this->data_loop == NULL) { - spa_log_error(this->log, "a data loop is needed"); - return -EINVAL; - } - if (this->data_system == NULL) { - spa_log_error(this->log, "a data system is needed"); - return -EINVAL; - } - - this->node.iface = SPA_INTERFACE_INIT( - SPA_TYPE_INTERFACE_Node, - SPA_VERSION_NODE, - &impl_node, this); - spa_hook_list_init(&this->hooks); - - reset_props(&this->props); - - this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | - SPA_NODE_CHANGE_MASK_PARAMS | - SPA_NODE_CHANGE_MASK_PROPS; - this->info = SPA_NODE_INFO_INIT(); - this->info.max_input_ports = 1; - this->info.max_output_ports = 0; - this->info.flags = SPA_NODE_FLAG_RT; - this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); - this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); - this->info.params = this->params; - this->info.n_params = N_NODE_PARAMS; - - port = &this->port; - port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | - SPA_PORT_CHANGE_MASK_PARAMS; - port->info = SPA_PORT_INFO_INIT(); - port->info.flags = SPA_PORT_FLAG_LIVE | - SPA_PORT_FLAG_PHYSICAL | - SPA_PORT_FLAG_TERMINAL; - port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); - port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); - port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); - port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); - port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); - port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); - port->info.params = port->params; - port->info.n_params = N_PORT_PARAMS; - - port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); - port->latency.min_quantum = 1.0f; - port->latency.max_quantum = 1.0f; - - spa_list_init(&port->ready); - - this->quantum_limit = 8192; - - if (info && (str = spa_dict_lookup(info, "clock.quantum-limit"))) - spa_atou32(str, &this->quantum_limit, 0); - - if (info && (str = spa_dict_lookup(info, "api.bluez5.internal")) != NULL) - this->is_internal = spa_atob(str); - - if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT))) - sscanf(str, "pointer:%p", &this->transport); - - if (this->transport == NULL) { - spa_log_error(this->log, "a transport is needed"); - return -EINVAL; - } - - set_latency(this, false); - - spa_bt_transport_add_listener(this->transport, - &this->transport_listener, &transport_events, this); - - this->timerfd = spa_system_timerfd_create(this->data_system, - CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); - - this->flush_timerfd = spa_system_timerfd_create(this->data_system, - CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); - - return 0; -} - -static const struct spa_interface_info impl_interfaces[] = { - {SPA_TYPE_INTERFACE_Node,}, -}; - -static int -impl_enum_interface_info(const struct spa_handle_factory *factory, - const struct spa_interface_info **info, uint32_t *index) -{ - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(info != NULL, -EINVAL); - spa_return_val_if_fail(index != NULL, -EINVAL); - - switch (*index) { - case 0: - *info = &impl_interfaces[*index]; - break; - default: - return 0; - } - (*index)++; - return 1; -} - -static const struct spa_dict_item info_items[] = { - { SPA_KEY_FACTORY_AUTHOR, "Collabora Ltd. " }, - { SPA_KEY_FACTORY_DESCRIPTION, "Play bluetooth audio with hsp/hfp" }, - { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=" }, -}; - -static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); - -const struct spa_handle_factory spa_sco_sink_factory = { - SPA_VERSION_HANDLE_FACTORY, - SPA_NAME_API_BLUEZ5_SCO_SINK, - &info, - impl_get_size, - impl_init, - impl_enum_interface_info, -}; diff --git a/spa/plugins/bluez5/sco-source.c b/spa/plugins/bluez5/sco-source.c deleted file mode 100644 index dc2e1f070..000000000 --- a/spa/plugins/bluez5/sco-source.c +++ /dev/null @@ -1,1779 +0,0 @@ -/* Spa SCO Source */ -/* SPDX-FileCopyrightText: Copyright © 2019 Collabora Ltd. */ -/* SPDX-License-Identifier: MIT */ - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "defs.h" - -#ifdef HAVE_LC3 -#include -#endif - -SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.bluez5.source.sco"); -#undef SPA_LOG_TOPIC_DEFAULT -#define SPA_LOG_TOPIC_DEFAULT &log_topic - -#include "decode-buffer.h" - -#define DEFAULT_CLOCK_NAME "clock.system.monotonic" - -struct props { - char clock_name[64]; -}; - -#define MAX_BUFFERS 32 - -struct buffer { - uint32_t id; - unsigned int outstanding:1; - struct spa_buffer *buf; - struct spa_meta_header *h; - struct spa_list link; -}; - -struct port { - struct spa_audio_info current_format; - int frame_size; - unsigned int have_format:1; - - uint64_t info_all; - struct spa_port_info info; - struct spa_io_buffers *io; - struct spa_io_rate_match *rate_match; - struct spa_latency_info latency; -#define IDX_EnumFormat 0 -#define IDX_Meta 1 -#define IDX_IO 2 -#define IDX_Format 3 -#define IDX_Buffers 4 -#define IDX_Latency 5 -#define N_PORT_PARAMS 6 - struct spa_param_info params[N_PORT_PARAMS]; - - struct buffer buffers[MAX_BUFFERS]; - uint32_t n_buffers; - - struct spa_list free; - struct spa_list ready; - - struct spa_bt_decode_buffer buffer; -}; - -struct impl { - struct spa_handle handle; - struct spa_node node; - - struct spa_log *log; - struct spa_loop *data_loop; - struct spa_system *data_system; - - struct spa_hook_list hooks; - struct spa_callbacks callbacks; - - uint32_t quantum_limit; - - uint64_t info_all; - struct spa_node_info info; -#define IDX_PropInfo 0 -#define IDX_Props 1 -#define IDX_NODE_IO 2 -#define N_NODE_PARAMS 3 - struct spa_param_info params[N_NODE_PARAMS]; - struct props props; - - struct spa_bt_transport *transport; - struct spa_hook transport_listener; - - struct port port; - - unsigned int started:1; - unsigned int start_ready:1; - unsigned int transport_started:1; - unsigned int following:1; - unsigned int matching:1; - unsigned int resampling:1; - unsigned int io_error:1; - - unsigned int is_internal:1; - - struct spa_source timer_source; - int timerfd; - - struct spa_io_clock *clock; - struct spa_io_position *position; - - uint64_t current_time; - uint64_t next_time; - - /* Codecs */ - bool h2_seq_initialized; - uint8_t h2_seq; - - /* mSBC/LC3 frame parsing */ - uint8_t recv_buffer[HFP_CODEC_PACKET_SIZE]; - uint8_t recv_buffer_pos; - - /* mSBC */ - sbc_t msbc; - - /* LC3 */ -#ifdef HAVE_LC3 - lc3_decoder_t lc3; -#else - void *lc3; -#endif - - struct timespec now; -}; - -#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) - -static void reset_props(struct props *props) -{ - strncpy(props->clock_name, DEFAULT_CLOCK_NAME, sizeof(props->clock_name)); -} - -static int impl_node_enum_params(void *object, int seq, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) -{ - struct impl *this = object; - struct spa_pod *param; - struct spa_pod_builder b = { 0 }; - uint8_t buffer[1024]; - struct spa_result_node_params result; - uint32_t count = 0; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(num != 0, -EINVAL); - - result.id = id; - result.next = start; - next: - result.index = result.next++; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - - switch (id) { - case SPA_PARAM_PropInfo: - { - switch (result.index) { - default: - return 0; - } - break; - } - case SPA_PARAM_Props: - { - switch (result.index) { - default: - return 0; - } - break; - } - default: - return -ENOENT; - } - - if (spa_pod_filter(&b, &result.param, param, filter) < 0) - goto next; - - spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); - - if (++count != num) - goto next; - - return 0; -} - -static int set_timeout(struct impl *this, uint64_t time) -{ - struct itimerspec ts; - ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; - ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; - ts.it_interval.tv_sec = 0; - ts.it_interval.tv_nsec = 0; - return spa_system_timerfd_settime(this->data_system, - this->timerfd, SPA_FD_TIMER_ABSTIME, &ts, NULL); -} - -static int set_timers(struct impl *this) -{ - struct timespec now; - - spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now); - this->next_time = SPA_TIMESPEC_TO_NSEC(&now); - - return set_timeout(this, this->following ? 0 : this->next_time); -} - -static int do_reassign_follower(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *this = user_data; - struct port *port = &this->port; - - set_timers(this); - if (this->transport_started) - spa_bt_decode_buffer_recover(&port->buffer); - return 0; -} - -static inline bool is_following(struct impl *this) -{ - return this->position && this->clock && this->position->clock.id != this->clock->id; -} - -static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) -{ - struct impl *this = object; - bool following; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - switch (id) { - case SPA_IO_Clock: - this->clock = data; - if (this->clock != NULL) { - spa_scnprintf(this->clock->name, - sizeof(this->clock->name), - "%s", this->props.clock_name); - } - break; - case SPA_IO_Position: - this->position = data; - break; - default: - return -ENOENT; - } - - following = is_following(this); - if (this->started && following != this->following) { - spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); - this->following = following; - spa_loop_invoke(this->data_loop, do_reassign_follower, 0, NULL, 0, true, this); - } - - return 0; -} - -static void emit_node_info(struct impl *this, bool full); - -static int apply_props(struct impl *this, const struct spa_pod *param) -{ - struct props new_props = this->props; - int changed = 0; - - if (param == NULL) { - reset_props(&new_props); - } else { - /* noop */ - } - - changed = (memcmp(&new_props, &this->props, sizeof(struct props)) != 0); - this->props = new_props; - return changed; -} - -static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, - const struct spa_pod *param) -{ - struct impl *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - switch (id) { - case SPA_PARAM_Props: - { - if (apply_props(this, param) > 0) { - this->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; - this->params[IDX_Props].flags ^= SPA_PARAM_INFO_SERIAL; - emit_node_info(this, false); - } - break; - } - default: - return -ENOENT; - } - - return 0; -} - -static void reset_buffers(struct port *port) -{ - uint32_t i; - - spa_list_init(&port->free); - spa_list_init(&port->ready); - - for (i = 0; i < port->n_buffers; i++) { - struct buffer *b = &port->buffers[i]; - spa_list_append(&port->free, &b->link); - b->outstanding = false; - } -} - -static void recycle_buffer(struct impl *this, struct port *port, uint32_t buffer_id) -{ - struct buffer *b = &port->buffers[buffer_id]; - - if (b->outstanding) { - spa_log_trace(this->log, "%p: recycle buffer %u", this, buffer_id); - spa_list_append(&port->free, &b->link); - b->outstanding = false; - } -} - -/* Append data to recv buffer, syncing buffer start to headers */ -static void recv_buffer_append_byte(struct impl *this, uint8_t byte) -{ - /* Parse H2 sync header */ - if (this->recv_buffer_pos == 0) { - if (byte != 0x01) { - this->recv_buffer_pos = 0; - return; - } - } else if (this->recv_buffer_pos == 1) { - if (!((byte & 0x0F) == 0x08 && - ((byte >> 4) & 1) == ((byte >> 5) & 1) && - ((byte >> 6) & 1) == ((byte >> 7) & 1))) { - this->recv_buffer_pos = 0; - return; - } - } else if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { - /* Beginning of MSBC frame: SYNCWORD + 2 nul bytes */ - if (this->recv_buffer_pos == 2) { - if (byte != 0xAD) { - this->recv_buffer_pos = 0; - return; - } - } - else if (this->recv_buffer_pos == 3) { - if (byte != 0x00) { - this->recv_buffer_pos = 0; - return; - } - } - else if (this->recv_buffer_pos == 4) { - if (byte != 0x00) { - this->recv_buffer_pos = 0; - return; - } - } - } - - if (this->recv_buffer_pos >= HFP_CODEC_PACKET_SIZE) { - /* Packet completed. Reset. */ - this->recv_buffer_pos = 0; - recv_buffer_append_byte(this, byte); - return; - } - - this->recv_buffer[this->recv_buffer_pos] = byte; - ++this->recv_buffer_pos; -} - -/* Helper function for debugging */ -static SPA_UNUSED void hexdump_to_log(struct impl *this, uint8_t *data, size_t size) -{ - char buf[2048]; - size_t i, col = 0, pos = 0; - buf[0] = '\0'; - for (i = 0; i < size; ++i) { - int res; - res = spa_scnprintf(buf + pos, sizeof(buf) - pos, "%s%02x", - (col == 0) ? "\n\t" : " ", data[i]); - if (res < 0) - break; - pos += res; - col = (col + 1) % 16; - } - spa_log_trace(this->log, "hexdump (%d bytes):%s", (int)size, buf); -} - -/* helper function to detect if a packet consists only of zeros */ -static bool is_zero_packet(uint8_t *data, int size) -{ - for (int i = 0; i < size; ++i) { - if (data[i] != 0) { - return false; - } - } - return true; -} - -static int lc3_decode_frame(struct impl *this, const void *src, size_t src_size, void *dst, - size_t dst_size, size_t *dst_out) -{ -#ifdef HAVE_LC3 - int res; - - if (src_size != LC3_SWB_PAYLOAD_SIZE) - return -EINVAL; - if (dst_size < LC3_SWB_DECODED_SIZE) - return -EINVAL; - - res = lc3_decode(this->lc3, src, src_size, LC3_PCM_FORMAT_S24, dst, 1); - if (res != 0) - return -EINVAL; - - *dst_out = LC3_SWB_DECODED_SIZE; - return LC3_SWB_DECODED_SIZE; -#else - return -EOPNOTSUPP; -#endif -} - -static uint32_t preprocess_and_decode_codec_data(void *userdata, uint8_t *read_data, int size_read, uint64_t now) -{ - struct impl *this = userdata; - struct port *port = &this->port; - uint32_t decoded = 0; - int i; - uint32_t decoded_size = (this->transport->codec == HFP_AUDIO_CODEC_MSBC) ? MSBC_DECODED_SIZE : - LC3_SWB_DECODED_SIZE; - - spa_log_trace(this->log, "handling mSBC/LC3 data"); - - /* - * Check if the packet contains only zeros - if so ignore the packet. - * This is necessary, because some kernels insert bogus "all-zero" packets - * into the datastream. - * See https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/549 - */ - if (is_zero_packet(read_data, size_read)) - return 0; - - for (i = 0; i < size_read; ++i) { - void *buf; - uint32_t avail; - int seq, processed; - size_t written; - - recv_buffer_append_byte(this, read_data[i]); - - if (this->recv_buffer_pos != HFP_CODEC_PACKET_SIZE) - continue; - - /* - * Handle found mSBC/LC3 packet - */ - - buf = spa_bt_decode_buffer_get_write(&port->buffer, &avail); - - /* Check sequence number */ - seq = ((this->recv_buffer[1] >> 4) & 1) | - ((this->recv_buffer[1] >> 6) & 2); - - spa_log_trace(this->log, "mSBC/LC3 packet seq=%u", seq); - if (!this->h2_seq_initialized) { - this->h2_seq_initialized = true; - this->h2_seq = seq; - } else if (seq != this->h2_seq) { - /* TODO: PLC (too late to insert data now) */ - spa_log_info(this->log, - "missing mSBC/LC3 packet: %u != %u", seq, this->h2_seq); - this->h2_seq = seq; - } - - this->h2_seq = (this->h2_seq + 1) % 4; - - if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { - if (avail < decoded_size) - spa_log_warn(this->log, "Output buffer full, dropping msbc data"); - - /* decode frame */ - processed = sbc_decode( - &this->msbc, this->recv_buffer + 2, HFP_CODEC_PACKET_SIZE - 3, - buf, avail, &written); - } else { - processed = lc3_decode_frame(this, this->recv_buffer + 2, HFP_CODEC_PACKET_SIZE - 2, - buf, avail, &written); - } - - if (processed < 0) { - spa_log_warn(this->log, "decode failed: %d", processed); - /* TODO: manage errors */ - continue; - } - - spa_bt_decode_buffer_write_packet(&port->buffer, written, now); - decoded += written; - } - - return decoded; -} - -static int sco_source_cb(void *userdata, uint8_t *read_data, int size_read) -{ - struct impl *this = userdata; - struct port *port = &this->port; - uint32_t decoded; - uint64_t dt; - - /* Drop data when not started */ - if (!this->started) - return 0; - - if (this->transport == NULL) { - spa_log_debug(this->log, "no transport, stop reading"); - goto stop; - } - - /* update the current pts */ - dt = SPA_TIMESPEC_TO_NSEC(&this->now); - spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &this->now); - dt = SPA_TIMESPEC_TO_NSEC(&this->now) - dt; - - /* handle data read from socket */ -#if 0 - hexdump_to_log(this, read_data, size_read); -#endif - - if (this->transport->codec == HFP_AUDIO_CODEC_MSBC || - this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) { - decoded = preprocess_and_decode_codec_data(userdata, read_data, size_read, SPA_TIMESPEC_TO_NSEC(&this->now)); - } else { - uint32_t avail; - uint8_t *packet; - - if (size_read != 48 && is_zero_packet(read_data, size_read)) { - /* Adapter is returning non-standard CVSD stream. For example - * Intel 8087:0029 at Firmware revision 0.0 build 191 week 21 2021 - * on kernel 5.13.19 produces such data. - */ - return 0; - } - - if (size_read % port->frame_size != 0) { - /* Unaligned data: reception or adapter problem. - * Consider the whole packet lost and report. - */ - spa_log_debug(this->log, - "received bad Bluetooth SCO CVSD packet"); - return 0; - } - - packet = spa_bt_decode_buffer_get_write(&port->buffer, &avail); - avail = SPA_MIN(avail, (uint32_t)size_read); - spa_memmove(packet, read_data, avail); - spa_bt_decode_buffer_write_packet(&port->buffer, avail, SPA_TIMESPEC_TO_NSEC(&this->now)); - - decoded = avail; - } - - spa_log_trace(this->log, "read socket data size:%d decoded frames:%d dt:%d dms", - size_read, decoded / port->frame_size, - (int)(dt / 100000)); - - return 0; - -stop: - this->io_error = true; - return 1; -} - -static int setup_matching(struct impl *this) -{ - struct port *port = &this->port; - - if (!this->transport_started) - port->buffer.corr = 1.0; - - if (this->position && port->rate_match) { - port->rate_match->rate = 1 / port->buffer.corr; - - this->matching = this->following; - this->resampling = this->matching || - (port->current_format.info.raw.rate != this->position->clock.target_rate.denom); - } else { - this->matching = false; - this->resampling = false; - } - - if (port->rate_match) - SPA_FLAG_UPDATE(port->rate_match->flags, SPA_IO_RATE_MATCH_FLAG_ACTIVE, this->matching); - - return 0; -} - -static int produce_buffer(struct impl *this); - -static void sco_on_timeout(struct spa_source *source) -{ - struct impl *this = source->data; - struct port *port = &this->port; - uint64_t exp, duration; - uint32_t rate; - uint64_t prev_time, now_time; - int res; - - if (this->started) { - if ((res = spa_system_timerfd_read(this->data_system, this->timerfd, &exp)) < 0) { - if (res != -EAGAIN) - spa_log_warn(this->log, "error reading timerfd: %s", - spa_strerror(res)); - return; - } - } - - prev_time = this->current_time; - now_time = this->current_time = this->next_time; - - spa_log_trace(this->log, "%p: timer %"PRIu64" %"PRIu64"", this, - now_time, now_time - prev_time); - - if (SPA_LIKELY(this->position)) { - duration = this->position->clock.target_duration; - rate = this->position->clock.target_rate.denom; - } else { - duration = 1024; - rate = 48000; - } - - setup_matching(this); - - this->next_time = (uint64_t)(now_time + duration * SPA_NSEC_PER_SEC / port->buffer.corr / rate); - - if (SPA_LIKELY(this->clock)) { - this->clock->nsec = now_time; - this->clock->rate = this->clock->target_rate; - this->clock->position += this->clock->duration; - this->clock->duration = duration; - this->clock->rate_diff = port->buffer.corr; - this->clock->next_nsec = this->next_time; - } - - if (port->io) { - int io_status = port->io->status; - int status = produce_buffer(this); - spa_log_trace(this->log, "%p: io:%d->%d status:%d", this, io_status, port->io->status, status); - } - - spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA); - - set_timeout(this, this->next_time); -} - -static int do_add_source(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *this = user_data; - - spa_bt_sco_io_set_source_cb(this->transport->sco_io, sco_source_cb, this); - - return 0; -} - -static int transport_start(struct impl *this) -{ - struct port *port = &this->port; - int res; - - /* Don't do anything if the node has already started */ - if (this->transport_started) - return 0; - if (!this->start_ready) - return -EIO; - - spa_log_debug(this->log, "%p: start transport", this); - - /* Make sure the transport is valid */ - spa_return_val_if_fail (this->transport != NULL, -EIO); - - /* Reset the buffers and sample count */ - reset_buffers(port); - - spa_bt_decode_buffer_clear(&port->buffer); - if ((res = spa_bt_decode_buffer_init(&port->buffer, this->log, - port->frame_size, port->current_format.info.raw.rate, - this->quantum_limit, this->quantum_limit)) < 0) - return res; - - /* 40 ms max buffer (on top of duration) */ - spa_bt_decode_buffer_set_max_extra_latency(&port->buffer, - port->current_format.info.raw.rate * 40 / 1000); - - /* Init mSBC/LC3 if needed */ - if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) { - res = sbc_init_msbc(&this->msbc, 0); - if (res < 0) - return res; - - /* Libsbc expects audio samples by default in host endianness, mSBC requires little endian */ - this->msbc.endian = SBC_LE; - this->h2_seq_initialized = false; - - this->recv_buffer_pos = 0; - } else if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) { -#ifdef HAVE_LC3 - this->lc3 = lc3_setup_decoder(7500, 32000, 0, - calloc(1, lc3_decoder_size(7500, 32000))); - if (!this->lc3) - return -EINVAL; - - spa_assert(lc3_frame_samples(7500, 32000) * port->frame_size == LC3_SWB_DECODED_SIZE); - - this->h2_seq_initialized = false; - this->recv_buffer_pos = 0; -#else - res = -EINVAL; - goto fail; -#endif - } - - this->io_error = false; - - /* Start socket i/o */ - if ((res = spa_bt_transport_ensure_sco_io(this->transport, this->data_loop)) < 0) - goto fail; - spa_loop_invoke(this->data_loop, do_add_source, 0, NULL, 0, true, this); - - /* Set the started flag */ - this->transport_started = true; - - return 0; - -fail: - sbc_finish(&this->msbc); - free(this->lc3); - this->lc3 = NULL; - return res; -} - -static int do_start(struct impl *this) -{ - bool do_accept; - int res; - - if (this->started) - return 0; - - spa_return_val_if_fail(this->transport, -EIO); - - this->following = is_following(this); - - this->start_ready = true; - - spa_log_debug(this->log, "%p: start following:%d", this, this->following); - - /* Do accept if Gateway; otherwise do connect for Head Unit */ - do_accept = this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY; - - /* acquire the socket fd (false -> connect | true -> accept) */ - if ((res = spa_bt_transport_acquire(this->transport, do_accept)) < 0) { - this->start_ready = false; - return res; - } - - /* Start timer */ - this->timer_source.data = this; - this->timer_source.fd = this->timerfd; - this->timer_source.func = sco_on_timeout; - this->timer_source.mask = SPA_IO_IN; - this->timer_source.rmask = 0; - spa_loop_add_source(this->data_loop, &this->timer_source); - - setup_matching(this); - - set_timers(this); - - this->started = true; - - return 0; -} - -static int do_remove_source(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *this = user_data; - - if (this->timer_source.loop) - spa_loop_remove_source(this->data_loop, &this->timer_source); - set_timeout(this, 0); - - return 0; -} - -static int do_remove_transport_source(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *this = user_data; - - this->transport_started = false; - - if (this->transport && this->transport->sco_io) - spa_bt_sco_io_set_source_cb(this->transport->sco_io, NULL, NULL); - - return 0; -} - -static void transport_stop(struct impl *this) -{ - struct port *port = &this->port; - - if (!this->transport_started) - return; - - spa_log_debug(this->log, "sco-source %p: transport stop", this); - - spa_loop_invoke(this->data_loop, do_remove_transport_source, 0, NULL, 0, true, this); - - spa_bt_decode_buffer_clear(&port->buffer); - - sbc_finish(&this->msbc); - free(this->lc3); - this->lc3 = NULL; -} - -static int do_stop(struct impl *this) -{ - int res; - - if (!this->started) - return 0; - - spa_log_debug(this->log, "%p: stop", this); - - this->start_ready = false; - - spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, this); - - transport_stop(this); - - if (this->transport) - res = spa_bt_transport_release(this->transport); - else - res = 0; - - this->started = false; - - return res; -} - -static int impl_node_send_command(void *object, const struct spa_command *command) -{ - struct impl *this = object; - struct port *port; - int res; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(command != NULL, -EINVAL); - - port = &this->port; - - switch (SPA_NODE_COMMAND_ID(command)) { - case SPA_NODE_COMMAND_Start: - if (!port->have_format) - return -EIO; - if (port->n_buffers == 0) - return -EIO; - - if ((res = do_start(this)) < 0) - return res; - break; - case SPA_NODE_COMMAND_Pause: - case SPA_NODE_COMMAND_Suspend: - if ((res = do_stop(this)) < 0) - return res; - break; - default: - return -ENOTSUP; - } - return 0; -} - -static void emit_node_info(struct impl *this, bool full) -{ - const struct spa_dict_item hu_node_info_items[] = { - { SPA_KEY_DEVICE_API, "bluez5" }, - { SPA_KEY_MEDIA_CLASS, this->is_internal ? "Audio/Source/Internal" : "Audio/Source" }, - { SPA_KEY_NODE_DRIVER, "true" }, - }; - const struct spa_dict_item ag_node_info_items[] = { - { SPA_KEY_DEVICE_API, "bluez5" }, - { SPA_KEY_MEDIA_CLASS, "Stream/Output/Audio" }, - { "media.name", ((this->transport && this->transport->device->name) ? - this->transport->device->name : "HSP/HFP") }, - { SPA_KEY_MEDIA_ROLE, "Communication" }, - }; - bool is_ag = this->transport && (this->transport->profile & SPA_BT_PROFILE_HEADSET_AUDIO_GATEWAY); - uint64_t old = full ? this->info.change_mask : 0; - - if (full) - this->info.change_mask = this->info_all; - if (this->info.change_mask) { - this->info.props = is_ag ? - &SPA_DICT_INIT_ARRAY(ag_node_info_items) : - &SPA_DICT_INIT_ARRAY(hu_node_info_items); - spa_node_emit_info(&this->hooks, &this->info); - this->info.change_mask = old; - } -} - -static void emit_port_info(struct impl *this, struct port *port, bool full) -{ - uint64_t old = full ? port->info.change_mask : 0; - if (full) - port->info.change_mask = port->info_all; - if (port->info.change_mask) { - spa_node_emit_port_info(&this->hooks, - SPA_DIRECTION_OUTPUT, 0, &port->info); - port->info.change_mask = old; - } -} - -static int -impl_node_add_listener(void *object, - struct spa_hook *listener, - const struct spa_node_events *events, - void *data) -{ - struct impl *this = object; - struct spa_hook_list save; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_hook_list_isolate(&this->hooks, &save, listener, events, data); - - emit_node_info(this, true); - emit_port_info(this, &this->port, true); - - spa_hook_list_join(&this->hooks, &save); - - return 0; -} - -static int -impl_node_set_callbacks(void *object, - const struct spa_node_callbacks *callbacks, - void *data) -{ - struct impl *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); - - return 0; -} - -static int impl_node_sync(void *object, int seq) -{ - struct impl *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_node_emit_result(&this->hooks, seq, 0, 0, NULL); - - return 0; -} - -static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, - const struct spa_dict *props) -{ - return -ENOTSUP; -} - -static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) -{ - return -ENOTSUP; -} - -static int -impl_node_port_enum_params(void *object, int seq, - enum spa_direction direction, uint32_t port_id, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) -{ - - struct impl *this = object; - struct port *port; - struct spa_pod *param; - struct spa_pod_builder b = { 0 }; - uint8_t buffer[1024]; - struct spa_result_node_params result; - uint32_t count = 0; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(num != 0, -EINVAL); - - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - port = &this->port; - - result.id = id; - result.next = start; - next: - result.index = result.next++; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - - switch (id) { - case SPA_PARAM_EnumFormat: - if (result.index > 0) - return 0; - if (this->transport == NULL) - return -EIO; - - /* set the info structure */ - struct spa_audio_info_raw info = { 0, }; - if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) - info.format = SPA_AUDIO_FORMAT_S24_32_LE; - else - info.format = SPA_AUDIO_FORMAT_S16_LE; - info.channels = 1; - info.position[0] = SPA_AUDIO_CHANNEL_MONO; - - /* CVSD format has a rate of 8kHz - * MSBC format has a rate of 16kHz - * LC3-SWB format has a rate of 32kHz - */ - if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) - info.rate = 32000; - else if (this->transport->codec == HFP_AUDIO_CODEC_MSBC) - info.rate = 16000; - else - info.rate = 8000; - - /* build the param */ - param = spa_format_audio_raw_build(&b, id, &info); - break; - - case SPA_PARAM_Format: - if (!port->have_format) - return -EIO; - if (result.index > 0) - return 0; - - param = spa_format_audio_raw_build(&b, id, &port->current_format.info.raw); - break; - - case SPA_PARAM_Buffers: - if (!port->have_format) - return -EIO; - if (result.index > 0) - return 0; - - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamBuffers, id, - SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(2, 1, MAX_BUFFERS), - SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), - SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( - this->quantum_limit * port->frame_size, - 16 * port->frame_size, - INT32_MAX), - SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->frame_size)); - break; - - case SPA_PARAM_Meta: - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamMeta, id, - SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), - SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); - break; - default: - return 0; - } - break; - - case SPA_PARAM_IO: - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamIO, id, - SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), - SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); - break; - case 1: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamIO, id, - SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_RateMatch), - SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_rate_match))); - break; - default: - return 0; - } - break; - - case SPA_PARAM_Latency: - switch (result.index) { - case 0: - param = spa_latency_build(&b, id, &port->latency); - break; - default: - return 0; - } - break; - - default: - return -ENOENT; - } - - if (spa_pod_filter(&b, &result.param, param, filter) < 0) - goto next; - - spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); - - if (++count != num) - goto next; - - return 0; -} - -static int clear_buffers(struct impl *this, struct port *port) -{ - do_stop(this); - if (port->n_buffers > 0) { - spa_list_init(&port->free); - spa_list_init(&port->ready); - port->n_buffers = 0; - } - return 0; -} - -static int port_set_format(struct impl *this, struct port *port, - uint32_t flags, - const struct spa_pod *format) -{ - int err; - - if (format == NULL) { - spa_log_debug(this->log, "clear format"); - clear_buffers(this, port); - port->have_format = false; - } else { - struct spa_audio_info info = { 0 }; - - if (!this->transport) - return -EIO; - - if ((err = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) - return err; - - if (info.media_type != SPA_MEDIA_TYPE_audio || - info.media_subtype != SPA_MEDIA_SUBTYPE_raw) - return -EINVAL; - - if (spa_format_audio_raw_parse(format, &info.info.raw) < 0) - return -EINVAL; - - if (info.info.raw.rate == 0 || - info.info.raw.channels != 1) - return -EINVAL; - - switch (info.info.raw.format) { - case SPA_AUDIO_FORMAT_S16_LE: - if (this->transport->codec == HFP_AUDIO_CODEC_LC3_SWB) - return -EINVAL; - port->frame_size = info.info.raw.channels * 2; - break; - case SPA_AUDIO_FORMAT_S24_32_LE: - if (this->transport->codec != HFP_AUDIO_CODEC_LC3_SWB) - return -EINVAL; - port->frame_size = info.info.raw.channels * 4; - break; - default: - return -EINVAL; - } - - port->current_format = info; - port->have_format = true; - } - - port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; - if (port->have_format) { - port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; - port->info.rate = SPA_FRACTION(1, port->current_format.info.raw.rate); - port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); - port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); - port->params[IDX_Latency].flags ^= SPA_PARAM_INFO_SERIAL; - } else { - port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); - port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); - } - emit_port_info(this, port, false); - - return 0; -} - -static int -impl_node_port_set_param(void *object, - enum spa_direction direction, uint32_t port_id, - uint32_t id, uint32_t flags, - const struct spa_pod *param) -{ - struct impl *this = object; - struct port *port; - int res; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_return_val_if_fail(CHECK_PORT(node, direction, port_id), -EINVAL); - port = &this->port; - - switch (id) { - case SPA_PARAM_Format: - res = port_set_format(this, port, flags, param); - break; - case SPA_PARAM_Latency: - res = 0; - break; - default: - res = -ENOENT; - break; - } - return res; -} - -static int -impl_node_port_use_buffers(void *object, - enum spa_direction direction, uint32_t port_id, - uint32_t flags, - struct spa_buffer **buffers, uint32_t n_buffers) -{ - struct impl *this = object; - struct port *port; - uint32_t i; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - port = &this->port; - - spa_log_debug(this->log, "use buffers %d", n_buffers); - - clear_buffers(this, port); - - if (n_buffers > 0 && !port->have_format) - return -EIO; - if (n_buffers > MAX_BUFFERS) - return -ENOSPC; - - for (i = 0; i < n_buffers; i++) { - struct buffer *b = &port->buffers[i]; - struct spa_data *d = buffers[i]->datas; - - b->buf = buffers[i]; - b->id = i; - - b->h = spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); - - if (d[0].data == NULL) { - spa_log_error(this->log, "%p: need mapped memory", this); - return -EINVAL; - } - spa_list_append(&port->free, &b->link); - b->outstanding = false; - } - port->n_buffers = n_buffers; - - return 0; -} - -static int -impl_node_port_set_io(void *object, - enum spa_direction direction, - uint32_t port_id, - uint32_t id, - void *data, size_t size) -{ - struct impl *this = object; - struct port *port; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - port = &this->port; - - switch (id) { - case SPA_IO_Buffers: - port->io = data; - break; - case SPA_IO_RateMatch: - port->rate_match = data; - break; - default: - return -ENOENT; - } - return 0; -} - -static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) -{ - struct impl *this = object; - struct port *port; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_return_val_if_fail(port_id == 0, -EINVAL); - port = &this->port; - - if (port->n_buffers == 0) - return -EIO; - - if (buffer_id >= port->n_buffers) - return -EINVAL; - - recycle_buffer(this, port, buffer_id); - - return 0; -} - -static uint32_t get_samples(struct impl *this, uint32_t *result_duration) -{ - struct port *port = &this->port; - uint32_t samples, rate_denom; - uint64_t duration; - - if (SPA_LIKELY(this->position)) { - duration = this->position->clock.duration; - rate_denom = this->position->clock.rate.denom; - } else { - duration = 1024; - rate_denom = port->current_format.info.raw.rate; - } - - *result_duration = duration * port->current_format.info.raw.rate / rate_denom; - - if (SPA_LIKELY(port->rate_match) && this->resampling) - samples = port->rate_match->size; - else - samples = *result_duration; - - return samples; -} - -#define WARN_ONCE(cond, ...) \ - if (SPA_UNLIKELY(cond)) { static bool __once; if (!__once) { __once = true; spa_log_warn(__VA_ARGS__); } } - -static void process_buffering(struct impl *this) -{ - struct port *port = &this->port; - uint32_t duration; - const uint32_t samples = get_samples(this, &duration); - void *buf; - uint32_t avail; - - spa_bt_decode_buffer_process(&port->buffer, samples, duration, - this->position ? this->position->clock.rate_diff : 1.0, - this->position ? this->position->clock.next_nsec : 0); - - setup_matching(this); - - buf = spa_bt_decode_buffer_get_read(&port->buffer, &avail); - - /* copy data to buffers */ - if (!spa_list_is_empty(&port->free)) { - struct buffer *buffer; - struct spa_data *datas; - uint32_t data_size; - - buffer = spa_list_first(&port->free, struct buffer, link); - datas = buffer->buf->datas; - - data_size = samples * port->frame_size; - - WARN_ONCE(datas[0].maxsize < data_size && !this->following, - this->log, "source buffer too small (%u < %u)", - datas[0].maxsize, data_size); - - data_size = SPA_MIN(data_size, SPA_ROUND_DOWN(datas[0].maxsize, port->frame_size)); - - avail = SPA_MIN(avail, data_size); - - spa_bt_decode_buffer_read(&port->buffer, avail); - - spa_list_remove(&buffer->link); - - spa_log_trace(this->log, "dequeue %d", buffer->id); - - datas[0].chunk->offset = 0; - datas[0].chunk->size = data_size; - datas[0].chunk->stride = port->frame_size; - - memcpy(datas[0].data, buf, avail); - - /* pad with silence */ - if (avail < data_size) - memset(SPA_PTROFF(datas[0].data, avail, void), 0, data_size - avail); - - /* ready buffer if full */ - spa_log_trace(this->log, "queue %d frames:%d", buffer->id, (int)samples); - spa_list_append(&port->ready, &buffer->link); - } -} - -static int produce_buffer(struct impl *this) -{ - struct buffer *buffer; - struct port *port = &this->port; - struct spa_io_buffers *io = port->io; - - if (io == NULL) - return -EIO; - - /* Return if we already have a buffer */ - if (io->status == SPA_STATUS_HAVE_DATA && - (this->following || port->rate_match == NULL)) - return SPA_STATUS_HAVE_DATA; - - /* Recycle */ - if (io->buffer_id < port->n_buffers) { - recycle_buffer(this, port, io->buffer_id); - io->buffer_id = SPA_ID_INVALID; - } - - if (this->io_error) { - io->status = -EIO; - return SPA_STATUS_STOPPED; - } - - /* Handle buffering */ - if (this->transport_started) - process_buffering(this); - - /* Return if there are no buffers ready to be processed */ - if (spa_list_is_empty(&port->ready)) - return SPA_STATUS_OK; - - /* Get the new buffer from the ready list */ - buffer = spa_list_first(&port->ready, struct buffer, link); - spa_list_remove(&buffer->link); - buffer->outstanding = true; - - /* Set the new buffer in IO */ - io->buffer_id = buffer->id; - io->status = SPA_STATUS_HAVE_DATA; - - /* Notify we have a buffer ready to be processed */ - return SPA_STATUS_HAVE_DATA; -} - -static int impl_node_process(void *object) -{ - struct impl *this = object; - struct port *port; - struct spa_io_buffers *io; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - port = &this->port; - if ((io = port->io) == NULL) - return -EIO; - - if (!this->started || !this->transport_started) - return SPA_STATUS_OK; - - spa_log_trace(this->log, "%p status:%d", this, io->status); - - /* Return if we already have a buffer */ - if (io->status == SPA_STATUS_HAVE_DATA) - return SPA_STATUS_HAVE_DATA; - - /* Recycle */ - if (io->buffer_id < port->n_buffers) { - recycle_buffer(this, port, io->buffer_id); - io->buffer_id = SPA_ID_INVALID; - } - - /* Follower produces buffers here, driver in timeout */ - if (this->following) - return produce_buffer(this); - else - return SPA_STATUS_OK; -} - -static const struct spa_node_methods impl_node = { - SPA_VERSION_NODE_METHODS, - .add_listener = impl_node_add_listener, - .set_callbacks = impl_node_set_callbacks, - .sync = impl_node_sync, - .enum_params = impl_node_enum_params, - .set_param = impl_node_set_param, - .set_io = impl_node_set_io, - .send_command = impl_node_send_command, - .add_port = impl_node_add_port, - .remove_port = impl_node_remove_port, - .port_enum_params = impl_node_port_enum_params, - .port_set_param = impl_node_port_set_param, - .port_use_buffers = impl_node_port_use_buffers, - .port_set_io = impl_node_port_set_io, - .port_reuse_buffer = impl_node_port_reuse_buffer, - .process = impl_node_process, -}; - -static void transport_state_changed(void *data, - enum spa_bt_transport_state old, - enum spa_bt_transport_state state) -{ - struct impl *this = data; - - spa_log_debug(this->log, "%p: transport %p state %d->%d", this, this->transport, old, state); - - if (state == SPA_BT_TRANSPORT_STATE_ACTIVE) - transport_start(this); - else if (state < SPA_BT_TRANSPORT_STATE_ACTIVE) - transport_stop(this); - - if (state == SPA_BT_TRANSPORT_STATE_ERROR) { - uint8_t buffer[1024]; - struct spa_pod_builder b = { 0 }; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - spa_node_emit_event(&this->hooks, - spa_pod_builder_add_object(&b, - SPA_TYPE_EVENT_Node, SPA_NODE_EVENT_Error)); - } -} - -static int do_transport_destroy(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *this = user_data; - this->transport = NULL; - return 0; -} - -static void transport_destroy(void *data) -{ - struct impl *this = data; - spa_log_debug(this->log, "transport %p destroy", this->transport); - spa_loop_invoke(this->data_loop, do_transport_destroy, 0, NULL, 0, true, this); -} - -static const struct spa_bt_transport_events transport_events = { - SPA_VERSION_BT_TRANSPORT_EVENTS, - .state_changed = transport_state_changed, - .destroy = transport_destroy, -}; - -static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) -{ - struct impl *this; - - spa_return_val_if_fail(handle != NULL, -EINVAL); - spa_return_val_if_fail(interface != NULL, -EINVAL); - - this = (struct impl *) handle; - - if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) - *interface = &this->node; - else - return -ENOENT; - - return 0; -} - -static int impl_clear(struct spa_handle *handle) -{ - struct impl *this = (struct impl *) handle; - - do_stop(this); - if (this->transport) - spa_hook_remove(&this->transport_listener); - spa_system_close(this->data_system, this->timerfd); - spa_bt_decode_buffer_clear(&this->port.buffer); - return 0; -} - -static size_t -impl_get_size(const struct spa_handle_factory *factory, - const struct spa_dict *params) -{ - return sizeof(struct impl); -} - -static int -impl_init(const struct spa_handle_factory *factory, - struct spa_handle *handle, - const struct spa_dict *info, - const struct spa_support *support, - uint32_t n_support) -{ - struct impl *this; - struct port *port; - const char *str; - - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(handle != NULL, -EINVAL); - - handle->get_interface = impl_get_interface; - handle->clear = impl_clear; - - this = (struct impl *) handle; - - this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); - this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); - this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); - - spa_log_topic_init(this->log, &log_topic); - - if (this->data_loop == NULL) { - spa_log_error(this->log, "a data loop is needed"); - return -EINVAL; - } - if (this->data_system == NULL) { - spa_log_error(this->log, "a data system is needed"); - return -EINVAL; - } - - this->node.iface = SPA_INTERFACE_INIT( - SPA_TYPE_INTERFACE_Node, - SPA_VERSION_NODE, - &impl_node, this); - spa_hook_list_init(&this->hooks); - - reset_props(&this->props); - - /* set the node info */ - this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | - SPA_NODE_CHANGE_MASK_PROPS | - SPA_NODE_CHANGE_MASK_PARAMS; - this->info = SPA_NODE_INFO_INIT(); - this->info.flags = SPA_NODE_FLAG_RT; - this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); - this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); - this->params[IDX_NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); - this->info.params = this->params; - this->info.n_params = N_NODE_PARAMS; - - /* set the port info */ - port = &this->port; - port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | - SPA_PORT_CHANGE_MASK_PARAMS; - port->info = SPA_PORT_INFO_INIT(); - port->info.change_mask = SPA_PORT_CHANGE_MASK_FLAGS; - port->info.flags = SPA_PORT_FLAG_LIVE | - SPA_PORT_FLAG_PHYSICAL | - SPA_PORT_FLAG_TERMINAL; - port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); - port->params[IDX_Meta] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); - port->params[IDX_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); - port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); - port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); - port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); - port->info.params = port->params; - port->info.n_params = N_PORT_PARAMS; - - port->latency = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); - port->latency.min_quantum = 1.0f; - port->latency.max_quantum = 1.0f; - - /* Init the buffer lists */ - spa_list_init(&port->ready); - spa_list_init(&port->free); - - this->quantum_limit = 8192; - if (info && (str = spa_dict_lookup(info, "clock.quantum-limit"))) - spa_atou32(str, &this->quantum_limit, 0); - - if (info && (str = spa_dict_lookup(info, "api.bluez5.internal")) != NULL) - this->is_internal = spa_atob(str); - - if (info && (str = spa_dict_lookup(info, SPA_KEY_API_BLUEZ5_TRANSPORT))) - sscanf(str, "pointer:%p", &this->transport); - - if (this->transport == NULL) { - spa_log_error(this->log, "a transport is needed"); - return -EINVAL; - } - spa_bt_transport_add_listener(this->transport, - &this->transport_listener, &transport_events, this); - - this->timerfd = spa_system_timerfd_create(this->data_system, - CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); - - return 0; -} - -static const struct spa_interface_info impl_interfaces[] = { - {SPA_TYPE_INTERFACE_Node,}, -}; - -static int -impl_enum_interface_info(const struct spa_handle_factory *factory, - const struct spa_interface_info **info, uint32_t *index) -{ - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(info != NULL, -EINVAL); - spa_return_val_if_fail(index != NULL, -EINVAL); - - switch (*index) { - case 0: - *info = &impl_interfaces[*index]; - break; - default: - return 0; - } - (*index)++; - return 1; -} - -static const struct spa_dict_item info_items[] = { - { SPA_KEY_FACTORY_AUTHOR, "Collabora Ltd. " }, - { SPA_KEY_FACTORY_DESCRIPTION, "Capture bluetooth audio with hsp/hfp" }, - { SPA_KEY_FACTORY_USAGE, SPA_KEY_API_BLUEZ5_TRANSPORT"=" }, -}; - -static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items); - -const struct spa_handle_factory spa_sco_source_factory = { - SPA_VERSION_HANDLE_FACTORY, - SPA_NAME_API_BLUEZ5_SCO_SOURCE, - &info, - impl_get_size, - impl_init, - impl_enum_interface_info, -}; diff --git a/spa/plugins/bluez5/telephony.c b/spa/plugins/bluez5/telephony.c index c2f9d6741..7695c0b81 100644 --- a/spa/plugins/bluez5/telephony.c +++ b/spa/plugins/bluez5/telephony.c @@ -117,6 +117,8 @@ " " \ " " \ " " \ + " " \ + " " \ " " \ " " \ " " \ @@ -201,10 +203,8 @@ struct agimpl { struct spa_callbacks callbacks; void *user_data; - bool dial_in_progress; - struct callimpl *dial_return; - struct { + int volume[SPA_BT_VOLUME_ID_TERM]; struct spa_bt_telephony_ag_transport transport; } prev; }; @@ -225,20 +225,22 @@ struct callimpl { } prev; }; -#define ag_emit(ag,m,v,...) spa_callbacks_call(&ag->callbacks, struct spa_bt_telephony_ag_callbacks, m, v, ##__VA_ARGS__) -#define ag_emit_dial(s,n,e,cme) ag_emit(s,dial,0,n,e,cme) -#define ag_emit_swap_calls(s,e,cme) ag_emit(s,swap_calls,0,e,cme) -#define ag_emit_release_and_answer(s,e,cme) ag_emit(s,release_and_answer,0,e,cme) -#define ag_emit_release_and_swap(s,e,cme) ag_emit(s,release_and_swap,0,e,cme) -#define ag_emit_hold_and_answer(s,e,cme) ag_emit(s,hold_and_answer,0,e,cme) -#define ag_emit_hangup_all(s,e,cme) ag_emit(s,hangup_all,0,e,cme) -#define ag_emit_create_multiparty(s,e,cme) ag_emit(s,create_multiparty,0,e,cme) -#define ag_emit_send_tones(s,t,e,cme) ag_emit(s,send_tones,0,t,e,cme) -#define ag_emit_transport_activate(s,e,cme) ag_emit(s,transport_activate,0,e,cme) +#define ag_emit(ag,m,v,...) spa_callbacks_call(&ag->callbacks, struct spa_bt_telephony_ag_callbacks, m, v, ##__VA_ARGS__) +#define ag_emit_dial(s,n,m) ag_emit(s,dial,0,n,m) +#define ag_emit_swap_calls(s,m) ag_emit(s,swap_calls,0,m) +#define ag_emit_release_and_answer(s,m) ag_emit(s,release_and_answer,0,m) +#define ag_emit_release_and_swap(s,m) ag_emit(s,release_and_swap,0,m) +#define ag_emit_hold_and_answer(s,m) ag_emit(s,hold_and_answer,0,m) +#define ag_emit_hangup_all(s,m) ag_emit(s,hangup_all,0,m) +#define ag_emit_create_multiparty(s,m) ag_emit(s,create_multiparty,0,m) +#define ag_emit_send_tones(s,t,m) ag_emit(s,send_tones,0,t,m) +#define ag_emit_transport_activate(s,m) ag_emit(s,transport_activate,0,m) +#define ag_emit_set_speaker_volume(s,v,m) ag_emit(s,set_speaker_volume,0,v,m) +#define ag_emit_set_microphone_volume(s,v,m) ag_emit(s,set_microphone_volume,0,v,m) #define call_emit(c,m,v,...) spa_callbacks_call(&c->callbacks, struct spa_bt_telephony_call_callbacks, m, v, ##__VA_ARGS__) -#define call_emit_answer(s,e,cme) call_emit(s,answer,0,e,cme) -#define call_emit_hangup(s,e,cme) call_emit(s,hangup,0,e,cme) +#define call_emit_answer(s,m) call_emit(s,answer,0,m) +#define call_emit_hangup(s,m) call_emit(s,hangup,0,m) static void dbus_iter_append_ag_interfaces(DBusMessageIter *i, struct spa_bt_telephony_ag *ag); static void dbus_iter_append_call_properties(DBusMessageIter *i, struct spa_bt_telephony_call *call, bool all); @@ -516,6 +518,29 @@ void telephony_free(struct spa_bt_telephony *telephony) free(impl); } +void telephony_send_dbus_method_reply(struct spa_bt_telephony *telephony, DBusMessage *m, + enum spa_bt_telephony_error err, uint8_t cme_error) +{ + struct impl *impl = SPA_CONTAINER_OF(telephony, struct impl, this); + spa_autoptr(DBusMessage) reply = NULL; + + if (err == BT_TELEPHONY_ERROR_NONE) + reply = dbus_message_new_method_return(m); + else + reply = dbus_message_new_error(m, telephony_error_to_dbus (err), + telephony_error_to_description (err, cme_error)); + + dbus_connection_send(impl->conn, reply, NULL); +} + +static void telephony_ag_commit_properties(struct spa_bt_telephony_ag *ag) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + for (int i = 0; i < SPA_BT_VOLUME_ID_TERM; ++i) { + agimpl->prev.volume[i] = ag->volume[i]; + } +} + static void telephony_ag_transport_commit_properties(struct spa_bt_telephony_ag *ag) { struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); @@ -538,6 +563,7 @@ static const char * const * transport_state_to_string(int state) static bool dbus_iter_append_ag_properties(DBusMessageIter *i, struct spa_bt_telephony_ag *ag, bool all) { + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); DBusMessageIter dict, entry, variant; bool changed = false; @@ -558,6 +584,32 @@ dbus_iter_append_ag_properties(DBusMessageIter *i, struct spa_bt_telephony_ag *a changed = true; } + if (all || ag->volume[SPA_BT_VOLUME_ID_RX] != agimpl->prev.volume[SPA_BT_VOLUME_ID_RX]) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *name = "SpeakerVolume"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_BYTE_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_BYTE, &ag->volume[SPA_BT_VOLUME_ID_RX]); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + changed = true; + } + + if (all || ag->volume[SPA_BT_VOLUME_ID_TX] != agimpl->prev.volume[SPA_BT_VOLUME_ID_TX]) { + dbus_message_iter_open_container(&dict, DBUS_TYPE_DICT_ENTRY, NULL, &entry); + const char *name = "MicrophoneVolume"; + dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &name); + dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, + DBUS_TYPE_BYTE_AS_STRING, + &variant); + dbus_message_iter_append_basic(&variant, DBUS_TYPE_BYTE, &ag->volume[SPA_BT_VOLUME_ID_TX]); + dbus_message_iter_close_container(&entry, &variant); + dbus_message_iter_close_container(&dict, &entry); + changed = true; + } + dbus_message_iter_close_container(i, &dict); return changed; } @@ -709,6 +761,28 @@ static DBusMessage *ag_properties_get(struct agimpl *agimpl, DBusMessage *m) &agimpl->this.address); dbus_message_iter_close_container(&i, &v); return r; + } else if (spa_streq(name, "SpeakerVolume")) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT, + DBUS_TYPE_BYTE_AS_STRING, &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_BYTE, + &agimpl->this.volume[SPA_BT_VOLUME_ID_RX]); + dbus_message_iter_close_container(&i, &v); + return r; + } else if (spa_streq(name, "MicrophoneVolume")) { + r = dbus_message_new_method_return(m); + if (r == NULL) + return NULL; + dbus_message_iter_init_append(r, &i); + dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT, + DBUS_TYPE_BYTE_AS_STRING, &v); + dbus_message_iter_append_basic(&v, DBUS_TYPE_BYTE, + &agimpl->this.volume[SPA_BT_VOLUME_ID_TX]); + dbus_message_iter_close_container(&i, &v); + return r; } } else if (spa_streq(iface, PW_TELEPHONY_AG_TRANSPORT_IFACE)) { if (spa_streq(name, "Codec")) { @@ -796,7 +870,30 @@ static DBusMessage *ag_properties_set(struct agimpl *agimpl, DBusMessage *m) DBUS_TYPE_INVALID)) return NULL; - if (spa_streq(iface, PW_TELEPHONY_AG_TRANSPORT_IFACE)) { + if (spa_streq(iface, PW_TELEPHONY_AG_IFACE)) { + if (spa_streq(name, "SpeakerVolume")) { + dbus_message_iter_init(m, &i); + dbus_message_iter_next(&i); /* skip iface */ + dbus_message_iter_next(&i); /* skip name */ + dbus_message_iter_recurse(&i, &variant); /* value */ + dbus_message_iter_get_basic(&variant, &agimpl->this.volume[SPA_BT_VOLUME_ID_RX]); + + return ag_emit_set_speaker_volume(agimpl, agimpl->this.volume[SPA_BT_VOLUME_ID_RX], m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); + + } else if (spa_streq(name, "MicrophoneVolume")) { + dbus_message_iter_init(m, &i); + dbus_message_iter_next(&i); /* skip iface */ + dbus_message_iter_next(&i); /* skip name */ + dbus_message_iter_recurse(&i, &variant); /* value */ + dbus_message_iter_get_basic(&variant, &agimpl->this.volume[SPA_BT_VOLUME_ID_TX]); + + return ag_emit_set_microphone_volume(agimpl, agimpl->this.volume[SPA_BT_VOLUME_ID_TX], m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); + } + } else if (spa_streq(iface, PW_TELEPHONY_AG_TRANSPORT_IFACE)) { if (spa_streq(name, "RejectSCO")) { dbus_message_iter_init(m, &i); dbus_message_iter_next(&i); /* skip iface */ @@ -846,8 +943,6 @@ static DBusMessage *ag_dial(struct agimpl *agimpl, DBusMessage *m) { const char *number = NULL; enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - spa_autoptr(DBusMessage) r = NULL; if (!dbus_message_get_args(m, NULL, DBUS_TYPE_STRING, &number, @@ -859,111 +954,60 @@ static DBusMessage *ag_dial(struct agimpl *agimpl, DBusMessage *m) goto failed; } - agimpl->dial_in_progress = true; - if (!ag_emit_dial(agimpl, number, &err, &cme_error)) { - agimpl->dial_in_progress = false; - goto failed; - } - agimpl->dial_in_progress = false; - - if (!agimpl->dial_return || !agimpl->dial_return->path) - err = BT_TELEPHONY_ERROR_FAILED; - - if (err != BT_TELEPHONY_ERROR_NONE) - goto failed; - - if ((r = dbus_message_new_method_return(m)) == NULL) + if (ag_emit_dial(agimpl, number, m)) return NULL; - if (!dbus_message_append_args(r, DBUS_TYPE_OBJECT_PATH, - &agimpl->dial_return->path, DBUS_TYPE_INVALID)) - return NULL; - - agimpl->dial_return = NULL; - - return spa_steal_ptr(r); failed: return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + telephony_error_to_description (err, 0)); } static DBusMessage *ag_swap_calls(struct agimpl *agimpl, DBusMessage *m) { - enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - - if (ag_emit_swap_calls(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); - - return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + return ag_emit_swap_calls(agimpl, m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); } static DBusMessage *ag_release_and_answer(struct agimpl *agimpl, DBusMessage *m) { - enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - - if (ag_emit_release_and_answer(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); - - return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + return ag_emit_release_and_answer(agimpl, m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); } static DBusMessage *ag_release_and_swap(struct agimpl *agimpl, DBusMessage *m) { - enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - - if (ag_emit_release_and_swap(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); - - return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + return ag_emit_release_and_swap(agimpl, m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); } static DBusMessage *ag_hold_and_answer(struct agimpl *agimpl, DBusMessage *m) { - enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - - if (ag_emit_hold_and_answer(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); - - return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + return ag_emit_hold_and_answer(agimpl, m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); } static DBusMessage *ag_hangup_all(struct agimpl *agimpl, DBusMessage *m) { - enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - - if (ag_emit_hangup_all(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); - - return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + return ag_emit_hangup_all(agimpl, m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); } static DBusMessage *ag_create_multiparty(struct agimpl *agimpl, DBusMessage *m) { - enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - - if (ag_emit_create_multiparty(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); - - return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + return ag_emit_create_multiparty(agimpl, m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); } static DBusMessage *ag_send_tones(struct agimpl *agimpl, DBusMessage *m) { const char *tones = NULL; enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; if (!dbus_message_get_args(m, NULL, DBUS_TYPE_STRING, &tones, @@ -975,24 +1019,19 @@ static DBusMessage *ag_send_tones(struct agimpl *agimpl, DBusMessage *m) goto failed; } - if (ag_emit_send_tones(agimpl, tones, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); + if (ag_emit_send_tones(agimpl, tones, m)) + return NULL; failed: return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + telephony_error_to_description (err, 0)); } static DBusMessage *ag_transport_activate(struct agimpl *agimpl, DBusMessage *m) { - enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - - if (ag_emit_transport_activate(agimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); - - return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + return ag_emit_transport_activate(agimpl, m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); } static DBusHandlerResult ag_handler(DBusConnection *c, DBusMessage *m, void *userdata) @@ -1051,9 +1090,7 @@ static DBusHandlerResult ag_handler(DBusConnection *c, DBusMessage *m, void *use return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } - if (r == NULL) - return DBUS_HANDLER_RESULT_NEED_MEMORY; - if (!dbus_connection_send(impl->conn, r, NULL)) + if (r && !dbus_connection_send(impl->conn, r, NULL)) return DBUS_HANDLER_RESULT_NEED_MEMORY; return DBUS_HANDLER_RESULT_HANDLED; } @@ -1219,7 +1256,38 @@ void telephony_ag_unregister(struct spa_bt_telephony_ag *ag) agimpl->path = NULL; } -/* send message to notify about property changes */ +/* send message to notify about volume property changes */ +void telephony_ag_notify_updated_props(struct spa_bt_telephony_ag *ag) +{ + struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); + struct impl *impl = SPA_CONTAINER_OF(agimpl->this.telephony, struct impl, this); + + spa_autoptr(DBusMessage) msg = NULL; + const char *interface = PW_TELEPHONY_AG_IFACE; + DBusMessageIter i, a; + + msg = dbus_message_new_signal(agimpl->path, + DBUS_INTERFACE_PROPERTIES, + "PropertiesChanged"); + + dbus_message_iter_init_append(msg, &i); + dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &interface); + + if (!dbus_iter_append_ag_properties(&i, ag, false)) + return; + + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, &a); + dbus_message_iter_close_container(&i, &a); + + if (!dbus_connection_send(impl->conn, msg, NULL)){ + spa_log_warn(impl->log, "sending PropertiesChanged failed"); + } + + telephony_ag_commit_properties(ag); +} + +/* send message to notify about transport property changes */ void telephony_ag_transport_notify_updated_props(struct spa_bt_telephony_ag *ag) { struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); @@ -1262,7 +1330,6 @@ void telephony_ag_set_callbacks(struct spa_bt_telephony_ag *ag, struct spa_bt_telephony_call * telephony_call_new(struct spa_bt_telephony_ag *ag, size_t user_data_size) { - struct agimpl *agimpl = SPA_CONTAINER_OF(ag, struct agimpl, this); struct callimpl *callimpl; spa_assert(user_data_size < SIZE_MAX - sizeof(*callimpl)); @@ -1279,10 +1346,6 @@ telephony_call_new(struct spa_bt_telephony_ag *ag, size_t user_data_size) if (user_data_size > 0) callimpl->user_data = SPA_PTROFF(callimpl, sizeof(struct callimpl), void); - /* mark this object as the return value of the Dial method */ - if (agimpl->dial_in_progress) - agimpl->dial_return = callimpl; - return &callimpl->this; } @@ -1526,26 +1589,16 @@ static DBusMessage *call_properties_set(struct callimpl *callimpl, DBusMessage * static DBusMessage *call_answer(struct callimpl *callimpl, DBusMessage *m) { - enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - - if (call_emit_answer(callimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); - - return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + return call_emit_answer(callimpl, m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); } static DBusMessage *call_hangup(struct callimpl *callimpl, DBusMessage *m) { - enum spa_bt_telephony_error err = BT_TELEPHONY_ERROR_FAILED; - uint8_t cme_error; - - if (call_emit_hangup(callimpl, &err, &cme_error) && err == BT_TELEPHONY_ERROR_NONE) - return dbus_message_new_method_return(m); - - return dbus_message_new_error(m, telephony_error_to_dbus (err), - telephony_error_to_description (err, cme_error)); + return call_emit_hangup(callimpl, m) ? NULL : + dbus_message_new_error(m, telephony_error_to_dbus (BT_TELEPHONY_ERROR_FAILED), + telephony_error_to_description (BT_TELEPHONY_ERROR_FAILED, 0)); } static DBusHandlerResult call_handler(DBusConnection *c, DBusMessage *m, void *userdata) @@ -1583,9 +1636,7 @@ static DBusHandlerResult call_handler(DBusConnection *c, DBusMessage *m, void *u return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } - if (r == NULL) - return DBUS_HANDLER_RESULT_NEED_MEMORY; - if (!dbus_connection_send(impl->conn, r, NULL)) + if (r && !dbus_connection_send(impl->conn, r, NULL)) return DBUS_HANDLER_RESULT_NEED_MEMORY; return DBUS_HANDLER_RESULT_HANDLED; } diff --git a/spa/plugins/bluez5/telephony.h b/spa/plugins/bluez5/telephony.h index 8ba85ec47..1f7f67362 100644 --- a/spa/plugins/bluez5/telephony.h +++ b/spa/plugins/bluez5/telephony.h @@ -45,6 +45,7 @@ struct spa_bt_telephony_ag { /* D-Bus properties */ char *address; + int volume[SPA_BT_VOLUME_ID_TERM]; struct spa_bt_telephony_ag_transport transport; }; @@ -66,30 +67,38 @@ struct spa_bt_telephony_ag_callbacks { #define SPA_VERSION_BT_TELEPHONY_AG_CALLBACKS 0 uint32_t version; - void (*dial)(void *data, const char *number, enum spa_bt_telephony_error *err, uint8_t *cme_error); - void (*swap_calls)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); - void (*release_and_answer)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); - void (*release_and_swap)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); - void (*hold_and_answer)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); - void (*hangup_all)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); - void (*create_multiparty)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); - void (*send_tones)(void *data, const char *tones, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*dial)(void *data, const char *number, DBusMessage *m); + void (*swap_calls)(void *data, DBusMessage *m); + void (*release_and_answer)(void *data, DBusMessage *m); + void (*release_and_swap)(void *data, DBusMessage *m); + void (*hold_and_answer)(void *data, DBusMessage *m); + void (*hangup_all)(void *data, DBusMessage *m); + void (*create_multiparty)(void *data, DBusMessage *m); + void (*send_tones)(void *data, const char *tones, DBusMessage *m); - void (*transport_activate)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*transport_activate)(void *data, DBusMessage *m); + + void (*set_speaker_volume)(void *data, uint8_t volume, DBusMessage *m); + void (*set_microphone_volume)(void *data, uint8_t volume, DBusMessage *m); }; struct spa_bt_telephony_call_callbacks { #define SPA_VERSION_BT_TELEPHONY_CALL_CALLBACKS 0 uint32_t version; - void (*answer)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); - void (*hangup)(void *data, enum spa_bt_telephony_error *err, uint8_t *cme_error); + void (*answer)(void *data, DBusMessage *m); + void (*hangup)(void *data, DBusMessage *m); }; struct spa_bt_telephony *telephony_new(struct spa_log *log, struct spa_dbus *dbus, const struct spa_dict *info); void telephony_free(struct spa_bt_telephony *telephony); +/* send a reply to any of the methods (they all return void); + this is must be called from the callbacks either in sync or async */ +void telephony_send_dbus_method_reply(struct spa_bt_telephony *telephony, + DBusMessage *m, enum spa_bt_telephony_error err, uint8_t cme_error); + /* create/destroy the ag object */ struct spa_bt_telephony_ag * telephony_ag_new(struct spa_bt_telephony *telephony, @@ -103,6 +112,7 @@ void telephony_ag_set_callbacks(struct spa_bt_telephony_ag *ag, const struct spa_bt_telephony_ag_callbacks *cbs, void *data); +void telephony_ag_notify_updated_props(struct spa_bt_telephony_ag *ag); void telephony_ag_transport_notify_updated_props(struct spa_bt_telephony_ag *ag); /* register/unregister AudioGateway object on the bus */ diff --git a/spa/plugins/control/meson.build b/spa/plugins/control/meson.build index adabdfab3..b31986b05 100644 --- a/spa/plugins/control/meson.build +++ b/spa/plugins/control/meson.build @@ -8,3 +8,33 @@ controllib = shared_library('spa-control', dependencies : [ spa_dep, mathlib ], install : true, install_dir : spa_plugindir / 'control') + +test_inc = include_directories('../test') + +test_apps = [ + 'test-mixer-ump-sort', +] + +foreach a : test_apps + test(a, + executable(a, a + '.c', + dependencies : [ spa_dep ], + include_directories : [ configinc, test_inc ], + install_rpath : spa_plugindir / 'control', + install : installed_tests_enabled, + install_dir : installed_tests_execdir / 'control'), + env : [ + 'SPA_PLUGIN_DIR=@0@'.format(spa_dep.get_variable('plugindir')), + ]) + + if installed_tests_enabled + test_conf = configuration_data() + test_conf.set('exec', installed_tests_execdir / 'control' / a) + configure_file( + input: installed_tests_template, + output: a + '.test', + install_dir: installed_tests_metadir / 'control', + configuration: test_conf + ) + endif +endforeach diff --git a/spa/plugins/control/mixer.c b/spa/plugins/control/mixer.c index 439bea76b..caf3a6b06 100644 --- a/spa/plugins/control/mixer.c +++ b/spa/plugins/control/mixer.c @@ -39,6 +39,8 @@ struct buffer { }; struct port { + struct spa_list link; + uint32_t direction; uint32_t id; @@ -48,7 +50,6 @@ struct port { struct spa_port_info info; struct spa_param_info params[8]; - unsigned int valid:1; unsigned int have_format:1; uint32_t types; @@ -57,6 +58,13 @@ struct port { uint32_t n_buffers; struct spa_list queue; + + struct spa_list mix_link; + bool active; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_control control; + const void *control_body; }; struct impl { @@ -77,24 +85,23 @@ struct impl { struct spa_hook_list hooks; - uint32_t port_count; - uint32_t last_port; struct port *in_ports[MAX_PORTS]; struct port out_ports[1]; - - struct spa_pod_control *mix_ctrl[MAX_PORTS]; - struct spa_pod_sequence *mix_seq[MAX_PORTS]; + struct spa_list port_list; + struct spa_list free_list; int n_formats; unsigned int have_format:1; unsigned int started:1; + + struct spa_list mix_list; + struct port *mix_ports[MAX_PORTS]; }; -#define PORT_VALID(p) ((p) != NULL && (p)->valid) #define CHECK_ANY_IN(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) == SPA_ID_INVALID) -#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && !PORT_VALID(this->in_ports[(p)])) -#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && PORT_VALID(this->in_ports[(p)])) +#define CHECK_FREE_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && this->in_ports[(p)] == NULL) +#define CHECK_IN_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS && this->in_ports[(p)] != NULL) #define CHECK_OUT_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) == 0) #define CHECK_PORT(this,d,p) (CHECK_OUT_PORT(this,d,p) || CHECK_IN_PORT (this,d,p)) #define CHECK_PORT_ANY(this,d,p) (CHECK_ANY_IN(this,d,p) || CHECK_PORT(this,d,p)) @@ -180,7 +187,7 @@ static int impl_node_add_listener(void *object, { struct impl *this = object; struct spa_hook_list save; - uint32_t i; + struct port *port; spa_return_val_if_fail(this != NULL, -EINVAL); @@ -188,10 +195,8 @@ static int impl_node_add_listener(void *object, emit_node_info(this, true); emit_port_info(this, GET_OUT_PORT(this, 0), true); - for (i = 0; i < this->last_port; i++) { - if (PORT_VALID(this->in_ports[i])) - emit_port_info(this, GET_IN_PORT(this, i), true); - } + spa_list_for_each(port, &this->port_list, link) + emit_port_info(this, port, true); spa_hook_list_join(&this->hooks, &save); @@ -206,6 +211,18 @@ impl_node_set_callbacks(void *object, return 0; } +static struct port *get_free_port(struct impl *this) +{ + struct port *port; + if (!spa_list_is_empty(&this->free_list)) { + port = spa_list_first(&this->free_list, struct port, link); + spa_list_remove(&port->link); + } else { + port = calloc(1, sizeof(struct port)); + } + return port; +} + static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, const struct spa_dict *props) { @@ -216,12 +233,8 @@ static int impl_node_add_port(void *object, enum spa_direction direction, uint32 spa_return_val_if_fail(CHECK_FREE_IN_PORT(this, direction, port_id), -EINVAL); port = GET_IN_PORT (this, port_id); - if (port == NULL) { - port = calloc(1, sizeof(struct port)); - if (port == NULL) - return -errno; - this->in_ports[port_id] = port; - } + if ((port = get_free_port(this)) == NULL) + return -errno; port->direction = direction; port->id = port_id; @@ -242,12 +255,10 @@ static int impl_node_add_port(void *object, enum spa_direction direction, uint32 port->info.params = port->params; port->info.n_params = 5; - this->port_count++; - if (this->last_port <= port_id) - this->last_port = port_id + 1; - port->valid = true; + this->in_ports[port_id] = port; + spa_list_append(&this->port_list, &port->link); - spa_log_debug(this->log, "%p: add port %d %d", this, port_id, this->last_port); + spa_log_debug(this->log, "%p: add port %d:%d", this, direction, port_id); emit_port_info(this, port, true); return 0; @@ -263,25 +274,17 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_ spa_return_val_if_fail(CHECK_IN_PORT(this, direction, port_id), -EINVAL); port = GET_IN_PORT (this, port_id); + this->in_ports[port_id] = NULL; + spa_list_remove(&port->link); - port->valid = false; - this->port_count--; if (port->have_format && this->have_format) { if (--this->n_formats == 0) this->have_format = false; } spa_memzero(port, sizeof(struct port)); + spa_list_append(&this->free_list, &port->link); - if (port_id + 1 == this->last_port) { - int i; - - for (i = this->last_port - 1; i >= 0; i--) - if (GET_IN_PORT (this, i)->valid) - break; - - this->last_port = i + 1; - } - spa_log_debug(this->log, "%p: remove port %d %d", this, port_id, this->last_port); + spa_log_debug(this->log, "%p: remove port %d:%d", this, direction, port_id); spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); @@ -558,6 +561,7 @@ impl_node_port_use_buffers(void *object, } struct io_info { + struct impl *impl; struct port *port; void *data; size_t size; @@ -567,16 +571,28 @@ static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { struct io_info *info = user_data; - if (info->size >= sizeof(struct spa_io_async_buffers)) { - struct spa_io_async_buffers *ab = info->data; - info->port->io[0] = &ab->buffers[0]; - info->port->io[1] = &ab->buffers[1]; - } else if (info->size >= sizeof(struct spa_io_buffers)) { - info->port->io[0] = info->data; - info->port->io[1] = info->data; + struct port *port = info->port; + + if (info->data == NULL || info->size < sizeof(struct spa_io_buffers)) { + port->io[0] = NULL; + port->io[1] = NULL; + if (port->active) { + spa_list_remove(&port->mix_link); + port->active = false; + } } else { - info->port->io[0] = NULL; - info->port->io[1] = NULL; + if (info->size >= sizeof(struct spa_io_async_buffers)) { + struct spa_io_async_buffers *ab = info->data; + port->io[0] = &ab->buffers[port->direction]; + port->io[1] = &ab->buffers[port->direction^1]; + } else { + port->io[0] = info->data; + port->io[1] = info->data; + } + if (!port->active) { + spa_list_append(&info->impl->mix_list, &port->mix_link); + port->active = true; + } } return 0; } @@ -598,6 +614,7 @@ impl_node_port_set_io(void *object, spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); port = GET_PORT(this, direction, port_id); + info.impl = this; info.port = port; info.data = data; info.size = size; @@ -605,8 +622,8 @@ impl_node_port_set_io(void *object, switch (id) { case SPA_IO_Buffers: case SPA_IO_AsyncBuffers: - spa_loop_invoke(this->data_loop, - do_port_set_io, SPA_ID_INVALID, NULL, 0, true, &info); + spa_loop_locked(this->data_loop, + do_port_set_io, SPA_ID_INVALID, NULL, 0, &info); break; default: return -ENOENT; @@ -640,7 +657,8 @@ static inline int event_compare(uint8_t s1, uint8_t s2) return priotab[(s2>>4) & 7] - priotab[(s1>>4) & 7]; } -static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b) +static inline int event_sort(struct spa_pod_control *a, const void *abody, + struct spa_pod_control *b, const void *bbody) { if (a->offset < b->offset) return -1; @@ -651,18 +669,18 @@ static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control * switch(a->type) { case SPA_CONTROL_Midi: { - uint8_t *da = SPA_POD_BODY(&a->value), *db = SPA_POD_BODY(&b->value); + const uint8_t *da = abody, *db = bbody; if (SPA_POD_BODY_SIZE(&a->value) < 1 || SPA_POD_BODY_SIZE(&b->value) < 1) return 0; return event_compare(da[0], db[0]); } case SPA_CONTROL_UMP: { - uint32_t *da = SPA_POD_BODY(&a->value), *db = SPA_POD_BODY(&b->value); + const uint32_t *da = abody, *db = bbody; if (SPA_POD_BODY_SIZE(&a->value) < 4 || SPA_POD_BODY_SIZE(&b->value) < 4) return 0; - if ((da[0] >> 28) != 2 || (da[0] >> 28) != 4 || - (db[0] >> 28) != 2 || (db[0] >> 28) != 4) + if (((da[0] >> 28) != 2 && (da[0] >> 28) != 4) || + ((db[0] >> 28) != 2 && (db[0] >> 28) != 4)) return 0; return event_compare(da[0] >> 16, db[0] >> 16); } @@ -671,17 +689,24 @@ static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control * } } +static inline bool control_needs_conversion(struct port *port, uint32_t type) +{ + /* we only converter between midi and UMP and only when the port + * does not support the current type */ + return (type == SPA_CONTROL_Midi || type == SPA_CONTROL_UMP) && + port->types && (port->types & (1u << type)) == 0; +} + static int impl_node_process(void *object) { struct impl *this = object; - struct port *outport; + struct port *outport, *inport; struct spa_io_buffers *outio; - uint32_t n_seq, i; - struct spa_pod_sequence **seq; - struct spa_pod_control **ctrl; struct spa_pod_builder builder; + uint32_t n_mix_ports; + struct port **mix_ports; struct spa_pod_frame f; - struct buffer *outb; + struct buffer *outb; struct spa_data *d; uint32_t cycle = this->position->clock.cycle & 1; @@ -711,51 +736,47 @@ static int impl_node_process(void *object) return -EPIPE; } - ctrl = this->mix_ctrl; - seq = this->mix_seq; - n_seq = 0; + mix_ports = this->mix_ports; + n_mix_ports = 0; /* collect all sequence pod on input ports */ - for (i = 0; i < this->last_port; i++) { - struct port *inport = GET_IN_PORT(this, i); - struct spa_io_buffers *inio = NULL; - void *pod; + spa_list_for_each(inport, &this->mix_list, mix_link) { + struct spa_io_buffers *inio = inport->io[cycle]; + struct spa_pod_sequence seq; + const void *seq_body; - if (SPA_UNLIKELY(!PORT_VALID(inport) || (inio = inport->io[cycle]) == NULL)) { - spa_log_trace_fp(this->log, "%p: skip input idx:%d valid:%d io:%p/%p/%d", - this, i, PORT_VALID(inport), - inport->io[0], inport->io[1], cycle); - continue; - } if (inio->buffer_id >= inport->n_buffers || inio->status != SPA_STATUS_HAVE_DATA) { spa_log_trace_fp(this->log, "%p: skip input idx:%d " "io:%p status:%d buf_id:%d n_buffers:%d", this, - i, inio, inio->status, inio->buffer_id, inport->n_buffers); + inport->id, inio, inio->status, inio->buffer_id, inport->n_buffers); continue; } spa_log_trace_fp(this->log, "%p: mix input %d %p->%p %d %d", this, - i, inio, outio, inio->status, inio->buffer_id); + inport->id, inio, outio, inio->status, inio->buffer_id); d = inport->buffers[inio->buffer_id].buffer->datas; - if ((pod = spa_pod_from_data(d->data, d->maxsize, - d->chunk->offset, d->chunk->size)) == NULL) { + spa_pod_parser_init_from_data(&inport->parser, d->data, d->maxsize, + d->chunk->offset, d->chunk->size); + + if (spa_pod_parser_push_sequence_body(&inport->parser, + &inport->frame, &seq, &seq_body) < 0) { spa_log_trace_fp(this->log, "%p: skip input idx:%d max:%u " - "offset:%u size:%u", this, i, + "offset:%u size:%u", this, inport->id, d->maxsize, d->chunk->offset, d->chunk->size); continue; } - if (!spa_pod_is_sequence(pod)) { - spa_log_trace_fp(this->log, "%p: skip input idx:%d", this, i); + if (spa_pod_parser_get_control_body(&inport->parser, + &inport->control, &inport->control_body) < 0) { + spa_log_trace_fp(this->log, "%p: skip input idx:%d %u", this, inport->id, + inport->parser.state.offset); continue; } - seq[n_seq] = pod; - ctrl[n_seq] = spa_pod_control_first(&seq[n_seq]->body); + mix_ports[n_mix_ports++] = inport; inio->status = SPA_STATUS_NEED_DATA; - n_seq++; } d = outb->buffer->datas; @@ -766,36 +787,38 @@ static int impl_node_process(void *object) /* merge sort all sequences into output buffer */ while (true) { - struct spa_pod_control *next = NULL; - uint32_t next_index = 0; + uint32_t i, next_index = 0; + size_t size; + uint8_t *body; + struct port *next = NULL; + struct spa_pod_control *control; - for (i = 0; i < n_seq; i++) { - if (!spa_pod_control_is_inside(&seq[i]->body, - SPA_POD_BODY_SIZE(seq[i]), ctrl[i])) - continue; - - if (next == NULL || event_sort(ctrl[i], next) <= 0) { - next = ctrl[i]; + for (i = 0; i < n_mix_ports; i++) { + struct port *p = mix_ports[i]; + if (next == NULL || event_sort(&p->control, p->control_body, + &next->control, next->control_body) <= 0) { + next = p; next_index = i; } } if (next == NULL) break; - if (outport->types && (outport->types & (1u << next->type)) == 0) { - uint8_t *data = SPA_POD_BODY(&next->value); - size_t size = SPA_POD_BODY_SIZE(&next->value); + control = &next->control; + body = (uint8_t*)next->control_body; + size = SPA_POD_BODY_SIZE(&control->value); - switch (next->type) { + if (control_needs_conversion(outport, control->type)) { + switch (control->type) { case SPA_CONTROL_Midi: { uint32_t ump[4]; uint64_t state = 0; while (size > 0) { - int ump_size = spa_ump_from_midi(&data, &size, ump, sizeof(ump), 0, &state); + int ump_size = spa_ump_from_midi(&body, &size, ump, sizeof(ump), 0, &state); if (ump_size <= 0) break; - spa_pod_builder_control(&builder, next->offset, SPA_CONTROL_UMP); + spa_pod_builder_control(&builder, control->offset, SPA_CONTROL_UMP); spa_pod_builder_bytes(&builder, ump, ump_size); } break; @@ -803,20 +826,28 @@ static int impl_node_process(void *object) case SPA_CONTROL_UMP: { uint8_t ev[8]; - int ev_size = spa_ump_to_midi((uint32_t*)data, size, ev, sizeof(ev)); - if (ev_size <= 0) - break; - spa_pod_builder_control(&builder, next->offset, SPA_CONTROL_Midi); - spa_pod_builder_bytes(&builder, ev, ev_size); + const uint32_t *ump = (const uint32_t*)body; + uint64_t state = 0; + while (size > 0) { + int ev_size = spa_ump_to_midi(&ump, &size, ev, sizeof(ev), &state); + if (ev_size <= 0) + break; + spa_pod_builder_control(&builder, control->offset, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&builder, ev, ev_size); + } break; } } } else { - spa_pod_builder_control(&builder, next->offset, next->type); - spa_pod_builder_primitive(&builder, &next->value); + spa_pod_builder_control(&builder, control->offset, control->type); + spa_pod_builder_primitive_body(&builder, &control->value, body, size, NULL, 0); } - ctrl[next_index] = spa_pod_control_next(ctrl[next_index]); + if (spa_pod_parser_get_control_body(&next->parser, + &next->control, &next->control_body) < 0) { + spa_pod_parser_pop(&next->parser, &next->frame); + mix_ports[next_index] = mix_ports[--n_mix_ports]; + } } spa_pod_builder_pop(&builder, &f); @@ -875,14 +906,17 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void static int impl_clear(struct spa_handle *handle) { struct impl *this; - uint32_t i; + struct port *port; spa_return_val_if_fail(handle != NULL, -EINVAL); this = (struct impl *) handle; - for (i = 0; i < MAX_PORTS; i++) - free(this->in_ports[i]); + spa_list_insert_list(&this->free_list, &this->port_list); + spa_list_consume(port, &this->free_list, link) { + spa_list_remove(&port->link); + free(port); + } return 0; } @@ -931,6 +965,9 @@ impl_init(const struct spa_handle_factory *factory, } spa_hook_list_init(&this->hooks); + spa_list_init(&this->port_list); + spa_list_init(&this->free_list); + spa_list_init(&this->mix_list); this->node.iface = SPA_INTERFACE_INIT( SPA_TYPE_INTERFACE_Node, @@ -943,7 +980,6 @@ impl_init(const struct spa_handle_factory *factory, this->info.flags = SPA_NODE_FLAG_RT | SPA_NODE_FLAG_IN_DYNAMIC_PORTS; port = GET_OUT_PORT(this, 0); - port->valid = true; port->direction = SPA_DIRECTION_OUTPUT; port->id = 0; port->info = SPA_PORT_INFO_INIT(); diff --git a/spa/plugins/control/test-mixer-ump-sort.c b/spa/plugins/control/test-mixer-ump-sort.c new file mode 100644 index 000000000..23a87e18d --- /dev/null +++ b/spa/plugins/control/test-mixer-ump-sort.c @@ -0,0 +1,151 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 Claude Code */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +#include +#include +#include + +/* Include the mixer source to access static inline functions */ +#include "mixer.c" + +/* Simple test framework macros */ +#define TEST_ASSERT(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s\n", msg); \ + return 1; \ + } \ + } while (0) + +#define TEST_PASS() \ + do { \ + printf("PASS\n"); \ + return 0; \ + } while (0) + +/* Helper to create mock control structures for testing */ +static struct spa_pod_control *create_mock_control(uint8_t *buffer, size_t *offset, + uint64_t timestamp, uint32_t type, const void *data, size_t data_size) +{ + struct spa_pod_control *control = (struct spa_pod_control *)(buffer + *offset); + control->offset = timestamp; + control->type = type; + control->value.size = data_size; + control->value.type = SPA_TYPE_Bytes; + + /* Copy data after the control structure */ + memcpy(buffer + *offset + sizeof(struct spa_pod_control), data, data_size); + *offset += sizeof(struct spa_pod_control) + SPA_ROUND_UP_N(data_size, 8); + + return control; +} + +static int test_ump_event_sort_offset_priority(void) +{ + uint8_t buffer[1024]; + size_t offset = 0; + uint32_t ump_early = 0x20904060; /* Note On Ch 0 */ + uint32_t ump_late = 0x20904060; /* Note On Ch 0 */ + + struct spa_pod_control *a = create_mock_control(buffer, &offset, 100, SPA_CONTROL_UMP, &ump_early, 4); + const void *abody = (uint8_t*)a + sizeof(struct spa_pod_control); + + struct spa_pod_control *b = create_mock_control(buffer, &offset, 200, SPA_CONTROL_UMP, &ump_late, 4); + const void *bbody = (uint8_t*)b + sizeof(struct spa_pod_control); + + /* Earlier offset should sort before later offset */ + TEST_ASSERT(event_sort(a, abody, b, bbody) < 0, "Earlier offset should sort before later offset"); + /* Later offset should sort after earlier offset */ + TEST_ASSERT(event_sort(b, bbody, a, abody) > 0, "Later offset should sort after earlier offset"); + + TEST_PASS(); +} + +static int test_ump_event_sort_same_offset_different_channels(void) +{ + uint8_t buffer[1024]; + size_t offset = 0; + uint32_t ump_ch0 = 0x20904060; /* Note On Ch 0 */ + uint32_t ump_ch1 = 0x20914060; /* Note On Ch 1 */ + + struct spa_pod_control *a = create_mock_control(buffer, &offset, 100, SPA_CONTROL_UMP, &ump_ch0, 4); + const void *abody = (uint8_t*)a + sizeof(struct spa_pod_control); + + struct spa_pod_control *b = create_mock_control(buffer, &offset, 100, SPA_CONTROL_UMP, &ump_ch1, 4); + const void *bbody = (uint8_t*)b + sizeof(struct spa_pod_control); + + /* Different channels at same offset should return 0 (no preference) */ + TEST_ASSERT(event_sort(a, abody, b, bbody) == 0, "Different channels at same offset should return 0"); + TEST_ASSERT(event_sort(b, bbody, a, abody) == 0, "Different channels at same offset should return 0"); + + TEST_PASS(); +} + +static int test_ump_event_sort_priority_controller_vs_note(void) +{ + uint8_t buffer[1024]; + size_t offset = 0; + uint32_t ump_note_on = 0x20904060; /* Note On Ch 0 (priority 4) */ + uint32_t ump_controller = 0x20B04060; /* Controller Ch 0 (priority 2) */ + + struct spa_pod_control *note_on = create_mock_control(buffer, &offset, 100, SPA_CONTROL_UMP, &ump_note_on, 4); + const void *note_body = (uint8_t*)note_on + sizeof(struct spa_pod_control); + + struct spa_pod_control *controller = create_mock_control(buffer, &offset, 100, SPA_CONTROL_UMP, &ump_controller, 4); + const void *ctrl_body = (uint8_t*)controller + sizeof(struct spa_pod_control); + + /* Controller (higher priority) should sort before Note On (lower priority) */ + TEST_ASSERT(event_sort(note_on, note_body, controller, ctrl_body) > 0, "Controller should sort before Note On"); + TEST_ASSERT(event_sort(controller, ctrl_body, note_on, note_body) <= 0, "Controller should sort before Note On"); + + TEST_PASS(); +} + +static int test_event_compare_priority_table(void) +{ + /* Test controller (0xB0) vs note on (0x90) on same channel */ + TEST_ASSERT(event_compare(0x90, 0xB0) > 0, "Controller has higher priority than Note On"); + TEST_ASSERT(event_compare(0xB0, 0x90) < 0, "Controller has higher priority than Note On"); + + /* Test program change (0xC0) vs note off (0x80) on same channel */ + TEST_ASSERT(event_compare(0x80, 0xC0) > 0, "Program change has higher priority than Note Off"); + TEST_ASSERT(event_compare(0xC0, 0x80) < 0, "Program change has higher priority than Note Off"); + + /* Test different channels should return 0 */ + TEST_ASSERT(event_compare(0x90, 0x91) == 0, "Different channels should return 0"); + + TEST_PASS(); +} + +int main(void) +{ + int result = 0; + + printf("Running mixer UMP sort tests...\n"); + + printf("test_ump_event_sort_offset_priority: "); + result |= test_ump_event_sort_offset_priority(); + + printf("test_ump_event_sort_same_offset_different_channels: "); + result |= test_ump_event_sort_same_offset_different_channels(); + + printf("test_ump_event_sort_priority_controller_vs_note: "); + result |= test_ump_event_sort_priority_controller_vs_note(); + + printf("test_event_compare_priority_table: "); + result |= test_event_compare_priority_table(); + + if (result == 0) { + printf("All tests passed!\n"); + } else { + printf("Some tests failed!\n"); + } + + return result; +} \ No newline at end of file diff --git a/spa/plugins/filter-graph/audio-dsp-avx.c b/spa/plugins/filter-graph/audio-dsp-avx.c index 1509284c7..9b49fb556 100644 --- a/spa/plugins/filter-graph/audio-dsp-avx.c +++ b/spa/plugins/filter-graph/audio-dsp-avx.c @@ -2,13 +2,14 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include #include -#include "config.h" #ifndef HAVE_FFTW #include "pffft.h" #endif diff --git a/spa/plugins/filter-graph/audio-dsp-c.c b/spa/plugins/filter-graph/audio-dsp-c.c index f2a509a76..6ea559908 100644 --- a/spa/plugins/filter-graph/audio-dsp-c.c +++ b/spa/plugins/filter-graph/audio-dsp-c.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,7 +12,6 @@ #include -#include "config.h" #ifdef HAVE_FFTW #include #else @@ -217,7 +218,7 @@ void dsp_delay_c(void *obj, float *buffer, uint32_t *pos, uint32_t n_buffer, for (i = 0; i < n_samples; i++) { buffer[w] = buffer[w + n_buffer] = src[i]; dst[i] = buffer[w + o]; - w = w + 1 > n_buffer ? 0 : w + 1; + w = w + 1 >= n_buffer ? 0 : w + 1; } *pos = w; } diff --git a/spa/plugins/filter-graph/audio-dsp-sse.c b/spa/plugins/filter-graph/audio-dsp-sse.c index 8c2ffa8e6..59aee8271 100644 --- a/spa/plugins/filter-graph/audio-dsp-sse.c +++ b/spa/plugins/filter-graph/audio-dsp-sse.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,7 +12,6 @@ #include -#include "config.h" #ifndef HAVE_FFTW #include "pffft.h" #endif diff --git a/spa/plugins/filter-graph/audio-plugin.h b/spa/plugins/filter-graph/audio-plugin.h index e05d7a80a..9f9d1bce6 100644 --- a/spa/plugins/filter-graph/audio-plugin.h +++ b/spa/plugins/filter-graph/audio-plugin.h @@ -9,7 +9,6 @@ #include #include -#include #include #define SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin SPA_TYPE_INFO_INTERFACE_BASE "FilterGraph:AudioPlugin" @@ -27,15 +26,18 @@ struct spa_fga_plugin_methods { struct spa_fga_port { uint32_t index; const char *name; -#define SPA_FGA_PORT_INPUT (1ULL << 0) -#define SPA_FGA_PORT_OUTPUT (1ULL << 1) -#define SPA_FGA_PORT_CONTROL (1ULL << 2) -#define SPA_FGA_PORT_AUDIO (1ULL << 3) +#define SPA_FGA_PORT_INPUT (1ULL << 0) +#define SPA_FGA_PORT_OUTPUT (1ULL << 1) +#define SPA_FGA_PORT_CONTROL (1ULL << 2) +#define SPA_FGA_PORT_AUDIO (1ULL << 3) +#define SPA_FGA_PORT_SUPPORTS_NULL_DATA (1ULL << 4) +#define SPA_FGA_PORT_SEQUENCE (1ULL << 5) uint64_t flags; -#define SPA_FGA_HINT_BOOLEAN (1ULL << 2) -#define SPA_FGA_HINT_SAMPLE_RATE (1ULL << 3) -#define SPA_FGA_HINT_INTEGER (1ULL << 5) +#define SPA_FGA_HINT_BOOLEAN (1ULL << 0) +#define SPA_FGA_HINT_SAMPLE_RATE (1ULL << 1) +#define SPA_FGA_HINT_INTEGER (1ULL << 2) +#define SPA_FGA_HINT_LATENCY (1ULL << 3) uint64_t hint; float def; float min; @@ -46,6 +48,8 @@ struct spa_fga_port { #define SPA_FGA_IS_PORT_OUTPUT(x) ((x) & SPA_FGA_PORT_OUTPUT) #define SPA_FGA_IS_PORT_CONTROL(x) ((x) & SPA_FGA_PORT_CONTROL) #define SPA_FGA_IS_PORT_AUDIO(x) ((x) & SPA_FGA_PORT_AUDIO) +#define SPA_FGA_SUPPORTS_NULL_DATA(x) ((x) & SPA_FGA_PORT_SUPPORTS_NULL_DATA) +#define SPA_FGA_IS_PORT_SEQUENCE(x) ((x) & SPA_FGA_PORT_SEQUENCE) struct spa_fga_descriptor { const char *name; @@ -63,7 +67,7 @@ struct spa_fga_descriptor { void (*cleanup) (void *instance); - void (*connect_port) (void *instance, unsigned long port, float *data); + void (*connect_port) (void *instance, unsigned long port, void *data); void (*control_changed) (void *instance); void (*activate) (void *instance); diff --git a/spa/plugins/filter-graph/convolver.c b/spa/plugins/filter-graph/convolver.c index e5bd47b30..a077c6ec1 100644 --- a/spa/plugins/filter-graph/convolver.c +++ b/spa/plugins/filter-graph/convolver.c @@ -19,14 +19,13 @@ struct convolver1 { float **segments; float **segmentsIr; - float *fft_buffer; + float *fft_buffer[2]; void *fft; void *ifft; float *pre_mult; float *conv; - float *overlap; float *inputBuffer; int inputBufferFill; @@ -48,7 +47,8 @@ static void convolver1_reset(struct spa_fga_dsp *dsp, struct convolver1 *conv) int i; for (i = 0; i < conv->segCount; i++) spa_fga_dsp_fft_memclear(dsp, conv->segments[i], conv->fftComplexSize, false); - spa_fga_dsp_fft_memclear(dsp, conv->overlap, conv->blockSize, true); + spa_fga_dsp_fft_memclear(dsp, conv->fft_buffer[0], conv->segSize, true); + spa_fga_dsp_fft_memclear(dsp, conv->fft_buffer[1], conv->segSize, true); spa_fga_dsp_fft_memclear(dsp, conv->inputBuffer, conv->segSize, true); spa_fga_dsp_fft_memclear(dsp, conv->pre_mult, conv->fftComplexSize, false); spa_fga_dsp_fft_memclear(dsp, conv->conv, conv->fftComplexSize, false); @@ -69,13 +69,14 @@ static void convolver1_free(struct spa_fga_dsp *dsp, struct convolver1 *conv) spa_fga_dsp_fft_free(dsp, conv->fft); if (conv->ifft) spa_fga_dsp_fft_free(dsp, conv->ifft); - if (conv->fft_buffer) - spa_fga_dsp_fft_memfree(dsp, conv->fft_buffer); + if (conv->fft_buffer[0]) + spa_fga_dsp_fft_memfree(dsp, conv->fft_buffer[0]); + if (conv->fft_buffer[1]) + spa_fga_dsp_fft_memfree(dsp, conv->fft_buffer[1]); free(conv->segments); free(conv->segmentsIr); spa_fga_dsp_fft_memfree(dsp, conv->pre_mult); spa_fga_dsp_fft_memfree(dsp, conv->conv); - spa_fga_dsp_fft_memfree(dsp, conv->overlap); spa_fga_dsp_fft_memfree(dsp, conv->inputBuffer); free(conv); } @@ -110,8 +111,9 @@ static struct convolver1 *convolver1_new(struct spa_fga_dsp *dsp, int block, con if (conv->ifft == NULL) goto error; - conv->fft_buffer = spa_fga_dsp_fft_memalloc(dsp, conv->segSize, true); - if (conv->fft_buffer == NULL) + conv->fft_buffer[0] = spa_fga_dsp_fft_memalloc(dsp, conv->segSize, true); + conv->fft_buffer[1] = spa_fga_dsp_fft_memalloc(dsp, conv->segSize, true); + if (conv->fft_buffer[0] == NULL || conv->fft_buffer[1] == NULL) goto error; conv->segments = calloc(conv->segCount, sizeof(float*)); @@ -128,18 +130,16 @@ static struct convolver1 *convolver1_new(struct spa_fga_dsp *dsp, int block, con if (conv->segments[i] == NULL || conv->segmentsIr[i] == NULL) goto error; - spa_fga_dsp_copy(dsp, conv->fft_buffer, &ir[i * conv->blockSize], copy); + spa_fga_dsp_copy(dsp, conv->fft_buffer[0], &ir[i * conv->blockSize], copy); if (copy < conv->segSize) - spa_fga_dsp_fft_memclear(dsp, conv->fft_buffer + copy, conv->segSize - copy, true); + spa_fga_dsp_fft_memclear(dsp, conv->fft_buffer[0] + copy, conv->segSize - copy, true); - spa_fga_dsp_fft_run(dsp, conv->fft, 1, conv->fft_buffer, conv->segmentsIr[i]); + spa_fga_dsp_fft_run(dsp, conv->fft, 1, conv->fft_buffer[0], conv->segmentsIr[i]); } conv->pre_mult = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false); conv->conv = spa_fga_dsp_fft_memalloc(dsp, conv->fftComplexSize, false); - conv->overlap = spa_fga_dsp_fft_memalloc(dsp, conv->blockSize, true); conv->inputBuffer = spa_fga_dsp_fft_memalloc(dsp, conv->segSize, true); - if (conv->pre_mult == NULL || conv->conv == NULL || conv->overlap == NULL || - conv->inputBuffer == NULL) + if (conv->pre_mult == NULL || conv->conv == NULL || conv->inputBuffer == NULL) goto error; conv->scale = 1.0f / conv->segSize; convolver1_reset(dsp, conv); @@ -159,18 +159,18 @@ static int convolver1_run(struct spa_fga_dsp *dsp, struct convolver1 *conv, cons return len; } + int inputBufferFill = conv->inputBufferFill; while (processed < len) { - const int processing = SPA_MIN(len - processed, conv->blockSize - conv->inputBufferFill); - const int inputBufferPos = conv->inputBufferFill; - - spa_fga_dsp_copy(dsp, conv->inputBuffer + inputBufferPos, input + processed, processing); - if (inputBufferPos == 0 && processing < conv->blockSize) - spa_fga_dsp_fft_memclear(dsp, conv->inputBuffer + processing, conv->blockSize - processing, true); + const int processing = SPA_MIN(len - processed, conv->blockSize - inputBufferFill); + spa_fga_dsp_copy(dsp, conv->inputBuffer + inputBufferFill, input + processed, processing); + if (inputBufferFill == 0 && processing < conv->blockSize) + spa_fga_dsp_fft_memclear(dsp, conv->inputBuffer + processing, + conv->blockSize - processing, true); spa_fga_dsp_fft_run(dsp, conv->fft, 1, conv->inputBuffer, conv->segments[conv->current]); if (conv->segCount > 1) { - if (conv->inputBufferFill == 0) { + if (inputBufferFill == 0) { int indexAudio = (conv->current + 1) % conv->segCount; spa_fga_dsp_fft_cmul(dsp, conv->fft, conv->pre_mult, @@ -203,22 +203,23 @@ static int convolver1_run(struct spa_fga_dsp *dsp, struct convolver1 *conv, cons conv->fftComplexSize, conv->scale); } - spa_fga_dsp_fft_run(dsp, conv->ifft, -1, conv->conv, conv->fft_buffer); + spa_fga_dsp_fft_run(dsp, conv->ifft, -1, conv->conv, conv->fft_buffer[0]); - spa_fga_dsp_sum(dsp, output + processed, conv->fft_buffer + inputBufferPos, - conv->overlap + inputBufferPos, processing); + spa_fga_dsp_sum(dsp, output + processed, conv->fft_buffer[0] + inputBufferFill, + conv->fft_buffer[1] + conv->blockSize + inputBufferFill, processing); - conv->inputBufferFill += processing; - if (conv->inputBufferFill == conv->blockSize) { - conv->inputBufferFill = 0; + inputBufferFill += processing; + if (inputBufferFill == conv->blockSize) { + inputBufferFill = 0; - spa_fga_dsp_copy(dsp, conv->overlap, conv->fft_buffer + conv->blockSize, conv->blockSize); + SPA_SWAP(conv->fft_buffer[0], conv->fft_buffer[1]); conv->current = (conv->current > 0) ? (conv->current - 1) : (conv->segCount - 1); } processed += processing; } + conv->inputBufferFill = inputBufferFill; return len; } @@ -236,7 +237,6 @@ struct convolver float *tailPrecalculated; float *tailInput; int tailInputFill; - int precalculatedPos; }; void convolver_reset(struct convolver *conv) @@ -256,7 +256,6 @@ void convolver_reset(struct convolver *conv) spa_fga_dsp_fft_memclear(dsp, conv->tailPrecalculated, conv->tailBlockSize, true); } conv->tailInputFill = 0; - conv->precalculatedPos = 0; } struct convolver *convolver_new(struct spa_fga_dsp *dsp, int head_block, int tail_block, const float *ir, int irlen) @@ -358,13 +357,12 @@ int convolver_run(struct convolver *conv, const float *input, float *output, int if (conv->tailPrecalculated0) spa_fga_dsp_sum(dsp, &output[processed], &output[processed], - &conv->tailPrecalculated0[conv->precalculatedPos], + &conv->tailPrecalculated0[conv->tailInputFill], processing); if (conv->tailPrecalculated) spa_fga_dsp_sum(dsp, &output[processed], &output[processed], - &conv->tailPrecalculated[conv->precalculatedPos], + &conv->tailPrecalculated[conv->tailInputFill], processing); - conv->precalculatedPos += processing; spa_fga_dsp_copy(dsp, conv->tailInput + conv->tailInputFill, input + processed, processing); conv->tailInputFill += processing; @@ -385,10 +383,9 @@ int convolver_run(struct convolver *conv, const float *input, float *output, int convolver1_run(dsp, conv->tailConvolver, conv->tailInput, conv->tailOutput, conv->tailBlockSize); } - if (conv->tailInputFill == conv->tailBlockSize) { + if (conv->tailInputFill == conv->tailBlockSize) conv->tailInputFill = 0; - conv->precalculatedPos = 0; - } + processed += processing; } } diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c index bc9ff154a..9271ace34 100644 --- a/spa/plugins/filter-graph/filter-graph.c +++ b/spa/plugins/filter-graph/filter-graph.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -13,8 +15,6 @@ #include #include -#include "config.h" - #include #include #include @@ -39,6 +39,7 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.filter-graph"); #define MAX_HNDL 64 +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define DEFAULT_RATE 48000 @@ -67,7 +68,7 @@ struct descriptor { struct spa_list link; int ref; struct plugin *plugin; - char label[256]; + char *label; const struct spa_fga_descriptor *desc; @@ -116,9 +117,16 @@ struct node { void *hndl[MAX_HNDL]; unsigned int n_deps; - unsigned int visited:1; + uint32_t latency_index; + + float min_latency; + float max_latency; + unsigned int disabled:1; unsigned int control_changed:1; + + unsigned int n_sort_deps; + unsigned int sorted:1; }; struct link { @@ -135,6 +143,7 @@ struct graph_port { const struct spa_fga_descriptor *desc; void **hndl; uint32_t port; + struct node *node; unsigned next:1; }; @@ -146,20 +155,21 @@ struct graph_hndl { struct volume { bool mute; uint32_t n_volumes; - float volumes[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; uint32_t n_ports; - struct port *ports[SPA_AUDIO_MAX_CHANNELS]; - float min[SPA_AUDIO_MAX_CHANNELS]; - float max[SPA_AUDIO_MAX_CHANNELS]; + struct port *ports[MAX_CHANNELS]; + float min[MAX_CHANNELS]; + float max[MAX_CHANNELS]; #define SCALE_LINEAR 0 #define SCALE_CUBIC 1 - int scale[SPA_AUDIO_MAX_CHANNELS]; + int scale[MAX_CHANNELS]; }; struct graph { struct impl *impl; + uint32_t n_nodes; struct spa_list node_list; struct spa_list link_list; @@ -175,9 +185,26 @@ struct graph { uint32_t n_control; struct port **control_port; + uint32_t n_input_names; + char **input_names; + + uint32_t n_output_names; + char **output_names; + struct volume volume[2]; + uint32_t n_inputs; + uint32_t n_outputs; + uint32_t inputs_position[MAX_CHANNELS]; + uint32_t n_inputs_position; + uint32_t outputs_position[MAX_CHANNELS]; + uint32_t n_outputs_position; + + float min_latency; + float max_latency; + unsigned activated:1; + unsigned setup:1; }; struct impl { @@ -205,14 +232,57 @@ struct impl { float *discard_data; }; +static inline void print_channels(char *buffer, size_t max_size, uint32_t n_positions, uint32_t *positions) +{ + uint32_t i; + struct spa_strbuf buf; + char pos[8]; + + spa_strbuf_init(&buf, buffer, max_size); + spa_strbuf_append(&buf, "["); + for (i = 0; i < n_positions; i++) { + spa_strbuf_append(&buf, "%s%s", i ? "," : "", + spa_type_audio_channel_make_short_name(positions[i], + pos, sizeof(pos), "UNK")); + } + spa_strbuf_append(&buf, "]"); +} + static void emit_filter_graph_info(struct impl *impl, bool full) { uint64_t old = full ? impl->info.change_mask : 0; + struct graph *graph = &impl->graph; if (full) impl->info.change_mask = impl->info_all; if (impl->info.change_mask || full) { + char n_inputs[64], n_outputs[64], latency[64]; + struct spa_dict_item items[6]; + struct spa_dict dict = SPA_DICT(items, 0); + char in_pos[MAX_CHANNELS * 8]; + char out_pos[MAX_CHANNELS * 8]; + + snprintf(n_inputs, sizeof(n_inputs), "%d", impl->graph.n_inputs); + snprintf(n_outputs, sizeof(n_outputs), "%d", impl->graph.n_outputs); + + items[dict.n_items++] = SPA_DICT_ITEM("n_inputs", n_inputs); + items[dict.n_items++] = SPA_DICT_ITEM("n_outputs", n_outputs); + if (graph->n_inputs_position) { + print_channels(in_pos, sizeof(in_pos), + graph->n_inputs_position, graph->inputs_position); + items[dict.n_items++] = SPA_DICT_ITEM("inputs.audio.position", in_pos); + } + if (graph->n_outputs_position) { + print_channels(out_pos, sizeof(out_pos), + graph->n_outputs_position, graph->outputs_position); + items[dict.n_items++] = SPA_DICT_ITEM("outputs.audio.position", out_pos); + } + items[dict.n_items++] = SPA_DICT_ITEM("latency", + spa_dtoa(latency, sizeof(latency), + (graph->min_latency + graph->max_latency) / 2.0f)); + impl->info.props = &dict; spa_filter_graph_emit_info(&impl->hooks, &impl->info); + impl->info.props = NULL; impl->info.change_mask = old; } } @@ -243,7 +313,7 @@ static int impl_process(void *object, uint32_t i, j, n_hndl = graph->n_hndl; struct graph_port *port; - for (i = 0, j = 0; i < impl->info.n_inputs; i++) { + for (i = 0, j = 0; i < graph->n_inputs; i++) { while (j < graph->n_input) { port = &graph->input[j++]; if (port->desc && in[i]) @@ -252,13 +322,12 @@ static int impl_process(void *object, break; } } - for (i = 0; i < impl->info.n_outputs; i++) { + for (i = 0; i < graph->n_outputs; i++) { if (out[i] == NULL) continue; - port = i < graph->n_output ? &graph->output[i] : NULL; - - if (port && port->desc) + port = &graph->output[i]; + if (port->desc) port->desc->connect_port(*port->hndl, port->port, out[i]); else memset(out[i], 0, n_samples * sizeof(float)); @@ -285,6 +354,15 @@ static struct node *find_node(struct graph *graph, const char *name) } return NULL; } +#if !defined(strdupa) +# define strdupa(s) \ + ({ \ + const char *__old = (s); \ + size_t __len = strlen(__old) + 1; \ + char *__new = (char *) alloca(__len); \ + (char *) memcpy(__new, __old, __len); \ + }) +#endif /* find a port by name. Valid syntax is: * ":" @@ -538,11 +616,12 @@ static int parse_params(struct graph *graph, const struct spa_pod *pod) return 0; while (true) { - const char *name; + const char *name, *str_val; float value, *val = NULL; double dbl_val; bool bool_val; int32_t int_val; + int64_t long_val; if (spa_pod_parser_get_string(&prs, &name) < 0) break; @@ -554,9 +633,15 @@ static int parse_params(struct graph *graph, const struct spa_pod *pod) } else if (spa_pod_parser_get_int(&prs, &int_val) >= 0) { value = int_val; val = &value; + } else if (spa_pod_parser_get_long(&prs, &long_val) >= 0) { + value = long_val; + val = &value; } else if (spa_pod_parser_get_bool(&prs, &bool_val) >= 0) { value = bool_val ? 1.0f : 0.0f; val = &value; + } else if (spa_pod_parser_get_string(&prs, &str_val) >= 0 && + spa_json_parse_float(str_val, strlen(str_val), &value) >= 0) { + val = &value; } else { struct spa_pod *pod; spa_pod_parser_get_pod(&prs, &pod); @@ -663,10 +748,10 @@ static int impl_set_props(void *object, enum spa_direction direction, const stru case SPA_PROP_channelVolumes: { uint32_t i, n_vols; - float vols[SPA_AUDIO_MAX_CHANNELS]; + float vols[MAX_CHANNELS]; if ((n_vols = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, vols, - SPA_AUDIO_MAX_CHANNELS)) > 0) { + SPA_N_ELEMENTS(vols))) > 0) { if (vol->n_volumes != n_vols) do_volume = true; vol->n_volumes = n_vols; @@ -690,7 +775,7 @@ static int impl_set_props(void *object, enum spa_direction direction, const stru } } if (do_volume && vol->n_ports != 0) { - float soft_vols[SPA_AUDIO_MAX_CHANNELS]; + float soft_vols[MAX_CHANNELS]; uint32_t i; for (i = 0; i < vol->n_volumes; i++) @@ -836,6 +921,7 @@ static void descriptor_unref(struct descriptor *desc) if (desc->desc) spa_fga_descriptor_free(desc->desc); plugin_unref(desc->plugin); + free(desc->label); free(desc->input); free(desc->output); free(desc->control); @@ -879,12 +965,12 @@ static struct descriptor *descriptor_load(struct impl *impl, const char *type, spa_list_init(&desc->link); if ((d = spa_fga_plugin_make_desc(pl->plugin, label)) == NULL) { - spa_log_error(impl->log, "cannot find label %s", label); + spa_log_error(impl->log, "cannot create label %s", label); res = -ENOENT; goto exit; } desc->desc = d; - snprintf(desc->label, sizeof(desc->label), "%s", label); + desc->label = strdup(label); n_input = n_output = n_control = n_notify = 0; for (p = 0; p < d->n_ports; p++) { @@ -960,38 +1046,36 @@ exit: * ... * } */ -static int parse_config(struct node *node, struct spa_json *config) +static char *copy_value(struct impl *impl, struct spa_json *value) { - const char *val, *s = config->cur; - struct impl *impl = node->graph->impl; - int res = 0, len; + const char *val, *s = value->cur; + int len; struct spa_error_location loc; + char *result = NULL; - if ((len = spa_json_next(config, &val)) <= 0) { - res = -EINVAL; + if ((len = spa_json_next(value, &val)) <= 0) { + errno = EINVAL; goto done; } if (spa_json_is_null(val, len)) goto done; if (spa_json_is_container(val, len)) { - len = spa_json_container_len(config, val, len); + len = spa_json_container_len(value, val, len); if (len == 0) { - res = -EINVAL; + errno = EINVAL; goto done; } } - if ((node->config = malloc(len+1)) == NULL) { - res = -errno; + if ((result = malloc(len+1)) == NULL) goto done; - } - spa_json_parse_stringn(val, len, node->config, len+1); + spa_json_parse_stringn(val, len, result, len+1); done: - if (spa_json_get_error(config, s, &loc)) + if (spa_json_get_error(value, s, &loc)) spa_debug_log_error_location(impl->log, SPA_LOG_LEVEL_WARN, &loc, "error: %s", loc.reason); - return res; + return result; } /** @@ -1066,7 +1150,7 @@ static int parse_link(struct graph *graph, struct spa_json *json) out_port = find_port(def_out_node, output, SPA_FGA_PORT_OUTPUT); in_port = find_port(def_in_node, input, SPA_FGA_PORT_INPUT); - if (out_port == NULL && out_port == NULL) { + if (out_port == NULL && in_port == NULL) { /* try control ports */ out_port = find_port(def_out_node, output, SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL); in_port = find_port(def_in_node, input, SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL); @@ -1183,7 +1267,7 @@ static int parse_volume(struct graph *graph, struct spa_json *json, enum spa_dir spa_log_error(impl->log, "unknown control port %s", control); return -ENOENT; } - if (vol->n_ports >= SPA_AUDIO_MAX_CHANNELS) { + if (vol->n_ports >= MAX_CHANNELS) { spa_log_error(impl->log, "too many volume controls"); return -ENOSPC; } @@ -1221,7 +1305,7 @@ static int parse_volume(struct graph *graph, struct spa_json *json, enum spa_dir static int load_node(struct graph *graph, struct spa_json *json) { struct impl *impl = graph->impl; - struct spa_json control, config; + struct spa_json control, it; struct descriptor *desc; struct node *node; const char *val; @@ -1229,11 +1313,11 @@ static int load_node(struct graph *graph, struct spa_json *json) char type[256] = ""; char name[256] = ""; char plugin[256] = ""; - char label[256] = ""; + spa_autofree char *label = NULL; + spa_autofree char *config = NULL; bool have_control = false; - bool have_config = false; uint32_t i; - int res, len; + int len; while ((len = spa_json_object_next(json, key, sizeof(key), &val)) > 0) { if (spa_streq("type", key)) { @@ -1252,8 +1336,9 @@ static int load_node(struct graph *graph, struct spa_json *json) return -EINVAL; } } else if (spa_streq("label", key)) { - if (spa_json_parse_stringn(val, len, label, sizeof(label)) <= 0) { - spa_log_error(impl->log, "label expects a string"); + it = SPA_JSON_START(json, val); + if ((label = copy_value(impl, &it)) == NULL) { + spa_log_warn(impl->log, "error parsing label: %s", spa_strerror(-errno)); return -EINVAL; } } else if (spa_streq("control", key)) { @@ -1264,8 +1349,9 @@ static int load_node(struct graph *graph, struct spa_json *json) spa_json_enter(json, &control); have_control = true; } else if (spa_streq("config", key)) { - config = SPA_JSON_START(json, val); - have_config = true; + it = SPA_JSON_START(json, val); + if ((config = copy_value(impl, &it)) == NULL) + spa_log_warn(impl->log, "error parsing config: %s", spa_strerror(-errno)); } else { spa_log_warn(impl->log, "unexpected node key '%s'", key); } @@ -1279,7 +1365,7 @@ static int load_node(struct graph *graph, struct spa_json *json) spa_log_info(impl->log, "loading type:%s plugin:%s label:%s", type, plugin, label); - if ((desc = descriptor_load(graph->impl, type, plugin, label)) == NULL) + if ((desc = descriptor_load(graph->impl, type, plugin, label ? label : "")) == NULL) return -errno; node = calloc(1, sizeof(*node)); @@ -1289,6 +1375,8 @@ static int load_node(struct graph *graph, struct spa_json *json) node->graph = graph; node->desc = desc; snprintf(node->name, sizeof(node->name), "%s", name); + node->latency_index = SPA_IDX_INVALID; + node->config = spa_steal_ptr(config); node->input_port = calloc(desc->n_input, sizeof(struct port)); node->output_port = calloc(desc->n_output, sizeof(struct port)); @@ -1330,15 +1418,16 @@ static int load_node(struct graph *graph, struct spa_json *json) port->idx = i; port->external = SPA_ID_INVALID; port->p = desc->notify[i]; + if (desc->desc->ports[port->p].hint & SPA_FGA_HINT_LATENCY) + node->latency_index = i; spa_list_init(&port->link_list); } - if (have_config) - if ((res = parse_config(node, &config)) < 0) - spa_log_warn(impl->log, "error parsing config: %s", spa_strerror(res)); if (have_control) parse_control(node, &control); spa_list_append(&graph->node_list, &node->link); + graph->n_nodes++; + graph->n_control += desc->n_control; return 0; } @@ -1423,6 +1512,38 @@ static int impl_deactivate(void *object) return 0; } +static void sort_reset(struct graph *graph) +{ + struct node *node; + spa_list_for_each(node, &graph->node_list, link) { + node->sorted = false; + node->n_sort_deps = node->n_deps; + } +} +static struct node *sort_next_node(struct graph *graph) +{ + struct node *node; + spa_list_for_each(node, &graph->node_list, link) { + if (node->n_sort_deps == 0 && !node->sorted) { + uint32_t i; + struct link *link; + node->sorted = true; + for (i = 0; i < node->desc->n_output; i++) { + spa_list_for_each(link, &node->output_port[i].link_list, output_link) + link->input->node->n_sort_deps--; + } + for (i = 0; i < node->desc->n_notify; i++) { + spa_list_for_each(link, &node->notify_port[i].link_list, output_link) + link->input->node->n_sort_deps--; + } + return node; + } + } + return NULL; +} + +static int setup_graph(struct graph *graph); + static int impl_activate(void *object, const struct spa_dict *props) { struct impl *impl = object; @@ -1433,10 +1554,10 @@ static int impl_activate(void *object, const struct spa_dict *props) struct descriptor *desc; const struct spa_fga_descriptor *d; const struct spa_fga_plugin *p; - uint32_t i, j, max_samples = impl->quantum_limit; + uint32_t i, j, max_samples = impl->quantum_limit, n_ports; int res; - float *sd, *dd, *data; - const char *rate; + float *sd, *dd, *data, min_latency, max_latency; + const char *rate, *str; if (graph->activated) return 0; @@ -1446,6 +1567,30 @@ static int impl_activate(void *object, const struct spa_dict *props) rate = spa_dict_lookup(props, SPA_KEY_AUDIO_RATE); impl->rate = rate ? atoi(rate) : DEFAULT_RATE; + if ((str = spa_dict_lookup(props, "filter-graph.n_inputs")) != NULL) { + if (spa_atou32(str, &n_ports, 0) && + n_ports != graph->n_inputs) { + graph->n_inputs = n_ports; + graph->n_outputs = 0; + impl->info.change_mask |= SPA_FILTER_GRAPH_CHANGE_MASK_PROPS; + graph->setup = false; + } + } + if ((str = spa_dict_lookup(props, "filter-graph.n_outputs")) != NULL) { + if (spa_atou32(str, &n_ports, 0) && + n_ports != graph->n_outputs) { + graph->n_outputs = n_ports; + graph->n_inputs = 0; + impl->info.change_mask |= SPA_FILTER_GRAPH_CHANGE_MASK_PROPS; + graph->setup = false; + } + } + if (!graph->setup) { + if ((res = setup_graph(graph)) < 0) + return res; + graph->setup = true; + } + /* first make instances */ spa_list_for_each(node, &graph->node_list, link) { node_cleanup(node); @@ -1484,6 +1629,8 @@ static int impl_activate(void *object, const struct spa_dict *props) if ((res = port_ensure_data(link->output, i, max_samples)) < 0) goto error; data = link->output->audio_data[i]; + } else if (SPA_FGA_SUPPORTS_NULL_DATA(d->ports[port->p].flags)) { + data = NULL; } else { data = sd; } @@ -1494,9 +1641,13 @@ static int impl_activate(void *object, const struct spa_dict *props) for (j = 0; j < desc->n_output; j++) { port = &node->output_port[j]; if (port->audio_data[i] == NULL) { + if (SPA_FGA_SUPPORTS_NULL_DATA(d->ports[port->p].flags)) + data = NULL; + else + data = dd; spa_log_info(impl->log, "connect output port %s[%d]:%s %p", - node->name, i, d->ports[port->p].name, dd); - d->connect_port(node->hndl[i], port->p, dd); + node->name, i, d->ports[port->p].name, data); + d->connect_port(node->hndl[i], port->p, data); } } for (j = 0; j < desc->n_control; j++) { @@ -1534,7 +1685,66 @@ static int impl_activate(void *object, const struct spa_dict *props) d->control_changed(node->hndl[i]); } } + /* calculate latency */ + sort_reset(graph); + while ((node = sort_next_node(graph)) != NULL) { + min_latency = FLT_MAX; + max_latency = 0.0f; + for (i = 0; i < node->desc->n_input; i++) { + spa_list_for_each(link, &node->input_port[i].link_list, input_link) { + min_latency = fminf(min_latency, link->output->node->min_latency); + max_latency = fmaxf(max_latency, link->output->node->max_latency); + } + } + min_latency = min_latency == FLT_MAX ? 0.0f : min_latency; + + if (node->latency_index != SPA_IDX_INVALID) { + port = &node->notify_port[node->latency_index]; + min_latency += port->control_data[0]; + max_latency += port->control_data[0]; + + } + node->min_latency = min_latency; + node->max_latency = max_latency; + spa_log_info(impl->log, "%s latency:%f-%f", node->name, min_latency, max_latency); + } + min_latency = FLT_MAX; + max_latency = 0.0f; + for (i = 0; i < graph->n_outputs; i++) { + struct graph_port *port = &graph->output[i]; + /* ports with no descriptor are ignored */ + if (port->desc == NULL) + continue; + max_latency = fmaxf(max_latency, port->node->max_latency); + min_latency = fminf(min_latency, port->node->min_latency); + } + min_latency = min_latency == FLT_MAX ? 0.0f : min_latency; + + spa_log_info(impl->log, "graph latency min:%f max:%f", min_latency, max_latency); + if (min_latency != max_latency) { + spa_log_warn(impl->log, "graph has unaligned latency min:%f max:%f, " + "consider adding delays or tweak node latency to " + "align the signals", min_latency, max_latency); + for (i = 0; i < graph->n_outputs; i++) { + struct graph_port *port = &graph->output[i]; + /* port with no descriptor are ignored */ + if (port->desc == NULL) + continue; + if (min_latency != port->node->min_latency || + max_latency != port->node->max_latency) + spa_log_warn(impl->log, "output port %d from %s min:%f max:%f", + i, port->node->name, + port->node->min_latency, port->node->max_latency); + } + + } + if (graph->min_latency != min_latency || graph->max_latency != max_latency) { + graph->min_latency = min_latency; + graph->max_latency = max_latency; + impl->info.change_mask |= SPA_FILTER_GRAPH_CHANGE_MASK_PROPS; + } + emit_filter_graph_info(impl, false); spa_filter_graph_emit_props_changed(&impl->hooks, SPA_DIRECTION_INPUT); return 0; error: @@ -1542,69 +1752,67 @@ error: return res; } -/* any default values for the controls are set in the first instance - * of the control data. Duplicate this to the other instances now. */ -static void setup_node_controls(struct node *node) -{ - uint32_t i, j; - uint32_t n_hndl = node->n_hndl; - uint32_t n_ports = node->desc->n_control; - struct port *ports = node->control_port; - - for (i = 0; i < n_ports; i++) { - struct port *port = &ports[i]; - for (j = 1; j < n_hndl; j++) - port->control_data[j] = port->control_data[0]; - } -} - -static struct node *find_next_node(struct graph *graph) +static void unsetup_graph(struct graph *graph) { struct node *node; + uint32_t i; + + free(graph->input); + graph->input = NULL; + free(graph->output); + graph->output = NULL; + free(graph->hndl); + graph->hndl = NULL; + spa_list_for_each(node, &graph->node_list, link) { - if (node->n_deps == 0 && !node->visited) { - node->visited = true; - return node; + struct descriptor *desc = node->desc; + for (i = 0; i < desc->n_input; i++) { + struct port *port = &node->input_port[i]; + port->external = SPA_ID_INVALID; + } + for (i = 0; i < desc->n_output; i++) { + struct port *port = &node->output_port[i]; + port->external = SPA_ID_INVALID; } } - return NULL; } -static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_json *outputs) +static int setup_graph(struct graph *graph) { struct impl *impl = graph->impl; struct node *node, *first, *last; struct port *port; - struct link *link; struct graph_port *gp; struct graph_hndl *gh; - uint32_t i, j, n_nodes, n_input, n_output, n_control, n_hndl = 0; + uint32_t i, j, n, n_input, n_output, n_hndl = 0; int res; struct descriptor *desc; const struct spa_fga_descriptor *d; - char v[256]; + char *pname; bool allow_unused; + unsetup_graph(graph); + first = spa_list_first(&graph->node_list, struct node, link); last = spa_list_last(&graph->node_list, struct node, link); /* calculate the number of inputs and outputs into the graph. - * If we have a list of inputs/outputs, just count them. Otherwise + * If we have a list of inputs/outputs, just use them. Otherwise * we count all input ports of the first node and all output * ports of the last node */ - if (inputs != NULL) - n_input = count_array(inputs); + if (graph->n_input_names != 0) + n_input = graph->n_input_names; else n_input = first->desc->n_input; - if (outputs != NULL) - n_output = count_array(outputs); + if (graph->n_output_names != 0) + n_output = graph->n_output_names; else n_output = last->desc->n_output; /* we allow unconnected ports when not explicitly given and the nodes support * NULL data */ - allow_unused = inputs == NULL && outputs == NULL && + allow_unused = graph->n_input_names == 0 && graph->n_output_names == 0 && SPA_FLAG_IS_SET(first->desc->desc->flags, SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA) && SPA_FLAG_IS_SET(last->desc->desc->flags, SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA); @@ -1618,24 +1826,28 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ res = -EINVAL; goto error; } + if (graph->n_inputs == 0) + graph->n_inputs = impl->info.n_inputs; + if (graph->n_inputs == 0) + graph->n_inputs = n_input; - if (impl->info.n_inputs == 0) - impl->info.n_inputs = n_input; + if (graph->n_outputs == 0) + graph->n_outputs = impl->info.n_outputs; /* compare to the requested number of inputs and duplicate the * graph n_hndl times when needed. */ - n_hndl = impl->info.n_inputs / n_input; + n_hndl = graph->n_inputs / n_input; - if (impl->info.n_outputs == 0) - impl->info.n_outputs = n_output * n_hndl; + if (graph->n_outputs == 0) + graph->n_outputs = n_output * n_hndl; - if (n_hndl != impl->info.n_outputs / n_output) { + if (n_hndl != graph->n_outputs / n_output) { spa_log_error(impl->log, "invalid ports. The input stream has %1$d ports and " "the filter has %2$d inputs. The output stream has %3$d ports " "and the filter has %4$d outputs. input:%1$d / input:%2$d != " "output:%3$d / output:%4$d. Check inputs and outputs objects.", - impl->info.n_inputs, n_input, - impl->info.n_outputs, n_output); + graph->n_inputs, n_input, + graph->n_outputs, n_output); res = -EINVAL; goto error; } @@ -1651,24 +1863,14 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ "the filter has %2$d inputs. The output stream has %3$d ports " "and the filter has %4$d outputs. Some filter ports will be " "unconnected..", - impl->info.n_inputs, n_input, - impl->info.n_outputs, n_output); + graph->n_inputs, n_input, + graph->n_outputs, n_output); - if (impl->info.n_outputs == 0) - impl->info.n_outputs = n_output * n_hndl; + if (graph->n_outputs == 0) + graph->n_outputs = n_output * n_hndl; } spa_log_info(impl->log, "using %d instances %d %d", n_hndl, n_input, n_output); - /* now go over all nodes and create instances. */ - n_control = 0; - n_nodes = 0; - spa_list_for_each(node, &graph->node_list, link) { - node->n_hndl = n_hndl; - desc = node->desc; - n_control += desc->n_control; - n_nodes++; - setup_node_controls(node); - } graph->n_input = 0; graph->input = calloc(n_input * 16 * n_hndl, sizeof(struct graph_port)); graph->n_output = 0; @@ -1676,7 +1878,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ /* now collect all input and output ports for all the handles. */ for (i = 0; i < n_hndl; i++) { - if (inputs == NULL) { + if (graph->n_input_names == 0) { desc = first->desc; d = desc->desc; for (j = 0; j < desc->n_input; j++) { @@ -1684,19 +1886,20 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ spa_log_info(impl->log, "input port %s[%d]:%s", first->name, i, d->ports[desc->input[j]].name); gp->desc = d; + gp->node = first; gp->hndl = &first->hndl[i]; gp->port = desc->input[j]; } } else { - struct spa_json it = *inputs; - while (spa_json_get_string(&it, v, sizeof(v)) > 0) { - if (spa_streq(v, "null")) { + for (n = 0; n < graph->n_input_names; n++) { + pname = graph->input_names[n]; + if (spa_streq(pname, "null")) { gp = &graph->input[graph->n_input++]; gp->desc = NULL; spa_log_info(impl->log, "ignore input port %d", graph->n_input); - } else if ((port = find_port(first, v, SPA_FGA_PORT_INPUT)) == NULL) { + } else if ((port = find_port(first, pname, SPA_FGA_PORT_INPUT)) == NULL) { res = -ENOENT; - spa_log_error(impl->log, "input port %s not found", v); + spa_log_error(impl->log, "input port %s not found", pname); goto error; } else { bool disabled = false; @@ -1732,6 +1935,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ peer->external = graph->n_input; gp = &graph->input[graph->n_input++]; gp->desc = peer->node->desc->desc; + gp->node = peer->node; gp->hndl = &peer->node->hndl[i]; gp->port = peer->p; gp->next = true; @@ -1748,6 +1952,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ port->external = graph->n_input; gp = &graph->input[graph->n_input++]; gp->desc = d; + gp->node = port->node; gp->hndl = &port->node->hndl[i]; gp->port = port->p; gp->next = false; @@ -1755,7 +1960,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ } } } - if (outputs == NULL) { + if (graph->n_output_names == 0) { desc = last->desc; d = desc->desc; for (j = 0; j < desc->n_output; j++) { @@ -1763,19 +1968,20 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ spa_log_info(impl->log, "output port %s[%d]:%s", last->name, i, d->ports[desc->output[j]].name); gp->desc = d; + gp->node = last; gp->hndl = &last->hndl[i]; gp->port = desc->output[j]; } } else { - struct spa_json it = *outputs; - while (spa_json_get_string(&it, v, sizeof(v)) > 0) { + for (n = 0; n < graph->n_output_names; n++) { + pname = graph->output_names[n]; gp = &graph->output[graph->n_output]; - if (spa_streq(v, "null")) { + if (spa_streq(pname, "null")) { gp->desc = NULL; spa_log_info(impl->log, "silence output port %d", graph->n_output); - } else if ((port = find_port(last, v, SPA_FGA_PORT_OUTPUT)) == NULL) { + } else if ((port = find_port(last, pname, SPA_FGA_PORT_OUTPUT)) == NULL) { res = -ENOENT; - spa_log_error(impl->log, "output port %s not found", v); + spa_log_error(impl->log, "output port %s not found", pname); goto error; } else { desc = port->node->desc; @@ -1797,6 +2003,7 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ port->node->name, i, d->ports[port->p].name); port->external = graph->n_output; gp->desc = d; + gp->node = port->node; gp->hndl = &port->node->hndl[i]; gp->port = port->p; } @@ -1805,15 +2012,12 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ } } - /* order all nodes based on dependencies */ graph->n_hndl = 0; - graph->hndl = calloc(n_nodes * n_hndl, sizeof(struct graph_hndl)); - graph->n_control = 0; - graph->control_port = calloc(n_control, sizeof(struct port *)); - while (true) { - if ((node = find_next_node(graph)) == NULL) - break; - + graph->hndl = calloc(graph->n_nodes * n_hndl, sizeof(struct graph_hndl)); + /* order all nodes based on dependencies, first reset fields */ + sort_reset(graph); + while ((node = sort_next_node(graph)) != NULL) { + node->n_hndl = n_hndl; desc = node->desc; d = desc->desc; @@ -1824,19 +2028,12 @@ static int setup_graph(struct graph *graph, struct spa_json *inputs, struct spa_ gh->desc = d; } } - for (i = 0; i < desc->n_output; i++) { - spa_list_for_each(link, &node->output_port[i].link_list, output_link) - link->input->node->n_deps--; - } - for (i = 0; i < desc->n_notify; i++) { - spa_list_for_each(link, &node->notify_port[i].link_list, output_link) - link->input->node->n_deps--; - } - - /* collect all control ports on the graph */ for (i = 0; i < desc->n_control; i++) { - graph->control_port[graph->n_control] = &node->control_port[i]; - graph->n_control++; + /* any default values for the controls are set in the first instance + * of the control data. Duplicate this to the other instances now. */ + struct port *port = &node->control_port[i]; + for (j = 1; j < n_hndl; j++) + port->control_data[j] = port->control_data[0]; } } res = 0; @@ -1844,6 +2041,23 @@ error: return res; } +static int setup_graph_controls(struct graph *graph) +{ + struct node *node; + uint32_t i, n_control = 0; + + graph->control_port = calloc(graph->n_control, sizeof(struct port *)); + if (graph->control_port == NULL) + return -errno; + + spa_list_for_each(node, &graph->node_list, link) { + /* collect all control ports on the graph */ + for (i = 0; i < node->desc->n_control; i++) + graph->control_port[n_control++] = &node->control_port[i]; + } + return 0; +} + /** * filter.graph = { * nodes = [ @@ -1901,6 +2115,28 @@ static int load_graph(struct graph *graph, const struct spa_dict *props) } impl->info.n_outputs = res; } + else if (spa_streq("inputs.audio.position", key)) { + if (!spa_json_is_array(val, len) || + (len = spa_json_container_len(&it[0], val, len)) < 0) { + spa_log_error(impl->log, "%s expects an array", key); + return -EINVAL; + } + spa_audio_parse_position_n(val, len, graph->inputs_position, + SPA_N_ELEMENTS(graph->inputs_position), + &graph->n_inputs_position); + impl->info.n_inputs = graph->n_inputs_position; + } + else if (spa_streq("outputs.audio.position", key)) { + if (!spa_json_is_array(val, len) || + (len = spa_json_container_len(&it[0], val, len)) < 0) { + spa_log_error(impl->log, "%s expects an array", key); + return -EINVAL; + } + spa_audio_parse_position_n(val, len, graph->outputs_position, + SPA_N_ELEMENTS(graph->outputs_position), + &graph->n_outputs_position); + impl->info.n_outputs = graph->n_outputs_position; + } else if (spa_streq("nodes", key)) { if (!spa_json_is_array(val, len)) { spa_log_error(impl->log, "%s expects an array", key); @@ -1980,21 +2216,45 @@ static int load_graph(struct graph *graph, const struct spa_dict *props) return res; } } - return setup_graph(graph, pinputs, poutputs); + if (pinputs != NULL) { + graph->n_input_names = count_array(pinputs); + graph->input_names = calloc(graph->n_input_names, sizeof(char *)); + graph->n_input_names = 0; + while (spa_json_get_string(pinputs, key, sizeof(key)) > 0) + graph->input_names[graph->n_input_names++] = strdup(key); + } + if (poutputs != NULL) { + graph->n_output_names = count_array(poutputs); + graph->output_names = calloc(graph->n_output_names, sizeof(char *)); + graph->n_output_names = 0; + while (spa_json_get_string(poutputs, key, sizeof(key)) > 0) + graph->output_names[graph->n_output_names++] = strdup(key); + } + if ((res = setup_graph_controls(graph)) < 0) + return res; + return 0; } static void graph_free(struct graph *graph) { struct link *link; struct node *node; + uint32_t i; + + unsetup_graph(graph); + spa_list_consume(link, &graph->link_list, link) link_free(link); spa_list_consume(node, &graph->node_list, link) node_free(node); - free(graph->input); - free(graph->output); - free(graph->hndl); + for (i = 0; i < graph->n_input_names; i++) + free(graph->input_names[i]); + free(graph->input_names); + for (i = 0; i < graph->n_output_names; i++) + free(graph->output_names[i]); + free(graph->output_names); free(graph->control_port); + graph->control_port = NULL; } static const struct spa_filter_graph_methods impl_filter_graph = { diff --git a/spa/plugins/filter-graph/meson.build b/spa/plugins/filter-graph/meson.build index c346a9649..1995ae4c1 100644 --- a/spa/plugins/filter-graph/meson.build +++ b/spa/plugins/filter-graph/meson.build @@ -67,7 +67,7 @@ filter_graph_dependencies = [ ] spa_filter_graph_plugin_builtin = shared_library('spa-filter-graph-plugin-builtin', - [ 'builtin_plugin.c', + [ 'plugin_builtin.c', 'convolver.c' ], include_directories : [configinc], install : true, @@ -77,7 +77,7 @@ spa_filter_graph_plugin_builtin = shared_library('spa-filter-graph-plugin-builti ) spa_filter_graph_plugin_ladspa = shared_library('spa-filter-graph-plugin-ladspa', - [ 'ladspa_plugin.c' ], + [ 'plugin_ladspa.c' ], include_directories : [configinc], install : true, install_dir : spa_plugindir / 'filter-graph', @@ -86,7 +86,7 @@ spa_filter_graph_plugin_ladspa = shared_library('spa-filter-graph-plugin-ladspa' if libmysofa_dep.found() spa_filter_graph_plugin_sofa = shared_library('spa-filter-graph-plugin-sofa', - [ 'sofa_plugin.c', + [ 'plugin_sofa.c', 'convolver.c' ], include_directories : [configinc], install : true, @@ -97,7 +97,7 @@ endif if lilv_lib.found() spa_filter_graph_plugin_lv2 = shared_library('spa-filter-graph-plugin-lv2', - [ 'lv2_plugin.c' ], + [ 'plugin_lv2.c' ], include_directories : [configinc], install : true, install_dir : spa_plugindir / 'filter-graph', @@ -107,7 +107,7 @@ endif if ebur128_lib.found() spa_filter_graph_plugin_ebur128 = shared_library('spa-filter-graph-plugin-ebur128', - [ 'ebur128_plugin.c' ], + [ 'plugin_ebur128.c' ], include_directories : [configinc], install : true, install_dir : spa_plugindir / 'filter-graph', @@ -115,3 +115,24 @@ spa_filter_graph_plugin_ebur128 = shared_library('spa-filter-graph-plugin-ebur12 ) endif +if avfilter_dep.found() +spa_filter_graph_plugin_ffmpeg = shared_library('spa-filter-graph-plugin-ffmpeg', + [ 'plugin_ffmpeg.c' ], + include_directories : [configinc], + install : true, + install_dir : spa_plugindir / 'filter-graph', + dependencies : [ filter_graph_dependencies, avfilter_dep, avutil_dep] +) +endif + +if onnxruntime_dep.found() +spa_filter_graph_plugin_onnx = shared_library('spa-filter-graph-plugin-onnx', + [ 'plugin_onnx.c' ], + include_directories : [configinc], + install : true, + install_dir : spa_plugindir / 'filter-graph', + dependencies : [ filter_graph_dependencies, onnxruntime_dep] +) +endif + + diff --git a/spa/plugins/filter-graph/builtin_plugin.c b/spa/plugins/filter-graph/plugin_builtin.c similarity index 80% rename from spa/plugins/filter-graph/builtin_plugin.c rename to spa/plugins/filter-graph/plugin_builtin.c index f50ffd770..647c9a51b 100644 --- a/spa/plugins/filter-graph/builtin_plugin.c +++ b/spa/plugins/filter-graph/plugin_builtin.c @@ -11,12 +11,17 @@ #endif #include #include +#include +#include +#include #include #include +#include #include #include #include +#include #include "audio-plugin.h" @@ -51,6 +56,13 @@ struct builtin { float b0, b1, b2; float a0, a1, a2; float accum; + + int mode; + uint32_t count; + float last; + + float gate; + float hold; }; static void *builtin_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, @@ -71,7 +83,7 @@ static void *builtin_instantiate(const struct spa_fga_plugin *plugin, const stru return impl; } -static void builtin_connect_port(void *Instance, unsigned long Port, float * DataLocation) +static void builtin_connect_port(void *Instance, unsigned long Port, void * DataLocation) { struct builtin *impl = Instance; impl->port[Port] = DataLocation; @@ -686,7 +698,8 @@ struct convolver_impl { struct spa_log *log; struct spa_fga_dsp *dsp; unsigned long rate; - float *port[2]; + float *port[3]; + float latency; struct convolver *conv; }; @@ -792,7 +805,7 @@ static float *create_hilbert(struct plugin *pl, const char *filename, float gain int delay = (int) (delay_sec * rate); if (length <= 0) - length = 1024; + length = 64; length -= SPA_MIN(offset, length); @@ -812,6 +825,7 @@ static float *create_hilbert(struct plugin *pl, const char *filename, float gain samples[delay + h - i] = v; } *n_samples = n; + spa_log_info(pl->log, "created hilbert function"); return samples; } @@ -830,6 +844,7 @@ static float *create_dirac(struct plugin *pl, const char *filename, float gain, samples[delay] = gain; + spa_log_info(pl->log, "created dirac function"); *n_samples = n; return samples; } @@ -906,7 +921,7 @@ error: #else spa_log_error(impl->log, "compiled without spa-plugins support, can't resample"); float *out_samples = calloc(*n_samples, sizeof(float)); - memcpy(out_samples, samples, *n_samples * sizeof(float)); + spa_memcpy(out_samples, samples, *n_samples * sizeof(float)); return out_samples; #endif } @@ -925,7 +940,7 @@ static void * convolver_instantiate(const struct spa_fga_plugin *plugin, const s char *filenames[MAX_RATES] = { 0 }; int blocksize = 0, tailsize = 0; int resample_quality = RESAMPLE_DEFAULT_QUALITY; - float gain = 1.0f, delay = 0.0f; + float gain = 1.0f, delay = 0.0f, latency = -1.0f; unsigned long rate; errno = EINVAL; @@ -1007,6 +1022,12 @@ static void * convolver_instantiate(const struct spa_fga_plugin *plugin, const s return NULL; } } + else if (spa_streq(key, "latency")) { + if (spa_json_parse_float(val, len, &latency) <= 0) { + spa_log_error(pl->log, "convolver:latency requires a number"); + return NULL; + } + } else { spa_log_warn(pl->log, "convolver: ignoring config key: '%s'", key); } @@ -1067,6 +1088,11 @@ static void * convolver_instantiate(const struct spa_fga_plugin *plugin, const s if (impl->conv == NULL) goto error; + if (latency < 0.0f) + impl->latency = n_samples; + else + impl->latency = latency * impl->rate; + free(samples); return impl; @@ -1077,7 +1103,7 @@ error: } static void convolver_connect_port(void * Instance, unsigned long Port, - float * DataLocation) + void * DataLocation) { struct convolver_impl *impl = Instance; impl->port[Port] = DataLocation; @@ -1100,8 +1126,20 @@ static struct spa_fga_port convolve_ports[] = { .name = "In", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, }, + { .index = 2, + .name = "latency", + .hint = SPA_FGA_HINT_LATENCY, + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, }; +static void convolver_activate(void * Instance) +{ + struct convolver_impl *impl = Instance; + if (impl->port[2] != NULL) + impl->port[2][0] = impl->latency; +} + static void convolver_deactivate(void * Instance) { struct convolver_impl *impl = Instance; @@ -1113,17 +1151,20 @@ static void convolve_run(void * Instance, unsigned long SampleCount) struct convolver_impl *impl = Instance; if (impl->port[1] != NULL && impl->port[0] != NULL) convolver_run(impl->conv, impl->port[1], impl->port[0], SampleCount); + if (impl->port[2] != NULL) + impl->port[2][0] = impl->latency; } static const struct spa_fga_descriptor convolve_desc = { .name = "convolver", .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, - .n_ports = 2, + .n_ports = SPA_N_ELEMENTS(convolve_ports), .ports = convolve_ports, .instantiate = convolver_instantiate, .connect_port = convolver_connect_port, + .activate = convolver_activate, .deactivate = convolver_deactivate, .run = convolve_run, .cleanup = convolver_cleanup, @@ -1144,6 +1185,7 @@ struct delay_impl { uint32_t buffer_samples; float *buffer; uint32_t ptr; + float latency; }; static void delay_cleanup(void * Instance) @@ -1161,7 +1203,7 @@ static void *delay_instantiate(const struct spa_fga_plugin *plugin, const struct struct spa_json it[1]; const char *val; char key[256]; - float max_delay = 1.0f; + float max_delay = 1.0f, latency = 0.0f; int len; if (config == NULL) { @@ -1181,12 +1223,19 @@ static void *delay_instantiate(const struct spa_fga_plugin *plugin, const struct spa_log_error(pl->log, "delay:max-delay requires a number"); return NULL; } + } else if (spa_streq(key, "latency")) { + if (spa_json_parse_float(val, len, &latency) <= 0) { + spa_log_error(pl->log, "delay:latency requires a number"); + return NULL; + } } else { spa_log_warn(pl->log, "delay: ignoring config key: '%s'", key); } } if (max_delay <= 0.0f) max_delay = 1.0f; + if (latency <= 0.0f) + latency = 0.0f; impl = calloc(1, sizeof(*impl)); if (impl == NULL) @@ -1197,7 +1246,9 @@ static void *delay_instantiate(const struct spa_fga_plugin *plugin, const struct impl->log = pl->log; impl->rate = SampleRate; impl->buffer_samples = SPA_ROUND_UP_N((uint32_t)(max_delay * impl->rate), 64); - spa_log_info(impl->log, "max-delay:%f seconds rate:%lu samples:%d", max_delay, impl->rate, impl->buffer_samples); + impl->latency = latency * impl->rate; + spa_log_info(impl->log, "max-delay:%f seconds rate:%lu samples:%d latency:%f", + max_delay, impl->rate, impl->buffer_samples, impl->latency); impl->buffer = calloc(impl->buffer_samples * 2 + 64, sizeof(float)); if (impl->buffer == NULL) { @@ -1208,29 +1259,35 @@ static void *delay_instantiate(const struct spa_fga_plugin *plugin, const struct } static void delay_connect_port(void * Instance, unsigned long Port, - float * DataLocation) + void * DataLocation) { struct delay_impl *impl = Instance; - if (Port > 2) - return; impl->port[Port] = DataLocation; } +static void delay_activate(void * Instance) +{ + struct delay_impl *impl = Instance; + if (impl->port[3] != NULL) + impl->port[3][0] = impl->latency; +} + static void delay_run(void * Instance, unsigned long SampleCount) { struct delay_impl *impl = Instance; float *in = impl->port[1], *out = impl->port[0]; float delay = impl->port[2][0]; - if (in == NULL || out == NULL) - return; - if (delay != impl->delay) { impl->delay_samples = SPA_CLAMP((uint32_t)(delay * impl->rate), 0u, impl->buffer_samples-1); impl->delay = delay; } - spa_fga_dsp_delay(impl->dsp, impl->buffer, &impl->ptr, impl->buffer_samples, - impl->delay_samples, out, in, SampleCount); + if (in != NULL && out != NULL) { + spa_fga_dsp_delay(impl->dsp, impl->buffer, &impl->ptr, impl->buffer_samples, + impl->delay_samples, out, in, SampleCount); + } + if (impl->port[3] != NULL) + impl->port[3][0] = impl->latency; } static struct spa_fga_port delay_ports[] = { @@ -1247,17 +1304,23 @@ static struct spa_fga_port delay_ports[] = { .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, .def = 0.0f, .min = 0.0f, .max = 100.0f }, + { .index = 3, + .name = "latency", + .hint = SPA_FGA_HINT_LATENCY, + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, }; static const struct spa_fga_descriptor delay_desc = { .name = "delay", .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, - .n_ports = 3, + .n_ports = SPA_N_ELEMENTS(delay_ports), .ports = delay_ports, .instantiate = delay_instantiate, .connect_port = delay_connect_port, + .activate = delay_activate, .run = delay_run, .cleanup = delay_cleanup, }; @@ -1983,7 +2046,7 @@ static void *param_eq_instantiate(const struct spa_fga_plugin *plugin, const str } if (idx == 0) { for (i = 1; i < 8; i++) - memcpy(&impl->bq[i*PARAM_EQ_MAX], impl->bq, + spa_memcpy(&impl->bq[i*PARAM_EQ_MAX], impl->bq, sizeof(struct biquad) * PARAM_EQ_MAX); } } @@ -1994,7 +2057,7 @@ error: } static void param_eq_connect_port(void * Instance, unsigned long Port, - float * DataLocation) + void * DataLocation) { struct param_eq_impl *impl = Instance; impl->port[Port] = DataLocation; @@ -2092,24 +2155,33 @@ static const struct spa_fga_descriptor param_eq_desc = { static void max_run(void * Instance, unsigned long SampleCount) { struct builtin *impl = Instance; - float *out = impl->port[0], *in1 = impl->port[1], *in2 = impl->port[2]; - unsigned long n; + float *out = impl->port[0]; + float *src[8]; + unsigned long n, p, n_srcs = 0; if (out == NULL) return; - if (in1 != NULL && in2 != NULL) { - for (n = 0; n < SampleCount; n++) - out[n] = SPA_MAX(in1[n], in2[n]); - } else if (in1 != NULL) { - for (n = 0; n < SampleCount; n++) - out[n] = in1[n]; - } else if (in2 != NULL) { - for (n = 0; n < SampleCount; n++) - out[n] = in2[n]; + for (p = 1; p < 9; p++) { + if (impl->port[p] != NULL) + src[n_srcs++] = impl->port[p]; + } + + if (n_srcs == 0) { + spa_memzero(out, SampleCount * sizeof(float)); + } else if (n_srcs == 1) { + spa_memcpy(out, src[0], SampleCount * sizeof(float)); } else { - for (n = 0; n < SampleCount; n++) - out[n] = 0.0f; + for (p = 0; p < n_srcs; p++) { + if (p == 0) { + for (n = 0; n < SampleCount; n++) + out[n] = SPA_MAX(src[p][n], src[p + 1][n]); + p++; + } else { + for (n = 0; n < SampleCount; n++) + out[n] = SPA_MAX(out[n], src[p][n]); + } + } } } @@ -2126,7 +2198,31 @@ static struct spa_fga_port max_ports[] = { { .index = 2, .name = "In 2", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, - } + }, + { .index = 3, + .name = "In 3", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 4, + .name = "In 4", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 5, + .name = "In 5", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 6, + .name = "In 6", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 7, + .name = "In 7", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 8, + .name = "In 8", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, }; static const struct spa_fga_descriptor max_desc = { @@ -2213,7 +2309,7 @@ static void dcblock_run(void * Instance, unsigned long SampleCount) } static void dcblock_connect_port(void * Instance, unsigned long Port, - float * DataLocation) + void * DataLocation) { struct dcblock_impl *impl = Instance; impl->port[Port] = DataLocation; @@ -2453,6 +2549,511 @@ static const struct spa_fga_descriptor sqrt_desc = { .cleanup = builtin_cleanup, }; +/* debug */ +static void debug_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *in = impl->port[0], *out = impl->port[1]; + float *control = impl->port[2], *notify = impl->port[3]; + + if (in != NULL) { + spa_debug_log_mem(impl->log, SPA_LOG_LEVEL_INFO, 0, in, SampleCount * sizeof(float)); + if (out != NULL) + spa_memcpy(out, in, SampleCount * sizeof(float)); + } + if (control != NULL) { + spa_log_info(impl->log, "control: %f", control[0]); + if (notify != NULL) + notify[0] = control[0]; + } +} + + +static struct spa_fga_port debug_ports[] = { + { .index = 0, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Control", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + }, + { .index = 3, + .name = "Notify", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, +}; + +static const struct spa_fga_descriptor debug_desc = { + .name = "debug", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(debug_ports), + .ports = debug_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = debug_run, + .cleanup = builtin_cleanup, +}; + +/* pipe */ +struct pipe_impl { + struct plugin *plugin; + + struct spa_log *log; + struct spa_fga_dsp *dsp; + unsigned long rate; + float *port[3]; + float latency; + + int write_fd; + int read_fd; + size_t written; + size_t read; +}; + +static int do_exec(struct pipe_impl *impl, const char *command) +{ + int pid, res, len, argc = 0; + char *argv[512]; + struct spa_json it[2]; + const char *value; + int stdin_pipe[2]; + int stdout_pipe[2]; + + if (spa_json_begin_array_relax(&it[0], command, strlen(command)) <= 0) + return -EINVAL; + + while ((len = spa_json_next(&it[0], &value)) > 0) { + char *s; + + if ((s = malloc(len+1)) == NULL) + return -errno; + + spa_json_parse_stringn(value, len, s, len+1); + + argv[argc++] = s; + } + argv[argc++] = NULL; + + pipe2(stdin_pipe, 0); + pipe2(stdout_pipe, 0); + + impl->write_fd = stdin_pipe[1]; + impl->read_fd = stdout_pipe[0]; + + pid = fork(); + + if (pid == 0) { + char buf[1024]; + char *const *p; + struct spa_strbuf s; + + /* Double fork to avoid zombies; we don't want to set SIGCHLD handler */ + pid = fork(); + + if (pid < 0) { + spa_log_error(impl->log, "fork error: %m"); + goto done; + } else if (pid != 0) { + exit(0); + } + + dup2(stdin_pipe[0], 0); + dup2(stdout_pipe[1], 1); + + spa_strbuf_init(&s, buf, sizeof(buf)); + for (p = argv; *p; ++p) + spa_strbuf_append(&s, " '%s'", *p); + + spa_log_info(impl->log, "exec%s", s.buffer); + res = execvp(argv[0], argv); + + if (res == -1) { + res = -errno; + spa_log_error(impl->log, "execvp error '%s': %m", argv[0]); + } +done: + exit(1); + } else if (pid < 0) { + spa_log_error(impl->log, "fork error: %m"); + } else { + int status = 0; + do { + errno = 0; + res = waitpid(pid, &status, 0); + } while (res < 0 && errno == EINTR); + spa_log_debug(impl->log, "exec got pid %d res:%d status:%d", (int)pid, res, status); + } + return 0; +} + +static void pipe_transfer(struct pipe_impl *impl, float *in, float *out, int count) +{ + ssize_t sz; + + sz = read(impl->read_fd, out, count * sizeof(float)); + if (sz > 0) { + impl->read += sz; + if (impl->read == (size_t)sz) { + while ((sz = read(impl->read_fd, out, count * sizeof(float))) != -1) + impl->read += sz; + } + } else { + memset(out, 0, count * sizeof(float)); + } + if ((sz = write(impl->write_fd, in, count * sizeof(float))) != -1) + impl->written += sz; +} + +static void *pipe_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ + struct plugin *pl = SPA_CONTAINER_OF(plugin, struct plugin, plugin); + struct pipe_impl *impl; + struct spa_json it[2]; + const char *val; + char key[256]; + spa_autofree char*command = NULL; + int len; + + errno = EINVAL; + if (config == NULL) { + spa_log_error(pl->log, "pipe: requires a config section"); + return NULL; + } + + if (spa_json_begin_object(&it[0], config, strlen(config)) <= 0) { + spa_log_error(pl->log, "pipe: config must be an object"); + return NULL; + } + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "command")) { + if ((command = malloc(len+1)) == NULL) + return NULL; + + if (spa_json_parse_stringn(val, len, command, len+1) <= 0) { + spa_log_error(pl->log, "pipe: command requires a string"); + return NULL; + } + } + else { + spa_log_warn(pl->log, "pipe: ignoring config key: '%s'", key); + } + } + if (command == NULL || command[0] == '\0') { + spa_log_error(pl->log, "pipe: command must be given and can not be empty"); + return NULL; + } + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + return NULL; + + impl->plugin = pl; + impl->log = pl->log; + impl->dsp = pl->dsp; + impl->rate = SampleRate; + + do_exec(impl, command); + + fcntl(impl->write_fd, F_SETFL, fcntl(impl->write_fd, F_GETFL) | O_NONBLOCK); + fcntl(impl->read_fd, F_SETFL, fcntl(impl->read_fd, F_GETFL) | O_NONBLOCK); + + return impl; +} + +static void pipe_connect_port(void *Instance, unsigned long Port, void * DataLocation) +{ + struct pipe_impl *impl = Instance; + impl->port[Port] = DataLocation; +} + +static void pipe_run(void * Instance, unsigned long SampleCount) +{ + struct pipe_impl *impl = Instance; + float *in = impl->port[0], *out = impl->port[1]; + + if (in != NULL && out != NULL) + pipe_transfer(impl, in, out, SampleCount); +} + +static void pipe_cleanup(void * Instance) +{ + struct pipe_impl *impl = Instance; + close(impl->write_fd); + close(impl->read_fd); + free(impl); +} + +static struct spa_fga_port pipe_ports[] = { + { .index = 0, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, +}; + +static const struct spa_fga_descriptor pipe_desc = { + .name = "pipe", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(pipe_ports), + .ports = pipe_ports, + + .instantiate = pipe_instantiate, + .connect_port = pipe_connect_port, + .run = pipe_run, + .cleanup = pipe_cleanup, +}; + +/* zeroramp */ +static struct spa_fga_port zeroramp_ports[] = { + { .index = 0, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Gap (s)", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.000666f, .min = 0.0f, .max = 1.0f + }, + { .index = 3, + .name = "Duration (s)", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.000666f, .min = 0.0f, .max = 1.0f + }, +}; + +#ifndef M_PIf +# define M_PIf 3.14159265358979323846f /* pi */ +#endif + +static void zeroramp_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *in = impl->port[0]; + float *out = impl->port[1]; + uint32_t n, i, c; + uint32_t gap = (uint32_t)(impl->port[2][0] * impl->rate); + uint32_t duration = (uint32_t)(impl->port[3][0] * impl->rate); + + if (out == NULL) + return; + + if (in == NULL) { + memset(out, 0, SampleCount * sizeof(float)); + return; + } + + for (n = 0; n < SampleCount; n++) { + if (impl->mode == 0) { + /* normal mode, finding gaps */ + out[n] = in[n]; + if (in[n] == 0.0f) { + if (++impl->count == gap) { + /* we found gap zeroes, fade out last + * sample and go into zero mode */ + for (c = 1, i = n; c < duration && i > 0; i--, c++) + out[i-1] = impl->last * + (0.5f + 0.5f * cosf(M_PIf + M_PIf * c / duration)); + impl->mode = 1; + } + } else { + /* keep last sample to fade out when needed */ + impl->count = 0; + impl->last = in[n]; + } + } + if (impl->mode == 1) { + /* zero mode */ + if (in[n] != 0.0f) { + /* gap ended, move to fade-in mode */ + impl->mode = 2; + impl->count = 0; + } else { + out[n] = 0.0f; + } + } + if (impl->mode == 2) { + /* fade-in mode */ + out[n] = in[n] * (0.5f + 0.5f * cosf(M_PIf + (M_PIf * ++impl->count / duration))); + if (impl->count == duration) { + /* fade in complete, back to normal mode */ + impl->count = 0; + impl->mode = 0; + } + } + } +} + +static const struct spa_fga_descriptor zeroramp_desc = { + .name = "zeroramp", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(zeroramp_ports), + .ports = zeroramp_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = zeroramp_run, + .cleanup = builtin_cleanup, +}; + + +/* noisegate */ +static struct spa_fga_port noisegate_ports[] = { + { .index = 0, + .name = "In", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 1, + .name = "Out", + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO, + }, + { .index = 2, + .name = "Level", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = NAN + }, + { .index = 3, + .name = "Open Threshold", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.04f, .min = 0.0f, .max = 1.0f + }, + { .index = 4, + .name = "Close Threshold", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.03f, .min = 0.0f, .max = 1.0f + }, + { .index = 5, + .name = "Attack (s)", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.005f, .min = 0.0f, .max = 1.0f + }, + { .index = 6, + .name = "Hold (s)", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.050f, .min = 0.0f, .max = 1.0f + }, + { .index = 7, + .name = "Release (s)", + .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.010f, .min = 0.0f, .max = 1.0f + }, +}; + +static void noisegate_run(void * Instance, unsigned long SampleCount) +{ + struct builtin *impl = Instance; + float *in = impl->port[0]; + float *out = impl->port[1]; + float in_lev = impl->port[2][0]; + unsigned long n; + float o_thres = impl->port[3][0]; + float c_thres = impl->port[4][0]; + float gate, hold, o_rate, c_rate, level; + int mode; + + if (out == NULL) + return; + + if (in == NULL) { + memset(out, 0, SampleCount * sizeof(float)); + return; + } + + o_rate = 1.0f / (impl->port[5][0] * impl->rate); + c_rate = 1.0f / (impl->port[7][0] * impl->rate); + gate = impl->gate; + hold = impl->hold; + mode = impl->mode; + level = impl->last; + + spa_log_trace_fp(impl->log, "%f %d %f", level, mode, gate); + + for (n = 0; n < SampleCount; n++) { + if (isnan(in_lev)) { + float lev = fabsf(in[n]); + if (lev > level) + level = lev; + else + level = lev * 0.05f + level * 0.95f; + } else { + level = in_lev; + } + + switch (mode) { + case 0: + /* closed */ + if (level >= o_thres) + mode = 1; + break; + case 1: + /* opening */ + gate += o_rate; + if (gate >= 1.0f) { + gate = 1.0f; + mode = 2; + hold = impl->port[6][0] * impl->rate; + } + break; + case 2: + /* hold */ + hold -= 1.0f; + if (hold <= 0.0f) + mode = 3; + break; + case 3: + /* open */ + if (level < c_thres) + mode = 4; + break; + case 4: + /* closing */ + gate -= c_rate; + if (level >= o_thres) + mode = 1; + else if (gate <= 0.0f) { + gate = 0.0f; + mode = 0; + } + break; + } + out[n] = in[n] * gate; + } + impl->gate = gate; + impl->hold = hold; + impl->mode = mode; + impl->last = level; +} + +static const struct spa_fga_descriptor noisegate_desc = { + .name = "noisegate", + .flags = SPA_FGA_DESCRIPTOR_SUPPORTS_NULL_DATA, + + .n_ports = SPA_N_ELEMENTS(noisegate_ports), + .ports = noisegate_ports, + + .instantiate = builtin_instantiate, + .connect_port = builtin_connect_port, + .run = noisegate_run, + .cleanup = builtin_cleanup, +}; + static const struct spa_fga_descriptor * builtin_descriptor(unsigned long Index) { switch(Index) { @@ -2510,6 +3111,14 @@ static const struct spa_fga_descriptor * builtin_descriptor(unsigned long Index) return &abs_desc; case 26: return &sqrt_desc; + case 27: + return &debug_desc; + case 28: + return &pipe_desc; + case 29: + return &zeroramp_desc; + case 30: + return &noisegate_desc; } return NULL; } diff --git a/spa/plugins/filter-graph/ebur128_plugin.c b/spa/plugins/filter-graph/plugin_ebur128.c similarity index 96% rename from spa/plugins/filter-graph/ebur128_plugin.c rename to spa/plugins/filter-graph/plugin_ebur128.c index 22bf5f513..fb79de590 100644 --- a/spa/plugins/filter-graph/ebur128_plugin.c +++ b/spa/plugins/filter-graph/plugin_ebur128.c @@ -153,7 +153,7 @@ static void ebur128_run(void * Instance, unsigned long SampleCount) ebur128_add_frames_float(st[i], in, SampleCount); if (out != NULL) - memcpy(out, in, SampleCount * sizeof(float)); + spa_memcpy(out, in, SampleCount * sizeof(float)); } if (impl->port[PORT_OUT_MOMENTARY] != NULL) { double sum = 0.0; @@ -219,11 +219,10 @@ static void ebur128_run(void * Instance, unsigned long SampleCount) } static void ebur128_connect_port(void * Instance, unsigned long Port, - float * DataLocation) + void * DataLocation) { struct ebur128_impl *impl = Instance; - if (Port < PORT_MAX) - impl->port[Port] = DataLocation; + impl->port[Port] = DataLocation; } static void ebur128_cleanup(void * Instance) @@ -235,6 +234,8 @@ static void ebur128_cleanup(void * Instance) static void ebur128_activate(void * Instance) { struct ebur128_impl *impl = Instance; + unsigned long max_window; + int major, minor, patch; int mode = 0, i; int modes[] = { EBUR128_MODE_M, @@ -264,12 +265,17 @@ static void ebur128_activate(void * Instance) mode |= modes[i]; } + ebur128_get_version(&major, &minor, &patch); + max_window = impl->max_window; + if (major == 1 && minor == 2 && (patch == 5 || patch == 6)) + max_window = (max_window + 999) / 1000; + for (i = 0; i < 7; i++) { impl->st[i] = ebur128_init(1, impl->rate, mode); if (impl->st[i]) { ebur128_set_channel(impl->st[i], i, channels[i]); ebur128_set_max_history(impl->st[i], impl->max_history); - ebur128_set_max_window(impl->st[i], impl->max_window); + ebur128_set_max_window(impl->st[i], max_window); } } } @@ -349,7 +355,7 @@ static struct spa_fga_port ebur128_ports[] = { .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, }, { .index = PORT_OUT_SHORTTERM, - .name = "Shorttem LUFS", + .name = "Shortterm LUFS", .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, }, { .index = PORT_OUT_GLOBAL, @@ -436,11 +442,10 @@ static void * lufs2gain_instantiate(const struct spa_fga_plugin *plugin, const s } static void lufs2gain_connect_port(void * Instance, unsigned long Port, - float * DataLocation) + void * DataLocation) { struct lufs2gain_impl *impl = Instance; - if (Port < 3) - impl->port[Port] = DataLocation; + impl->port[Port] = DataLocation; } static void lufs2gain_run(void * Instance, unsigned long SampleCount) diff --git a/spa/plugins/filter-graph/plugin_ffmpeg.c b/spa/plugins/filter-graph/plugin_ffmpeg.c new file mode 100644 index 000000000..4e4b9f4a0 --- /dev/null +++ b/spa/plugins/filter-graph/plugin_ffmpeg.c @@ -0,0 +1,473 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "audio-plugin.h" + +#define MAX_PORTS 256 +#define MAX_CTX 64 + +struct plugin { + struct spa_handle handle; + struct spa_fga_plugin plugin; + + struct spa_log *log; +}; + +struct descriptor { + struct spa_fga_descriptor desc; + struct plugin *p; + + AVFilterGraph *filter_graph; + const AVFilter *format; + const AVFilter *buffersrc; + const AVFilter *buffersink; + + AVChannelLayout layout[MAX_CTX]; + uint32_t latency_idx; +}; + +struct instance { + struct descriptor *desc; + + AVFilterGraph *filter_graph; + + uint32_t rate; + + AVFrame *frame; + AVFilterContext *ctx[MAX_CTX]; + uint32_t n_ctx; + uint32_t n_src; + uint32_t n_sink; + + uint64_t frame_num; + float *data[MAX_PORTS]; +}; + +static void layout_from_name(AVChannelLayout *layout, const char *name) +{ + const char *chan; + + if ((chan = strrchr(name, '_')) != NULL) + chan++; + else + chan = "FC"; + + if (av_channel_layout_from_string(layout, chan) < 0) + av_channel_layout_from_string(layout, "FC"); +} + +static void *ffmpeg_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor *desc, + unsigned long SampleRate, int index, const char *config) +{ + struct descriptor *d = (struct descriptor *)desc; + struct plugin *p = d->p; + struct instance *i; + AVFilterInOut *fp, *in, *out; + AVFilterContext *cnv, *ctx; + int res; + char channel[512]; + char options_str[1024]; + uint32_t n_fp; + + i = calloc(1, sizeof(*i)); + if (i == NULL) + return NULL; + + i->desc = d; + i->rate = SampleRate; + + i->filter_graph = avfilter_graph_alloc(); + if (i->filter_graph == NULL) { + errno = ENOMEM; + return NULL; + } + + res = avfilter_graph_parse2(i->filter_graph, d->desc.name, &in, &out); + if (res < 0) { + spa_log_error(p->log, "can parse filter graph %s", d->desc.name); + errno = EINVAL; + return NULL; + } + + for (n_fp = 0, fp = in; fp != NULL; fp = fp->next, n_fp++) { + ctx = avfilter_graph_alloc_filter(i->filter_graph, d->buffersrc, "src"); + if (ctx == NULL) { + spa_log_error(p->log, "can't alloc buffersrc"); + return NULL; + } + av_channel_layout_describe(&d->layout[n_fp], channel, sizeof(channel)); + + snprintf(options_str, sizeof(options_str), + "sample_fmt=%s:sample_rate=%ld:channel_layout=%s", + av_get_sample_fmt_name(AV_SAMPLE_FMT_FLTP), SampleRate, channel); + + spa_log_info(p->log, "%d buffersrc %s", n_fp, options_str); + avfilter_init_str(ctx, options_str); + avfilter_link(ctx, 0, fp->filter_ctx, fp->pad_idx); + + i->ctx[n_fp] = ctx; + } + i->n_src = n_fp; + for (fp = out; fp != NULL; fp = fp->next, n_fp++) { + cnv = avfilter_graph_alloc_filter(i->filter_graph, d->format, "format"); + if (cnv == NULL) { + spa_log_error(p->log, "can't alloc format"); + return NULL; + } + + av_channel_layout_describe(&d->layout[n_fp], channel, sizeof(channel)); + + snprintf(options_str, sizeof(options_str), + "sample_fmts=%s:sample_rates=%ld:channel_layouts=%s", + av_get_sample_fmt_name(AV_SAMPLE_FMT_FLTP), SampleRate, channel); + + spa_log_info(p->log, "%d format %s", n_fp, options_str); + avfilter_init_str(cnv, options_str); + avfilter_link(fp->filter_ctx, fp->pad_idx, cnv, 0); + + ctx = avfilter_graph_alloc_filter(i->filter_graph, d->buffersink, "sink"); + if (ctx == NULL) { + spa_log_error(p->log, "can't alloc buffersink"); + return NULL; + } + avfilter_init_str(ctx, NULL); + avfilter_link(cnv, 0, ctx, 0); + i->ctx[n_fp] = ctx; + } + i->n_sink = n_fp; + + avfilter_graph_config(i->filter_graph, NULL); + + i->frame = av_frame_alloc(); + +#if 0 + char *dump = avfilter_graph_dump(i->filter_graph, NULL); + spa_log_debug(p->log, "%s", dump); + free(dump); +#endif + return i; +} + +static void ffmpeg_cleanup(void *instance) +{ + struct instance *i = instance; + avfilter_graph_free(&i->filter_graph); + av_frame_free(&i->frame); + free(i); +} + +static void ffmpeg_free(const struct spa_fga_descriptor *desc) +{ + struct descriptor *d = (struct descriptor*)desc; + uint32_t i; + avfilter_graph_free(&d->filter_graph); + for (i = 0; i < d->desc.n_ports; i++) + free((void*)d->desc.ports[i].name); + free((char*)d->desc.name); + free(d->desc.ports); + free(d); +} + +static void ffmpeg_connect_port(void *instance, unsigned long port, void *data) +{ + struct instance *i = instance; + i->data[port] = data; +} + +static void ffmpeg_run(void *instance, unsigned long SampleCount) +{ + struct instance *i = instance; + struct descriptor *desc = i->desc; + char buf[1024]; + int err, j; + uint32_t c, d = 0; + float delay; + + spa_log_trace(i->desc->p->log, "run %ld", SampleCount); + + for (c = 0; c < i->n_src; c++) { + i->frame->nb_samples = SampleCount; + i->frame->sample_rate = i->rate; + i->frame->format = AV_SAMPLE_FMT_FLTP; + i->frame->pts = i->frame_num; + + av_channel_layout_copy(&i->frame->ch_layout, &desc->layout[c]); + + for (j = 0; j < desc->layout[c].nb_channels; j++) + i->frame->data[j] = (uint8_t*)i->data[d++]; + + if ((err = av_buffersrc_add_frame_flags(i->ctx[c], i->frame, + AV_BUFFERSRC_FLAG_NO_CHECK_FORMAT)) < 0) { + av_strerror(err, buf, sizeof(buf)); + spa_log_warn(i->desc->p->log, "can't add frame: %s", buf); + av_frame_unref(i->frame); + continue; + } + } + delay = 0.0f; + for (; c < i->n_sink; c++) { + if ((err = av_buffersink_get_samples(i->ctx[c], i->frame, SampleCount)) < 0) { + av_strerror(err, buf, sizeof(buf)); + spa_log_debug(i->desc->p->log, "can't get frame: %s", buf); + for (j = 0; j < desc->layout[c].nb_channels; j++) + memset(i->data[d++], 0, SampleCount * sizeof(float)); + continue; + } + delay = fmaxf(delay, i->frame_num - i->frame->pts); + + spa_log_trace(i->desc->p->log, "got frame %d %d %d %s %f", + i->frame->nb_samples, + i->frame->ch_layout.nb_channels, + i->frame->sample_rate, + av_get_sample_fmt_name(i->frame->format), + delay); + + for (j = 0; j < desc->layout[c].nb_channels; j++) + memcpy(i->data[d++], i->frame->data[j], SampleCount * sizeof(float)); + + av_frame_unref(i->frame); + } + i->frame_num += SampleCount; + if (i->data[desc->latency_idx] != NULL) + i->data[desc->latency_idx][0] = delay; +} + +static const struct spa_fga_descriptor *ffmpeg_plugin_make_desc(void *plugin, const char *name) +{ + struct plugin *p = (struct plugin *)plugin; + struct descriptor *desc; + uint32_t n_fp, n_p; + AVFilterInOut *in = NULL, *out = NULL, *fp; + int res, j; + + spa_log_info(p->log, "%s", name); + + desc = calloc(1, sizeof(*desc)); + if (desc == NULL) + return NULL; + + desc->p = p; + desc->filter_graph = avfilter_graph_alloc(); + if (desc->filter_graph == NULL) { + errno = ENOMEM; + return NULL; + } + + res = avfilter_graph_parse2(desc->filter_graph, name, &in, &out); + if (res < 0) { + spa_log_error(p->log, "can parse filter graph %s", name); + errno = EINVAL; + return NULL; + } + + desc->desc.n_ports = 0; + for (n_fp = 0, fp = in; fp != NULL && n_fp < MAX_CTX; fp = fp->next, n_fp++) { + layout_from_name(&desc->layout[n_fp], fp->name); + spa_log_info(p->log, "%p: in %s %p:%d channels:%d", fp, fp->name, + fp->filter_ctx, fp->pad_idx, desc->layout[n_fp].nb_channels); + desc->desc.n_ports += desc->layout[n_fp].nb_channels; + } + for (fp = out; fp != NULL && n_fp < MAX_CTX; fp = fp->next, n_fp++) { + layout_from_name(&desc->layout[n_fp], fp->name); + spa_log_info(p->log, "%p: out %s %p:%d channels:%d", fp, fp->name, + fp->filter_ctx, fp->pad_idx, desc->layout[n_fp].nb_channels); + desc->desc.n_ports += desc->layout[n_fp].nb_channels; + } + /* one for the latency */ + desc->desc.n_ports++; + + if (n_fp >= MAX_CTX) { + spa_log_error(p->log, "%p: too many in/out ports %d > %d", desc, + n_fp, MAX_CTX); + errno = ENOSPC; + return NULL; + } + if (desc->desc.n_ports >= MAX_PORTS) { + spa_log_error(p->log, "%p: too many ports %d > %d", desc, + desc->desc.n_ports, MAX_PORTS); + errno = ENOSPC; + return NULL; + } + + desc->desc.instantiate = ffmpeg_instantiate; + desc->desc.cleanup = ffmpeg_cleanup; + desc->desc.free = ffmpeg_free; + desc->desc.connect_port = ffmpeg_connect_port; + desc->desc.run = ffmpeg_run; + + desc->desc.name = strdup(name); + desc->desc.flags = 0; + + desc->desc.ports = calloc(desc->desc.n_ports, sizeof(struct spa_fga_port)); + + for (n_fp = 0, n_p = 0, fp = in; fp != NULL; fp = fp->next, n_fp++) { + for (j = 0; j < desc->layout[n_fp].nb_channels; j++, n_p++) { + desc->desc.ports[n_p].index = n_p; + if (desc->layout[n_fp].nb_channels == 1) + desc->desc.ports[n_p].name = spa_aprintf("%s", fp->name); + else + desc->desc.ports[n_p].name = spa_aprintf("%s_%d", fp->name, j); + desc->desc.ports[n_p].flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_AUDIO; + } + } + for (fp = out; fp != NULL; fp = fp->next, n_fp++) { + for (j = 0; j < desc->layout[n_fp].nb_channels; j++, n_p++) { + desc->desc.ports[n_p].index = n_p; + if (desc->layout[n_fp].nb_channels == 1) + desc->desc.ports[n_p].name = spa_aprintf("%s", fp->name); + else + desc->desc.ports[n_p].name = spa_aprintf("%s_%d", fp->name, j); + desc->desc.ports[n_p].flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_AUDIO; + } + } + desc->desc.ports[n_p].index = n_p; + desc->desc.ports[n_p].name = strdup("latency"); + desc->desc.ports[n_p].flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL; + desc->desc.ports[n_p].hint = SPA_FGA_HINT_LATENCY; + desc->latency_idx = n_p++; + + desc->buffersrc = avfilter_get_by_name("abuffer"); + desc->buffersink = avfilter_get_by_name("abuffersink"); + desc->format = avfilter_get_by_name("aformat"); + + return &desc->desc; +} + +static struct spa_fga_plugin_methods impl_plugin = { + SPA_VERSION_FGA_PLUGIN_METHODS, + .make_desc = ffmpeg_plugin_make_desc, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct plugin *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct plugin *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin)) + *interface = &impl->plugin; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct plugin); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct plugin *impl; + uint32_t i; + const char *path = NULL; + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct plugin *) handle; + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "filter.graph.path")) + path = s; + } + if (!spa_streq(path, "filtergraph")) + return -EINVAL; + + impl->plugin.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, + SPA_VERSION_FGA_PLUGIN, + &impl_plugin, impl); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + { SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin }, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static struct spa_handle_factory spa_fga_plugin_ffmpeg_factory = { + SPA_VERSION_HANDLE_FACTORY, + "filter.graph.plugin.ffmpeg", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_fga_plugin_ffmpeg_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/filter-graph/ladspa_plugin.c b/spa/plugins/filter-graph/plugin_ladspa.c similarity index 95% rename from spa/plugins/filter-graph/ladspa_plugin.c rename to spa/plugins/filter-graph/plugin_ladspa.c index 45026c8e7..d5c8ef488 100644 --- a/spa/plugins/filter-graph/ladspa_plugin.c +++ b/spa/plugins/filter-graph/plugin_ladspa.c @@ -116,7 +116,15 @@ static void ladspa_port_update_ranges(struct descriptor *dd, struct spa_fga_port lower = d->PortRangeHints[p].LowerBound; upper = d->PortRangeHints[p].UpperBound; - port->hint = hint; + port->hint = 0; + if (hint & LADSPA_HINT_TOGGLED) + port->hint |= SPA_FGA_HINT_BOOLEAN; + if (hint & LADSPA_HINT_SAMPLE_RATE) + port->hint |= SPA_FGA_HINT_SAMPLE_RATE; + if (hint & LADSPA_HINT_INTEGER) + port->hint |= SPA_FGA_HINT_INTEGER; + if (spa_streq(port->name, "latency")) + port->hint |= SPA_FGA_HINT_LATENCY; port->def = get_default(port, hint, lower, upper); port->min = lower; port->max = upper; @@ -145,7 +153,7 @@ static const struct spa_fga_descriptor *ladspa_plugin_make_desc(void *plugin, co desc->desc.instantiate = ladspa_instantiate; desc->desc.cleanup = d->cleanup; - desc->desc.connect_port = d->connect_port; + desc->desc.connect_port = (__typeof__(desc->desc.connect_port))d->connect_port; desc->desc.activate = d->activate; desc->desc.deactivate = d->deactivate; desc->desc.run = d->run; diff --git a/spa/plugins/filter-graph/lv2_plugin.c b/spa/plugins/filter-graph/plugin_lv2.c similarity index 80% rename from spa/plugins/filter-graph/lv2_plugin.c rename to spa/plugins/filter-graph/plugin_lv2.c index a2af22d6a..712b728e2 100644 --- a/spa/plugins/filter-graph/lv2_plugin.c +++ b/spa/plugins/filter-graph/plugin_lv2.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -19,16 +20,20 @@ #include #include #include + #include #include #include + #include # else #include #include #include + #include #include #include + #include # endif @@ -101,6 +106,7 @@ struct context { LilvNode *boundedBlockLength; LilvNode* worker_schedule; LilvNode* worker_iface; + LilvNode* state_iface; URITable uri_table; LV2_URID_Map map; @@ -169,14 +175,15 @@ static struct context *context_new(void) c->boundedBlockLength = lilv_new_uri(c->world, LV2_BUF_SIZE__boundedBlockLength); c->worker_schedule = lilv_new_uri(c->world, LV2_WORKER__schedule); c->worker_iface = lilv_new_uri(c->world, LV2_WORKER__interface); + c->state_iface = lilv_new_uri(c->world, LV2_STATE__interface); c->map.handle = &c->uri_table; c->map.map = uri_table_map; - c->map_feature.URI = LV2_URID_MAP_URI; + c->map_feature.URI = LV2_URID__map; c->map_feature.data = &c->map; c->unmap.handle = &c->uri_table; c->unmap.unmap = uri_table_unmap; - c->unmap_feature.URI = LV2_URID_UNMAP_URI; + c->unmap_feature.URI = LV2_URID__unmap; c->unmap_feature.data = &c->unmap; c->atom_Int = context_map(c, LV2_ATOM__Int); @@ -231,12 +238,15 @@ struct instance { LilvInstance *instance; LV2_Worker_Schedule work_schedule; LV2_Feature work_schedule_feature; + LV2_Log_Log log; + LV2_Feature log_feature; LV2_Options_Option options[6]; LV2_Feature options_feature; - const LV2_Feature *features[7]; + const LV2_Feature *features[10]; const LV2_Worker_Interface *work_iface; + const LV2_State_Interface *state_iface; int32_t block_length; LV2_Atom empty_atom; @@ -278,6 +288,76 @@ work_schedule(LV2_Worker_Schedule_Handle handle, uint32_t size, const void *data return LV2_WORKER_SUCCESS; } +struct state_data { + struct instance *i; + const char *config; + char *tmp; +}; + +static const void *state_retrieve_function(LV2_State_Handle handle, + uint32_t key, size_t *size, uint32_t *type, uint32_t *flags) +{ + struct state_data *sd = (struct state_data*)handle; + struct plugin *p = sd->i->p; + struct context *c = p->c; + const char *uri = c->unmap.unmap(c->unmap.handle, key), *val; + struct spa_json it[3]; + char k[strlen(uri)+3]; + int len; + + if (sd->config == NULL) { + spa_log_info(p->log, "lv2: restore %d %s without a config", key, uri); + return NULL; + } + + if (spa_json_begin_object(&it[0], sd->config, strlen(sd->config)) <= 0) { + spa_log_error(p->log, "lv2: config must be an object"); + return NULL; + } + + while ((len = spa_json_object_next(&it[0], k, sizeof(k), &val)) > 0) { + if (!spa_streq(k, uri)) + continue; + + if (spa_json_is_container(val, len)) + if ((len = spa_json_container_len(&it[0], val, len)) <= 0) + return NULL; + + sd->tmp = realloc(sd->tmp, len+1); + spa_json_parse_stringn(val, len, sd->tmp, len+1); + + spa_log_info(p->log, "lv2: restore %d %s %s", key, uri, sd->tmp); + if (size) + *size = strlen(sd->tmp); + if (type) + *type = 0; + if (flags) + *flags = LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE; + return sd->tmp; + } + spa_log_info(p->log, "lv2: restore %d %s not found in config", key, uri); + return NULL; +} + +SPA_PRINTF_FUNC(3, 0) +static int log_vprintf(LV2_Log_Handle handle, LV2_URID type, const char* fmt, va_list ap) +{ + struct instance *i = (struct instance*)handle; + spa_log_logv(i->p->log, SPA_LOG_LEVEL_INFO, __FILE__,__LINE__,__func__, fmt, ap); + return 0; +} + +SPA_PRINTF_FUNC(3, 4) +static int log_printf(LV2_Log_Handle handle, LV2_URID type, const char* fmt, ...) +{ + va_list args; + int ret; + va_start(args, fmt); + ret = log_vprintf(handle, type, fmt, args); + va_end(args); + return ret; +} + static void *lv2_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor *desc, unsigned long SampleRate, int index, const char *config) { @@ -298,6 +378,12 @@ static void *lv2_instantiate(const struct spa_fga_plugin *plugin, const struct s i->block_length = 1024; i->desc = d; i->p = p; + i->log.handle = i; + i->log.printf = log_printf; + i->log.vprintf = log_vprintf; + i->log_feature.URI = LV2_LOG__log; + i->log_feature.data = &i->log; + i->features[n_features++] = &i->log_feature; i->features[n_features++] = &c->map_feature; i->features[n_features++] = &c->unmap_feature; i->features[n_features++] = &buf_size_features[0]; @@ -328,9 +414,11 @@ static void *lv2_instantiate(const struct spa_fga_plugin *plugin, const struct s c->atom_Float, &fsample_rate }; i->options[5] = (LV2_Options_Option) { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, NULL }; - i->options_feature.URI = LV2_OPTIONS__options; - i->options_feature.data = i->options; - i->features[n_features++] = &i->options_feature; + i->options_feature.URI = LV2_OPTIONS__options; + i->options_feature.data = i->options; + i->features[n_features++] = &i->options_feature; + i->features[n_features++] = NULL; + spa_assert(n_features < SPA_N_ELEMENTS(i->features)); i->instance = lilv_plugin_instantiate(p->p, SampleRate, i->features); if (i->instance == NULL) { @@ -341,24 +429,35 @@ static void *lv2_instantiate(const struct spa_fga_plugin *plugin, const struct s i->work_iface = (const LV2_Worker_Interface*) lilv_instance_get_extension_data(i->instance, LV2_WORKER__interface); } + if (lilv_plugin_has_extension_data(p->p, c->state_iface)) { + i->state_iface = (const LV2_State_Interface*) + lilv_instance_get_extension_data(i->instance, LV2_STATE__interface); + } for (n = 0; n < desc->n_ports; n++) { const LilvPort *port = lilv_plugin_get_port_by_index(p->p, n); if (lilv_port_is_a(p->p, port, c->atom_AtomPort)) { lilv_instance_connect_port(i->instance, n, &i->empty_atom); } } - + if (i->state_iface && i->state_iface->restore) { + struct state_data sd = { .i = i, .config = config, .tmp = NULL }; + i->state_iface->restore(i->instance->lv2_handle, state_retrieve_function, + &sd, 0, i->features); + free(sd.tmp); + } return i; } static void lv2_cleanup(void *instance) { struct instance *i = instance; + spa_loop_invoke(i->p->data_loop, NULL, 0, NULL, 0, true, NULL); + spa_loop_invoke(i->p->main_loop, NULL, 0, NULL, 0, true, NULL); lilv_instance_free(i->instance); free(i); } -static void lv2_connect_port(void *instance, unsigned long port, float *data) +static void lv2_connect_port(void *instance, unsigned long port, void *data) { struct instance *i = instance; lilv_instance_connect_port(i->instance, port, data); @@ -387,6 +486,9 @@ static void lv2_run(void *instance, unsigned long SampleCount) static void lv2_free(const struct spa_fga_descriptor *desc) { struct descriptor *d = (struct descriptor*)desc; + uint32_t i; + for (i = 0; i < d->desc.n_ports; i++) + free((void*)d->desc.ports[i].name); free((char*)d->desc.name); free(d->desc.ports); free(d); @@ -399,6 +501,8 @@ static const struct spa_fga_descriptor *lv2_plugin_make_desc(void *plugin, const struct descriptor *desc; uint32_t i; float *mins, *maxes, *controls; + bool latent; + uint32_t latency_index; desc = calloc(1, sizeof(*desc)); if (desc == NULL) @@ -424,6 +528,9 @@ static const struct spa_fga_descriptor *lv2_plugin_make_desc(void *plugin, const maxes = alloca(desc->desc.n_ports * sizeof(float)); controls = alloca(desc->desc.n_ports * sizeof(float)); + latent = lilv_plugin_has_latency(p->p); + latency_index = latent ? lilv_plugin_get_latency_port_index(p->p) : 0; + lilv_plugin_get_port_ranges_float(p->p, mins, maxes, controls); for (i = 0; i < desc->desc.n_ports; i++) { @@ -443,8 +550,13 @@ static const struct spa_fga_descriptor *lv2_plugin_make_desc(void *plugin, const fp->flags |= SPA_FGA_PORT_CONTROL; if (lilv_port_is_a(p->p, port, c->lv2_AudioPort)) fp->flags |= SPA_FGA_PORT_AUDIO; + if (lilv_port_has_property(p->p, port, c->lv2_Optional)) + fp->flags |= SPA_FGA_PORT_SUPPORTS_NULL_DATA; fp->hint = 0; + if (latent && latency_index == i) + fp->hint |= SPA_FGA_HINT_LATENCY; + fp->min = mins[i]; fp->max = maxes[i]; fp->def = controls[i]; diff --git a/spa/plugins/filter-graph/plugin_onnx.c b/spa/plugins/filter-graph/plugin_onnx.c new file mode 100644 index 000000000..3ef12e963 --- /dev/null +++ b/spa/plugins/filter-graph/plugin_onnx.c @@ -0,0 +1,772 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "audio-plugin.h" + +#define MAX_PORTS 256 +#define MAX_CTX 64 + +const OrtApi* ort = NULL; + +#define CHECK(expr) \ + if ((status = (expr)) != NULL) \ + goto error_onnx; + +struct plugin { + struct spa_handle handle; + struct spa_fga_plugin plugin; + + struct spa_log *log; + + OrtEnv *env; + OrtAllocator *allocator; + OrtSessionOptions *session_options; +}; + +struct tensor_info { + int index; + enum spa_direction direction; + char *name; + enum ONNXTensorElementDataType type; + int64_t dimensions[64]; + size_t n_dimensions; + int retain; +#define DATA_NONE 0 +#define DATA_PORT 1 +#define DATA_CONTROL 2 +#define DATA_PARAM_RATE 3 +#define DATA_TENSOR 4 + uint32_t data_type; + char data_name[128]; + uint32_t data_index; + uint32_t data_size; +}; + +struct descriptor { + struct spa_fga_descriptor desc; + struct plugin *p; + + int blocksize; + OrtSession *session; + struct tensor_info tensors[MAX_PORTS]; + size_t n_tensors; +}; + +struct instance { + struct descriptor *desc; + + uint32_t rate; + + OrtRunOptions *run_options; + OrtValue *tensor[MAX_PORTS]; + + uint32_t offset; + float *data[MAX_PORTS]; +}; + +static struct tensor_info *find_tensor(struct descriptor *d, const char *name, enum spa_direction direction) +{ + size_t i; + for (i = 0; i < d->n_tensors; i++) { + struct tensor_info *ti = &d->tensors[i]; + if (spa_streq(ti->name, name) && ti->direction == direction) + return ti; + } + return NULL; +} + +/* + * { + * dimensions = [ 1, 576 ] + * retain = 64 + * data = "tensor:"|"param:rate"|"port:"|"control:" + * } + */ +static int parse_tensor_info(struct descriptor *d, struct spa_json *it, + struct tensor_info *info) +{ + struct plugin *p = d->p; + struct spa_json sub; + const char *val; + int len; + char key[256]; + char data[512]; + + while ((len = spa_json_object_next(it, key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "dimensions")) { + int64_t dimensions[64]; + size_t i, n_dimensions = 0; + if (!spa_json_is_array(val, len)) { + spa_log_error(p->log, "onnx: %s expects an array", key); + return -EINVAL; + } + spa_json_enter(it, &sub); + while (spa_json_get_string(&sub, data, sizeof(data)) > 0 && n_dimensions < 64) + dimensions[n_dimensions++] = atoi(data); + + if (info->n_dimensions == 0) + info->n_dimensions = n_dimensions; + else if (n_dimensions != info->n_dimensions) { + spa_log_error(p->log, "onnx: %s expected %zu dimensions, got %zu", + key, info->n_dimensions, n_dimensions); + return -EINVAL; + } + for (i = 0; i < n_dimensions; i++) { + if (info->dimensions[i] <= 0) + info->dimensions[i] = dimensions[i]; + else if (info->dimensions[i] != dimensions[i]) { + spa_log_error(p->log, "onnx: %s mismatched %zu dimension, got %" + PRIi64" expected %"PRIi64, + key, i, dimensions[i], info->dimensions[i]); + return -EINVAL; + } + } + } else if (spa_streq(key, "retain")) { + if (spa_json_parse_int(val, len, &info->retain) <= 0) { + spa_log_error(p->log, "onnx: %s expects an int", key); + return -EINVAL; + } + } else if (spa_streq(key, "data")) { + if (spa_json_parse_stringn(val, len, data, sizeof(data)) <= 0) { + spa_log_error(p->log, "onnx: %s expects a string", key); + return -EINVAL; + } + if (spa_strstartswith(data, "tensor:")) { + struct tensor_info *ti; + spa_scnprintf(info->data_name, sizeof(info->data_name), "%s", data+7); + ti = find_tensor(d, info->data_name, SPA_DIRECTION_REVERSE(info->direction)); + if (ti == NULL) { + spa_log_error(p->log, "onnx: unknown tensor %s", info->data_name); + return -EINVAL; + } + info->data_type = DATA_TENSOR; + info->data_index = ti->index; + } + else if (spa_strstartswith(data, "param:rate")) { + info->data_type = DATA_PARAM_RATE; + } + else if (spa_strstartswith(data, "port:")) { + info->data_type = DATA_PORT; + spa_scnprintf(info->data_name, sizeof(info->data_name), "%s", data+5); + } + else if (spa_strstartswith(data, "control:")) { + info->data_type = DATA_CONTROL; + spa_scnprintf(info->data_name, sizeof(info->data_name), "%s", data+8); + } + else { + spa_log_warn(p->log, "onnx: unknown %s value: %s", key, data); + } + } else { + spa_log_warn(p->log, "unexpected onnx tensor-info key '%s'", key); + } + } + return 0; +} + +/* + * { + * = { + * + * } + * .... + * } + */ +static int parse_tensors(struct descriptor *d, struct spa_json *it, enum spa_direction direction) +{ + struct plugin *p = d->p; + struct spa_json sub; + const char *val; + int len, res; + char key[256]; + + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + struct tensor_info *info; + + if ((info = find_tensor(d, key, direction)) == NULL) { + spa_log_error(p->log, "onnx: unknown tensor name %s", key); + return -EINVAL; + } + if (!spa_json_is_object(val, len)) { + spa_log_error(p->log, "onnx: tensors %s expects an object", key); + return -EINVAL; + } + spa_json_enter(it, &sub); + if ((res = parse_tensor_info(d, &sub, info)) < 0) + return res; + } + return 0; +} + +#define SET_VAL(data,type,val) \ +{ type _v = (type) (val); memcpy(data, &_v, sizeof(_v)); } \ + +static int set_value(void *data, enum ONNXTensorElementDataType type, double val) +{ + switch (type) { + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8: + SET_VAL(data, uint8_t, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8: + SET_VAL(data, int8_t, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT16: + SET_VAL(data, uint16_t, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT16: + SET_VAL(data, int16_t, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32: + SET_VAL(data, int32_t, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64: + SET_VAL(data, int64_t, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_BOOL: + SET_VAL(data, bool, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE: + SET_VAL(data, double, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT32: + SET_VAL(data, uint32_t, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT64: + SET_VAL(data, uint64_t, val); + break; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT: + SET_VAL(data, float, val); + break; + default: + return -ENOTSUP; + } + return 0; +} +/* + * config = { + * blocksize = 512 + * input-tensors = { + * + * ... + * } + * output-tensors = { + * + * ... + * } + * } + */ + +static void *onnx_instantiate(const struct spa_fga_plugin *plugin, const struct spa_fga_descriptor *desc, + unsigned long SampleRate, int index, const char *config) +{ + struct descriptor *d = (struct descriptor *)desc; + struct plugin *p = d->p; + struct instance *i; + OrtStatus *status; + size_t n, j; + int res; + + errno = EINVAL; + + i = calloc(1, sizeof(*i)); + if (i == NULL) + return NULL; + + i->desc = d; + i->rate = SampleRate; + + for (n = 0; n < d->n_tensors; n++) { + struct tensor_info *ti = &d->tensors[n]; + void *data; + + spa_log_debug(p->log, "%zd %s %zd", n, ti->name, ti->n_dimensions); + ti->data_size = 1; + for (j = 0; j < ti->n_dimensions; j++) { + spa_log_debug(p->log, "%zd %zd/%zd %"PRIi64, n, j, ti->n_dimensions, + ti->dimensions[j]); + if (ti->dimensions[j] != -1) + ti->data_size *= ti->dimensions[j]; + + } + CHECK(ort->CreateTensorAsOrtValue(p->allocator, ti->dimensions, ti->n_dimensions, + ti->type, &i->tensor[n])); + CHECK(ort->GetTensorMutableData(i->tensor[n], (void**)&data)); + + if (ti->data_type == DATA_PARAM_RATE) { + if ((res = set_value(data, ti->type, (double)i->rate)) < 0) { + errno = -res; + goto error; + } + + } + } + return i; + +error_onnx: + const char* msg = ort->GetErrorMessage(status); + spa_log_error(p->log, "%s", msg); + ort->ReleaseStatus(status); +error: + free(i); + return NULL; +} + +static void onnx_cleanup(void *instance) +{ + struct instance *i = instance; + free(i); +} + +static void onnx_free(const struct spa_fga_descriptor *desc) +{ + struct descriptor *d = (struct descriptor*)desc; + free((char*)d->desc.name); + free(d->desc.ports); + free(d); +} + +static void onnx_connect_port(void *instance, unsigned long port, void *data) +{ + struct instance *i = instance; + i->data[port] = data; +} + +static void move_samples(float *dst, uint32_t dst_offs, float *src, uint32_t src_offs, uint32_t n_samples) +{ + memmove(SPA_PTROFF(dst, dst_offs * sizeof(float), void), + SPA_PTROFF(src, src_offs * sizeof(float), void), n_samples * sizeof(float)); +} + +static void onnx_run(void *instance, unsigned long SampleCount) +{ + OrtStatus *status; + struct instance *i = instance; + struct descriptor *d = i->desc; + struct plugin *p = d->p; + const char *input_names[MAX_PORTS]; + const OrtValue *inputs[MAX_PORTS]; + const char *output_names[MAX_PORTS]; + OrtValue *outputs[MAX_PORTS]; + size_t n, n_inputs = 0, n_outputs = 0; + float *data; + uint32_t offset = i->offset, blocksize = d->blocksize; + + while (SampleCount > 0) { + uint32_t chunk = SPA_MIN(SampleCount, blocksize - offset); + uint32_t next_offset; + + for (n = 0; n < d->n_tensors; n++) { + struct tensor_info *ti = &d->tensors[n]; + if (ti->direction == SPA_DIRECTION_INPUT) { + input_names[n_inputs] = ti->name; + inputs[n_inputs++] = i->tensor[ti->index]; + + if (ti->data_type == DATA_PORT) { + CHECK(ort->GetTensorMutableData(i->tensor[ti->index], (void**)&data)); + + if (ti->retain > 0 && offset == 0) + move_samples(data, 0, data, ti->data_size - ti->retain, ti->retain); + + move_samples(data, ti->retain + offset, + i->data[ti->data_index], offset, chunk); + } + else if (ti->data_type == DATA_TENSOR) { + if (offset == 0) { + void *src, *dst; + CHECK(ort->GetTensorMutableData(i->tensor[ti->data_index], &src)); + CHECK(ort->GetTensorMutableData(i->tensor[ti->index], &dst)); + move_samples(dst, 0, src, 0, ti->data_size); + } + } + } else { + output_names[n_outputs] = ti->name; + outputs[n_outputs++] = i->tensor[ti->index]; + } + } + if (offset + chunk >= blocksize) { + CHECK(ort->Run(d->session, i->run_options, + input_names, (const OrtValue *const*)inputs, n_inputs, + output_names, n_outputs, (OrtValue **)outputs)); + next_offset = 0; + } else { + next_offset = offset + chunk; + } + + for (n = 0; n < d->n_tensors; n++) { + struct tensor_info *ti = &d->tensors[n]; + if (ti->direction != SPA_DIRECTION_OUTPUT) + continue; + + if (ti->data_type == DATA_CONTROL) { + if (next_offset == 0) { + float *src, *dst; + CHECK(ort->GetTensorMutableData(i->tensor[ti->index], (void**)&src)); + dst = i->data[ti->data_index]; + if (src && dst) + dst[0] = src[0]; + } + } + else if (ti->data_type == DATA_PORT) { + CHECK(ort->GetTensorMutableData(i->tensor[ti->index], (void**)&data)); + move_samples(i->data[ti->data_index], offset, data, offset, chunk); + } + } + SampleCount -= chunk; + offset = next_offset; + } + i->offset = offset; + return; + +error_onnx: + const char* msg = ort->GetErrorMessage(status); + spa_log_error(p->log, "%s", msg); + ort->ReleaseStatus(status); +} + +static const struct spa_fga_descriptor *onnx_plugin_make_desc(void *plugin, const char *name) +{ + OrtStatus *status; + struct plugin *p = (struct plugin *)plugin; + struct descriptor *desc; + size_t i, j, n_inputs, n_outputs; + OrtTypeInfo *tinfo; + const OrtTensorTypeAndShapeInfo *tt; + char path[PATH_MAX]; + struct spa_json it[2]; + const char *val; + int len; + char key[256]; + + if (spa_json_begin_object(&it[0], name, strlen(name)) <= 0) { + spa_log_error(p->log, "onnx: expected object in label"); + return NULL; + } + if (spa_json_str_object_find(name, strlen(name), "filename", path, sizeof(path)) <= 0) { + spa_log_error(p->log, "onnx: could not find filename in label"); + return NULL; + } + + desc = calloc(1, sizeof(*desc)); + if (desc == NULL) + return NULL; + + desc->p = p; + + desc->desc.instantiate = onnx_instantiate; + desc->desc.cleanup = onnx_cleanup; + desc->desc.free = onnx_free; + desc->desc.connect_port = onnx_connect_port; + desc->desc.run = onnx_run; + + desc->desc.name = strdup(name); + desc->desc.flags = 0; + + spa_log_info(p->log, "onnx: loading model %s", path); + CHECK(ort->CreateSession(p->env, path, p->session_options, &desc->session)); + + CHECK(ort->SessionGetInputCount(desc->session, &n_inputs)); + CHECK(ort->SessionGetOutputCount(desc->session, &n_outputs)); + + spa_log_info(p->log, "found %zd input and %zd output tensors", n_inputs, n_outputs); + + /* first go over all tensors and collect info */ + for (i = 0; i < n_inputs; i++) { + struct tensor_info *ti = &desc->tensors[i]; + + ti->index = i; + ti->direction = SPA_DIRECTION_INPUT; + CHECK(ort->SessionGetInputName(desc->session, i, p->allocator, (char**)&ti->name)); + + CHECK(ort->SessionGetInputTypeInfo(desc->session, i, &tinfo)); + CHECK(ort->CastTypeInfoToTensorInfo(tinfo, &tt)); + + CHECK(ort->GetTensorElementType(tt, &ti->type)); + CHECK(ort->GetDimensionsCount(tt, &ti->n_dimensions)); + if (ti->n_dimensions > SPA_N_ELEMENTS(ti->dimensions)) { + spa_log_warn(p->log, "too many dimensions"); + errno = ENOTSUP; + goto error; + } + CHECK(ort->GetDimensions(tt, ti->dimensions, ti->n_dimensions)); + + spa_log_debug(p->log, "%zd %s %zd", i, ti->name, ti->n_dimensions); + for (j = 0; j < ti->n_dimensions; j++) { + spa_log_debug(p->log, "%zd %zd/%zd %"PRIi64, i, j, ti->n_dimensions, + ti->dimensions[j]); + } + } + for (i = 0; i < n_outputs; i++) { + struct tensor_info *ti = &desc->tensors[i + n_inputs]; + + ti->index = i + n_inputs; + ti->direction = SPA_DIRECTION_OUTPUT; + CHECK(ort->SessionGetOutputName(desc->session, i, p->allocator, (char**)&ti->name)); + + CHECK(ort->SessionGetOutputTypeInfo(desc->session, i, &tinfo)); + CHECK(ort->CastTypeInfoToTensorInfo(tinfo, &tt)); + + CHECK(ort->GetTensorElementType(tt, &ti->type)); + CHECK(ort->GetDimensionsCount(tt, &ti->n_dimensions)); + if (ti->n_dimensions > SPA_N_ELEMENTS(ti->dimensions)) { + spa_log_error(p->log, "too many dimensions"); + errno = ENOTSUP; + goto error; + } + CHECK(ort->GetDimensions(tt, ti->dimensions, ti->n_dimensions)); + + spa_log_debug(p->log, "%zd %s %zd", i, ti->name, ti->n_dimensions); + for (j = 0; j < ti->n_dimensions; j++) { + spa_log_debug(p->log, "%zd %zd/%zd %"PRIi64, i, j, ti->n_dimensions, + ti->dimensions[j]); + } + } + desc->n_tensors = n_inputs + n_outputs; + + /* enhance the tensor info */ + while ((len = spa_json_object_next(&it[0], key, sizeof(key), &val)) > 0) { + if (spa_streq(key, "blocksize")) { + if (spa_json_parse_int(val, len, &desc->blocksize) <= 0) { + spa_log_error(p->log, "onnx:blocksize requires a number"); + errno = EINVAL; + goto error; + } + } + else if (spa_streq(key, "input-tensors")) { + if (!spa_json_is_object(val, len)) { + spa_log_error(p->log, "onnx: %s expects an object", key); + errno = EINVAL; + goto error; + } + spa_json_enter(&it[0], &it[1]); + parse_tensors(desc, &it[1], SPA_DIRECTION_INPUT); + } + else if (spa_streq(key, "output-tensors")) { + if (!spa_json_is_object(val, len)) { + spa_log_error(p->log, "onnx: %s expects an object", key); + errno = EINVAL; + goto error; + } + spa_json_enter(&it[0], &it[1]); + parse_tensors(desc, &it[1], SPA_DIRECTION_OUTPUT); + } + } + + desc->desc.ports = calloc(desc->n_tensors, sizeof(struct spa_fga_port)); + desc->desc.n_ports = 0; + + /* make ports */ + for (i = 0; i < desc->n_tensors; i++) { + struct tensor_info *ti = &desc->tensors[i]; + struct spa_fga_port *fp = &desc->desc.ports[desc->desc.n_ports]; + + fp->flags = 0; + fp->index = desc->desc.n_ports; + if (ti->type != ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT) + continue; + + if (ti->data_type == DATA_PORT) + fp->flags |= SPA_FGA_PORT_AUDIO; + else if (ti->data_type == DATA_CONTROL) + fp->flags |= SPA_FGA_PORT_CONTROL; + else + continue; + + if (ti->direction == SPA_DIRECTION_INPUT) + fp->flags |= SPA_FGA_PORT_INPUT; + else + fp->flags |= SPA_FGA_PORT_OUTPUT; + + fp->name = ti->data_name; + ti->data_index = desc->desc.n_ports; + + desc->desc.n_ports++; + if (desc->desc.n_ports > MAX_PORTS) { + spa_log_error(p->log, "too many ports"); + errno = -ENOSPC; + goto error; + } + } + return &desc->desc; + +error_onnx: + const char* msg = ort->GetErrorMessage(status); + spa_log_error(p->log, "%s", msg); + ort->ReleaseStatus(status); + +error: + if (desc->session) + ort->ReleaseSession(desc->session); + onnx_free(&desc->desc); + return NULL; +} + +static int load_model(struct plugin *impl, const char *path) +{ + OrtStatus *status; + + ort = OrtGetApiBase()->GetApi(ORT_API_VERSION); + if (ort == NULL) { + spa_log_error(impl->log, "Failed to init ONNX Runtime engine"); + return -EINVAL; + } + CHECK(ort->CreateEnv(ORT_LOGGING_LEVEL_WARNING, "onnx-filter-graph", &impl->env)); + CHECK(ort->GetAllocatorWithDefaultOptions(&impl->allocator)); + + CHECK(ort->CreateSessionOptions(&impl->session_options)); + CHECK(ort->SetIntraOpNumThreads(impl->session_options, 1)); + CHECK(ort->SetInterOpNumThreads(impl->session_options, 1)); + CHECK(ort->SetSessionGraphOptimizationLevel(impl->session_options, ORT_ENABLE_ALL)); + + return 0; + +error_onnx: + const char* msg = ort->GetErrorMessage(status); + spa_log_error(impl->log, "%s", msg); + ort->ReleaseStatus(status); + + if (impl->env) + ort->ReleaseEnv(impl->env); + impl->env = NULL; + if (impl->session_options) + ort->ReleaseSessionOptions(impl->session_options); + impl->session_options = NULL; + + return -EINVAL; +} + +static struct spa_fga_plugin_methods impl_plugin = { + SPA_VERSION_FGA_PLUGIN_METHODS, + .make_desc = onnx_plugin_make_desc, +}; + +static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +{ + struct plugin *impl; + + spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(interface != NULL, -EINVAL); + + impl = (struct plugin *) handle; + + if (spa_streq(type, SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin)) + *interface = &impl->plugin; + else + return -ENOENT; + + return 0; +} + +static int impl_clear(struct spa_handle *handle) +{ + return 0; +} + +static size_t +impl_get_size(const struct spa_handle_factory *factory, + const struct spa_dict *params) +{ + return sizeof(struct plugin); +} + +static int +impl_init(const struct spa_handle_factory *factory, + struct spa_handle *handle, + const struct spa_dict *info, + const struct spa_support *support, + uint32_t n_support) +{ + struct plugin *impl; + uint32_t i; + const char *path = NULL; + int res; + + handle->get_interface = impl_get_interface; + handle->clear = impl_clear; + + impl = (struct plugin *) handle; + + impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); + + for (i = 0; info && i < info->n_items; i++) { + const char *k = info->items[i].key; + const char *s = info->items[i].value; + if (spa_streq(k, "filter.graph.path")) + path = s; + } + + if ((res = load_model(impl, path)) < 0) + return res; + + impl->plugin.iface = SPA_INTERFACE_INIT( + SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin, + SPA_VERSION_FGA_PLUGIN, + &impl_plugin, impl); + + return 0; +} + +static const struct spa_interface_info impl_interfaces[] = { + { SPA_TYPE_INTERFACE_FILTER_GRAPH_AudioPlugin }, +}; + +static int +impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(info != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *info = &impl_interfaces[*index]; + break; + default: + return 0; + } + (*index)++; + return 1; +} + +static struct spa_handle_factory spa_fga_plugin_onnx_factory = { + SPA_VERSION_HANDLE_FACTORY, + "filter.graph.plugin.onnx", + NULL, + impl_get_size, + impl_init, + impl_enum_interface_info, +}; + +SPA_EXPORT +int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index) +{ + spa_return_val_if_fail(factory != NULL, -EINVAL); + spa_return_val_if_fail(index != NULL, -EINVAL); + + switch (*index) { + case 0: + *factory = &spa_fga_plugin_onnx_factory; + break; + default: + return 0; + } + (*index)++; + return 1; +} diff --git a/spa/plugins/filter-graph/sofa_plugin.c b/spa/plugins/filter-graph/plugin_sofa.c similarity index 96% rename from spa/plugins/filter-graph/sofa_plugin.c rename to spa/plugins/filter-graph/plugin_sofa.c index d348bc97a..7ec73ea2b 100644 --- a/spa/plugins/filter-graph/sofa_plugin.c +++ b/spa/plugins/filter-graph/plugin_sofa.c @@ -30,7 +30,7 @@ struct spatializer_impl { struct spa_log *log; unsigned long rate; - float *port[6]; + float *port[7]; int n_samples, blocksize, tailsize; float *tmp[2]; @@ -262,7 +262,7 @@ static void spatializer_reload(void * Instance) spa_log_error(impl->log, "reloading left or right convolver failed"); return; } - spa_loop_invoke(impl->plugin->data_loop, do_switch, 1, NULL, 0, true, impl); + spa_loop_locked(impl->plugin->data_loop, do_switch, 1, NULL, 0, impl); } struct free_data { @@ -312,14 +312,13 @@ static void spatializer_run(void * Instance, unsigned long SampleCount) convolver_run(impl->l_conv[0], impl->port[2], impl->port[0], SampleCount); convolver_run(impl->r_conv[0], impl->port[2], impl->port[1], SampleCount); } + impl->port[6][0] = impl->n_samples; } static void spatializer_connect_port(void * Instance, unsigned long Port, - float * DataLocation) + void * DataLocation) { struct spatializer_impl *impl = Instance; - if (Port > 5) - return; impl->port[Port] = DataLocation; } @@ -346,6 +345,12 @@ static void spatializer_control_changed(void * Instance) spatializer_reload(Instance); } +static void spatializer_activate(void * Instance) +{ + struct spatializer_impl *impl = Instance; + impl->port[6][0] = impl->n_samples; +} + static void spatializer_deactivate(void * Instance) { struct spatializer_impl *impl = Instance; @@ -385,17 +390,23 @@ static struct spa_fga_port spatializer_ports[] = { .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, .def = 1.0f, .min = 0.0f, .max = 100.0f }, + { .index = 6, + .name = "latency", + .hint = SPA_FGA_HINT_LATENCY, + .flags = SPA_FGA_PORT_OUTPUT | SPA_FGA_PORT_CONTROL, + }, }; static const struct spa_fga_descriptor spatializer_desc = { .name = "spatializer", - .n_ports = 6, + .n_ports = SPA_N_ELEMENTS(spatializer_ports), .ports = spatializer_ports, .instantiate = spatializer_instantiate, .connect_port = spatializer_connect_port, .control_changed = spatializer_control_changed, + .activate = spatializer_activate, .deactivate = spatializer_deactivate, .run = spatializer_run, .cleanup = spatializer_cleanup, diff --git a/spa/plugins/jack/jack-client.h b/spa/plugins/jack/jack-client.h index decd9aea4..8ae37f93c 100644 --- a/spa/plugins/jack/jack-client.h +++ b/spa/plugins/jack/jack-client.h @@ -5,16 +5,16 @@ #ifndef SPA_JACK_CLIENT_H #define SPA_JACK_CLIENT_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + struct spa_jack_client_events { #define SPA_VERSION_JACK_CLIENT_EVENTS 0 uint32_t version; diff --git a/spa/plugins/libcamera/libcamera-device.cpp b/spa/plugins/libcamera/libcamera-device.cpp index 5eb46a1ae..2d6532f82 100644 --- a/spa/plugins/libcamera/libcamera-device.cpp +++ b/spa/plugins/libcamera/libcamera-device.cpp @@ -4,10 +4,7 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ -#include "config.h" - -#include - +#include #include #include @@ -53,9 +50,7 @@ struct impl { std::string device_id); }; -} - -static const libcamera::Span cameraDevice(const Camera& camera) +const libcamera::Span cameraDevice(const Camera& camera) { if (auto devices = camera.properties().get(properties::SystemDevices)) return devices.value(); @@ -63,15 +58,12 @@ static const libcamera::Span cameraDevice(const Camera& camera) return {}; } -static std::string cameraModel(const Camera& camera) +std::string cameraModel(const Camera& camera) { - if (auto model = camera.properties().get(properties::Model)) - return std::move(model.value()); - - return camera.id(); + return std::string(camera.properties().get(properties::Model).value_or(camera.id())); } -static const char *cameraLoc(const Camera& camera) +const char *cameraLoc(const Camera& camera) { if (auto location = camera.properties().get(properties::Location)) { switch (location.value()) { @@ -87,7 +79,7 @@ static const char *cameraLoc(const Camera& camera) return nullptr; } -static const char *cameraRot(const Camera& camera) +const char *cameraRot(const Camera& camera) { if (auto rotation = camera.properties().get(properties::Rotation)) { switch (rotation.value()) { @@ -105,7 +97,7 @@ static const char *cameraRot(const Camera& camera) return nullptr; } -static int emit_info(struct impl *impl, bool full) +int emit_info(struct impl *impl, bool full) { struct spa_dict_item items[10]; struct spa_dict dict; @@ -145,7 +137,6 @@ static int emit_info(struct impl *impl, bool full) if (!device_numbers.empty()) { std::ostringstream s; - /* encode device numbers into a json array */ s << "[ "; for (const auto& devid : device_numbers) @@ -183,17 +174,17 @@ static int emit_info(struct impl *impl, bool full) return 0; } -static int impl_add_listener(void *object, - struct spa_hook *listener, - const struct spa_device_events *events, - void *data) +int impl_add_listener(void *object, + struct spa_hook *listener, + const struct spa_device_events *events, + void *data) { struct impl *impl = (struct impl*)object; struct spa_hook_list save; int res = 0; - spa_return_val_if_fail(impl != NULL, -EINVAL); - spa_return_val_if_fail(events != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); + spa_return_val_if_fail(events != nullptr, -EINVAL); spa_hook_list_isolate(&impl->hooks, &save, listener, events, data); @@ -205,32 +196,32 @@ static int impl_add_listener(void *object, return res; } -static int impl_sync(void *object, int seq) +int impl_sync(void *object, int seq) { struct impl *impl = (struct impl*) object; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); - spa_device_emit_result(&impl->hooks, seq, 0, 0, NULL); + spa_device_emit_result(&impl->hooks, seq, 0, 0, nullptr); return 0; } -static int impl_enum_params(void *object, int seq, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) +int impl_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) { return -ENOTSUP; } -static int impl_set_param(void *object, - uint32_t id, uint32_t flags, - const struct spa_pod *param) +int impl_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) { return -ENOTSUP; } -static const struct spa_device_methods impl_device = { +const struct spa_device_methods impl_device = { .version = SPA_VERSION_DEVICE_METHODS, .add_listener = impl_add_listener, .sync = impl_sync, @@ -238,14 +229,12 @@ static const struct spa_device_methods impl_device = { .set_param = impl_set_param, }; -static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) { - struct impl *impl; + auto *impl = reinterpret_cast(handle); - spa_return_val_if_fail(handle != NULL, -EINVAL); - spa_return_val_if_fail(interface != NULL, -EINVAL); - - impl = (struct impl *) handle; + spa_return_val_if_fail(handle != nullptr, -EINVAL); + spa_return_val_if_fail(interface != nullptr, -EINVAL); if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) *interface = &impl->device; @@ -255,7 +244,7 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void return 0; } -static int impl_clear(struct spa_handle *handle) +int impl_clear(struct spa_handle *handle) { std::destroy_at(reinterpret_cast(handle)); return 0; @@ -281,14 +270,14 @@ impl::impl(spa_log *log, &impl_device, this); } -static size_t +size_t impl_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params) { return sizeof(struct impl); } -static int +int impl_init(const struct spa_handle_factory *factory, struct spa_handle *handle, const struct spa_dict *info, @@ -298,8 +287,8 @@ impl_init(const struct spa_handle_factory *factory, const char *str; int res; - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(factory != nullptr, -EINVAL); + spa_return_val_if_fail(handle != nullptr, -EINVAL); auto log = static_cast(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log)); @@ -324,17 +313,17 @@ impl_init(const struct spa_handle_factory *factory, return 0; } -static const struct spa_interface_info impl_interfaces[] = { +const struct spa_interface_info impl_interfaces[] = { {SPA_TYPE_INTERFACE_Device,}, }; -static int impl_enum_interface_info(const struct spa_handle_factory *factory, - const struct spa_interface_info **info, - uint32_t *index) +int impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) { - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(info != NULL, -EINVAL); - spa_return_val_if_fail(index != NULL, -EINVAL); + spa_return_val_if_fail(factory != nullptr, -EINVAL); + spa_return_val_if_fail(info != nullptr, -EINVAL); + spa_return_val_if_fail(index != nullptr, -EINVAL); if (*index >= SPA_N_ELEMENTS(impl_interfaces)) return 0; @@ -343,11 +332,13 @@ static int impl_enum_interface_info(const struct spa_handle_factory *factory, return 1; } +} + extern "C" { const struct spa_handle_factory spa_libcamera_device_factory = { SPA_VERSION_HANDLE_FACTORY, SPA_NAME_API_LIBCAMERA_DEVICE, - NULL, + nullptr, impl_get_size, impl_init, impl_enum_interface_info, diff --git a/spa/plugins/libcamera/libcamera-manager.cpp b/spa/plugins/libcamera/libcamera-manager.cpp index db2c73d54..5c1620571 100644 --- a/spa/plugins/libcamera/libcamera-manager.cpp +++ b/spa/plugins/libcamera/libcamera-manager.cpp @@ -2,13 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ -#include -#include -#include -#include -#include -#include -#include +#include +#include #include #include #include @@ -17,8 +12,6 @@ #include #include -#include - using namespace libcamera; #include @@ -35,16 +28,15 @@ using namespace libcamera; #include "libcamera.h" #include "libcamera-manager.hpp" -#define MAX_DEVICES 64 - namespace { struct device { - uint32_t id; std::shared_ptr camera; }; struct impl { + static constexpr std::size_t max_devices = 64; + struct spa_handle handle; struct spa_device device = {}; @@ -57,11 +49,7 @@ struct impl { struct spa_device_info info = SPA_DEVICE_INFO_INIT(); std::shared_ptr manager; - void addCamera(std::shared_ptr camera); - void removeCamera(std::shared_ptr camera); - - struct device devices[MAX_DEVICES]; - uint32_t n_devices = 0; + struct device devices[max_devices]; struct hotplug_event { enum class type { add, remove } type; @@ -72,82 +60,64 @@ struct impl { std::queue hotplug_events; struct spa_source *hotplug_event_source; - impl(spa_log *log, spa_loop_utils *loop_utils, spa_source *hotplug_event_source); + impl(spa_log *log, spa_loop_utils *loop_utils, spa_source *hotplug_event_source, + std::shared_ptr&& manager); ~impl() { + manager->cameraAdded.disconnect(this); + manager->cameraRemoved.disconnect(this); spa_loop_utils_destroy_source(loop_utils, hotplug_event_source); } + + std::uint32_t id_of(const struct device& d) const + { + spa_assert(std::begin(devices) <= &d && &d < std::end(devices)); + return &d - std::begin(devices); + } + +private: + void queue_hotplug_event(enum hotplug_event::type type, std::shared_ptr&& camera) + { + { + std::lock_guard guard(hotplug_events_lock); + hotplug_events.push({ type, std::move(camera) }); + } + + spa_loop_utils_signal_event(loop_utils, hotplug_event_source); + } }; -} - -static std::weak_ptr global_manager; - -std::shared_ptr libcamera_manager_acquire(int& res) +struct device *add_device(struct impl *impl, std::shared_ptr camera) { - if (auto manager = global_manager.lock()) - return manager; - - auto manager = std::make_shared(); - if ((res = manager->start()) < 0) - return {}; - - global_manager = manager; - - return manager; -} -static uint32_t get_free_id(struct impl *impl) -{ - for (std::size_t i = 0; i < MAX_DEVICES; i++) - if (impl->devices[i].camera == nullptr) - return i; - return 0; -} - -static struct device *add_device(struct impl *impl, std::shared_ptr camera) -{ - struct device *device; - uint32_t id; - - if (impl->n_devices >= MAX_DEVICES) - return NULL; - id = get_free_id(impl); - device = &impl->devices[id]; - device->id = id; - device->camera = std::move(camera); - impl->n_devices++; - return device; -} - -static struct device *find_device(struct impl *impl, const Camera *camera) -{ - uint32_t i; - for (i = 0; i < impl->n_devices; i++) { - if (impl->devices[i].camera.get() == camera) - return &impl->devices[i]; + for (auto& d : impl->devices) { + if (!d.camera) { + d.camera = std::move(camera); + return &d; + } } - return NULL; + + return nullptr; } -static void remove_device(struct impl *impl, struct device *device) +struct device *find_device(struct impl *impl, const Camera *camera) { - uint32_t old = --impl->n_devices; - device->camera.reset(); - *device = std::move(impl->devices[old]); - impl->devices[old].camera = nullptr; + for (auto& d : impl->devices) { + if (d.camera.get() == camera) + return &d; + } + + return nullptr; } -static void clear_devices(struct impl *impl) +void remove_device(struct impl *impl, struct device *device) { - while (impl->n_devices > 0) - impl->devices[--impl->n_devices].camera.reset(); + *device = {}; } -static int emit_object_info(struct impl *impl, struct device *device) +int emit_object_info(struct impl *impl, const struct device *device) { struct spa_device_object_info info; - uint32_t id = device->id; struct spa_dict_item items[20]; struct spa_dict dict; uint32_t n_items = 0; @@ -169,40 +139,42 @@ static int emit_object_info(struct impl *impl, struct device *device) dict = SPA_DICT_INIT(items, n_items); info.props = &dict; - spa_device_emit_object_info(&impl->hooks, id, &info); + spa_device_emit_object_info(&impl->hooks, impl->id_of(*device), &info); return 1; } -static void try_add_camera(struct impl *impl, std::shared_ptr camera) +void try_add_camera(struct impl *impl, std::shared_ptr camera) { struct device *device; - if ((device = find_device(impl, camera.get())) != NULL) + if ((device = find_device(impl, camera.get())) != nullptr) return; - if ((device = add_device(impl, std::move(camera))) == NULL) + if ((device = add_device(impl, std::move(camera))) == nullptr) return; - spa_log_info(impl->log, "camera added: id:%d %s", device->id, - device->camera->id().c_str()); + spa_log_info(impl->log, "camera added: id:%" PRIu32 " %s", + impl->id_of(*device), device->camera->id().c_str()); emit_object_info(impl, device); } -static void try_remove_camera(struct impl *impl, const Camera *camera) +void try_remove_camera(struct impl *impl, const Camera *camera) { struct device *device; - if ((device = find_device(impl, camera)) == NULL) + if ((device = find_device(impl, camera)) == nullptr) return; - spa_log_info(impl->log, "camera removed: id:%d %s", device->id, - device->camera->id().c_str()); - spa_device_emit_object_info(&impl->hooks, device->id, NULL); + auto id = impl->id_of(*device); + + spa_log_info(impl->log, "camera removed: id:%" PRIu32 " %s", + id, device->camera->id().c_str()); + spa_device_emit_object_info(&impl->hooks, id, nullptr); remove_device(impl, device); } -static void consume_hotplug_event(struct impl *impl, impl::hotplug_event& event) +void consume_hotplug_event(struct impl *impl, impl::hotplug_event& event) { auto& [ type, camera ] = event; @@ -218,7 +190,7 @@ static void consume_hotplug_event(struct impl *impl, impl::hotplug_event& event) } } -static void on_hotplug_event(void *data, std::uint64_t) +void on_hotplug_event(void *data, std::uint64_t) { auto impl = static_cast(data); @@ -241,56 +213,12 @@ static void on_hotplug_event(void *data, std::uint64_t) } } -void impl::addCamera(std::shared_ptr camera) -{ - { - std::unique_lock guard(hotplug_events_lock); - hotplug_events.push({ hotplug_event::type::add, std::move(camera) }); - } - - spa_loop_utils_signal_event(loop_utils, hotplug_event_source); -} - -void impl::removeCamera(std::shared_ptr camera) -{ - { - std::unique_lock guard(hotplug_events_lock); - hotplug_events.push({ hotplug_event::type::remove, std::move(camera) }); - } - - spa_loop_utils_signal_event(loop_utils, hotplug_event_source); -} - -static void start_monitor(struct impl *impl) -{ - impl->manager->cameraAdded.connect(impl, &impl::addCamera); - impl->manager->cameraRemoved.connect(impl, &impl::removeCamera); -} - -static int stop_monitor(struct impl *impl) -{ - if (impl->manager) { - impl->manager->cameraAdded.disconnect(impl, &impl::addCamera); - impl->manager->cameraRemoved.disconnect(impl, &impl::removeCamera); - } - clear_devices (impl); - return 0; -} - -static void collect_existing_devices(struct impl *impl) -{ - auto cameras = impl->manager->cameras(); - - for (std::shared_ptr& camera : cameras) - try_add_camera(impl, std::move(camera)); -} - -static const struct spa_dict_item device_info_items[] = { +const struct spa_dict_item device_info_items[] = { { SPA_KEY_DEVICE_API, "libcamera" }, { SPA_KEY_DEVICE_NICK, "libcamera-manager" }, }; -static void emit_device_info(struct impl *impl, bool full) +void emit_device_info(struct impl *impl, bool full) { uint64_t old = full ? impl->info.change_mask : 0; if (full) @@ -304,64 +232,41 @@ static void emit_device_info(struct impl *impl, bool full) } } -static void impl_hook_removed(struct spa_hook *hook) -{ - struct impl *impl = (struct impl*)hook->priv; - if (spa_hook_list_is_empty(&impl->hooks)) { - stop_monitor(impl); - impl->manager.reset(); - } -} - -static int +int impl_device_add_listener(void *object, struct spa_hook *listener, const struct spa_device_events *events, void *data) { - int res; struct impl *impl = (struct impl*) object; struct spa_hook_list save; - bool had_manager = !!impl->manager; - spa_return_val_if_fail(impl != NULL, -EINVAL); - spa_return_val_if_fail(events != NULL, -EINVAL); - - if (!impl->manager && !(impl->manager = libcamera_manager_acquire(res))) - return res; + spa_return_val_if_fail(impl != nullptr, -EINVAL); + spa_return_val_if_fail(events != nullptr, -EINVAL); spa_hook_list_isolate(&impl->hooks, &save, listener, events, data); emit_device_info(impl, true); - if (had_manager) { - for (std::size_t i = 0; i < impl->n_devices; i++) - emit_object_info(impl, &impl->devices[i]); - } - else { - collect_existing_devices(impl); - start_monitor(impl); + for (const auto& d : impl->devices) { + if (d.camera) + emit_object_info(impl, &d); } spa_hook_list_join(&impl->hooks, &save); - listener->removed = impl_hook_removed; - listener->priv = impl; - return 0; } -static const struct spa_device_methods impl_device = { +const struct spa_device_methods impl_device = { .version = SPA_VERSION_DEVICE_METHODS, .add_listener = impl_device_add_listener, }; -static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) { - struct impl *impl; + auto *impl = reinterpret_cast(handle); - spa_return_val_if_fail(handle != NULL, -EINVAL); - spa_return_val_if_fail(interface != NULL, -EINVAL); - - impl = (struct impl *) handle; + spa_return_val_if_fail(handle != nullptr, -EINVAL); + spa_return_val_if_fail(interface != nullptr, -EINVAL); if (spa_streq(type, SPA_TYPE_INTERFACE_Device)) *interface = &impl->device; @@ -371,20 +276,21 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void return 0; } -static int impl_clear(struct spa_handle *handle) +int impl_clear(struct spa_handle *handle) { auto impl = reinterpret_cast(handle); - stop_monitor(impl); std::destroy_at(impl); return 0; } -impl::impl(spa_log *log, spa_loop_utils *loop_utils, spa_source *hotplug_event_source) +impl::impl(spa_log *log, spa_loop_utils *loop_utils, spa_source *hotplug_event_source, + std::shared_ptr&& manager) : handle({ SPA_VERSION_HANDLE, impl_get_interface, impl_clear }), log(log), loop_utils(loop_utils), + manager(std::move(manager)), hotplug_event_source(hotplug_event_source) { libcamera_log_topic_init(log); @@ -395,24 +301,35 @@ impl::impl(spa_log *log, spa_loop_utils *loop_utils, spa_source *hotplug_event_s SPA_TYPE_INTERFACE_Device, SPA_VERSION_DEVICE, &impl_device, this); + + this->manager->cameraAdded.connect(this, [this](std::shared_ptr camera) { + queue_hotplug_event(hotplug_event::type::add, std::move(camera)); + }); + + this->manager->cameraRemoved.connect(this, [this](std::shared_ptr camera) { + queue_hotplug_event(hotplug_event::type::remove, std::move(camera)); + }); + + for (auto&& camera : this->manager->cameras()) + try_add_camera(this, std::move(camera)); } -static size_t +size_t impl_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params) { return sizeof(struct impl); } -static int +int impl_init(const struct spa_handle_factory *factory, struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support) { - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(factory != nullptr, -EINVAL); + spa_return_val_if_fail(handle != nullptr, -EINVAL); auto log = static_cast(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log)); @@ -429,23 +346,30 @@ impl_init(const struct spa_handle_factory *factory, return res; } - new (handle) impl(log, loop_utils, hotplug_event_source); + int res = 0; + auto manager = libcamera_manager_acquire(res); + if (!manager) { + spa_log_error(log, "failed to start camera manager: %s", spa_strerror(res)); + return res; + } + + new (handle) impl(log, loop_utils, hotplug_event_source, std::move(manager)); return 0; } -static const struct spa_interface_info impl_interfaces[] = { +const struct spa_interface_info impl_interfaces[] = { {SPA_TYPE_INTERFACE_Device,}, }; -static int +int impl_enum_interface_info(const struct spa_handle_factory *factory, const struct spa_interface_info **info, uint32_t *index) { - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(info != NULL, -EINVAL); - spa_return_val_if_fail(index != NULL, -EINVAL); + spa_return_val_if_fail(factory != nullptr, -EINVAL); + spa_return_val_if_fail(info != nullptr, -EINVAL); + spa_return_val_if_fail(index != nullptr, -EINVAL); if (*index >= SPA_N_ELEMENTS(impl_interfaces)) return 0; @@ -454,13 +378,34 @@ impl_enum_interface_info(const struct spa_handle_factory *factory, return 1; } +} + extern "C" { const struct spa_handle_factory spa_libcamera_manager_factory = { SPA_VERSION_HANDLE_FACTORY, SPA_NAME_API_LIBCAMERA_ENUM_MANAGER, - NULL, + nullptr, impl_get_size, impl_init, impl_enum_interface_info, }; } + +std::shared_ptr libcamera_manager_acquire(int& res) +{ + static std::weak_ptr global_manager; + static std::mutex lock; + + std::lock_guard guard(lock); + + if (auto manager = global_manager.lock()) + return manager; + + auto manager = std::make_shared(); + if ((res = manager->start()) < 0) + return {}; + + global_manager = manager; + + return manager; +} diff --git a/spa/plugins/libcamera/libcamera-source.cpp b/spa/plugins/libcamera/libcamera-source.cpp index f570499d1..08f92c5d7 100644 --- a/spa/plugins/libcamera/libcamera-source.cpp +++ b/spa/plugins/libcamera/libcamera-source.cpp @@ -1,14 +1,17 @@ /* Spa libcamera source */ /* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ /* @author Raghavendra Rao Sidlagatta */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ -#include -#include -#include -#include -#include +#include +#include +#include #include +#include +#include + +#include #include #include @@ -29,9 +32,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -48,8 +53,6 @@ namespace { #define MASK_BUFFERS 31 #define BUFFER_FLAG_OUTSTANDING (1<<0) -#define BUFFER_FLAG_ALLOCATED (1<<1) -#define BUFFER_FLAG_MAPPED (1<<2) struct buffer { uint32_t id; @@ -58,7 +61,6 @@ struct buffer { struct spa_buffer *outbuf; struct spa_meta_header *h; struct spa_meta_videotransform *videotransform; - void *ptr; }; struct port { @@ -69,20 +71,19 @@ struct port { struct spa_fraction rate = {}; StreamConfiguration streamConfig; - uint32_t memtype = 0; + spa_data_type memtype = SPA_DATA_Invalid; uint32_t buffers_blocks = 1; struct buffer buffers[MAX_BUFFERS]; uint32_t n_buffers = 0; struct spa_list queue; - struct spa_ringbuffer ring = SPA_RINGBUFFER_INIT(); - uint32_t ring_ids[MAX_BUFFERS]; static constexpr uint64_t info_all = SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_PROPS | SPA_PORT_CHANGE_MASK_PARAMS; struct spa_port_info info = SPA_PORT_INFO_INIT(); struct spa_io_buffers *io = nullptr; struct spa_io_sequence *control = nullptr; + uint32_t control_size; #define PORT_PropInfo 0 #define PORT_EnumFormat 1 #define PORT_Meta 2 @@ -93,9 +94,8 @@ struct port { #define N_PORT_PARAMS 7 struct spa_param_info params[N_PORT_PARAMS]; - uint32_t fmt_index = 0; - PixelFormat enum_fmt; - uint32_t size_index = 0; + std::size_t fmt_index = 0; + std::size_t size_index = 0; port(struct impl *impl) : impl(impl) @@ -136,9 +136,6 @@ struct impl { #define N_NODE_PARAMS 4 struct spa_param_info params[N_NODE_PARAMS]; - std::string device_id; - std::string device_name; - struct spa_hook_list hooks; struct spa_callbacks callbacks = {}; @@ -151,41 +148,1259 @@ struct impl { std::shared_ptr manager; std::shared_ptr camera; + const std::unique_ptr config; - FrameBufferAllocator *allocator = nullptr; + FrameBufferAllocator allocator; std::vector> requestPool; - std::deque pendingRequests; + spa_ringbuffer completed_requests_rb = SPA_RINGBUFFER_INIT(); + std::array completed_requests; void requestComplete(libcamera::Request *request); - std::unique_ptr config; - struct spa_source source = {}; ControlList ctrls; + ControlList initial_controls; bool active = false; bool acquired = false; impl(spa_log *log, spa_loop *data_loop, spa_system *system, - std::shared_ptr manager, std::shared_ptr camera, std::string device_id); + std::shared_ptr manager, std::shared_ptr camera, + std::unique_ptr config); struct spa_dll dll; -}; -} + void stop() + { + spa_loop_locked( + data_loop, + [](spa_loop *, bool, uint32_t, const void *, size_t, void *user_data) { + auto *self = static_cast(user_data); + + if (self->source.loop) + spa_loop_remove_source(self->data_loop, &self->source); + + return 0; + }, + 0, nullptr, 0, this + ); + + if (source.fd >= 0) + spa_system_close(system, std::exchange(source.fd, -1)); + + camera->requestCompleted.disconnect(this, &impl::requestComplete); + + if (int res = camera->stop(); res < 0) { + spa_log_warn(log, "failed to stop camera %s: %s", + camera->id().c_str(), spa_strerror(res)); + } + + completed_requests_rb = SPA_RINGBUFFER_INIT(); + active = false; + + for (auto& p : out_ports) + spa_list_init(&p.queue); + } +}; #define CHECK_PORT(impl,direction,port_id) ((direction) == SPA_DIRECTION_OUTPUT && (port_id) == 0) #define GET_OUT_PORT(impl,p) (&impl->out_ports[p]) #define GET_PORT(impl,d,p) GET_OUT_PORT(impl,p) -#include "libcamera-utils.cpp" +void setup_initial_controls(const ControlInfoMap& ctrl_infos, ControlList& ctrls) +{ + /* Libcamera recommends cameras default to manual focus mode, but we don't + * expose any focus controls. So, specifically enable autofocus on + * cameras which support it. */ + auto af_it = ctrl_infos.find(libcamera::controls::AF_MODE); + if (af_it != ctrl_infos.end()) { + const ControlInfo &ctrl_info = af_it->second; + auto is_af_continuous = [](const ControlValue &value) { + return value.get() == libcamera::controls::AfModeContinuous; + }; + if (std::any_of(ctrl_info.values().begin(), + ctrl_info.values().end(), is_af_continuous)) { + ctrls.set(libcamera::controls::AF_MODE, + libcamera::controls::AfModeContinuous); + } + } -static int port_get_format(struct impl *impl, struct port *port, - uint32_t index, - const struct spa_pod *filter, - struct spa_pod **param, - struct spa_pod_builder *builder) + auto ae_it = ctrl_infos.find(libcamera::controls::AE_ENABLE); + if (ae_it != ctrl_infos.end()) { + ctrls.set(libcamera::controls::AE_ENABLE, true); + } +} + +int spa_libcamera_open(struct impl *impl) +{ + if (impl->acquired) + return 0; + + spa_log_info(impl->log, "open camera %s", impl->camera->id().c_str()); + + if (int res = impl->camera->acquire(); res < 0) + return res; + + spa_assert(!impl->allocator.allocated()); + + const ControlInfoMap &controls = impl->camera->controls(); + setup_initial_controls(controls, impl->initial_controls); + + impl->acquired = true; + return 0; +} + +int spa_libcamera_close(struct impl *impl) +{ + struct port *port = &impl->out_ports[0]; + if (!impl->acquired) + return 0; + if (impl->active || port->current_format) + return 0; + + spa_log_info(impl->log, "close camera %s", impl->camera->id().c_str()); + + spa_assert(!impl->allocator.allocated()); + + impl->camera->release(); + + impl->acquired = false; + return 0; +} + +int spa_libcamera_buffer_recycle(struct impl *impl, struct port *port, uint32_t buffer_id) +{ + struct buffer *b = &port->buffers[buffer_id]; + int res; + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) + return 0; + + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUTSTANDING); + + if (buffer_id >= impl->requestPool.size()) { + spa_log_warn(impl->log, "invalid buffer_id %u >= %zu", + buffer_id, impl->requestPool.size()); + return -EINVAL; + } + Request *request = impl->requestPool[buffer_id].get(); + if (impl->active) { + request->controls().merge(impl->ctrls); + impl->ctrls.clear(); + if ((res = impl->camera->queueRequest(request)) < 0) { + spa_log_warn(impl->log, "can't queue buffer %u: %s", + buffer_id, spa_strerror(res)); + return res == -EACCES ? -EBUSY : res; + } + } + return 0; +} + +void freeBuffers(struct impl *impl, struct port *port) +{ + impl->requestPool.clear(); + std::ignore = impl->allocator.free(port->streamConfig.stream()); +} + +[[nodiscard]] +std::size_t count_unique_fds(libcamera::Span planes) +{ + std::size_t c = 0; + int fd = -1; + + for (const auto& plane : planes) { + const int current_fd = plane.fd.get(); + if (current_fd >= 0 && current_fd != fd) { + c += 1; + fd = current_fd; + } + } + + return c; +} + +int allocBuffers(struct impl *impl, struct port *port, unsigned int count) +{ + libcamera::Stream *stream = port->streamConfig.stream(); + int res; + + if (!impl->requestPool.empty()) + return -EBUSY; + + if ((res = impl->allocator.allocate(stream)) < 0) + return res; + + const auto& bufs = impl->allocator.buffers(stream); + if (bufs.empty() || bufs.size() != count) { + res = -ENOBUFS; + goto err; + } + + for (std::size_t i = 0; i < bufs.size(); i++) { + std::unique_ptr request = impl->camera->createRequest(i); + if (!request) { + res = -ENOMEM; + goto err; + } + + res = request->addBuffer(stream, bufs[i].get()); + if (res < 0) + goto err; + + impl->requestPool.push_back(std::move(request)); + } + + /* Some devices require data for each output video frame to be + * placed in discontiguous memory buffers. In such cases, one + * video frame has to be addressed using more than one memory. + * address. Therefore, need calculate the number of discontiguous + * memory and allocate the specified amount of memory */ + port->buffers_blocks = count_unique_fds(bufs.front()->planes()); + if (port->buffers_blocks <= 0) { + res = -ENOBUFS; + goto err; + } + + return 0; + +err: + freeBuffers(impl, port); + + return res; +} + +int spa_libcamera_clear_buffers(struct port *port) +{ + for (std::size_t i = 0; i < port->n_buffers; i++) { + buffer *b = &port->buffers[i]; + spa_buffer *sb = b->outbuf; + + for (std::size_t j = 0; j < sb->n_datas; j++) { + auto *d = &sb->datas[j]; + + d->type = SPA_ID_INVALID; + d->data = nullptr; + d->fd = -1; + } + + *b = {}; + } + + port->n_buffers = 0; + + return 0; +} + +struct format_info { + PixelFormat pix; + spa_video_format format; + spa_media_type media_type; + spa_media_subtype media_subtype; +}; + +#define MAKE_FMT(pix,fmt,mt,mst) { pix, SPA_VIDEO_FORMAT_ ##fmt, SPA_MEDIA_TYPE_ ##mt, SPA_MEDIA_SUBTYPE_ ##mst } +const struct format_info format_info[] = { + /* RGB formats */ + MAKE_FMT(formats::R8, GRAY8, video, raw), + MAKE_FMT(formats::RGB565, RGB16, video, raw), + MAKE_FMT(formats::RGB565_BE, RGB16, video, raw), + MAKE_FMT(formats::RGB888, BGR, video, raw), + MAKE_FMT(formats::BGR888, RGB, video, raw), + MAKE_FMT(formats::XRGB8888, BGRx, video, raw), + MAKE_FMT(formats::XBGR8888, RGBx, video, raw), + MAKE_FMT(formats::RGBX8888, xBGR, video, raw), + MAKE_FMT(formats::BGRX8888, xRGB, video, raw), + MAKE_FMT(formats::ARGB8888, BGRA, video, raw), + MAKE_FMT(formats::ABGR8888, RGBA, video, raw), + MAKE_FMT(formats::RGBA8888, ABGR, video, raw), + MAKE_FMT(formats::BGRA8888, ARGB, video, raw), + + MAKE_FMT(formats::YUYV, YUY2, video, raw), + MAKE_FMT(formats::YVYU, YVYU, video, raw), + MAKE_FMT(formats::UYVY, UYVY, video, raw), + MAKE_FMT(formats::VYUY, VYUY, video, raw), + + MAKE_FMT(formats::NV12, NV12, video, raw), + MAKE_FMT(formats::NV21, NV21, video, raw), + MAKE_FMT(formats::NV16, NV16, video, raw), + MAKE_FMT(formats::NV61, NV61, video, raw), + MAKE_FMT(formats::NV24, NV24, video, raw), + + MAKE_FMT(formats::YUV420, I420, video, raw), + MAKE_FMT(formats::YVU420, YV12, video, raw), + MAKE_FMT(formats::YUV422, Y42B, video, raw), + + MAKE_FMT(formats::MJPEG, ENCODED, video, mjpg), +#undef MAKE_FMT +}; + +const struct format_info *video_format_to_info(const PixelFormat &pix) +{ + for (const auto& f : format_info) { + if (f.pix == pix) + return &f; + } + + return nullptr; +} + +const struct format_info *find_format_info_by_media_type( + uint32_t type, uint32_t subtype, uint32_t format) +{ + for (const auto& f : format_info) { + if (f.media_type == type && f.media_subtype == subtype + && (f.format == SPA_VIDEO_FORMAT_UNKNOWN || f.format == format)) + return &f; + } + + return nullptr; +} + +int score_size(const Size &a, const Size &b) +{ + int x, y; + x = (int)a.width - (int)b.width; + y = (int)a.height - (int)b.height; + return x * x + y * y; +} + +[[nodiscard]] +spa_video_colorimetry +color_space_to_colorimetry(const libcamera::ColorSpace& colorspace) +{ + spa_video_colorimetry res = {}; + + switch (colorspace.range) { + case ColorSpace::Range::Full: + res.range = SPA_VIDEO_COLOR_RANGE_0_255; + break; + case ColorSpace::Range::Limited: + res.range = SPA_VIDEO_COLOR_RANGE_16_235; + break; + } + + switch (colorspace.ycbcrEncoding) { + case ColorSpace::YcbcrEncoding::None: + res.matrix = SPA_VIDEO_COLOR_MATRIX_RGB; + break; + case ColorSpace::YcbcrEncoding::Rec601: + res.matrix = SPA_VIDEO_COLOR_MATRIX_BT601; + break; + case ColorSpace::YcbcrEncoding::Rec709: + res.matrix = SPA_VIDEO_COLOR_MATRIX_BT709; + break; + case ColorSpace::YcbcrEncoding::Rec2020: + res.matrix = SPA_VIDEO_COLOR_MATRIX_BT2020; + break; + } + + switch (colorspace.transferFunction) { + case ColorSpace::TransferFunction::Linear: + res.transfer = SPA_VIDEO_TRANSFER_GAMMA10; + break; + case ColorSpace::TransferFunction::Srgb: + res.transfer = SPA_VIDEO_TRANSFER_SRGB; + break; + case ColorSpace::TransferFunction::Rec709: + res.transfer = SPA_VIDEO_TRANSFER_BT709; + break; + } + + switch (colorspace.primaries) { + case ColorSpace::Primaries::Raw: + res.primaries = SPA_VIDEO_COLOR_PRIMARIES_UNKNOWN; + break; + case ColorSpace::Primaries::Smpte170m: + res.primaries = SPA_VIDEO_COLOR_PRIMARIES_SMPTE170M; + break; + case ColorSpace::Primaries::Rec709: + res.primaries = SPA_VIDEO_COLOR_PRIMARIES_BT709; + break; + case ColorSpace::Primaries::Rec2020: + res.primaries = SPA_VIDEO_COLOR_PRIMARIES_BT2020; + break; + } + + return res; +} + +int +spa_libcamera_enum_format(struct impl *impl, struct port *port, int seq, + uint32_t start, uint32_t num, const struct spa_pod *filter) +{ + uint8_t buffer[1024]; + struct spa_pod_builder b = { 0 }; + struct spa_pod_frame f[2]; + struct spa_result_node_params result; + uint32_t count = 0; + + const StreamConfiguration& streamConfig = impl->config->at(0); + const StreamFormats &formats = streamConfig.formats(); + const auto &pixel_formats = formats.pixelformats(); + + result.id = SPA_PARAM_EnumFormat; + result.next = start; + + if (result.next == 0) { + port->fmt_index = 0; + port->size_index = 0; + } +next: + result.index = result.next++; + +next_fmt: + if (port->fmt_index >= pixel_formats.size()) + return 0; + + auto format = pixel_formats[port->fmt_index]; + spa_log_debug(impl->log, "format: %s", format.toString().c_str()); + + const auto *info = video_format_to_info(format); + if (info == nullptr) { + spa_log_debug(impl->log, "unknown format"); + port->fmt_index++; + goto next_fmt; + } + + const auto& sizes = formats.sizes(format); + SizeRange sizeRange; + Size frameSize; + + if (!sizes.empty() && port->size_index <= sizes.size()) { + if (port->size_index == 0) { + Size wanted = Size(640, 480); + int best = std::numeric_limits::max(); + + for (const auto& test : sizes) { + int score = score_size(wanted, test); + if (score < best) { + best = score; + frameSize = test; + } + } + } + else { + frameSize = sizes[port->size_index - 1]; + } + } else if (port->size_index < 1) { + sizeRange = formats.range(format); + if (sizeRange.hStep == 0 || sizeRange.vStep == 0) { + port->size_index = 0; + port->fmt_index++; + goto next_fmt; + } + } else { + port->size_index = 0; + port->fmt_index++; + goto next_fmt; + } + port->size_index++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(&b, + SPA_FORMAT_mediaType, SPA_POD_Id(info->media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(info->media_subtype), + 0); + + if (info->media_subtype == SPA_MEDIA_SUBTYPE_raw) { + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_format, 0); + spa_pod_builder_id(&b, info->format); + } + if (info->pix.modifier()) { + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_modifier, 0); + spa_pod_builder_long(&b, info->pix.modifier()); + } + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_size, 0); + + if (sizeRange.hStep != 0 && sizeRange.vStep != 0) { + spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Step, 0); + spa_pod_builder_frame(&b, &f[1]); + spa_pod_builder_rectangle(&b, + sizeRange.min.width, + sizeRange.min.height); + spa_pod_builder_rectangle(&b, + sizeRange.min.width, + sizeRange.min.height); + spa_pod_builder_rectangle(&b, + sizeRange.max.width, + sizeRange.max.height); + spa_pod_builder_rectangle(&b, + sizeRange.hStep, + sizeRange.vStep); + spa_pod_builder_pop(&b, &f[1]); + + } else { + spa_pod_builder_rectangle(&b, frameSize.width, frameSize.height); + } + + if (streamConfig.colorSpace) { + auto colorimetry = color_space_to_colorimetry(*streamConfig.colorSpace); + + spa_pod_builder_add(&b, + SPA_FORMAT_VIDEO_colorRange, + SPA_POD_Id(colorimetry.range), + SPA_FORMAT_VIDEO_colorMatrix, + SPA_POD_Id(colorimetry.matrix), + SPA_FORMAT_VIDEO_transferFunction, + SPA_POD_Id(colorimetry.transfer), + SPA_FORMAT_VIDEO_colorPrimaries, + SPA_POD_Id(colorimetry.primaries), 0); + } + + const auto *fmt = reinterpret_cast(spa_pod_builder_pop(&b, &f[0])); + if (spa_pod_filter(&b, &result.param, fmt, filter) < 0) + goto next; + + spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +int spa_libcamera_set_format(struct impl *impl, struct port *port, + struct spa_video_info *format, bool try_only) +{ + const struct format_info *info = nullptr; + uint32_t video_format; + struct spa_rectangle *size = nullptr; + struct spa_fraction *framerate = nullptr; + CameraConfiguration::Status validation; + int res; + + switch (format->media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + video_format = format->info.raw.format; + size = &format->info.raw.size; + framerate = &format->info.raw.framerate; + break; + case SPA_MEDIA_SUBTYPE_mjpg: + case SPA_MEDIA_SUBTYPE_jpeg: + video_format = SPA_VIDEO_FORMAT_ENCODED; + size = &format->info.mjpg.size; + framerate = &format->info.mjpg.framerate; + break; + case SPA_MEDIA_SUBTYPE_h264: + video_format = SPA_VIDEO_FORMAT_ENCODED; + size = &format->info.h264.size; + framerate = &format->info.h264.framerate; + break; + default: + video_format = SPA_VIDEO_FORMAT_ENCODED; + break; + } + + info = find_format_info_by_media_type(format->media_type, + format->media_subtype, video_format); + if (info == nullptr || size == nullptr || framerate == nullptr) { + spa_log_error(impl->log, "unknown media type %d %d %d", format->media_type, + format->media_subtype, video_format); + return -EINVAL; + } + StreamConfiguration& streamConfig = impl->config->at(0); + + streamConfig.pixelFormat = info->pix; + streamConfig.size.width = size->width; + streamConfig.size.height = size->height; + streamConfig.bufferCount = 8; + + validation = impl->config->validate(); + if (validation == CameraConfiguration::Invalid) + return -EINVAL; + + if (try_only) + return 0; + + if ((res = spa_libcamera_open(impl)) < 0) + return res; + + res = impl->camera->configure(impl->config.get()); + if (res != 0) + goto error; + + port->streamConfig = impl->config->at(0); + + if ((res = allocBuffers(impl, port, port->streamConfig.bufferCount)) < 0) + goto error; + + port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_RATE; + port->info.flags = SPA_PORT_FLAG_CAN_ALLOC_BUFFERS | + SPA_PORT_FLAG_LIVE | + SPA_PORT_FLAG_PHYSICAL | + SPA_PORT_FLAG_TERMINAL; + port->info.rate = SPA_FRACTION(port->rate.num, port->rate.denom); + + return 0; +error: + spa_libcamera_close(impl); + return res; + +} + +const struct { + uint32_t id; + uint32_t spa_id; +} control_map[] = { + { libcamera::controls::BRIGHTNESS, SPA_PROP_brightness }, + { libcamera::controls::CONTRAST, SPA_PROP_contrast }, + { libcamera::controls::SATURATION, SPA_PROP_saturation }, + { libcamera::controls::EXPOSURE_TIME, SPA_PROP_exposure }, + { libcamera::controls::ANALOGUE_GAIN, SPA_PROP_gain }, + { libcamera::controls::SHARPNESS, SPA_PROP_sharpness }, +}; + +uint32_t control_to_prop_id(uint32_t control_id) +{ + for (const auto& c : control_map) { + if (c.id == control_id) + return c.spa_id; + } + + return SPA_PROP_START_CUSTOM + control_id; +} + +uint32_t prop_id_to_control(uint32_t prop_id) +{ + if (prop_id >= SPA_PROP_START_CUSTOM) + return prop_id - SPA_PROP_START_CUSTOM; + + for (const auto& c : control_map) { + if (c.spa_id == prop_id) + return c.id; + } + + return SPA_ID_INVALID; +} + +[[nodiscard]] +ControlValue control_value_from_pod(const libcamera::ControlId& cid, const spa_pod *value, const void *body) +{ + if (cid.isArray()) + return {}; + + switch (cid.type()) { + case libcamera::ControlTypeBool: { + bool v; + if (spa_pod_body_get_bool(value, body, &v) < 0) + return {}; + + return v; + } + case libcamera::ControlTypeInteger32: { + int32_t v; + if (spa_pod_body_get_int(value, body, &v) < 0) + return {}; + + return v; + } + case libcamera::ControlTypeFloat: { + float v; + if (spa_pod_body_get_float(value, body, &v) < 0) + return {}; + + return v; + } + default: + return {}; + } + + return {}; +} + +int control_list_update_from_prop(libcamera::ControlList& list, const spa_pod_prop *prop, const void *body) +{ + auto id = prop_id_to_control(prop->key); + if (id == SPA_ID_INVALID) + return -ENOENT; + + auto it = list.idMap()->find(id); + if (it == list.idMap()->end()) + return -ENOENT; + + if (!list.infoMap()->count(it->second)) + return -ENOENT; + + auto val = control_value_from_pod(*it->second, &prop->value, body); + if (val.isNone()) + return -EINVAL; + + list.set(id, std::move(val)); + + return 0; +} + +[[nodiscard]] +bool control_value_to_pod(spa_pod_builder& b, const libcamera::ControlValue& cv) +{ + if (cv.isArray()) + return false; + + switch (cv.type()) { + case libcamera::ControlTypeBool: { + spa_pod_builder_bool(&b, cv.get()); + break; + } + case libcamera::ControlTypeInteger32: { + spa_pod_builder_int(&b, cv.get()); + break; + } + case libcamera::ControlTypeFloat: { + spa_pod_builder_float(&b, cv.get()); + break; + } + default: + return false; + } + + return true; +} + +template +[[nodiscard]] +std::array control_info_to_range(const libcamera::ControlInfo& cinfo) +{ + static_assert(std::is_arithmetic_v); + + auto min = cinfo.min().get(); + auto max = cinfo.max().get(); + spa_assert(min <= max); + + auto def = !cinfo.def().isNone() + ? cinfo.def().get() + : (min + ((max - min) / 2)); + + return {{ min, max, def }}; +} + +[[nodiscard]] +spa_pod *control_details_to_pod(spa_pod_builder& b, + const libcamera::ControlId& cid, const libcamera::ControlInfo& cinfo) +{ + if (cid.isArray()) + return nullptr; + + auto id = control_to_prop_id(cid.id()); + spa_pod_frame f; + + spa_pod_builder_push_object(&b, &f, SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo); + spa_pod_builder_add(&b, + SPA_PROP_INFO_id, SPA_POD_Id(id), + SPA_PROP_INFO_description, SPA_POD_String(cid.name().c_str()), + 0); + + if (cinfo.values().empty()) { + switch (cid.type()) { + case ControlTypeBool: { + auto min = cinfo.min().get(); + auto max = cinfo.max().get(); + auto def = !cinfo.def().isNone() + ? cinfo.def().get() + : min; + spa_pod_frame f; + + spa_pod_builder_prop(&b, SPA_PROP_INFO_type, 0); + spa_pod_builder_push_choice(&b, &f, SPA_CHOICE_Enum, 0); + spa_pod_builder_bool(&b, def); + spa_pod_builder_bool(&b, min); + if (max != min) + spa_pod_builder_bool(&b, max); + spa_pod_builder_pop(&b, &f); + break; + } + case ControlTypeFloat: { + auto [ min, max, def ] = control_info_to_range(cinfo); + + spa_pod_builder_add(&b, + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + def, min, max), + 0); + break; + } + case ControlTypeInteger32: { + auto [ min, max, def ] = control_info_to_range(cinfo); + + spa_pod_builder_add(&b, + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int( + def, min, max), + 0); + break; + } + default: + return nullptr; + } + } + else { + spa_pod_frame f; + + spa_pod_builder_prop(&b, SPA_PROP_INFO_type, 0); + spa_pod_builder_push_choice(&b, &f, SPA_CHOICE_Enum, 0); + + if (!control_value_to_pod(b, cinfo.def())) + return nullptr; + + for (const auto& cv : cinfo.values()) { + if (!control_value_to_pod(b, cv)) + return nullptr; + } + + spa_pod_builder_pop(&b, &f); + + if (cid.type() == libcamera::ControlTypeInteger32) { + spa_pod_builder_prop(&b, SPA_PROP_INFO_labels, 0); + + spa_pod_builder_push_struct(&b, &f); + for (const auto& cv : cinfo.values()) { + auto it = cid.enumerators().find(cv.get()); + if (it == cid.enumerators().end()) + continue; + + spa_pod_builder_int(&b, it->first); + spa_pod_builder_string_len(&b, it->second.data(), it->second.size()); + } + spa_pod_builder_pop(&b, &f); + } + } + + return reinterpret_cast(spa_pod_builder_pop(&b, &f)); +} + +int +spa_libcamera_enum_controls(struct impl *impl, struct port *port, int seq, + uint32_t start, uint32_t offset, uint32_t num, + const struct spa_pod *filter) +{ + const ControlInfoMap &info = impl->camera->controls(); + spa_auto(spa_pod_dynamic_builder) b = {}; + spa_pod_builder_state state; + uint8_t buffer[4096]; + spa_result_node_params result = { + .id = SPA_PARAM_PropInfo, + }; + + auto it = info.begin(); + for (auto skip = start - offset; skip && it != info.end(); skip--) + it++; + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_get_state(&b.b, &state); + + for (result.index = start; num > 0 && it != info.end(); ++it, result.index++) { + spa_log_debug(impl->log, "%p: controls[%" PRIu32 "]: %s::%s", + impl, result.index, it->first->vendor().c_str(), + it->first->name().c_str()); + + spa_pod_builder_reset(&b.b, &state); + + const auto *ctrl = control_details_to_pod(b.b, *it->first, it->second); + if (!ctrl) + continue; + + if (spa_pod_filter(&b.b, &result.param, ctrl, filter) < 0) + continue; + + result.next = result.index + 1; + + spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + num -= 1; + } + + return 0; +} + +int spa_libcamera_apply_controls(struct impl *impl, libcamera::ControlList&& controls) +{ + if (controls.empty()) + return 0; + + struct invoke_data { + ControlList *controls; + } d = { + .controls = &controls, + }; + + return spa_loop_locked( + impl->data_loop, + [](spa_loop *, bool, uint32_t, const void *data, size_t, void *user_data) + { + const auto *d = static_cast(data); + auto *impl = static_cast(user_data); + + impl->ctrls.merge(std::move(*d->controls), + libcamera::ControlList::MergePolicy::OverwriteExisting); + + return 0; + }, + 0, &d, sizeof(d), impl + ); +} + +void handle_completed_request(struct impl *impl, libcamera::Request *request) +{ + const auto request_id = request->cookie(); + struct port *port = &impl->out_ports[0]; + buffer *b = &port->buffers[request_id]; + + spa_log_trace(impl->log, "%p: request %p[%" PRIu64 "] process status:%u seq:%" PRIu32, + impl, request, request_id, static_cast(request->status()), + request->sequence()); + + if (request->status() == libcamera::Request::Status::RequestCancelled) { + spa_log_trace(impl->log, "%p: request %p[%" PRIu64 "] cancelled", + impl, request, request_id); + request->reuse(libcamera::Request::ReuseFlag::ReuseBuffers); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + spa_libcamera_buffer_recycle(impl, port, b->id); + return; + } + + const FrameBuffer *buffer = request->findBuffer(port->streamConfig.stream()); + if (buffer == nullptr) { + spa_log_warn(impl->log, "%p: request %p[%" PRIu64 "] has no buffer for stream %p", + impl, request, request_id, port->streamConfig.stream()); + return; + } + + const FrameMetadata &fmd = buffer->metadata(); + + if (impl->clock) { + double target = (double)port->info.rate.num / port->info.rate.denom; + double corr; + + if (impl->dll.bw == 0.0) { + spa_dll_set_bw(&impl->dll, SPA_DLL_BW_MAX, port->info.rate.denom, port->info.rate.denom); + impl->clock->next_nsec = fmd.timestamp; + corr = 1.0; + } else { + double diff = ((double)impl->clock->next_nsec - (double)fmd.timestamp) / SPA_NSEC_PER_SEC; + double error = port->info.rate.denom * (diff - target); + corr = spa_dll_update(&impl->dll, SPA_CLAMPD(error, -128., 128.)); + } + /* FIXME, we should follow the driver clock and target_ values. + * for now we ignore and use our own. */ + impl->clock->target_rate = port->rate; + impl->clock->target_duration = 1; + + impl->clock->nsec = fmd.timestamp; + impl->clock->rate = port->rate; + impl->clock->position = fmd.sequence; + impl->clock->duration = 1; + impl->clock->delay = 0; + impl->clock->rate_diff = corr; + impl->clock->next_nsec += (uint64_t) (target * SPA_NSEC_PER_SEC * corr); + } + + if (b->h) { + b->h->flags = 0; + if (fmd.status != libcamera::FrameMetadata::Status::FrameSuccess) + b->h->flags |= SPA_META_HEADER_FLAG_CORRUPTED; + b->h->offset = 0; + b->h->seq = fmd.sequence; + b->h->pts = fmd.timestamp; + b->h->dts_offset = 0; + } + + for (std::size_t i = 0; i < b->outbuf->n_datas; i++) { + auto *d = &b->outbuf->datas[i]; + + d->chunk->flags = 0; + + if (fmd.status != libcamera::FrameMetadata::Status::FrameSuccess) + d->chunk->flags |= SPA_CHUNK_FLAG_CORRUPTED; + } + + request->reuse(libcamera::Request::ReuseFlag::ReuseBuffers); + + spa_list_append(&port->queue, &b->link); + + spa_io_buffers *io = port->io; + if (io == nullptr) { + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + spa_libcamera_buffer_recycle(impl, port, b->id); + } else if (io->status != SPA_STATUS_HAVE_DATA) { + if (io->buffer_id < port->n_buffers) + spa_libcamera_buffer_recycle(impl, port, io->buffer_id); + + b = spa_list_first(&port->queue, struct buffer, link); + spa_list_remove(&b->link); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); + + io->buffer_id = b->id; + io->status = SPA_STATUS_HAVE_DATA; + spa_log_trace(impl->log, "%p: now queued %" PRIu32, impl, b->id); + } + + spa_node_call_ready(&impl->callbacks, SPA_STATUS_HAVE_DATA); +} + +void libcamera_on_fd_events(struct spa_source *source) +{ + struct impl *impl = (struct impl*) source->data; + uint32_t index; + uint64_t cnt; + + if (source->rmask & SPA_IO_ERR) { + spa_log_error(impl->log, "libcamera %p: error %08x", impl, source->rmask); + if (impl->source.loop) + spa_loop_remove_source(impl->data_loop, &impl->source); + return; + } + + if (!(source->rmask & SPA_IO_IN)) { + spa_log_warn(impl->log, "libcamera %p: spurious wakeup %d", impl, source->rmask); + return; + } + + if (spa_system_eventfd_read(impl->system, impl->source.fd, &cnt) < 0) { + spa_log_error(impl->log, "Failed to read on event fd"); + return; + } + + auto avail = spa_ringbuffer_get_read_index(&impl->completed_requests_rb, &index); + for (; avail > 0; avail--, index++) { + auto *request = impl->completed_requests[index & MASK_BUFFERS]; + + spa_ringbuffer_read_update(&impl->completed_requests_rb, index + 1); + handle_completed_request(impl, request); + } +} + +int spa_libcamera_use_buffers(struct impl *impl, struct port *port, + struct spa_buffer **buffers, uint32_t n_buffers) +{ + return -ENOTSUP; +} + +const struct { + Orientation libcamera_orientation; /* clockwise rotation then horizontal mirroring */ + uint32_t spa_transform_value; /* horizontal mirroring then counter-clockwise rotation */ +} orientation_map[] = { + { Orientation::Rotate0, SPA_META_TRANSFORMATION_None }, + { Orientation::Rotate0Mirror, SPA_META_TRANSFORMATION_Flipped }, + { Orientation::Rotate90, SPA_META_TRANSFORMATION_270 }, + { Orientation::Rotate90Mirror, SPA_META_TRANSFORMATION_Flipped90 }, + { Orientation::Rotate180, SPA_META_TRANSFORMATION_180 }, + { Orientation::Rotate180Mirror, SPA_META_TRANSFORMATION_Flipped180 }, + { Orientation::Rotate270, SPA_META_TRANSFORMATION_90 }, + { Orientation::Rotate270Mirror, SPA_META_TRANSFORMATION_Flipped270 }, +}; + +uint32_t libcamera_orientation_to_spa_transform_value(Orientation orientation) +{ + for (const auto& t : orientation_map) { + if (t.libcamera_orientation == orientation) + return t.spa_transform_value; + } + return SPA_META_TRANSFORMATION_None; +} + +int +spa_libcamera_alloc_buffers(struct impl *impl, struct port *port, + struct spa_buffer **buffers, + uint32_t n_buffers) +{ + if (port->n_buffers > 0) + return -EIO; + + Stream *stream = impl->config->at(0).stream(); + const std::vector> &bufs = + impl->allocator.buffers(stream); + + if (n_buffers > 0) { + if (bufs.size() != n_buffers) + return -EINVAL; + + spa_data *d = buffers[0]->datas; + + if (d[0].type != SPA_ID_INVALID && d[0].type & (1u << SPA_DATA_DmaBuf)) { + port->memtype = SPA_DATA_DmaBuf; + } else if (d[0].type & (1u << SPA_DATA_MemFd)) { + port->memtype = SPA_DATA_MemFd; + } else { + spa_log_error(impl->log, "can't use buffers of type %d", d[0].type); + return -EINVAL; + } + } + + for (uint32_t i = 0; i < n_buffers; i++) { + struct buffer *b; + + if (buffers[i]->n_datas < 1) { + spa_log_error(impl->log, "invalid buffer data"); + return -EINVAL; + } + + b = &port->buffers[i]; + b->id = i; + b->outbuf = buffers[i]; + b->flags = 0; + b->h = (struct spa_meta_header*)spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); + + b->videotransform = (struct spa_meta_videotransform*)spa_buffer_find_meta_data( + buffers[i], SPA_META_VideoTransform, sizeof(*b->videotransform)); + if (b->videotransform) { + b->videotransform->transform = + libcamera_orientation_to_spa_transform_value(impl->config->orientation); + spa_log_debug(impl->log, "Setting videotransform for buffer %u to %u", + i, b->videotransform->transform); + + } + + const auto& planes = bufs[i]->planes(); + spa_data *d = buffers[i]->datas; + + for(uint32_t j = 0; j < buffers[i]->n_datas; ++j) { + d[j].type = port->memtype; + d[j].flags = SPA_DATA_FLAG_READABLE; + d[j].mapoffset = 0; + d[j].chunk->stride = port->streamConfig.stride; + d[j].chunk->flags = 0; + /* Update parameters according to the plane information */ + unsigned int numPlanes = planes.size(); + if (buffers[i]->n_datas < numPlanes) { + if (j < buffers[i]->n_datas - 1) { + d[j].maxsize = planes[j].length; + d[j].chunk->offset = planes[j].offset; + d[j].chunk->size = planes[j].length; + } else { + d[j].chunk->offset = planes[j].offset; + for (uint8_t k = j; k < numPlanes; k++) { + d[j].maxsize += planes[k].length; + d[j].chunk->size += planes[k].length; + } + } + } else if (buffers[i]->n_datas == numPlanes) { + d[j].maxsize = planes[j].length; + d[j].chunk->offset = planes[j].offset; + d[j].chunk->size = planes[j].length; + } else { + spa_log_warn(impl->log, "buffer index: i: %d, data member " + "numbers: %d is greater than plane number: %d", + i, buffers[i]->n_datas, numPlanes); + d[j].maxsize = port->streamConfig.frameSize; + d[j].chunk->offset = 0; + d[j].chunk->size = port->streamConfig.frameSize; + } + + if (port->memtype == SPA_DATA_DmaBuf || + port->memtype == SPA_DATA_MemFd) { + d[j].flags |= SPA_DATA_FLAG_MAPPABLE; + d[j].fd = planes[j].fd.get(); + spa_log_debug(impl->log, "Got fd = %" PRId64 " for buffer: #%d", d[j].fd, i); + d[j].data = nullptr; + } else { + spa_log_error(impl->log, "invalid buffer type"); + return -EIO; + } + } + } + + port->n_buffers = n_buffers; + spa_log_debug(impl->log, "we have %d buffers", n_buffers); + + return 0; +} + + +void impl::requestComplete(libcamera::Request *request) +{ + struct impl *impl = this; + uint32_t index; + + spa_log_trace(impl->log, "%p: request %p[%" PRIu64 "] completed status:%u seq:%" PRIu32, + impl, request, request->cookie(), + static_cast(request->status()), + request->sequence()); + + spa_ringbuffer_get_write_index(&impl->completed_requests_rb, &index); + impl->completed_requests[index & MASK_BUFFERS] = request; + spa_ringbuffer_write_update(&impl->completed_requests_rb, index + 1); + + if (spa_system_eventfd_write(impl->system, impl->source.fd, 1) < 0) + spa_log_error(impl->log, "Failed to write on event fd"); + +} + +int spa_libcamera_stream_on(struct impl *impl) +{ + struct port *port = &impl->out_ports[0]; + int res; + + if (!port->current_format) { + spa_log_error(impl->log, "Exiting %s with -EIO", __FUNCTION__); + return -EIO; + } + + if (impl->active) + return 0; + + spa_log_info(impl->log, "starting camera %s", impl->camera->id().c_str()); + if ((res = impl->camera->start(&impl->initial_controls)) < 0) + return res == -EACCES ? -EBUSY : res; + + impl->camera->requestCompleted.connect(impl, &impl::requestComplete); + + res = spa_system_eventfd_create(impl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); + if (res < 0) + goto err_stop; + + impl->source.fd = res; + impl->source.func = libcamera_on_fd_events; + impl->source.data = impl; + impl->source.mask = SPA_IO_IN | SPA_IO_ERR; + impl->source.rmask = 0; + + for (auto& req : impl->requestPool) { + req->reuse(libcamera::Request::ReuseFlag::ReuseBuffers); + + if ((res = impl->camera->queueRequest(req.get())) < 0) + goto err_stop; + } + + impl->dll.bw = 0.0; + impl->active = true; + + res = spa_loop_locked( + impl->data_loop, + [](spa_loop *, bool, uint32_t, const void *, size_t, void *user_data) + { + auto *impl = static_cast(user_data); + return spa_loop_add_source(impl->data_loop, &impl->source); + }, + 0, nullptr, 0, impl + ); + if (res < 0) + goto err_stop; + + return 0; + +err_stop: + impl->stop(); + + return res; +} + +int spa_libcamera_stream_off(struct impl *impl) +{ + if (!impl->active) + return 0; + + spa_log_info(impl->log, "stopping camera %s", impl->camera->id().c_str()); + + impl->stop(); + + return 0; +} + +int port_get_format(struct impl *impl, struct port *port, + uint32_t index, + const struct spa_pod *filter, + struct spa_pod **param, + struct spa_pod_builder *builder) { struct spa_pod_frame f; @@ -230,9 +1445,9 @@ static int port_get_format(struct impl *impl, struct port *port, return 1; } -static int impl_node_enum_params(void *object, int seq, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) +int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) { struct impl *impl = (struct impl*)object; struct spa_pod *param; @@ -242,7 +1457,7 @@ static int impl_node_enum_params(void *object, int seq, uint32_t count = 0; int res; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); spa_return_val_if_fail(num != 0, -EINVAL); result.id = id; @@ -256,36 +1471,16 @@ next: case SPA_PARAM_PropInfo: { switch (result.index) { - case 0: - param = (struct spa_pod*)spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device), - SPA_PROP_INFO_description, SPA_POD_String("The libcamera device"), - SPA_PROP_INFO_type, SPA_POD_String(impl->device_id.c_str())); - break; - case 1: - param = (struct spa_pod*)spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_PropInfo, id, - SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_deviceName), - SPA_PROP_INFO_description, SPA_POD_String("The libcamera device name"), - SPA_PROP_INFO_type, SPA_POD_String(impl->device_name.c_str())); - break; default: return spa_libcamera_enum_controls(impl, GET_OUT_PORT(impl, 0), - seq, result.index - 2, num, filter); + seq, result.index, 0, num, filter); } break; } case SPA_PARAM_Props: { switch (result.index) { - case 0: - param = (struct spa_pod*)spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_Props, id, - SPA_PROP_device, SPA_POD_String(impl->device_id.c_str()), - SPA_PROP_deviceName, SPA_POD_String(impl->device_name.c_str())); - break; default: return 0; } @@ -313,39 +1508,41 @@ next: return 0; } -static int impl_node_set_param(void *object, - uint32_t id, uint32_t flags, - const struct spa_pod *param) +int impl_node_set_param(void *object, + uint32_t id, uint32_t flags, + const struct spa_pod *param) { - struct impl *impl = (struct impl*)object; + auto *impl = static_cast(object); - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); switch (id) { case SPA_PARAM_Props: { - struct spa_pod_object *obj = (struct spa_pod_object *) param; - struct spa_pod_prop *prop; + const auto *obj = reinterpret_cast(param); + const struct spa_pod_prop *prop; - if (param == NULL) { - impl->device_id.clear(); - impl->device_name.clear(); + if (param == nullptr) return 0; - } - SPA_POD_OBJECT_FOREACH(obj, prop) { - char device[128]; + libcamera::ControlList controls(impl->camera->controls()); + int res; + + SPA_POD_OBJECT_FOREACH(obj, prop) { switch (prop->key) { - case SPA_PROP_device: - strncpy(device, (char *)SPA_POD_CONTENTS(struct spa_pod_string, &prop->value), - sizeof(device)-1); - impl->device_id = device; - break; default: - spa_libcamera_set_control(impl, prop); + res = control_list_update_from_prop(controls, prop, SPA_POD_BODY_CONST(&prop->value)); + if (res < 0) + return res; + break; } } + + res = spa_libcamera_apply_controls(impl, std::move(controls)); + if (res < 0) + return res; + break; } default: @@ -354,11 +1551,11 @@ static int impl_node_set_param(void *object, return 0; } -static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) +int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) { struct impl *impl = (struct impl*)object; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); switch (id) { case SPA_IO_Clock: @@ -375,13 +1572,13 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) return 0; } -static int impl_node_send_command(void *object, const struct spa_command *command) +int impl_node_send_command(void *object, const struct spa_command *command) { struct impl *impl = (struct impl*)object; int res; - spa_return_val_if_fail(impl != NULL, -EINVAL); - spa_return_val_if_fail(command != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); + spa_return_val_if_fail(command != nullptr, -EINVAL); switch (SPA_NODE_COMMAND_ID(command)) { case SPA_NODE_COMMAND_Start: @@ -409,7 +1606,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman return 0; } -static void emit_node_info(struct impl *impl, bool full) +void emit_node_info(struct impl *impl, bool full) { static const struct spa_dict_item info_items[] = { { SPA_KEY_DEVICE_API, "libcamera" }, @@ -428,7 +1625,7 @@ static void emit_node_info(struct impl *impl, bool full) } } -static void emit_port_info(struct impl *impl, struct port *port, bool full) +void emit_port_info(struct impl *impl, struct port *port, bool full) { static const struct spa_dict_item info_items[] = { { SPA_KEY_PORT_GROUP, "stream.0" }, @@ -445,7 +1642,7 @@ static void emit_port_info(struct impl *impl, struct port *port, bool full) } } -static int +int impl_node_add_listener(void *object, struct spa_hook *listener, const struct spa_node_events *events, @@ -454,7 +1651,7 @@ impl_node_add_listener(void *object, struct impl *impl = (struct impl*)object; struct spa_hook_list save; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); spa_hook_list_isolate(&impl->hooks, &save, listener, events, data); @@ -466,49 +1663,49 @@ impl_node_add_listener(void *object, return 0; } -static int impl_node_set_callbacks(void *object, - const struct spa_node_callbacks *callbacks, - void *data) +int impl_node_set_callbacks(void *object, + const struct spa_node_callbacks *callbacks, + void *data) { struct impl *impl = (struct impl*)object; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); impl->callbacks = SPA_CALLBACKS_INIT(callbacks, data); return 0; } -static int impl_node_sync(void *object, int seq) +int impl_node_sync(void *object, int seq) { struct impl *impl = (struct impl*)object; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); - spa_node_emit_result(&impl->hooks, seq, 0, 0, NULL); + spa_node_emit_result(&impl->hooks, seq, 0, 0, nullptr); return 0; } -static int impl_node_add_port(void *object, - enum spa_direction direction, - uint32_t port_id, const struct spa_dict *props) +int impl_node_add_port(void *object, + enum spa_direction direction, + uint32_t port_id, const struct spa_dict *props) { return -ENOTSUP; } -static int impl_node_remove_port(void *object, - enum spa_direction direction, - uint32_t port_id) +int impl_node_remove_port(void *object, + enum spa_direction direction, + uint32_t port_id) { return -ENOTSUP; } -static int impl_node_port_enum_params(void *object, int seq, - enum spa_direction direction, - uint32_t port_id, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) +int impl_node_port_enum_params(void *object, int seq, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) { struct impl *impl = (struct impl*)object; @@ -520,7 +1717,7 @@ static int impl_node_port_enum_params(void *object, int seq, uint32_t count = 0; int res; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); spa_return_val_if_fail(num != 0, -EINVAL); spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL); @@ -535,7 +1732,7 @@ next: switch (id) { case SPA_PARAM_PropInfo: - return spa_libcamera_enum_controls(impl, port, seq, start, num, filter); + return spa_libcamera_enum_controls(impl, port, seq, start, 0, num, filter); case SPA_PARAM_EnumFormat: return spa_libcamera_enum_format(impl, port, seq, start, num, filter); @@ -630,24 +1827,27 @@ next: return 0; } -static int port_set_format(struct impl *impl, struct port *port, - uint32_t flags, const struct spa_pod *format) +int port_set_format(struct impl *impl, struct port *port, + uint32_t flags, const struct spa_pod *format) { - struct spa_video_info info; - int res; - - if (format == NULL) { - if (!port->current_format) - return 0; + const bool try_only = SPA_FLAG_IS_SET(flags, SPA_NODE_PARAM_FLAG_TEST_ONLY); + if (!try_only) { spa_libcamera_stream_off(impl); - spa_libcamera_clear_buffers(impl, port); + spa_libcamera_clear_buffers(port); + freeBuffers(impl, port); port->current_format.reset(); + } - spa_libcamera_close(impl); - goto done; + if (format == nullptr) { + if (!try_only) + spa_libcamera_close(impl); } else { + spa_video_info info; + int res; + spa_zero(info); + if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) return res; @@ -663,54 +1863,32 @@ static int port_set_format(struct impl *impl, struct port *port, return -EINVAL; } - if (port->current_format && info.media_type == port->current_format->media_type && - info.media_subtype == port->current_format->media_subtype && - info.info.raw.format == port->current_format->info.raw.format && - info.info.raw.size.width == port->current_format->info.raw.size.width && - info.info.raw.size.height == port->current_format->info.raw.size.height && - info.info.raw.flags == port->current_format->info.raw.flags && - (!(info.info.raw.flags & SPA_VIDEO_FLAG_MODIFIER) || - (info.info.raw.modifier == port->current_format->info.raw.modifier))) - return 0; break; case SPA_MEDIA_SUBTYPE_mjpg: if (spa_format_video_mjpg_parse(format, &info.info.mjpg) < 0) return -EINVAL; - if (port->current_format && info.media_type == port->current_format->media_type && - info.media_subtype == port->current_format->media_subtype && - info.info.mjpg.size.width == port->current_format->info.mjpg.size.width && - info.info.mjpg.size.height == port->current_format->info.mjpg.size.height) - return 0; break; case SPA_MEDIA_SUBTYPE_h264: if (spa_format_video_h264_parse(format, &info.info.h264) < 0) return -EINVAL; - if (port->current_format && info.media_type == port->current_format->media_type && - info.media_subtype == port->current_format->media_subtype && - info.info.h264.size.width == port->current_format->info.h264.size.width && - info.info.h264.size.height == port->current_format->info.h264.size.height) - return 0; break; default: return -EINVAL; } + + res = spa_libcamera_set_format(impl, port, &info, try_only); + if (res < 0) + return res; + + if (!try_only) + port->current_format = info; } - if (port->current_format && !(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) { - spa_libcamera_use_buffers(impl, port, NULL, 0); - port->current_format.reset(); - } + if (try_only) + return 0; - if (spa_libcamera_set_format(impl, port, &info, flags & SPA_NODE_PARAM_FLAG_TEST_ONLY) < 0) - return -EINVAL; - - if (!(flags & SPA_NODE_PARAM_FLAG_TEST_ONLY)) { - port->current_format = info; - } - - done: impl->info.change_mask |= SPA_NODE_CHANGE_MASK_PARAMS; port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; if (port->current_format) { @@ -728,16 +1906,16 @@ static int port_set_format(struct impl *impl, struct port *port, return 0; } -static int impl_node_port_set_param(void *object, - enum spa_direction direction, uint32_t port_id, - uint32_t id, uint32_t flags, - const struct spa_pod *param) +int impl_node_port_set_param(void *object, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t flags, + const struct spa_pod *param) { struct impl *impl = (struct impl*)object; struct port *port; int res; - spa_return_val_if_fail(object != NULL, -EINVAL); + spa_return_val_if_fail(object != nullptr, -EINVAL); spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL); port = GET_PORT(impl, direction, port_id); @@ -752,32 +1930,32 @@ static int impl_node_port_set_param(void *object, return res; } -static int impl_node_port_use_buffers(void *object, - enum spa_direction direction, - uint32_t port_id, - uint32_t flags, - struct spa_buffer **buffers, - uint32_t n_buffers) +int impl_node_port_use_buffers(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + struct spa_buffer **buffers, + uint32_t n_buffers) { struct impl *impl = (struct impl*)object; struct port *port; int res; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL); port = GET_PORT(impl, direction, port_id); if (port->n_buffers) { spa_libcamera_stream_off(impl); - if ((res = spa_libcamera_clear_buffers(impl, port)) < 0) + if ((res = spa_libcamera_clear_buffers(port)) < 0) return res; } if (n_buffers > 0 && !port->current_format) return -EIO; if (n_buffers > MAX_BUFFERS) return -ENOSPC; - if (buffers == NULL) + if (buffers == nullptr) return 0; if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) { @@ -788,16 +1966,16 @@ static int impl_node_port_use_buffers(void *object, return res; } -static int impl_node_port_set_io(void *object, - enum spa_direction direction, - uint32_t port_id, - uint32_t id, - void *data, size_t size) +int impl_node_port_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) { struct impl *impl = (struct impl*)object; struct port *port; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); spa_return_val_if_fail(CHECK_PORT(impl, direction, port_id), -EINVAL); port = GET_PORT(impl, direction, port_id); @@ -808,6 +1986,7 @@ static int impl_node_port_set_io(void *object, break; case SPA_IO_Control: port->control = (struct spa_io_sequence*)data; + port->control_size = size; break; default: return -ENOENT; @@ -815,15 +1994,15 @@ static int impl_node_port_set_io(void *object, return 0; } -static int impl_node_port_reuse_buffer(void *object, - uint32_t port_id, - uint32_t buffer_id) +int impl_node_port_reuse_buffer(void *object, + uint32_t port_id, + uint32_t buffer_id) { struct impl *impl = (struct impl*)object; struct port *port; int res; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); spa_return_val_if_fail(port_id == 0, -EINVAL); port = GET_OUT_PORT(impl, port_id); @@ -835,30 +2014,51 @@ static int impl_node_port_reuse_buffer(void *object, return res; } -static int process_control(struct impl *impl, struct spa_pod_sequence *control) +int process_control(struct impl *impl, struct spa_pod_sequence *control, uint32_t size) { - struct spa_pod_control *c; + libcamera::ControlList controls(impl->camera->controls()); + struct spa_pod_parser parser[2]; + struct spa_pod_frame frame[2]; + struct spa_pod_sequence seq; + const void *seq_body, *c_body; + struct spa_pod_control c; + int res; - SPA_POD_SEQUENCE_FOREACH(control, c) { - switch (c->type) { - case SPA_CONTROL_Properties: - { - struct spa_pod_prop *prop; - struct spa_pod_object *obj = (struct spa_pod_object *) &c->value; + spa_pod_parser_init_from_data(&parser[0], control, size, 0, size); + if (spa_pod_parser_push_sequence_body(&parser[0], &frame[0], &seq, &seq_body) < 0) + return 0; - SPA_POD_OBJECT_FOREACH(obj, prop) { - spa_libcamera_set_control(impl, prop); + while (spa_pod_parser_get_control_body(&parser[0], &c, &c_body) >= 0) { + switch (c.type) { + case SPA_CONTROL_Properties: { + struct spa_pod_object obj; + struct spa_pod_prop prop; + const void *obj_body, *prop_body; + + if (spa_pod_parser_init_object_body(&parser[1], &frame[1], + &c.value, c_body, &obj, &obj_body) < 0) + continue; + while (spa_pod_parser_get_prop_body(&parser[1], &prop, &prop_body) >= 0) { + res = control_list_update_from_prop(controls, &prop, prop_body); + if (res < 0) + return res; } + break; } default: break; } } + + res = spa_libcamera_apply_controls(impl, std::move(controls)); + if (res < 0) + return res; + return 0; } -static int impl_node_process(void *object) +int impl_node_process(void *object) { struct impl *impl = (struct impl*)object; int res; @@ -866,16 +2066,16 @@ static int impl_node_process(void *object) struct port *port; struct buffer *b; - spa_return_val_if_fail(impl != NULL, -EINVAL); + spa_return_val_if_fail(impl != nullptr, -EINVAL); port = GET_OUT_PORT(impl, 0); - if ((io = port->io) == NULL) + if ((io = port->io) == nullptr) return -EIO; if (port->control) - process_control(impl, &port->control->sequence); + process_control(impl, &port->control->sequence, port->control_size); - spa_log_trace(impl->log, "%p; status %d", impl, io->status); + spa_log_trace(impl->log, "%p: status %d", impl, io->status); if (io->status == SPA_STATUS_HAVE_DATA) { return SPA_STATUS_HAVE_DATA; @@ -904,7 +2104,7 @@ static int impl_node_process(void *object) return SPA_STATUS_HAVE_DATA; } -static const struct spa_node_methods impl_node = { +const struct spa_node_methods impl_node = { .version = SPA_VERSION_NODE_METHODS, .add_listener = impl_node_add_listener, .set_callbacks = impl_node_set_callbacks, @@ -923,14 +2123,12 @@ static const struct spa_node_methods impl_node = { .process = impl_node_process, }; -static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) +int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) { - struct impl *impl; + auto *impl = reinterpret_cast(handle); - spa_return_val_if_fail(handle != NULL, -EINVAL); - spa_return_val_if_fail(interface != NULL, -EINVAL); - - impl = (struct impl *) handle; + spa_return_val_if_fail(handle != nullptr, -EINVAL); + spa_return_val_if_fail(interface != nullptr, -EINVAL); if (spa_streq(type, SPA_TYPE_INTERFACE_Node)) *interface = &impl->node; @@ -940,22 +2138,24 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void return 0; } -static int impl_clear(struct spa_handle *handle) +int impl_clear(struct spa_handle *handle) { std::destroy_at(reinterpret_cast(handle)); return 0; } impl::impl(spa_log *log, spa_loop *data_loop, spa_system *system, - std::shared_ptr manager, std::shared_ptr camera, std::string device_id) + std::shared_ptr manager, std::shared_ptr camera, + std::unique_ptr config) : handle({ SPA_VERSION_HANDLE, impl_get_interface, impl_clear }), log(log), data_loop(data_loop), system(system), - device_id(std::move(device_id)), out_ports{{this}}, manager(std::move(manager)), - camera(std::move(camera)) + camera(std::move(camera)), + config(std::move(config)), + allocator(this->camera) { libcamera_log_topic_init(log); @@ -980,25 +2180,24 @@ impl::impl(spa_log *log, spa_loop *data_loop, spa_system *system, latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); } -static size_t +size_t impl_get_size(const struct spa_handle_factory *factory, const struct spa_dict *params) { return sizeof(struct impl); } -static int +int impl_init(const struct spa_handle_factory *factory, struct spa_handle *handle, const struct spa_dict *info, const struct spa_support *support, uint32_t n_support) { - const char *str; int res; - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(handle != NULL, -EINVAL); + spa_return_val_if_fail(factory != nullptr, -EINVAL); + spa_return_val_if_fail(handle != nullptr, -EINVAL); auto log = static_cast(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log)); auto data_loop = static_cast(spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop)); @@ -1020,33 +2219,40 @@ impl_init(const struct spa_handle_factory *factory, return res; } - std::string device_id; - if (info && (str = spa_dict_lookup(info, SPA_KEY_API_LIBCAMERA_PATH))) - device_id = str; + const char *device_id = info + ? spa_dict_lookup(info, SPA_KEY_API_LIBCAMERA_PATH) + : nullptr; - auto camera = manager->get(device_id); + auto camera = device_id ? manager->get(device_id) : nullptr; if (!camera) { - spa_log_error(log, "unknown camera id %s", device_id.c_str()); + spa_log_error(log, "unknown camera id: %s", device_id); return -ENOENT; } + auto config = camera->generateConfiguration({ libcamera::StreamRole::VideoRecording }); + if (!config) { + spa_log_error(log, "cannot generate configuration for camera"); + return -EINVAL; + } + new (handle) impl(log, data_loop, system, - std::move(manager), std::move(camera), std::move(device_id)); + std::move(manager), std::move(camera), + std::move(config)); return 0; } -static const struct spa_interface_info impl_interfaces[] = { +const struct spa_interface_info impl_interfaces[] = { {SPA_TYPE_INTERFACE_Node,}, }; -static int impl_enum_interface_info(const struct spa_handle_factory *factory, - const struct spa_interface_info **info, - uint32_t *index) +int impl_enum_interface_info(const struct spa_handle_factory *factory, + const struct spa_interface_info **info, + uint32_t *index) { - spa_return_val_if_fail(factory != NULL, -EINVAL); - spa_return_val_if_fail(info != NULL, -EINVAL); - spa_return_val_if_fail(index != NULL, -EINVAL); + spa_return_val_if_fail(factory != nullptr, -EINVAL); + spa_return_val_if_fail(info != nullptr, -EINVAL); + spa_return_val_if_fail(index != nullptr, -EINVAL); if (*index >= SPA_N_ELEMENTS(impl_interfaces)) return 0; @@ -1055,11 +2261,13 @@ static int impl_enum_interface_info(const struct spa_handle_factory *factory, return 1; } +} + extern "C" { const struct spa_handle_factory spa_libcamera_source_factory = { SPA_VERSION_HANDLE_FACTORY, SPA_NAME_API_LIBCAMERA_SOURCE, - NULL, + nullptr, impl_get_size, impl_init, impl_enum_interface_info, diff --git a/spa/plugins/libcamera/libcamera-utils.cpp b/spa/plugins/libcamera/libcamera-utils.cpp deleted file mode 100644 index 92185c4b0..000000000 --- a/spa/plugins/libcamera/libcamera-utils.cpp +++ /dev/null @@ -1,1071 +0,0 @@ -/* Spa */ -/* SPDX-FileCopyrightText: Copyright © 2020 Collabora Ltd. */ -/* @author Raghavendra Rao Sidlagatta */ -/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -int spa_libcamera_open(struct impl *impl) -{ - if (impl->acquired) - return 0; - - spa_log_info(impl->log, "open camera %s", impl->device_id.c_str()); - impl->camera->acquire(); - - impl->allocator = new FrameBufferAllocator(impl->camera); - - impl->acquired = true; - return 0; -} - -int spa_libcamera_close(struct impl *impl) -{ - struct port *port = &impl->out_ports[0]; - if (!impl->acquired) - return 0; - if (impl->active || port->current_format) - return 0; - - spa_log_info(impl->log, "close camera %s", impl->device_id.c_str()); - delete impl->allocator; - impl->allocator = nullptr; - - impl->camera->release(); - - impl->acquired = false; - return 0; -} - -static void spa_libcamera_get_config(struct impl *impl) -{ - if (impl->config) - return; - - impl->config = impl->camera->generateConfiguration({ StreamRole::VideoRecording }); -} - -static int spa_libcamera_buffer_recycle(struct impl *impl, struct port *port, uint32_t buffer_id) -{ - struct buffer *b = &port->buffers[buffer_id]; - int res; - - if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) - return 0; - - SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUTSTANDING); - - if (buffer_id >= impl->requestPool.size()) { - spa_log_warn(impl->log, "invalid buffer_id %u >= %zu", - buffer_id, impl->requestPool.size()); - return -EINVAL; - } - Request *request = impl->requestPool[buffer_id].get(); - Stream *stream = port->streamConfig.stream(); - FrameBuffer *buffer = impl->allocator->buffers(stream)[buffer_id].get(); - if ((res = request->addBuffer(stream, buffer)) < 0) { - spa_log_warn(impl->log, "can't add buffer %u for request: %s", - buffer_id, spa_strerror(res)); - return -ENOMEM; - } - if (!impl->active) { - impl->pendingRequests.push_back(request); - return 0; - } else { - request->controls().merge(impl->ctrls); - impl->ctrls.clear(); - if ((res = impl->camera->queueRequest(request)) < 0) { - spa_log_warn(impl->log, "can't queue buffer %u: %s", - buffer_id, spa_strerror(res)); - return res == -EACCES ? -EBUSY : res; - } - } - return 0; -} - -static int allocBuffers(struct impl *impl, struct port *port, unsigned int count) -{ - int res; - - if ((res = impl->allocator->allocate(port->streamConfig.stream())) < 0) - return res; - - for (unsigned int i = 0; i < count; i++) { - std::unique_ptr request = impl->camera->createRequest(i); - if (!request) { - impl->requestPool.clear(); - return -ENOMEM; - } - impl->requestPool.push_back(std::move(request)); - } - - /* Some devices require data for each output video frame to be - * placed in discontiguous memory buffers. In such cases, one - * video frame has to be addressed using more than one memory. - * address. Therefore, need calculate the number of discontiguous - * memory and allocate the specified amount of memory */ - Stream *stream = impl->config->at(0).stream(); - const std::vector> &bufs = - impl->allocator->buffers(stream); - const std::vector &planes = bufs[0]->planes(); - int fd = -1; - uint32_t buffers_blocks = 0; - - for (const FrameBuffer::Plane &plane : planes) { - const int current_fd = plane.fd.get(); - if (current_fd >= 0 && current_fd != fd) { - buffers_blocks += 1; - fd = current_fd; - } - } - - if (buffers_blocks > 0) { - port->buffers_blocks = buffers_blocks; - } - return res; -} - -static void freeBuffers(struct impl *impl, struct port *port) -{ - impl->pendingRequests.clear(); - impl->requestPool.clear(); - impl->allocator->free(port->streamConfig.stream()); -} - -static int spa_libcamera_clear_buffers(struct impl *impl, struct port *port) -{ - uint32_t i; - - if (port->n_buffers == 0) - return 0; - - for (i = 0; i < port->n_buffers; i++) { - struct buffer *b; - struct spa_data *d; - - b = &port->buffers[i]; - d = b->outbuf->datas; - - if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_OUTSTANDING)) { - spa_log_debug(impl->log, "queueing outstanding buffer %p", b); - spa_libcamera_buffer_recycle(impl, port, i); - } - if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) { - munmap(SPA_PTROFF(b->ptr, -d[0].mapoffset, void), - d[0].maxsize - d[0].mapoffset); - } - if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_ALLOCATED)) { - close(d[0].fd); - } - d[0].type = SPA_ID_INVALID; - } - - freeBuffers(impl, port); - port->n_buffers = 0; - port->ring = SPA_RINGBUFFER_INIT(); - - return 0; -} - -struct format_info { - PixelFormat pix; - uint32_t format; - uint32_t media_type; - uint32_t media_subtype; -}; - -#define MAKE_FMT(pix,fmt,mt,mst) { pix, SPA_VIDEO_FORMAT_ ##fmt, SPA_MEDIA_TYPE_ ##mt, SPA_MEDIA_SUBTYPE_ ##mst } -static const struct format_info format_info[] = { - /* RGB formats */ - MAKE_FMT(formats::RGB565, RGB16, video, raw), - MAKE_FMT(formats::RGB565_BE, RGB16, video, raw), - MAKE_FMT(formats::RGB888, BGR, video, raw), - MAKE_FMT(formats::BGR888, RGB, video, raw), - MAKE_FMT(formats::XRGB8888, BGRx, video, raw), - MAKE_FMT(formats::XBGR8888, RGBx, video, raw), - MAKE_FMT(formats::RGBX8888, xBGR, video, raw), - MAKE_FMT(formats::BGRX8888, xRGB, video, raw), - MAKE_FMT(formats::ARGB8888, BGRA, video, raw), - MAKE_FMT(formats::ABGR8888, RGBA, video, raw), - MAKE_FMT(formats::RGBA8888, ABGR, video, raw), - MAKE_FMT(formats::BGRA8888, ARGB, video, raw), - - MAKE_FMT(formats::YUYV, YUY2, video, raw), - MAKE_FMT(formats::YVYU, YVYU, video, raw), - MAKE_FMT(formats::UYVY, UYVY, video, raw), - MAKE_FMT(formats::VYUY, VYUY, video, raw), - - MAKE_FMT(formats::NV12, NV12, video, raw), - MAKE_FMT(formats::NV21, NV21, video, raw), - MAKE_FMT(formats::NV16, NV16, video, raw), - MAKE_FMT(formats::NV61, NV61, video, raw), - MAKE_FMT(formats::NV24, NV24, video, raw), - - MAKE_FMT(formats::YUV420, I420, video, raw), - MAKE_FMT(formats::YVU420, YV12, video, raw), - MAKE_FMT(formats::YUV422, Y42B, video, raw), - - MAKE_FMT(formats::MJPEG, ENCODED, video, mjpg), -#undef MAKE_FMT -}; - -static const struct format_info *video_format_to_info(const PixelFormat &pix) { - size_t i; - - for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) { - if (format_info[i].pix == pix) - return &format_info[i]; - } - return NULL; -} - -static const struct format_info *find_format_info_by_media_type(uint32_t type, - uint32_t subtype, uint32_t format, int startidx) -{ - size_t i; - - for (i = startidx; i < SPA_N_ELEMENTS(format_info); i++) { - if ((format_info[i].media_type == type) && - (format_info[i].media_subtype == subtype) && - (format == 0 || format_info[i].format == format)) - return &format_info[i]; - } - return NULL; -} - -static int score_size(Size &a, Size &b) -{ - int x, y; - x = (int)a.width - (int)b.width; - y = (int)a.height - (int)b.height; - return x * x + y * y; -} - -static int -spa_libcamera_enum_format(struct impl *impl, struct port *port, int seq, - uint32_t start, uint32_t num, const struct spa_pod *filter) -{ - int res; - const struct format_info *info; - uint8_t buffer[1024]; - struct spa_pod_builder b = { 0 }; - struct spa_pod_frame f[2]; - struct spa_result_node_params result; - struct spa_pod *fmt; - uint32_t i, count = 0, num_sizes; - PixelFormat format; - Size frameSize; - SizeRange sizeRange = SizeRange(); - - spa_libcamera_get_config(impl); - - const StreamConfiguration& streamConfig = impl->config->at(0); - const StreamFormats &formats = streamConfig.formats(); - - result.id = SPA_PARAM_EnumFormat; - result.next = start; - - if (result.next == 0) { - port->fmt_index = 0; - port->size_index = 0; - } -next: - result.index = result.next++; - -next_fmt: - if (port->fmt_index >= formats.pixelformats().size()) - goto enum_end; - - format = formats.pixelformats()[port->fmt_index]; - - spa_log_debug(impl->log, "format: %s", format.toString().c_str()); - - info = video_format_to_info(format); - if (info == NULL) { - spa_log_debug(impl->log, "unknown format"); - port->fmt_index++; - goto next_fmt; - } - - num_sizes = formats.sizes(format).size(); - if (num_sizes > 0 && port->size_index <= num_sizes) { - if (port->size_index == 0) { - Size wanted = Size(640, 480), test; - int score, best = INT_MAX; - for (i = 0; i < num_sizes; i++) { - test = formats.sizes(format)[i]; - score = score_size(wanted, test); - if (score < best) { - best = score; - frameSize = test; - } - } - } - else { - frameSize = formats.sizes(format)[port->size_index - 1]; - } - } else if (port->size_index < 1) { - sizeRange = formats.range(format); - if (sizeRange.hStep == 0 || sizeRange.vStep == 0) { - port->size_index = 0; - port->fmt_index++; - goto next_fmt; - } - } else { - port->size_index = 0; - port->fmt_index++; - goto next_fmt; - } - port->size_index++; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); - spa_pod_builder_add(&b, - SPA_FORMAT_mediaType, SPA_POD_Id(info->media_type), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(info->media_subtype), - 0); - - if (info->media_subtype == SPA_MEDIA_SUBTYPE_raw) { - spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_format, 0); - spa_pod_builder_id(&b, info->format); - } - if (info->pix.modifier()) { - spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_modifier, 0); - spa_pod_builder_long(&b, info->pix.modifier()); - } - spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_size, 0); - - if (sizeRange.hStep != 0 && sizeRange.vStep != 0) { - spa_pod_builder_push_choice(&b, &f[1], SPA_CHOICE_Step, 0); - spa_pod_builder_frame(&b, &f[1]); - spa_pod_builder_rectangle(&b, - sizeRange.min.width, - sizeRange.min.height); - spa_pod_builder_rectangle(&b, - sizeRange.min.width, - sizeRange.min.height); - spa_pod_builder_rectangle(&b, - sizeRange.max.width, - sizeRange.max.height); - spa_pod_builder_rectangle(&b, - sizeRange.hStep, - sizeRange.vStep); - spa_pod_builder_pop(&b, &f[1]); - - } else { - spa_pod_builder_rectangle(&b, frameSize.width, frameSize.height); - } - - fmt = (struct spa_pod*) spa_pod_builder_pop(&b, &f[0]); - - if (spa_pod_filter(&b, &result.param, fmt, filter) < 0) - goto next; - - spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); - - if (++count != num) - goto next; - - enum_end: - res = 0; - return res; -} - -static int spa_libcamera_set_format(struct impl *impl, struct port *port, - struct spa_video_info *format, bool try_only) -{ - const struct format_info *info = NULL; - uint32_t video_format; - struct spa_rectangle *size = NULL; - struct spa_fraction *framerate = NULL; - CameraConfiguration::Status validation; - int res; - - switch (format->media_subtype) { - case SPA_MEDIA_SUBTYPE_raw: - video_format = format->info.raw.format; - size = &format->info.raw.size; - framerate = &format->info.raw.framerate; - break; - case SPA_MEDIA_SUBTYPE_mjpg: - case SPA_MEDIA_SUBTYPE_jpeg: - video_format = SPA_VIDEO_FORMAT_ENCODED; - size = &format->info.mjpg.size; - framerate = &format->info.mjpg.framerate; - break; - case SPA_MEDIA_SUBTYPE_h264: - video_format = SPA_VIDEO_FORMAT_ENCODED; - size = &format->info.h264.size; - framerate = &format->info.h264.framerate; - break; - default: - video_format = SPA_VIDEO_FORMAT_ENCODED; - break; - } - - info = find_format_info_by_media_type(format->media_type, - format->media_subtype, video_format, 0); - if (info == NULL || size == NULL || framerate == NULL) { - spa_log_error(impl->log, "unknown media type %d %d %d", format->media_type, - format->media_subtype, video_format); - return -EINVAL; - } - StreamConfiguration& streamConfig = impl->config->at(0); - - streamConfig.pixelFormat = info->pix; - streamConfig.size.width = size->width; - streamConfig.size.height = size->height; - streamConfig.bufferCount = 8; - - validation = impl->config->validate(); - if (validation == CameraConfiguration::Invalid) - return -EINVAL; - - if (try_only) - return 0; - - if ((res = spa_libcamera_open(impl)) < 0) - return res; - - res = impl->camera->configure(impl->config.get()); - if (res != 0) - goto error; - - port->streamConfig = impl->config->at(0); - - if ((res = allocBuffers(impl, port, port->streamConfig.bufferCount)) < 0) - goto error; - - port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_RATE; - port->info.flags = SPA_PORT_FLAG_CAN_ALLOC_BUFFERS | - SPA_PORT_FLAG_LIVE | - SPA_PORT_FLAG_PHYSICAL | - SPA_PORT_FLAG_TERMINAL; - port->info.rate = SPA_FRACTION(port->rate.num, port->rate.denom); - - return 0; -error: - spa_libcamera_close(impl); - return res; - -} - -static struct { - uint32_t id; - uint32_t spa_id; -} control_map[] = { - { libcamera::controls::BRIGHTNESS, SPA_PROP_brightness }, - { libcamera::controls::CONTRAST, SPA_PROP_contrast }, - { libcamera::controls::SATURATION, SPA_PROP_saturation }, - { libcamera::controls::EXPOSURE_TIME, SPA_PROP_exposure }, - { libcamera::controls::ANALOGUE_GAIN, SPA_PROP_gain }, - { libcamera::controls::SHARPNESS, SPA_PROP_sharpness }, -}; - -static uint32_t control_to_prop_id(struct impl *impl, uint32_t control_id) -{ - SPA_FOR_EACH_ELEMENT_VAR(control_map, c) { - if (c->id == control_id) - return c->spa_id; - } - return SPA_PROP_START_CUSTOM + control_id; -} - -static uint32_t prop_id_to_control(struct impl *impl, uint32_t prop_id) -{ - SPA_FOR_EACH_ELEMENT_VAR(control_map, c) { - if (c->spa_id == prop_id) - return c->id; - } - if (prop_id >= SPA_PROP_START_CUSTOM) - return prop_id - SPA_PROP_START_CUSTOM; - return SPA_ID_INVALID; -} - -static int -spa_libcamera_enum_controls(struct impl *impl, struct port *port, int seq, - uint32_t start, uint32_t num, - const struct spa_pod *filter) -{ - const ControlInfoMap &info = impl->camera->controls(); - uint8_t buffer[1024]; - struct spa_pod_builder b = { 0 }; - struct spa_pod_frame f[2]; - struct spa_result_node_params result; - struct spa_pod *ctrl; - uint32_t count = 0, skip, id; - int res; - const ControlId *ctrl_id; - ControlInfo ctrl_info; - - result.id = SPA_PARAM_PropInfo; - result.next = start; - - auto it = info.begin(); - for (skip = result.next; skip; skip--) - it++; - - if (false) { -next: - it++; - } - result.index = result.next++; - if (it == info.end()) - goto enum_end; - - ctrl_id = it->first; - ctrl_info = it->second; - - id = control_to_prop_id(impl, ctrl_id->id()); - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, SPA_PARAM_PropInfo); - spa_pod_builder_add(&b, - SPA_PROP_INFO_id, SPA_POD_Id(id), - SPA_PROP_INFO_description, SPA_POD_String(ctrl_id->name().c_str()), - 0); - - switch (ctrl_id->type()) { - case ControlTypeBool: { - bool def; - if (ctrl_info.def().isNone()) - def = ctrl_info.min().get(); - else - def = ctrl_info.def().get(); - - spa_pod_builder_add(&b, - SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool( - def), - 0); - } break; - case ControlTypeFloat: { - float min = ctrl_info.min().get(); - float max = ctrl_info.max().get(); - float def; - - if (ctrl_info.def().isNone()) - def = (min + max) / 2; - else - def = ctrl_info.def().get(); - - spa_pod_builder_add(&b, - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( - def, min, max), - 0); - } break; - case ControlTypeInteger32: { - int32_t min = ctrl_info.min().get(); - int32_t max = ctrl_info.max().get(); - int32_t def; - - if (ctrl_info.def().isNone()) - def = (min + max) / 2; - else - def = ctrl_info.def().get(); - - spa_pod_builder_add(&b, - SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Int( - def, min, max), - 0); - } break; - default: - goto next; - } - - ctrl = (struct spa_pod*) spa_pod_builder_pop(&b, &f[0]); - - if (spa_pod_filter(&b, &result.param, ctrl, filter) < 0) - goto next; - - spa_node_emit_result(&impl->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); - - if (++count != num) - goto next; - -enum_end: - res = 0; - return res; -} - -struct val { - uint32_t type; - float f_val; - int32_t i_val; - bool b_val; - uint32_t id; -}; - -static int do_update_ctrls(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *impl = (struct impl *)user_data; - const struct val *d = (const struct val *)data; - switch (d->type) { - case ControlTypeBool: - impl->ctrls.set(d->id, d->b_val); - break; - case ControlTypeFloat: - impl->ctrls.set(d->id, d->f_val); - break; - case ControlTypeInteger32: - impl->ctrls.set(d->id, (int32_t)d->i_val); - break; - default: - break; - } - return 0; -} - -static int -spa_libcamera_set_control(struct impl *impl, const struct spa_pod_prop *prop) -{ - const ControlInfoMap &info = impl->camera->controls(); - const ControlId *ctrl_id; - int res; - struct val d; - uint32_t control_id; - - control_id = prop_id_to_control(impl, prop->key); - if (control_id == SPA_ID_INVALID) - return -ENOENT; - - auto v = info.idmap().find(control_id); - if (v == info.idmap().end()) - return -ENOENT; - - ctrl_id = v->second; - - d.type = ctrl_id->type(); - d.id = ctrl_id->id(); - - switch (d.type) { - case ControlTypeBool: - if ((res = spa_pod_get_bool(&prop->value, &d.b_val)) < 0) - goto done; - break; - case ControlTypeFloat: - if ((res = spa_pod_get_float(&prop->value, &d.f_val)) < 0) - goto done; - break; - case ControlTypeInteger32: - if ((res = spa_pod_get_int(&prop->value, &d.i_val)) < 0) - goto done; - break; - default: - res = -EINVAL; - goto done; - } - spa_loop_invoke(impl->data_loop, do_update_ctrls, 0, &d, sizeof(d), true, impl); - res = 0; -done: - return res; -} - - -static void libcamera_on_fd_events(struct spa_source *source) -{ - struct impl *impl = (struct impl*) source->data; - struct spa_io_buffers *io; - struct port *port = &impl->out_ports[0]; - uint32_t index, buffer_id; - struct buffer *b; - uint64_t cnt; - - if (source->rmask & SPA_IO_ERR) { - spa_log_error(impl->log, "libcamera %p: error %08x", impl, source->rmask); - if (impl->source.loop) - spa_loop_remove_source(impl->data_loop, &impl->source); - return; - } - - if (!(source->rmask & SPA_IO_IN)) { - spa_log_warn(impl->log, "libcamera %p: spurious wakeup %d", impl, source->rmask); - return; - } - - if (spa_system_eventfd_read(impl->system, impl->source.fd, &cnt) < 0) { - spa_log_error(impl->log, "Failed to read on event fd"); - return; - } - - if (spa_ringbuffer_get_read_index(&port->ring, &index) < 1) { - spa_log_error(impl->log, "nothing is queued"); - return; - } - buffer_id = port->ring_ids[index & MASK_BUFFERS]; - spa_ringbuffer_read_update(&port->ring, index + 1); - - b = &port->buffers[buffer_id]; - spa_list_append(&port->queue, &b->link); - - io = port->io; - if (io == NULL) { - b = spa_list_first(&port->queue, struct buffer, link); - spa_list_remove(&b->link); - SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); - spa_libcamera_buffer_recycle(impl, port, b->id); - } else if (io->status != SPA_STATUS_HAVE_DATA) { - if (io->buffer_id < port->n_buffers) - spa_libcamera_buffer_recycle(impl, port, io->buffer_id); - - b = spa_list_first(&port->queue, struct buffer, link); - spa_list_remove(&b->link); - SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); - - io->buffer_id = b->id; - io->status = SPA_STATUS_HAVE_DATA; - spa_log_trace(impl->log, "libcamera %p: now queued %d", impl, b->id); - } - spa_node_call_ready(&impl->callbacks, SPA_STATUS_HAVE_DATA); -} - -static int spa_libcamera_use_buffers(struct impl *impl, struct port *port, - struct spa_buffer **buffers, uint32_t n_buffers) -{ - return -ENOTSUP; -} - -static const struct { - Orientation libcamera_orientation; /* clockwise rotation then horizontal mirroring */ - uint32_t spa_transform_value; /* horizontal mirroring then counter-clockwise rotation */ -} orientation_map[] = { - { Orientation::Rotate0, SPA_META_TRANSFORMATION_None }, - { Orientation::Rotate0Mirror, SPA_META_TRANSFORMATION_Flipped }, - { Orientation::Rotate90, SPA_META_TRANSFORMATION_270 }, - { Orientation::Rotate90Mirror, SPA_META_TRANSFORMATION_Flipped90 }, - { Orientation::Rotate180, SPA_META_TRANSFORMATION_180 }, - { Orientation::Rotate180Mirror, SPA_META_TRANSFORMATION_Flipped180 }, - { Orientation::Rotate270, SPA_META_TRANSFORMATION_90 }, - { Orientation::Rotate270Mirror, SPA_META_TRANSFORMATION_Flipped270 }, -}; - -static uint32_t libcamera_orientation_to_spa_transform_value(Orientation orientation) -{ - for (const auto& t : orientation_map) { - if (t.libcamera_orientation == orientation) - return t.spa_transform_value; - } - return SPA_META_TRANSFORMATION_None; -} - -static int -mmap_init(struct impl *impl, struct port *port, - struct spa_buffer **buffers, uint32_t n_buffers) -{ - unsigned int i, j; - struct spa_data *d; - Stream *stream = impl->config->at(0).stream(); - const std::vector> &bufs = - impl->allocator->buffers(stream); - - if (n_buffers > 0) { - if (bufs.size() != n_buffers) - return -EINVAL; - - d = buffers[0]->datas; - - if (d[0].type != SPA_ID_INVALID && - d[0].type & (1u << SPA_DATA_DmaBuf)) { - port->memtype = SPA_DATA_DmaBuf; - } else if (d[0].type != SPA_ID_INVALID && - d[0].type & (1u << SPA_DATA_MemFd)) { - port->memtype = SPA_DATA_MemFd; - } else if (d[0].type & (1u << SPA_DATA_MemPtr)) { - port->memtype = SPA_DATA_MemPtr; - } else { - spa_log_error(impl->log, "can't use buffers of type %d", d[0].type); - return -EINVAL; - } - } - - for (i = 0; i < n_buffers; i++) { - struct buffer *b; - - if (buffers[i]->n_datas < 1) { - spa_log_error(impl->log, "invalid buffer data"); - return -EINVAL; - } - - b = &port->buffers[i]; - b->id = i; - b->outbuf = buffers[i]; - b->flags = BUFFER_FLAG_OUTSTANDING; - b->h = (struct spa_meta_header*)spa_buffer_find_meta_data(buffers[i], SPA_META_Header, sizeof(*b->h)); - - b->videotransform = (struct spa_meta_videotransform*)spa_buffer_find_meta_data( - buffers[i], SPA_META_VideoTransform, sizeof(*b->videotransform)); - if (b->videotransform) { - b->videotransform->transform = - libcamera_orientation_to_spa_transform_value(impl->config->orientation); - spa_log_debug(impl->log, "Setting videotransform for buffer %u to %u", - i, b->videotransform->transform); - - } - - d = buffers[i]->datas; - for(j = 0; j < buffers[i]->n_datas; ++j) { - d[j].type = port->memtype; - d[j].flags = SPA_DATA_FLAG_READABLE; - d[j].mapoffset = 0; - d[j].chunk->stride = port->streamConfig.stride; - d[j].chunk->flags = 0; - /* Update parameters according to the plane information */ - unsigned int numPlanes = bufs[i]->planes().size(); - if (buffers[i]->n_datas < numPlanes) { - if (j < buffers[i]->n_datas - 1) { - d[j].maxsize = bufs[i]->planes()[j].length; - d[j].chunk->offset = bufs[i]->planes()[j].offset; - d[j].chunk->size = bufs[i]->planes()[j].length; - } else { - d[j].chunk->offset = bufs[i]->planes()[j].offset; - for (uint8_t k = j; k < numPlanes; k++) { - d[j].maxsize += bufs[i]->planes()[k].length; - d[j].chunk->size += bufs[i]->planes()[k].length; - } - } - } else if (buffers[i]->n_datas == numPlanes) { - d[j].maxsize = bufs[i]->planes()[j].length; - d[j].chunk->offset = bufs[i]->planes()[j].offset; - d[j].chunk->size = bufs[i]->planes()[j].length; - } else { - spa_log_warn(impl->log, "buffer index: i: %d, data member " - "numbers: %d is greater than plane number: %d", - i, buffers[i]->n_datas, numPlanes); - d[j].maxsize = port->streamConfig.frameSize; - d[j].chunk->offset = 0; - d[j].chunk->size = port->streamConfig.frameSize; - } - - if (port->memtype == SPA_DATA_DmaBuf || - port->memtype == SPA_DATA_MemFd) { - d[j].flags |= SPA_DATA_FLAG_MAPPABLE; - d[j].fd = bufs[i]->planes()[j].fd.get(); - spa_log_debug(impl->log, "Got fd = %" PRId64 " for buffer: #%d", d[j].fd, i); - d[j].data = NULL; - SPA_FLAG_SET(b->flags, BUFFER_FLAG_ALLOCATED); - } - else if(port->memtype == SPA_DATA_MemPtr) { - d[j].fd = -1; - d[j].data = mmap(NULL, - d[j].maxsize + d[j].mapoffset, - PROT_READ, MAP_SHARED, - bufs[i]->planes()[j].fd.get(), - 0); - if (d[j].data == MAP_FAILED) { - spa_log_error(impl->log, "mmap: %m"); - continue; - } - b->ptr = d[j].data; - SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); - spa_log_debug(impl->log, "mmap ptr:%p", d[j].data); - } else { - spa_log_error(impl->log, "invalid buffer type"); - return -EIO; - } - } - spa_libcamera_buffer_recycle(impl, port, i); - } - port->n_buffers = n_buffers; - spa_log_debug(impl->log, "we have %d buffers", n_buffers); - - return 0; -} - -static int -spa_libcamera_alloc_buffers(struct impl *impl, struct port *port, - struct spa_buffer **buffers, - uint32_t n_buffers) -{ - int res; - - if (port->n_buffers > 0) - return -EIO; - - if ((res = mmap_init(impl, port, buffers, n_buffers)) < 0) - return res; - - return 0; -} - - -void impl::requestComplete(libcamera::Request *request) -{ - struct impl *impl = this; - struct port *port = &impl->out_ports[0]; - Stream *stream = port->streamConfig.stream(); - uint32_t index, buffer_id; - struct buffer *b; - - spa_log_debug(impl->log, "request complete"); - - buffer_id = request->cookie(); - b = &port->buffers[buffer_id]; - - if ((request->status() == Request::RequestCancelled)) { - spa_log_debug(impl->log, "Request was cancelled"); - request->reuse(); - SPA_FLAG_SET(b->flags, BUFFER_FLAG_OUTSTANDING); - spa_libcamera_buffer_recycle(impl, port, b->id); - return; - } - FrameBuffer *buffer = request->findBuffer(stream); - if (buffer == nullptr) { - spa_log_warn(impl->log, "unknown buffer"); - return; - } - const FrameMetadata &fmd = buffer->metadata(); - - if (impl->clock) { - double target = (double)port->info.rate.num / port->info.rate.denom; - double corr; - - if (impl->dll.bw == 0.0) { - spa_dll_set_bw(&impl->dll, SPA_DLL_BW_MAX, port->info.rate.denom, port->info.rate.denom); - impl->clock->next_nsec = fmd.timestamp; - corr = 1.0; - } else { - double diff = ((double)impl->clock->next_nsec - (double)fmd.timestamp) / SPA_NSEC_PER_SEC; - double error = port->info.rate.denom * (diff - target); - corr = spa_dll_update(&impl->dll, SPA_CLAMPD(error, -128., 128.)); - } - /* FIXME, we should follow the driver clock and target_ values. - * for now we ignore and use our own. */ - impl->clock->target_rate = port->rate; - impl->clock->target_duration = 1; - - impl->clock->nsec = fmd.timestamp; - impl->clock->rate = port->rate; - impl->clock->position = fmd.sequence; - impl->clock->duration = 1; - impl->clock->delay = 0; - impl->clock->rate_diff = corr; - impl->clock->next_nsec += (uint64_t) (target * SPA_NSEC_PER_SEC * corr); - } - if (b->h) { - b->h->flags = 0; - b->h->offset = 0; - b->h->seq = fmd.sequence; - b->h->pts = fmd.timestamp; - b->h->dts_offset = 0; - } - request->reuse(); - - spa_ringbuffer_get_write_index(&port->ring, &index); - port->ring_ids[index & MASK_BUFFERS] = buffer_id; - spa_ringbuffer_write_update(&port->ring, index + 1); - - if (spa_system_eventfd_write(impl->system, impl->source.fd, 1) < 0) - spa_log_error(impl->log, "Failed to write on event fd"); - -} - -static int spa_libcamera_stream_on(struct impl *impl) -{ - struct port *port = &impl->out_ports[0]; - int res; - - if (!port->current_format) { - spa_log_error(impl->log, "Exiting %s with -EIO", __FUNCTION__); - return -EIO; - } - - if (impl->active) - return 0; - - impl->camera->requestCompleted.connect(impl, &impl::requestComplete); - - spa_log_info(impl->log, "starting camera %s", impl->device_id.c_str()); - if ((res = impl->camera->start()) < 0) - goto error; - - for (Request *req : impl->pendingRequests) { - if ((res = impl->camera->queueRequest(req)) < 0) - goto error_stop; - } - impl->pendingRequests.clear(); - - impl->dll.bw = 0.0; - - impl->source.func = libcamera_on_fd_events; - impl->source.data = impl; - impl->source.fd = spa_system_eventfd_create(impl->system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); - impl->source.mask = SPA_IO_IN | SPA_IO_ERR; - impl->source.rmask = 0; - if (impl->source.fd < 0) { - spa_log_error(impl->log, "Failed to create eventfd: %s", spa_strerror(impl->source.fd)); - res = impl->source.fd; - goto error_stop; - } - spa_loop_add_source(impl->data_loop, &impl->source); - - impl->active = true; - - return 0; - -error_stop: - impl->camera->stop(); -error: - impl->camera->requestCompleted.disconnect(impl, &impl::requestComplete); - return res == -EACCES ? -EBUSY : res; -} - -static int do_remove_source(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct impl *impl = (struct impl *)user_data; - if (impl->source.loop) - spa_loop_remove_source(loop, &impl->source); - return 0; -} - -static int spa_libcamera_stream_off(struct impl *impl) -{ - struct port *port = &impl->out_ports[0]; - int res; - - if (!impl->active) { - for (std::unique_ptr &req : impl->requestPool) - req->reuse(); - return 0; - } - - impl->active = false; - spa_log_info(impl->log, "stopping camera %s", impl->device_id.c_str()); - impl->pendingRequests.clear(); - - if ((res = impl->camera->stop()) < 0) { - spa_log_warn(impl->log, "error stopping camera %s: %s", - impl->device_id.c_str(), spa_strerror(res)); - } - - impl->camera->requestCompleted.disconnect(impl, &impl::requestComplete); - - spa_loop_invoke(impl->data_loop, do_remove_source, 0, NULL, 0, true, impl); - if (impl->source.fd >= 0) { - spa_system_close(impl->system, impl->source.fd); - impl->source.fd = -1; - } - - spa_list_init(&port->queue); - - return 0; -} diff --git a/spa/plugins/support/loop.c b/spa/plugins/support/loop.c index 8af61e7ae..fd53ef16d 100644 --- a/spa/plugins/support/loop.c +++ b/spa/plugins/support/loop.c @@ -80,28 +80,33 @@ struct impl { struct spa_system *system; struct spa_list source_list; - struct spa_list destroy_list; + struct spa_list free_list; struct spa_hook_list hooks_list; struct spa_ratelimit rate_limit; int retry_timeout; + bool prio_inherit; union tag head; uint32_t n_queues; struct queue *queues[QUEUES_MAX]; - pthread_mutex_t queue_lock; + pthread_mutex_t lock; + pthread_cond_t cond; + pthread_cond_t accept_cond; + int n_waiting; + int n_waiting_for_accept; int poll_fd; pthread_t thread; int enter_count; + int recurse; struct spa_source *wakeup; uint32_t count; uint32_t flush_count; - - unsigned int polling:1; + uint32_t remove_count; }; struct queue { @@ -184,13 +189,13 @@ static int remove_from_poll(struct impl *impl, struct spa_source *source) { spa_assert(source->loop == &impl->loop); + impl->remove_count++; return spa_system_pollfd_del(impl->system, impl->poll_fd, source->fd); } static int loop_remove_source(void *object, struct spa_source *source) { struct impl *impl = object; - spa_assert(!impl->polling); int res = remove_from_poll(impl, source); detach_source(source); @@ -458,12 +463,12 @@ again: * this invoking thread but we need to serialize the flushing here with * a mutex */ if (loop_thread == 0) - pthread_mutex_lock(&impl->queue_lock); + pthread_mutex_lock(&impl->lock); flush_all_queues(impl); if (loop_thread == 0) - pthread_mutex_unlock(&impl->queue_lock); + pthread_mutex_unlock(&impl->lock); res = item->res; } else { @@ -471,14 +476,26 @@ again: if (block && queue->ack_fd != -1) { uint64_t count = 1; + int i, recurse = 0; - spa_loop_control_hook_before(&impl->hooks_list); + if (pthread_mutex_trylock(&impl->lock) == 0) { + /* we are holding the lock, unlock recurse times */ + recurse = impl->recurse; + while (impl->recurse > 0) { + impl->recurse--; + pthread_mutex_unlock(&impl->lock); + } + pthread_mutex_unlock(&impl->lock); + } if ((res = spa_system_eventfd_read(impl->system, queue->ack_fd, &count)) < 0) spa_log_warn(impl->log, "%p: failed to read event fd:%d: %s", queue, queue->ack_fd, spa_strerror(res)); - spa_loop_control_hook_after(&impl->hooks_list); + for (i = 0; i < recurse; i++) { + pthread_mutex_lock(&impl->lock); + impl->recurse++; + } res = item->res; } @@ -548,6 +565,16 @@ static int loop_invoke(void *object, spa_invoke_func_t func, uint32_t seq, } return res; } +static int loop_locked(void *object, spa_invoke_func_t func, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct impl *impl = object; + int res; + pthread_mutex_lock(&impl->lock); + res = func(&impl->loop, false, seq, data, size, user_data); + pthread_mutex_unlock(&impl->lock); + return res; +} static int loop_get_fd(void *object) { @@ -572,6 +599,7 @@ static void loop_enter(void *object) struct impl *impl = object; pthread_t thread_id = pthread_self(); + pthread_mutex_lock(&impl->lock); if (impl->enter_count == 0) { spa_return_if_fail(impl->thread == 0); impl->thread = thread_id; @@ -597,31 +625,101 @@ static void loop_leave(void *object) if (--impl->enter_count == 0) { impl->thread = 0; flush_all_queues(impl); - impl->polling = false; } + pthread_mutex_unlock(&impl->lock); } static int loop_check(void *object) { struct impl *impl = object; pthread_t thread_id = pthread_self(); - return (impl->thread == 0 || pthread_equal(impl->thread, thread_id)) ? 1 : 0; + int res; + + /* we are in the thread running the loop */ + if (impl->thread == 0 || pthread_equal(impl->thread, thread_id)) + return 1; + + /* if lock taken by something else, error */ + if ((res = pthread_mutex_trylock(&impl->lock)) != 0) + return -res; + + /* we could take the lock, check if we actually locked it somewhere */ + res = impl->recurse > 0 ? 1 : -EPERM; + pthread_mutex_unlock(&impl->lock); + return res; +} +static int loop_lock(void *object) +{ + struct impl *impl = object; + int res; + + if ((res = pthread_mutex_lock(&impl->lock)) == 0) + impl->recurse++; + return -res; +} +static int loop_unlock(void *object) +{ + struct impl *impl = object; + int res; + spa_return_val_if_fail(impl->recurse > 0, -EIO); + impl->recurse--; + if ((res = pthread_mutex_unlock(&impl->lock)) != 0) + impl->recurse++; + return -res; +} +static int loop_get_time(void *object, struct timespec *abstime, int64_t timeout) +{ + if (clock_gettime(CLOCK_REALTIME, abstime) < 0) + return -errno; + + abstime->tv_sec += timeout / SPA_NSEC_PER_SEC; + abstime->tv_nsec += timeout % SPA_NSEC_PER_SEC; + if (abstime->tv_nsec >= SPA_NSEC_PER_SEC) { + abstime->tv_sec++; + abstime->tv_nsec -= SPA_NSEC_PER_SEC; + } + return 0; +} +static int loop_wait(void *object, const struct timespec *abstime) +{ + struct impl *impl = object; + int res; + + impl->n_waiting++; + impl->recurse--; + if (abstime) + res = pthread_cond_timedwait(&impl->cond, &impl->lock, abstime); + else + res = pthread_cond_wait(&impl->cond, &impl->lock); + impl->recurse++; + impl->n_waiting--; + return -res; } -static inline void free_source(struct source_impl *s) +static int loop_signal(void *object, bool wait_for_accept) { - detach_source(&s->source); - free(s); + struct impl *impl = object; + int res = 0; + if (impl->n_waiting > 0) + if ((res = pthread_cond_broadcast(&impl->cond)) != 0) + return -res; + + if (wait_for_accept) { + impl->n_waiting_for_accept++; + + while (impl->n_waiting_for_accept > 0) { + if ((res = pthread_cond_wait(&impl->accept_cond, &impl->lock)) != 0) + return -res; + } + } + return res; } -static inline void process_destroy(struct impl *impl) +static int loop_accept(void *object) { - struct source_impl *source, *tmp; - - spa_list_for_each_safe(source, tmp, &impl->destroy_list, link) - free_source(source); - - spa_list_init(&impl->destroy_list); + struct impl *impl = object; + impl->n_waiting_for_accept--; + return -pthread_cond_signal(&impl->accept_cond); } struct cancellation_handler_data { @@ -647,14 +745,18 @@ static int loop_iterate_cancel(void *object, int timeout) struct impl *impl = object; struct spa_poll_event ep[MAX_EP], *e; int i, nfds; + uint32_t remove_count; - impl->polling = true; + remove_count = impl->remove_count; spa_loop_control_hook_before(&impl->hooks_list); + pthread_mutex_unlock(&impl->lock); nfds = spa_system_pollfd_wait(impl->system, impl->poll_fd, ep, SPA_N_ELEMENTS(ep), timeout); + pthread_mutex_lock(&impl->lock); spa_loop_control_hook_after(&impl->hooks_list); - impl->polling = false; + if (remove_count != impl->remove_count) + nfds = 0; struct cancellation_handler_data cdata = { ep, nfds }; pthread_cleanup_push(cancellation_handler, &cdata); @@ -675,9 +777,6 @@ static int loop_iterate_cancel(void *object, int timeout) s->priv = &ep[i]; } - if (SPA_UNLIKELY(!spa_list_is_empty(&impl->destroy_list))) - process_destroy(impl); - for (i = 0; i < nfds; i++) { struct spa_source *s = ep[i].data; if (SPA_LIKELY(s && s->rmask)) @@ -694,14 +793,18 @@ static int loop_iterate(void *object, int timeout) struct impl *impl = object; struct spa_poll_event ep[MAX_EP], *e; int i, nfds; + uint32_t remove_count; - impl->polling = true; + remove_count = impl->remove_count; spa_loop_control_hook_before(&impl->hooks_list); + pthread_mutex_unlock(&impl->lock); nfds = spa_system_pollfd_wait(impl->system, impl->poll_fd, ep, SPA_N_ELEMENTS(ep), timeout); + pthread_mutex_lock(&impl->lock); spa_loop_control_hook_after(&impl->hooks_list); - impl->polling = false; + if (remove_count != impl->remove_count) + return 0; /* first we set all the rmasks, then call the callbacks. The reason is that * some callback might also want to look at other sources it manages and @@ -717,14 +820,12 @@ static int loop_iterate(void *object, int timeout) s->priv = &ep[i]; } - if (SPA_UNLIKELY(!spa_list_is_empty(&impl->destroy_list))) - process_destroy(impl); - for (i = 0; i < nfds; i++) { struct spa_source *s = ep[i].data; if (SPA_LIKELY(s && s->rmask)) s->func(s); } + for (i = 0; i < nfds; i++) { struct spa_source *s = ep[i].data; if (SPA_LIKELY(s)) { @@ -735,6 +836,24 @@ static int loop_iterate(void *object, int timeout) return nfds; } +static struct source_impl *get_source(struct impl *impl) +{ + struct source_impl *source; + + if (!spa_list_is_empty(&impl->free_list)) { + source = spa_list_first(&impl->free_list, struct source_impl, link); + spa_list_remove(&source->link); + spa_zero(*source); + } else { + source = calloc(1, sizeof(struct source_impl)); + } + if (source != NULL) { + source->impl = impl; + spa_list_insert(&impl->source_list, &source->link); + } + return source; +} + static void source_io_func(struct spa_source *source) { struct source_impl *s = SPA_CONTAINER_OF(source, struct source_impl, source); @@ -751,7 +870,7 @@ static struct spa_source *loop_add_io(void *object, struct source_impl *source; int res; - source = calloc(1, sizeof(struct source_impl)); + source = get_source(impl); if (source == NULL) goto error_exit; @@ -759,7 +878,6 @@ static struct spa_source *loop_add_io(void *object, source->source.data = data; source->source.fd = fd; source->source.mask = mask; - source->impl = impl; source->close = close; source->func.io = func; @@ -777,9 +895,6 @@ static struct spa_source *loop_add_io(void *object, spa_log_trace(impl->log, "%p: adding fallback %p", impl, source->fallback); } - - spa_list_insert(&impl->source_list, &source->link); - return &source->source; error_exit_free: @@ -844,7 +959,7 @@ static struct spa_source *loop_add_idle(void *object, struct source_impl *source; int res; - source = calloc(1, sizeof(struct source_impl)); + source = get_source(impl); if (source == NULL) goto error_exit; @@ -854,7 +969,6 @@ static struct spa_source *loop_add_idle(void *object, source->source.func = source_idle_func; source->source.data = data; source->source.fd = res; - source->impl = impl; source->close = true; source->source.mask = SPA_IO_IN; source->func.idle = func; @@ -862,8 +976,6 @@ static struct spa_source *loop_add_idle(void *object, if ((res = loop_add_source(impl, &source->source)) < 0) goto error_exit_close; - spa_list_insert(&impl->source_list, &source->link); - if (enabled) loop_enable_idle(impl, &source->source, true); @@ -900,7 +1012,7 @@ static struct spa_source *loop_add_event(void *object, struct source_impl *source; int res; - source = calloc(1, sizeof(struct source_impl)); + source = get_source(impl); if (source == NULL) goto error_exit; @@ -911,15 +1023,12 @@ static struct spa_source *loop_add_event(void *object, source->source.data = data; source->source.fd = res; source->source.mask = SPA_IO_IN; - source->impl = impl; source->close = true; source->func.event = func; if ((res = loop_add_source(impl, &source->source)) < 0) goto error_exit_close; - spa_list_insert(&impl->source_list, &source->link); - return &source->source; error_exit_close: @@ -968,7 +1077,7 @@ static struct spa_source *loop_add_timer(void *object, struct source_impl *source; int res; - source = calloc(1, sizeof(struct source_impl)); + source = get_source(impl); if (source == NULL) goto error_exit; @@ -980,15 +1089,12 @@ static struct spa_source *loop_add_timer(void *object, source->source.data = data; source->source.fd = res; source->source.mask = SPA_IO_IN; - source->impl = impl; source->close = true; source->func.timer = func; if ((res = loop_add_source(impl, &source->source)) < 0) goto error_exit_close; - spa_list_insert(&impl->source_list, &source->link); - return &source->source; error_exit_close: @@ -1052,7 +1158,7 @@ static struct spa_source *loop_add_signal(void *object, struct source_impl *source; int res; - source = calloc(1, sizeof(struct source_impl)); + source = get_source(impl); if (source == NULL) goto error_exit; @@ -1064,15 +1170,12 @@ static struct spa_source *loop_add_signal(void *object, source->source.data = data; source->source.fd = res; source->source.mask = SPA_IO_IN; - source->impl = impl; source->close = true; source->func.signal = func; if ((res = loop_add_source(impl, &source->source)) < 0) goto error_exit_close; - spa_list_insert(&impl->source_list, &source->link); - return &source->source; error_exit_close: @@ -1092,8 +1195,6 @@ static void loop_destroy_source(void *object, struct spa_source *source) spa_log_trace(s->impl->log, "%p ", s); - spa_list_remove(&s->link); - if (s->fallback) loop_destroy_source(s->impl, s->fallback); else @@ -1104,10 +1205,9 @@ static void loop_destroy_source(void *object, struct spa_source *source) source->fd = -1; } - if (!s->impl->polling) - free_source(s); - else - spa_list_insert(&s->impl->destroy_list, &s->link); + spa_list_remove(&s->link); + detach_source(source); + spa_list_insert(&s->impl->free_list, &s->link); } static const struct spa_loop_methods impl_loop = { @@ -1116,6 +1216,7 @@ static const struct spa_loop_methods impl_loop = { .update_source = loop_update_source, .remove_source = loop_remove_source, .invoke = loop_invoke, + .locked = loop_locked, }; static const struct spa_loop_control_methods impl_loop_control_cancel = { @@ -1126,6 +1227,12 @@ static const struct spa_loop_control_methods impl_loop_control_cancel = { .leave = loop_leave, .iterate = loop_iterate_cancel, .check = loop_check, + .lock = loop_lock, + .unlock = loop_unlock, + .get_time = loop_get_time, + .wait = loop_wait, + .signal = loop_signal, + .accept = loop_accept, }; static const struct spa_loop_control_methods impl_loop_control = { @@ -1136,6 +1243,12 @@ static const struct spa_loop_control_methods impl_loop_control = { .leave = loop_leave, .iterate = loop_iterate, .check = loop_check, + .lock = loop_lock, + .unlock = loop_unlock, + .get_time = loop_get_time, + .wait = loop_wait, + .signal = loop_signal, + .accept = loop_accept, }; static const struct spa_loop_utils_methods impl_loop_utils = { @@ -1185,18 +1298,24 @@ static int impl_clear(struct spa_handle *handle) spa_log_debug(impl->log, "%p: clear", impl); - if (impl->enter_count != 0 || impl->polling) - spa_log_warn(impl->log, "%p: loop is entered %d times polling:%d", - impl, impl->enter_count, impl->polling); + if (impl->enter_count != 0) + spa_log_warn(impl->log, "%p: loop is entered %d times", + impl, impl->enter_count); spa_list_consume(source, &impl->source_list, link) loop_destroy_source(impl, &source->source); + + spa_list_consume(source, &impl->free_list, link) { + spa_list_remove(&source->link); + free(source); + } for (i = 0; i < impl->n_queues; i++) loop_queue_destroy(impl->queues[i]); spa_system_close(impl->system, impl->poll_fd); - pthread_mutex_destroy(&impl->queue_lock); + pthread_cond_destroy(&impl->cond); + pthread_mutex_destroy(&impl->lock); return 0; } @@ -1227,6 +1346,7 @@ impl_init(const struct spa_handle_factory *factory, struct impl *impl; const char *str; pthread_mutexattr_t attr; + pthread_condattr_t cattr; int res; spa_return_val_if_fail(factory != NULL, -EINVAL); @@ -1258,11 +1378,24 @@ impl_init(const struct spa_handle_factory *factory, impl->control.iface.cb.funcs = &impl_loop_control_cancel; if ((str = spa_dict_lookup(info, "loop.retry-timeout")) != NULL) impl->retry_timeout = atoi(str); + if ((str = spa_dict_lookup(info, "loop.prio-inherit")) != NULL) + impl->prio_inherit = spa_atob(str); } CHECK(pthread_mutexattr_init(&attr), error_exit); - CHECK(pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE), error_exit); - CHECK(pthread_mutex_init(&impl->queue_lock, &attr), error_exit); + CHECK(pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE), error_exit_free_attr); + if (impl->prio_inherit) + CHECK(pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT), + error_exit_free_attr) + CHECK(pthread_mutex_init(&impl->lock, &attr), error_exit_free_attr); + pthread_mutexattr_destroy(&attr); + + CHECK(pthread_condattr_init(&cattr), error_exit_free_mutex); + CHECK(pthread_condattr_setclock(&cattr, CLOCK_REALTIME), error_exit_free_mutex); + + CHECK(pthread_cond_init(&impl->cond, &cattr), error_exit_free_mutex); + CHECK(pthread_cond_init(&impl->accept_cond, &cattr), error_exit_free_mutex); + pthread_condattr_destroy(&cattr); impl->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); spa_log_topic_init(impl->log, &log_topic); @@ -1271,17 +1404,17 @@ impl_init(const struct spa_handle_factory *factory, if (impl->system == NULL) { spa_log_error(impl->log, "%p: a System is needed", impl); res = -EINVAL; - goto error_exit_free_mutex; + goto error_exit_free_cond; } if ((res = spa_system_pollfd_create(impl->system, SPA_FD_CLOEXEC)) < 0) { spa_log_error(impl->log, "%p: can't create pollfd: %s", impl, spa_strerror(res)); - goto error_exit_free_mutex; + goto error_exit_free_cond; } impl->poll_fd = res; spa_list_init(&impl->source_list); - spa_list_init(&impl->destroy_list); + spa_list_init(&impl->free_list); spa_hook_list_init(&impl->hooks_list); impl->wakeup = loop_add_event(impl, wakeup_func, impl); @@ -1299,8 +1432,12 @@ impl_init(const struct spa_handle_factory *factory, error_exit_free_poll: spa_system_close(impl->system, impl->poll_fd); +error_exit_free_cond: + pthread_cond_destroy(&impl->cond); error_exit_free_mutex: - pthread_mutex_destroy(&impl->queue_lock); + pthread_mutex_destroy(&impl->lock); +error_exit_free_attr: + pthread_mutexattr_destroy(&attr); error_exit: return res; } diff --git a/spa/plugins/support/node-driver.c b/spa/plugins/support/node-driver.c index c8777caa3..fa9cf3426 100644 --- a/spa/plugins/support/node-driver.c +++ b/spa/plugins/support/node-driver.c @@ -26,6 +26,8 @@ #include #include #include +#include +#include SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.driver"); @@ -48,12 +50,16 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.driver"); #define BW_PERIOD (3 * SPA_NSEC_PER_SEC) #define MAX_ERROR_MS 1 +#define CLOCK_NAME_MAX 64 + struct props { bool freewheel; - char clock_name[64]; + char clock_name[CLOCK_NAME_MAX]; clockid_t clock_id; uint32_t freewheel_wait; float resync_ms; + char clock_device[CLOCK_NAME_MAX]; + char clock_interface[CLOCK_NAME_MAX]; }; struct clock_offset { @@ -73,7 +79,10 @@ struct impl { uint64_t info_all; struct spa_node_info info; - struct spa_param_info params[1]; +#define NODE_PropInfo 0 +#define NODE_Props 1 +#define N_NODE_PARAMS 2 + struct spa_param_info params[N_NODE_PARAMS]; struct spa_hook_list hooks; struct spa_callbacks callbacks; @@ -99,13 +108,20 @@ struct impl { struct clock_offset nsec_offset; }; +static void reset_props_strings(struct props *props) +{ + spa_zero(props->clock_name); + spa_zero(props->clock_device); + spa_zero(props->clock_interface); +} + static void reset_props(struct props *props) { props->freewheel = DEFAULT_FREEWHEEL; - spa_zero(props->clock_name); props->clock_id = CLOCK_MONOTONIC; props->freewheel_wait = DEFAULT_FREEWHEEL_WAIT; props->resync_ms = DEFAULT_RESYNC_MS; + reset_props_strings(props); } static const struct clock_info { @@ -154,11 +170,25 @@ static const char *clock_id_to_name(clockid_t id) static void set_timeout(struct impl *this, uint64_t next_time) { + /* The realtime system clock may be modified by the user. In such a case, + * a scheduled timer must be canceled, since its timeout is no longer + * correctly corresponding to the duration of a graph cycle. Worse, if + * for example the user resets the realtime clock way back to the past, + * then the timeout may now be far in the future, meaning that the next + * graph cycle never takes place. The SPA_FD_TIMER_CANCEL_ON_SET flag is + * used here to automatically cancel the timer if the user sets the realtime + * clock so that the driver can reschedule the cycle. (Timer cancelation + * will trigger an on_timeout() invocation with spa_system_timerfd_read() + * returning -ECANCELED.) + * If timerfd is used with a non-realtime clock, the flag is ignored. + * (Note that the flag only works in combination with SPA_FD_TIMER_ABSTIME.) */ + spa_log_trace(this->log, "set timeout %"PRIu64, next_time); this->timerspec.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; this->timerspec.it_value.tv_nsec = next_time % SPA_NSEC_PER_SEC; spa_system_timerfd_settime(this->data_system, - this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + this->timer_source.fd, SPA_FD_TIMER_ABSTIME | + SPA_FD_TIMER_CANCEL_ON_SET, &this->timerspec, NULL); } static inline uint64_t gettime_nsec(struct impl *this, clockid_t clock_id) @@ -280,7 +310,7 @@ static int reassign_follower(struct impl *this) if (following != this->following) { spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); this->following = following; - spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_set_timers, 0, NULL, 0, this); } return 0; } @@ -329,14 +359,25 @@ static void on_timeout(struct spa_source *source) uint32_t rate; double corr = 1.0, err = 0.0; int res; + bool timer_was_canceled = false; + + /* See set_timeout() for an explanation about timer cancelation. */ if ((res = spa_system_timerfd_read(this->data_system, this->timer_source.fd, &expirations)) < 0) { - if (res != -EAGAIN) + if (res == -EAGAIN) { + return; + } else if (res == -ECANCELED) { + spa_log_debug(this->log, "%p: timer was canceled; " + "rescheduling graph cycle", this); + timer_was_canceled = true; + } else { spa_log_error(this->log, "%p: timerfd error: %s", this, spa_strerror(res)); - return; + return; + } } + if (SPA_LIKELY(this->position)) { duration = this->position->clock.target_duration; rate = this->position->clock.target_rate.denom; @@ -344,24 +385,62 @@ static void on_timeout(struct spa_source *source) duration = 1024; rate = 48000; } - if (this->props.freewheel) + + /* In freewheel mode, graph cycles are run as fast as possible, + * especially if the "freewheel.wait" period is 0. In such a case, + * as soon as the mainloop encounters the scheduled timer timeout, + * it will execute it immediately. Since it is not possible to + * measure how long it takes the mainloop to do that, it is not + * possible to rely on this->next_time as the nsec value in + * freewheel mode (this->next_time does not factor in the mainloop + * invocation time mentioned earlier). Instead, sample the current + * monotonic time when freewheel mode is active, to account for + * that invocation time. + * + * Also, if the timer was canceled, the graph cycle needs to be + * rescheduled, and it cannot be assumed that the this->next_time + * and this->clock->position values are correct anymore. (Timer + * cancellations happen when the realtime clock is being used by + * this driver and the user modified the realtime clock for example.) + */ + if (this->props.freewheel || SPA_UNLIKELY(timer_was_canceled)) nsec = gettime_nsec(this, this->timer_clockid); else nsec = this->next_time; + /* "tracking" means that the driver is following a clock that is not + * usable by timerfd. It is an entirely separate clock, for example, + * a network interface PHC. If tracking is true, timer_clockid is + * always the monotonic clock, and this->props.clock_id is that entirely + * separate clock. If tracking is false, then this->props.clock_id + * equals timer_clockid, so "nsec" can directly be used as the current + * driver clock time in that case. */ if (this->tracking) - /* we are actually following another clock */ current_time = gettime_nsec(this, this->props.clock_id); else current_time = nsec; current_position = scale_u64(current_time, rate, SPA_NSEC_PER_SEC); - if (this->last_time == 0) { + if ((this->last_time == 0) || SPA_UNLIKELY(timer_was_canceled)) { spa_dll_set_bw(&this->dll, SPA_DLL_BW_MIN, duration, rate); this->max_error = rate * MAX_ERROR_MS / 1000; this->max_resync = rate * this->props.resync_ms / 1000; position = current_position; + + /* If the timer was canceled, then it is assumed that a + * discontinuity occurred. Accumulated nsec_offset values + * cannot be relied upon anymore, and need to be reset. + * Also, base_time is set back to 0 to make sure the log line + * further below (which prints current stats) continues to + * be printed. (If for example the clock was set to an + * earlier time, then the base_time might contain a future + * timestamp that the clock won't reach for a long while.) */ + if (timer_was_canceled) { + this->base_time = 0; + this->nsec_offset.offset = get_nsec_offset(this, NULL); + this->nsec_offset.err = 0; + } } else if (SPA_LIKELY(this->clock)) { position = this->clock->position + this->clock->duration; } else { @@ -415,6 +494,9 @@ static void on_timeout(struct spa_source *source) this->clock->delay = 0; this->clock->rate_diff = corr; this->clock->next_nsec = this->next_time + nsec_offset; + + SPA_FLAG_UPDATE(this->clock->flags, SPA_IO_CLOCK_FLAG_DISCONT, + timer_was_canceled); } spa_node_call_ready(&this->callbacks, @@ -431,7 +513,7 @@ static int do_start(struct impl *this) this->following = is_following(this); this->started = true; this->last_time = 0; - spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_set_timers, 0, NULL, 0, this); return 0; } @@ -440,7 +522,7 @@ static int do_stop(struct impl *this) if (!this->started) return 0; this->started = false; - spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_set_timers, 0, NULL, 0, this); return 0; } @@ -532,10 +614,280 @@ static int impl_node_process(void *object) return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA; } +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; +next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_clockId), + SPA_PROP_INFO_description, SPA_POD_String("The clock id (monotonic, realtime, etc.)"), + SPA_PROP_INFO_type, SPA_POD_String(clock_id_to_name(p->clock_id))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_clockDevice), + SPA_PROP_INFO_description, SPA_POD_String("The clock device (eg. /dev/ptp0)"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->clock_device, sizeof(p->clock_device))); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_clockInterface), + SPA_PROP_INFO_description, SPA_POD_String("The clock network interface (eg. eth0)"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->clock_interface, sizeof(p->clock_interface))); + break; + default: + return 0; + } + + break; + } + + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_clockId, SPA_POD_String(clock_id_to_name(p->clock_id)) + ); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_clockDevice, SPA_POD_Stringn(p->clock_device, sizeof(p->clock_device)) + ); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_clockInterface, SPA_POD_Stringn(p->clock_interface, sizeof(p->clock_interface)) + ); + break; + default: + return 0; + } + + break; + } + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int get_phc_index(struct spa_system *s, const char *name) { +#ifdef ETHTOOL_GET_TS_INFO + struct ethtool_ts_info info = {0}; + struct ifreq ifr = {0}; + int fd, err; + + info.cmd = ETHTOOL_GET_TS_INFO; + strncpy(ifr.ifr_name, name, IFNAMSIZ - 1); + ifr.ifr_data = (char *) &info; + + fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + return -errno; + + err = spa_system_ioctl(s, fd, SIOCETHTOOL, &ifr); + close(fd); + if (err < 0) + return -errno; + + return info.phc_index; +#else + return -ENOTSUP; +#endif +} + +static bool parse_clock_id(struct impl *this, const char *s) +{ + int id = clock_name_to_id(s); + if (id == -1) { + spa_log_info(this->log, "unknown clock id '%s'", s); + return false; + } + this->props.clock_id = id; + if (this->clock_fd >= 0) { + close(this->clock_fd); + this->clock_fd = -1; + } + return true; +} + +static bool parse_clock_device(struct impl *this, const char *s) +{ + int fd = open(s, O_RDONLY); + if (fd == -1) { + spa_log_info(this->log, "failed to open clock device '%s': %m", s); + return false; + } + if (this->clock_fd >= 0) { + close(this->clock_fd); + } + this->clock_fd = fd; + this->props.clock_id = FD_TO_CLOCKID(this->clock_fd); + return true; +} + +static bool parse_clock_interface(struct impl *this, const char *s) +{ + int phc_index = get_phc_index(this->data_system, s); + if (phc_index < 0) { + spa_log_info(this->log, "failed to get phc device index for interface '%s': %s", + s, spa_strerror(phc_index)); + return false; + } else { + char dev[19]; + spa_scnprintf(dev, sizeof(dev), "/dev/ptp%d", phc_index); + if (!parse_clock_device(this, dev)) { + spa_log_info(this->log, "failed to open clock device '%s' " + "for interface '%s': %m", dev, s); + return false; + } + } + return true; +} + +static void ensure_clock_name(struct impl *this) +{ + struct props *p = &this->props; + if (p->clock_name[0] == '\0') { + const char *name = clock_id_to_name(p->clock_id); + if (p->clock_device[0]) + name = p->clock_device; + if (p->clock_interface[0]) + name = p->clock_interface; + spa_scnprintf(p->clock_name, sizeof(p->clock_name), + "%s.%s", DEFAULT_CLOCK_PREFIX, name); + } +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + bool notify = false; + char buffer[CLOCK_NAME_MAX]; + int count; + + if (param == NULL) { + return 0; + } + + /* Note that the length passed to SPA_POD_OPT_Stringn() also + * includes room for the null terminator, so the content of the + * buffer variable is always guaranteed to be null terminated. */ + + spa_zero(buffer); + count = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_clockId, SPA_POD_OPT_Stringn(buffer, sizeof(buffer)) + ); + if (count && parse_clock_id(this, buffer)) + { + reset_props_strings(p); + notify = true; + } + + spa_zero(buffer); + count = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_clockDevice, SPA_POD_OPT_Stringn(buffer, sizeof(buffer)) + ); + if (count && parse_clock_device(this, buffer)) + { + reset_props_strings(p); + strncpy(p->clock_device, buffer, sizeof(p->clock_device)); + notify = true; + } + + spa_zero(buffer); + count = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_clockInterface, SPA_POD_OPT_Stringn(buffer, sizeof(buffer)) + ); + if (count && parse_clock_interface(this, buffer)) + { + reset_props_strings(p); + strncpy(p->clock_interface, buffer, sizeof(p->clock_interface)); + notify = true; + } + + if (notify) + { + ensure_clock_name(this); + spa_log_info(this->log, "%p: setting clock to '%s'", this, p->clock_name); + if (this->started) { + do_stop(this); + do_start(this); + } + emit_node_info(this, true); + } + + break; + } + + default: + return -ENOENT; + break; + } + + return 0; +} + static const struct spa_node_methods impl_node = { SPA_VERSION_NODE_METHODS, .add_listener = impl_node_add_listener, .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, .set_io = impl_node_set_io, .send_command = impl_node_send_command, .process = impl_node_process, @@ -573,7 +925,7 @@ static int impl_clear(struct spa_handle *handle) this = (struct impl *) handle; - spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_timer, 0, NULL, 0, this); spa_system_close(this->data_system, this->timer_source.fd); if (this->clock_fd != -1) @@ -589,31 +941,6 @@ impl_get_size(const struct spa_handle_factory *factory, return sizeof(struct impl); } -static int get_phc_index(struct spa_system *s, const char *name) { -#ifdef ETHTOOL_GET_TS_INFO - struct ethtool_ts_info info = {0}; - struct ifreq ifr = {0}; - int fd, err; - - info.cmd = ETHTOOL_GET_TS_INFO; - strncpy(ifr.ifr_name, name, IFNAMSIZ - 1); - ifr.ifr_data = (char *) &info; - - fd = socket(AF_INET, SOCK_DGRAM, 0); - if (fd < 0) - return -errno; - - err = spa_system_ioctl(s, fd, SIOCETHTOOL, &ifr); - close(fd); - if (err < 0) - return -errno; - - return info.phc_index; -#else - return -ENOTSUP; -#endif -} - static int impl_init(const struct spa_handle_factory *factory, struct spa_handle *handle, @@ -661,9 +988,10 @@ impl_init(const struct spa_handle_factory *factory, this->info.max_input_ports = 0; this->info.max_output_ports = 0; this->info.flags = SPA_NODE_FLAG_RT; - this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); this->info.params = this->params; - this->info.n_params = 0; + this->info.n_params = N_NODE_PARAMS; reset_props(&this->props); @@ -676,37 +1004,17 @@ impl_init(const struct spa_handle_factory *factory, spa_scnprintf(this->props.clock_name, sizeof(this->props.clock_name), "%s", s); } else if (spa_streq(k, "clock.id") && this->clock_fd < 0) { - this->props.clock_id = clock_name_to_id(s); - if (this->props.clock_id == -1) { - spa_log_warn(this->log, "unknown clock id '%s'", s); - this->props.clock_id = DEFAULT_CLOCK_ID; - } + if (parse_clock_id(this, s)) + reset_props_strings(&this->props); } else if (spa_streq(k, "clock.device")) { - if (this->clock_fd >= 0) { - close(this->clock_fd); - } - this->clock_fd = open(s, O_RDONLY); - - if (this->clock_fd == -1) { - spa_log_warn(this->log, "failed to open clock device '%s': %m", s); - } else { - this->props.clock_id = FD_TO_CLOCKID(this->clock_fd); + if (parse_clock_device(this, s)) { + reset_props_strings(&this->props); + strncpy(this->props.clock_device, s, sizeof(this->props.clock_device)-1); } } else if (spa_streq(k, "clock.interface") && this->clock_fd < 0) { - int phc_index = get_phc_index(this->data_system, s); - if (phc_index < 0) { - spa_log_warn(this->log, "failed to get phc device index for interface '%s': %s", - s, spa_strerror(phc_index)); - } else { - char dev[19]; - spa_scnprintf(dev, sizeof(dev), "/dev/ptp%d", phc_index); - this->clock_fd = open(dev, O_RDONLY); - if (this->clock_fd == -1) { - spa_log_warn(this->log, "failed to open clock device '%s' " - "for interface '%s': %m", dev, s); - } else { - this->props.clock_id = FD_TO_CLOCKID(this->clock_fd); - } + if (parse_clock_interface(this, s)) { + reset_props_strings(&this->props); + strncpy(this->props.clock_interface, s, sizeof(this->props.clock_interface)-1); } } else if (spa_streq(k, "freewheel.wait")) { this->props.freewheel_wait = atoi(s); @@ -719,6 +1027,7 @@ impl_init(const struct spa_handle_factory *factory, "%s.%s", DEFAULT_CLOCK_PREFIX, clock_id_to_name(this->props.clock_id)); } + ensure_clock_name(this); this->tracking = !clock_for_timerfd(this->props.clock_id); this->timer_clockid = this->tracking ? CLOCK_MONOTONIC : this->props.clock_id; diff --git a/spa/plugins/support/null-audio-sink.c b/spa/plugins/support/null-audio-sink.c index 997a66810..b804023b3 100644 --- a/spa/plugins/support/null-audio-sink.c +++ b/spa/plugins/support/null-audio-sink.c @@ -35,12 +35,13 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.null-audio-sink"); #define DEFAULT_CLOCK_NAME "clock.system.monotonic" +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS struct props { uint32_t format; uint32_t channels; uint32_t rate; - uint32_t pos[SPA_AUDIO_MAX_CHANNELS]; + uint32_t pos[MAX_CHANNELS]; char clock_name[64]; unsigned int debug:1; unsigned int driver:1; @@ -230,7 +231,7 @@ static int reassign_follower(struct impl *this) if (following != this->following) { spa_log_debug(this->log, "%p: reassign follower %d->%d", this, this->following, following); this->following = following; - spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_set_timers, 0, NULL, 0, this); } return 0; } @@ -314,7 +315,7 @@ static int do_start(struct impl *this) this->following = is_following(this); this->started = true; - spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_set_timers, 0, NULL, 0, this); return 0; } @@ -323,7 +324,7 @@ static int do_stop(struct impl *this) if (!this->started) return 0; this->started = false; - spa_loop_invoke(this->data_loop, do_set_timers, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_set_timers, 0, NULL, 0, this); return 0; } @@ -636,7 +637,7 @@ port_set_format(struct impl *this, if (info.info.raw.rate == 0 || info.info.raw.channels == 0 || - info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + info.info.raw.channels > MAX_CHANNELS) return -EINVAL; if (this->props.format != 0) { @@ -847,7 +848,7 @@ static int impl_clear(struct spa_handle *handle) this = (struct impl *) handle; - spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_timer, 0, NULL, 0, this); spa_system_close(this->data_system, this->timer_source.fd); return 0; @@ -949,7 +950,11 @@ impl_init(const struct spa_handle_factory *factory, } else if (spa_streq(k, SPA_KEY_NODE_DRIVER)) { this->props.driver = spa_atob(s); } else if (spa_streq(k, SPA_KEY_AUDIO_POSITION)) { - spa_audio_parse_position(s, strlen(s), this->props.pos, &this->props.channels); + spa_audio_parse_position_n(s, strlen(s), this->props.pos, + SPA_N_ELEMENTS(this->props.pos), &this->props.channels); + } else if (spa_streq(k, SPA_KEY_AUDIO_LAYOUT)) { + spa_audio_parse_layout(s, this->props.pos, + SPA_N_ELEMENTS(this->props.pos), &this->props.channels); } else if (spa_streq(k, "clock.name")) { spa_scnprintf(this->props.clock_name, sizeof(this->props.clock_name), diff --git a/spa/plugins/test/fakesink.c b/spa/plugins/test/fakesink.c index f71dc3d5e..71550300c 100644 --- a/spa/plugins/test/fakesink.c +++ b/spa/plugins/test/fakesink.c @@ -708,7 +708,7 @@ static int impl_clear(struct spa_handle *handle) this = (struct impl *) handle; if (this->data_loop) - spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_timer, 0, NULL, 0, this); spa_system_close(this->data_system, this->timer_source.fd); return 0; diff --git a/spa/plugins/test/fakesrc.c b/spa/plugins/test/fakesrc.c index d9658c9fc..28b37dab4 100644 --- a/spa/plugins/test/fakesrc.c +++ b/spa/plugins/test/fakesrc.c @@ -130,7 +130,7 @@ static int impl_node_enum_params(void *object, int seq, param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Props, id, SPA_PROP_live, SPA_POD_Bool(p->live), - SPA_PROP_patternType, SPA_POD_CHOICE_ENUM_Id(2, p->pattern, p->pattern)); + SPA_PROP_patternType, SPA_POD_CHOICE_ENUM_Int(2, p->pattern, p->pattern)); break; } default: @@ -173,7 +173,7 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, spa_pod_parse_object(param, SPA_TYPE_OBJECT_Props, NULL, SPA_PROP_live, SPA_POD_OPT_Bool(&p->live), - SPA_PROP_patternType, SPA_POD_OPT_Id(&p->pattern)); + SPA_PROP_patternType, SPA_POD_OPT_Int(&p->pattern)); if (p->live) port->info.flags |= SPA_PORT_FLAG_LIVE; @@ -739,7 +739,7 @@ static int impl_clear(struct spa_handle *handle) this = (struct impl *) handle; if (this->data_loop) - spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_timer, 0, NULL, 0, this); spa_system_close(this->data_system, this->timer_source.fd); return 0; diff --git a/spa/plugins/v4l2/v4l2-source.c b/spa/plugins/v4l2/v4l2-source.c index f19dffde8..2eb66b3da 100644 --- a/spa/plugins/v4l2/v4l2-source.c +++ b/spa/plugins/v4l2/v4l2-source.c @@ -90,6 +90,7 @@ struct port { struct v4l2_frmivalenum frmival; bool have_format; + bool have_modifier; struct spa_video_info current_format; struct spa_v4l2_device dev; @@ -112,6 +113,7 @@ struct port { struct spa_port_info info; struct spa_io_buffers *io; struct spa_io_sequence *control; + uint32_t control_size; #define PORT_PropInfo 0 #define PORT_EnumFormat 1 #define PORT_Meta 2 @@ -219,6 +221,12 @@ static int port_get_format(struct port *port, case SPA_MEDIA_SUBTYPE_raw: spa_pod_builder_add(builder, SPA_FORMAT_VIDEO_format, SPA_POD_Id(port->current_format.info.raw.format), + 0); + if (port->have_modifier) + spa_pod_builder_add(builder, + SPA_FORMAT_VIDEO_modifier, SPA_POD_Long(0), + 0); + spa_pod_builder_add(builder, SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&port->current_format.info.raw.size), SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&port->current_format.info.raw.framerate), 0); @@ -396,7 +404,7 @@ static int impl_node_set_param(void *object, sizeof(p->device)-1); break; default: - spa_v4l2_set_control(this, prop->key, prop); + spa_v4l2_set_control(this, prop, SPA_POD_BODY_CONST(&prop->value)); break; } } @@ -675,7 +683,7 @@ static int port_set_format(struct impl *this, struct port *port, const struct spa_pod *format) { struct spa_video_info info; - int res; + int res = 0; spa_zero(info); @@ -706,6 +714,7 @@ static int port_set_format(struct impl *this, struct port *port, spa_log_error(this->log, "can't parse video raw"); return -EINVAL; } + port->have_modifier = info.info.raw.flags & SPA_VIDEO_FLAG_MODIFIER; break; case SPA_MEDIA_SUBTYPE_mjpg: if (spa_format_video_mjpg_parse(format, &info.info.mjpg) < 0) @@ -755,7 +764,7 @@ static int port_set_format(struct impl *this, struct port *port, emit_port_info(this, port, false); emit_node_info(this, false); - return 0; + return res; } static int impl_node_port_set_param(void *object, @@ -855,6 +864,7 @@ static int impl_node_port_set_io(void *object, break; case SPA_IO_Control: port->control = data; + port->control_size = size; break; default: return -ENOENT; @@ -882,20 +892,31 @@ static int impl_node_port_reuse_buffer(void *object, return res; } -static int process_control(struct impl *this, struct spa_pod_sequence *control) +static int process_control(struct impl *this, struct spa_pod_sequence *control, uint32_t size) { - struct spa_pod_control *c; + struct spa_pod_parser parser[2]; + struct spa_pod_frame frame[2]; + struct spa_pod_sequence seq; + const void *seq_body, *c_body; + struct spa_pod_control c; - SPA_POD_SEQUENCE_FOREACH(control, c) { - switch (c->type) { + spa_pod_parser_init_from_data(&parser[0], control, size, 0, size); + if (spa_pod_parser_push_sequence_body(&parser[0], &frame[0], &seq, &seq_body) < 0) + return 0; + + while (spa_pod_parser_get_control_body(&parser[0], &c, &c_body) >= 0) { + switch (c.type) { case SPA_CONTROL_Properties: { - struct spa_pod_prop *prop; - struct spa_pod_object *obj = (struct spa_pod_object *) &c->value; + struct spa_pod_object obj; + struct spa_pod_prop prop; + const void *obj_body, *prop_body; - SPA_POD_OBJECT_FOREACH(obj, prop) { - spa_v4l2_set_control(this, prop->key, prop); - } + if (spa_pod_parser_init_object_body(&parser[1], &frame[1], + &c.value, c_body, &obj, &obj_body) < 0) + continue; + while (spa_pod_parser_get_prop_body(&parser[1], &prop, &prop_body) >= 0) + spa_v4l2_set_control(this, &prop, prop_body); break; } default: @@ -920,7 +941,7 @@ static int impl_node_process(void *object) return -EIO; if (port->control) - process_control(this, &port->control->sequence); + process_control(this, &port->control->sequence, port->control_size); spa_log_trace(this->log, "%p; status %d", this, io->status); diff --git a/spa/plugins/v4l2/v4l2-udev.c b/spa/plugins/v4l2/v4l2-udev.c index c45ef06ff..df99f7b4a 100644 --- a/spa/plugins/v4l2/v4l2-udev.c +++ b/spa/plugins/v4l2/v4l2-udev.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -24,7 +26,6 @@ #include #include -#include "config.h" #include "v4l2.h" #ifdef HAVE_LOGIND diff --git a/spa/plugins/v4l2/v4l2-utils.c b/spa/plugins/v4l2/v4l2-utils.c index be38ab32d..9cfd0afae 100644 --- a/spa/plugins/v4l2/v4l2-utils.c +++ b/spa/plugins/v4l2/v4l2-utils.c @@ -2,6 +2,7 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include #include #include #include @@ -398,7 +399,7 @@ enum_filter_format(uint32_t media_type, int32_t media_subtype, if (index == 0) video_format = values[0]; } else { - if (index + 1 < n_values) + if (index < n_values - 1) video_format = values[index + 1]; } } else { @@ -494,6 +495,162 @@ filter_framerate(struct v4l2_frmivalenum *frmival, return true; } +struct spa_video_colorimetry v4l2_colorimetry_map[] = { + { /* V4L2_COLORSPACE_DEFAULT */ + .range = SPA_VIDEO_COLOR_RANGE_UNKNOWN, + }, + { /* V4L2_COLORSPACE_SMPTE170M */ + .range = SPA_VIDEO_COLOR_RANGE_16_235, + .matrix = SPA_VIDEO_COLOR_MATRIX_BT601, + .transfer = SPA_VIDEO_TRANSFER_BT601, + .primaries = SPA_VIDEO_COLOR_PRIMARIES_SMPTE170M, + }, + { /* V4L2_COLORSPACE_SMPTE240M */ + .range = SPA_VIDEO_COLOR_RANGE_16_235, + .matrix = SPA_VIDEO_COLOR_MATRIX_SMPTE240M, + .transfer = SPA_VIDEO_TRANSFER_SMPTE240M, + .primaries = SPA_VIDEO_COLOR_PRIMARIES_SMPTE240M, + }, + { /* V4L2_COLORSPACE_REC709 */ + .range = SPA_VIDEO_COLOR_RANGE_16_235, + .matrix = SPA_VIDEO_COLOR_MATRIX_BT709, + .transfer = SPA_VIDEO_TRANSFER_BT709, + .primaries = SPA_VIDEO_COLOR_PRIMARIES_BT709, + }, + { /* V4L2_COLORSPACE_BT878 (deprecated) */ + .range = SPA_VIDEO_COLOR_RANGE_UNKNOWN, + }, + { /* V4L2_COLORSPACE_470_SYSTEM_M */ + .range = SPA_VIDEO_COLOR_RANGE_16_235, + .matrix = SPA_VIDEO_COLOR_MATRIX_BT601, + .transfer = SPA_VIDEO_TRANSFER_BT709, + .primaries = SPA_VIDEO_COLOR_PRIMARIES_BT470M, + }, + { /* V4L2_COLORSPACE_470_SYSTEM_BG */ + .range = SPA_VIDEO_COLOR_RANGE_16_235, + .matrix = SPA_VIDEO_COLOR_MATRIX_BT601, + .transfer = SPA_VIDEO_TRANSFER_BT709, + .primaries = SPA_VIDEO_COLOR_PRIMARIES_BT470BG, + }, + { /* V4L2_COLORSPACE_JPEG */ + .range = SPA_VIDEO_COLOR_RANGE_0_255, + .matrix = SPA_VIDEO_COLOR_MATRIX_BT601, + .transfer = SPA_VIDEO_TRANSFER_SRGB, + .primaries = SPA_VIDEO_COLOR_PRIMARIES_BT709, + }, + { /* V4L2_COLORSPACE_SRGB */ + .range = SPA_VIDEO_COLOR_RANGE_16_235, + .matrix = SPA_VIDEO_COLOR_MATRIX_BT601, + .transfer = SPA_VIDEO_TRANSFER_SRGB, + .primaries = SPA_VIDEO_COLOR_PRIMARIES_BT709, + }, + { /* V4L2_COLORSPACE_OPRGB */ + .range = SPA_VIDEO_COLOR_RANGE_16_235, + .matrix = SPA_VIDEO_COLOR_MATRIX_BT601, + .transfer = SPA_VIDEO_TRANSFER_ADOBERGB, + .primaries = SPA_VIDEO_COLOR_PRIMARIES_ADOBERGB, + }, + { /* V4L2_COLORSPACE_BT2020 */ + .range = SPA_VIDEO_COLOR_RANGE_16_235, + .matrix = SPA_VIDEO_COLOR_MATRIX_BT2020, + .transfer = SPA_VIDEO_TRANSFER_BT2020_12, + .primaries = SPA_VIDEO_COLOR_PRIMARIES_BT2020, + }, + { /* V4L2_COLORSPACE_RAW */ + .range = SPA_VIDEO_COLOR_RANGE_UNKNOWN, + } +}; + +enum spa_video_color_range v4l2_color_range_map[] = { + SPA_VIDEO_COLOR_RANGE_UNKNOWN, + SPA_VIDEO_COLOR_RANGE_0_255, + SPA_VIDEO_COLOR_RANGE_16_235 +}; + +enum spa_video_color_matrix v4l2_color_matrix_map[] = { + /* V4L2_YCBCR_ENC_DEFAULT */ + SPA_VIDEO_COLOR_MATRIX_UNKNOWN, + /* V4L2_YCBCR_ENC_601 */ + SPA_VIDEO_COLOR_MATRIX_BT601, + /* V4L2_YCBCR_ENC_709 */ + SPA_VIDEO_COLOR_MATRIX_BT709, + /* V4L2_YCBCR_ENC_XV601 */ + SPA_VIDEO_COLOR_MATRIX_BT601, + /* V4L2_YCBCR_ENC_XV709 */ + SPA_VIDEO_COLOR_MATRIX_BT709, + /* V4L2_YCBCR_ENC_SYCC */ + SPA_VIDEO_COLOR_MATRIX_BT601, + /* V4L2_YCBCR_ENC_BT2020 */ + SPA_VIDEO_COLOR_MATRIX_BT2020, + /* V4L2_YCBCR_ENC_BT2020_CONST_LUM */ + SPA_VIDEO_COLOR_MATRIX_BT2020, + /* V4L2_YCBCR_ENC_SMPTE240M */ + SPA_VIDEO_COLOR_MATRIX_SMPTE240M +}; + +enum spa_video_transfer_function v4l2_transfer_function_map[] = { + /* V4L2_XFER_FUNC_DEFAULT */ + SPA_VIDEO_TRANSFER_UNKNOWN, + /* V4L2_XFER_FUNC_709 */ + SPA_VIDEO_TRANSFER_BT709, + /* V4L2_XFER_FUNC_SRGB */ + SPA_VIDEO_TRANSFER_SRGB, + /* V4L2_XFER_FUNC_OPRGB */ + SPA_VIDEO_TRANSFER_ADOBERGB, + /* V4L2_XFER_FUNC_SMPTE240M */ + SPA_VIDEO_TRANSFER_SMPTE240M, + /* V4L2_XFER_FUNC_NONE */ + SPA_VIDEO_TRANSFER_GAMMA10, + /* V4L2_XFER_FUNC_DCI_P3 */ + SPA_VIDEO_TRANSFER_UNKNOWN, + /* V4L2_XFER_FUNC_SMPTE2084 */ + SPA_VIDEO_TRANSFER_SMPTE2084 +}; + +static bool +parse_colorimetry(struct impl *this, const struct v4l2_pix_format *pix, bool is_rgb, + struct spa_video_colorimetry *colorimetry) +{ + struct spa_video_colorimetry c = { 0 }; + + if (pix->colorspace < V4L2_COLORSPACE_RAW) + c = v4l2_colorimetry_map[pix->colorspace]; + + if (c.range == SPA_VIDEO_COLOR_RANGE_UNKNOWN) + return false; + + switch (pix->quantization) { + case V4L2_QUANTIZATION_FULL_RANGE: + case V4L2_QUANTIZATION_LIM_RANGE: + c.range = v4l2_color_range_map[pix->quantization]; + break; + case V4L2_QUANTIZATION_DEFAULT: + if (is_rgb) + c.range = SPA_VIDEO_COLOR_RANGE_0_255; + break; + default: + spa_log_warn(this->log, "Unknown enum v4l2_quantization value %d", + pix->quantization); + c.range = SPA_VIDEO_COLOR_RANGE_UNKNOWN; + break; + } + + if (pix->ycbcr_enc >= V4L2_YCBCR_ENC_SMPTE240M) + spa_log_warn(this->log, "Unknown enum v4l2_ycbcr_encoding value %d", + pix->ycbcr_enc); + else if (pix->ycbcr_enc > 0) + c.matrix = v4l2_color_matrix_map[pix->ycbcr_enc]; + + if (pix->xfer_func >= V4L2_XFER_FUNC_SMPTE2084) + spa_log_warn(this->log, "Unknown enum v4l2_xfer_func value %d", + pix->xfer_func); + else if (pix->xfer_func > 0) + c.transfer = v4l2_transfer_function_map[pix->xfer_func]; + + *colorimetry = c; + return true; +} + #define FOURCC_ARGS(f) (f)&0x7f,((f)>>8)&0x7f,((f)>>16)&0x7f,((f)>>24)&0x7f static int @@ -512,12 +669,14 @@ spa_v4l2_enum_format(struct impl *this, int seq, struct spa_pod_builder_state state; struct spa_pod_frame f[2]; struct spa_result_node_params result; - uint32_t count = 0; + struct v4l2_format fmt; + uint32_t count = 0, try_width = 0, try_height = 0; + bool with_modifier; if ((res = spa_v4l2_open(dev, this->props.device)) < 0) return res; - spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 8192); spa_pod_builder_get_state(&b.b, &state); result.id = SPA_PARAM_EnumFormat; @@ -537,6 +696,7 @@ spa_v4l2_enum_format(struct impl *this, int seq, if ((res = spa_format_parse(filter, &filter_media_type, &filter_media_subtype)) < 0) return res; } + with_modifier = !filter || spa_pod_find_prop(filter, NULL, SPA_FORMAT_VIDEO_modifier); if (false) { next_fmtdesc: @@ -729,6 +889,10 @@ do_frmsize_filter: if (info->media_subtype == SPA_MEDIA_SUBTYPE_raw) { spa_pod_builder_prop(&b.b, SPA_FORMAT_VIDEO_format, 0); spa_pod_builder_id(&b.b, info->format); + if (with_modifier) { + spa_pod_builder_prop(&b.b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY); + spa_pod_builder_long(&b.b, 0L); + } } spa_pod_builder_prop(&b.b, SPA_FORMAT_VIDEO_size, 0); @@ -736,6 +900,8 @@ do_frmsize_filter: spa_pod_builder_rectangle(&b.b, port->frmsize.discrete.width, port->frmsize.discrete.height); + try_width = port->frmsize.discrete.width; + try_height = port->frmsize.discrete.height; } else if (port->frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS || port->frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) { spa_pod_builder_push_choice(&b.b, &f[1], SPA_CHOICE_None, 0); @@ -760,6 +926,35 @@ do_frmsize_filter: port->frmsize.stepwise.max_height); } spa_pod_builder_pop(&b.b, &f[1]); + try_width = port->frmsize.stepwise.min_width; + try_height = port->frmsize.stepwise.min_height; + } + + spa_zero(fmt); + fmt.type = port->fmtdesc.type; + fmt.fmt.pix.pixelformat = info->fourcc; + fmt.fmt.pix.field = V4L2_FIELD_ANY; + fmt.fmt.pix.width = try_width; + fmt.fmt.pix.height = try_height; + + if ((res = xioctl(dev->fd, VIDIOC_TRY_FMT, &fmt)) < 0) { + spa_log_debug(this->log, "'%s' VIDIOC_TRY_FMT %08x: %m", + this->props.device, info->fourcc); + } else { + struct spa_video_colorimetry colorimetry; + bool is_rgb = spa_format_video_is_rgb(info->format); + + if (parse_colorimetry(this, &fmt.fmt.pix, is_rgb, &colorimetry)) { + spa_pod_builder_add(&b.b, + SPA_FORMAT_VIDEO_colorRange, + SPA_POD_Id(colorimetry.range), + SPA_FORMAT_VIDEO_colorMatrix, + SPA_POD_Id(colorimetry.matrix), + SPA_FORMAT_VIDEO_transferFunction, + SPA_POD_Id(colorimetry.transfer), + SPA_FORMAT_VIDEO_colorPrimaries, + SPA_POD_Id(colorimetry.primaries), 0); + } } spa_pod_builder_prop(&b.b, SPA_FORMAT_VIDEO_framerate, 0); @@ -899,6 +1094,26 @@ do_frminterval_filter: spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + if (++count == num) + goto enum_end; + + if (with_modifier && info->media_subtype == SPA_MEDIA_SUBTYPE_raw) { + struct spa_pod_object *op = (struct spa_pod_object *) result.param; + const struct spa_pod_prop *p; + + spa_pod_builder_push_object(&b.b, &f[0], op->body.type, op->body.id); + + SPA_POD_OBJECT_FOREACH(op, p) { + if (p->key != SPA_FORMAT_VIDEO_modifier) + spa_pod_builder_raw_padded(&b.b, p, SPA_POD_PROP_SIZE(p)); + } + + result.index = result.next++; + result.param = spa_pod_builder_pop(&b.b, &f[0]); + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + } + if (++count != num) goto next; @@ -1132,7 +1347,7 @@ static struct { { V4L2_CID_SATURATION, SPA_PROP_saturation }, { V4L2_CID_HUE, SPA_PROP_hue }, { V4L2_CID_GAMMA, SPA_PROP_gamma }, - { V4L2_CID_EXPOSURE, SPA_PROP_exposure }, + { V4L2_CID_EXPOSURE_ABSOLUTE, SPA_PROP_exposure }, { V4L2_CID_GAIN, SPA_PROP_gain }, { V4L2_CID_SHARPNESS, SPA_PROP_sharpness }, }; @@ -1354,8 +1569,7 @@ done: } static int -spa_v4l2_set_control(struct impl *this, uint32_t id, - const struct spa_pod_prop *prop) +spa_v4l2_set_control(struct impl *this, const struct spa_pod_prop *prop, const void *body) { struct port *port = &this->out_ports[0]; struct spa_v4l2_device *dev = &port->dev; @@ -1370,7 +1584,7 @@ spa_v4l2_set_control(struct impl *this, uint32_t id, if ((res = spa_v4l2_open(dev, this->props.device)) < 0) return res; - switch (SPA_POD_TYPE(&prop->value)) { + switch (prop->value.type) { case SPA_TYPE_Bool: { bool val; @@ -1873,7 +2087,12 @@ static int spa_v4l2_stream_on(struct impl *this) spa_log_debug(this->log, "starting"); - port->first_buffer = true; + if (port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_raw || + port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_mjpg || + port->current_format.media_subtype == SPA_MEDIA_SUBTYPE_jpeg) + port->first_buffer = true; + else + port->first_buffer = false; mmap_read(this); type = V4L2_BUF_TYPE_VIDEO_CAPTURE; @@ -1923,7 +2142,7 @@ static int spa_v4l2_stream_off(struct impl *this) spa_log_debug(this->log, "stopping"); - spa_loop_invoke(this->data_loop, do_remove_source, 0, NULL, 0, true, port); + spa_loop_locked(this->data_loop, do_remove_source, 0, NULL, 0, port); type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (xioctl(dev->fd, VIDIOC_STREAMOFF, &type) < 0) { diff --git a/spa/plugins/v4l2/v4l2.c b/spa/plugins/v4l2/v4l2.c index 71489320d..e080bf546 100644 --- a/spa/plugins/v4l2/v4l2.c +++ b/spa/plugins/v4l2/v4l2.c @@ -2,12 +2,13 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include -#include "config.h" #include "v4l2.h" extern const struct spa_handle_factory spa_v4l2_source_factory; diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c index 8c1c8de77..f4346fab6 100644 --- a/spa/plugins/videoconvert/videoadapter.c +++ b/spa/plugins/videoconvert/videoadapter.c @@ -15,10 +15,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -92,21 +94,48 @@ struct impl { struct spa_callbacks callbacks; unsigned int add_listener:1; + unsigned int have_rate_match:1; unsigned int have_format:1; unsigned int recheck_format:1; unsigned int started:1; unsigned int ready:1; unsigned int async:1; - unsigned int passthrough:1; + enum spa_param_port_config_mode mode; unsigned int follower_removing:1; unsigned int in_recalc; unsigned int warned:1; unsigned int driver:1; + + int in_enum_sync; }; /** \endcond */ +static int node_enum_params_sync(struct impl *impl, struct spa_node *node, + uint32_t id, uint32_t *index, const struct spa_pod *filter, + struct spa_pod **param, struct spa_pod_builder *builder) +{ + int res; + impl->in_enum_sync++; + res = spa_node_enum_params_sync(node, id, index, filter, param, builder); + impl->in_enum_sync--; + return res; +} + +static int node_port_enum_params_sync(struct impl *impl, struct spa_node *node, + enum spa_direction direction, uint32_t port_id, + uint32_t id, uint32_t *index, const struct spa_pod *filter, + struct spa_pod **param, struct spa_pod_builder *builder) +{ + int res; + impl->in_enum_sync++; + res = spa_node_port_enum_params_sync(node, direction, port_id, id, index, + filter, param, builder); + impl->in_enum_sync--; + return res; +} + static int follower_enum_params(struct impl *this, uint32_t id, uint32_t idx, @@ -115,19 +144,23 @@ static int follower_enum_params(struct impl *this, struct spa_pod_builder *builder) { int res; - if (result->next < 0x100000 && - this->follower != this->target) { - if ((res = spa_node_enum_params_sync(this->target, - id, &result->next, filter, &result->param, builder)) == 1) - return res; + if (result->next < 0x100000) { + if (this->follower != this->target && + this->convert_params_flags[idx] & SPA_PARAM_INFO_READ) { + if ((res = node_enum_params_sync(this, this->target, + id, &result->next, filter, &result->param, builder)) == 1) + return res; + } result->next = 0x100000; } - if (result->next < 0x200000 && this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) { - result->next &= 0xfffff; - if ((res = spa_node_enum_params_sync(this->follower, - id, &result->next, filter, &result->param, builder)) == 1) { - result->next |= 0x100000; - return res; + if (result->next < 0x200000) { + if (this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) { + result->next &= 0xfffff; + if ((res = node_enum_params_sync(this, this->follower, + id, &result->next, filter, &result->param, builder)) == 1) { + result->next |= 0x100000; + return res; + } } result->next = 0x200000; } @@ -188,7 +221,7 @@ next: switch (id) { case SPA_PARAM_EnumPortConfig: case SPA_PARAM_PortConfig: - if (this->passthrough) { + if (this->mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough) { switch (result.index) { case 0: result.param = spa_pod_builder_add_object(&b.b, @@ -222,7 +255,7 @@ next: case SPA_PARAM_Format: case SPA_PARAM_Latency: case SPA_PARAM_Tag: - res = spa_node_port_enum_params_sync(this->follower, + res = node_port_enum_params_sync(this, this->follower, this->direction, 0, id, &result.next, filter, &result.param, &b.b); break; @@ -252,7 +285,7 @@ static int link_io(struct impl *this) spa_zero(this->io_rate_match); this->io_rate_match.rate = 1.0; - if (this->follower == this->target) { + if (this->follower == this->target || !this->have_rate_match) { rate_match = NULL; rate_match_size = 0; } else { @@ -373,7 +406,7 @@ static int debug_params(struct impl *this, struct spa_node *node, state = 0; while (true) { spa_pod_builder_init(&b, buffer, sizeof(buffer)); - res = spa_node_port_enum_params_sync(node, + res = node_port_enum_params_sync(this, node, direction, port_id, id, &state, NULL, ¶m, &b); @@ -400,10 +433,14 @@ static int negotiate_buffers(struct impl *this) struct spa_pod *param; int res; bool follower_alloc, conv_alloc; - uint32_t i, size, buffers, blocks, align, flags, stride = 0; - uint32_t *aligns; + uint32_t i, size, buffers, blocks, align, flags, stride = 0, types; + uint32_t *aligns, data_flags; struct spa_data *datas; + struct spa_meta metas[1]; uint64_t follower_flags, conv_flags; + struct spa_node *alloc_node; + enum spa_direction alloc_direction; + uint32_t alloc_flags; spa_log_debug(this->log, "%p: n_buffers:%d", this, this->n_buffers); @@ -415,28 +452,32 @@ static int negotiate_buffers(struct impl *this) state = 0; param = NULL; - if ((res = spa_node_port_enum_params_sync(this->follower, - this->direction, 0, + if ((res = node_port_enum_params_sync(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) < 0) { if (res == -ENOENT) param = NULL; else { - debug_params(this, this->follower, this->direction, 0, - SPA_PARAM_Buffers, param, "follower buffers", res); + debug_params(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_Buffers, param, "target buffers", res); return res; } } state = 0; - if ((res = spa_node_port_enum_params_sync(this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, + if ((res = node_port_enum_params_sync(this, this->follower, + this->direction, 0, SPA_PARAM_Buffers, &state, param, ¶m, &b)) != 1) { - debug_params(this, this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_Buffers, param, "convert buffers", res); - return -ENOTSUP; + if (res == -ENOENT) + res = 0; + else { + debug_params(this, this->follower, this->direction, 0, + SPA_PARAM_Buffers, param, "follower buffers", res); + return res < 0 ? res : -ENOTSUP; + } } if (param == NULL) return -ENOTSUP; @@ -449,28 +490,29 @@ static int negotiate_buffers(struct impl *this) follower_alloc = SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); conv_alloc = SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS); - flags = 0; + flags = alloc_flags = 0; if (conv_alloc || follower_alloc) { flags |= SPA_BUFFER_ALLOC_FLAG_NO_DATA; - if (conv_alloc) - follower_alloc = false; + alloc_flags = SPA_NODE_BUFFERS_FLAG_ALLOC; } align = DEFAULT_ALIGN; + types = SPA_ID_INVALID; if ((res = spa_pod_parse_object(param, SPA_TYPE_OBJECT_ParamBuffers, NULL, - SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(&buffers), - SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(&blocks), - SPA_PARAM_BUFFERS_size, SPA_POD_Int(&size), - SPA_PARAM_BUFFERS_stride, SPA_POD_Int(&stride), - SPA_PARAM_BUFFERS_align, SPA_POD_OPT_Int(&align))) < 0) + SPA_PARAM_BUFFERS_buffers, SPA_POD_Int(&buffers), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(&blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(&size), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(&stride), + SPA_PARAM_BUFFERS_align, SPA_POD_OPT_Int(&align), + SPA_PARAM_BUFFERS_dataType, SPA_POD_OPT_Int(&types))) < 0) return res; if (this->async) buffers = SPA_MAX(2u, buffers); - spa_log_debug(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", + spa_log_info(this->log, "%p: buffers:%d, blocks:%d, size:%d, stride:%d align:%d %d:%d", this, buffers, blocks, size, stride, align, follower_alloc, conv_alloc); align = SPA_MAX(align, this->max_align); @@ -478,28 +520,51 @@ static int negotiate_buffers(struct impl *this) datas = alloca(sizeof(struct spa_data) * blocks); memset(datas, 0, sizeof(struct spa_data) * blocks); aligns = alloca(sizeof(uint32_t) * blocks); + + data_flags = SPA_DATA_FLAG_READWRITE; + if (SPA_FLAG_IS_SET(follower_flags, SPA_PORT_FLAG_DYNAMIC_DATA) && + SPA_FLAG_IS_SET(conv_flags, SPA_PORT_FLAG_DYNAMIC_DATA)) + data_flags |= SPA_DATA_FLAG_DYNAMIC; + + /* if we allocate, we allocate MemPtr memory */ + if (!SPA_FLAG_IS_SET(alloc_flags, SPA_NODE_BUFFERS_FLAG_ALLOC)) + types = SPA_DATA_MemPtr; + for (i = 0; i < blocks; i++) { - datas[i].type = SPA_DATA_MemPtr; - datas[i].flags = SPA_DATA_FLAG_READWRITE | SPA_DATA_FLAG_DYNAMIC; + datas[i].type = types; + datas[i].flags = data_flags; datas[i].maxsize = size; aligns[i] = align; } + metas[0].type = SPA_META_Header; + metas[0].size = sizeof(struct spa_meta_header); free(this->buffers); - this->buffers = spa_buffer_alloc_array(buffers, flags, 0, NULL, blocks, datas, aligns); + this->buffers = spa_buffer_alloc_array(buffers, flags, 1, metas, blocks, datas, aligns); if (this->buffers == NULL) return -errno; this->n_buffers = buffers; - if ((res = spa_node_port_use_buffers(this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - conv_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0, + /* prefer to let the follower alloc */ + if (follower_alloc) { + alloc_node = this->follower; + alloc_direction = this->direction; + } else { + alloc_node = this->target; + alloc_direction = SPA_DIRECTION_REVERSE(this->direction); + } + + if ((res = spa_node_port_use_buffers(alloc_node, + alloc_direction, 0, alloc_flags, this->buffers, this->n_buffers)) < 0) return res; - if ((res = spa_node_port_use_buffers(this->follower, - this->direction, 0, - follower_alloc ? SPA_NODE_BUFFERS_FLAG_ALLOC : 0, + alloc_node = alloc_node == this->follower ? this->target : this->follower; + alloc_direction = SPA_DIRECTION_REVERSE(alloc_direction); + alloc_flags = 0; + + if ((res = spa_node_port_use_buffers(alloc_node, + alloc_direction, 0, alloc_flags, this->buffers, this->n_buffers)) < 0) return res; @@ -544,7 +609,7 @@ static int configure_format(struct impl *this, uint32_t flags, const struct spa_ /* format was changed to nearest compatible format */ - if ((res = spa_node_port_enum_params_sync(this->follower, + if ((res = node_port_enum_params_sync(this, this->follower, this->direction, 0, SPA_PARAM_Format, &state, NULL, &fmt, &b)) != 1) @@ -610,7 +675,7 @@ static int recalc_latency(struct impl *this, struct spa_node *src, enum spa_dire while (true) { spa_pod_builder_init(&b, buffer, sizeof(buffer)); - if ((res = spa_node_port_enum_params_sync(src, + if ((res = node_port_enum_params_sync(this, src, direction, port_id, SPA_PARAM_Latency, &index, NULL, ¶m, &b)) != 1) { param = NULL; @@ -651,7 +716,7 @@ static int recalc_tag(struct impl *this, struct spa_node *src, enum spa_directio while (true) { void *tag_state = NULL; spa_pod_builder_reset(&b.b, &state); - if ((res = spa_node_port_enum_params_sync(src, + if ((res = node_port_enum_params_sync(this, src, direction, port_id, SPA_PARAM_Tag, &index, NULL, ¶m, &b.b)) != 1) { param = NULL; @@ -673,13 +738,14 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m int res = 0; struct spa_hook l; bool passthrough = mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough; + bool old_passthrough = this->mode == SPA_PARAM_PORT_CONFIG_MODE_passthrough; spa_log_debug(this->log, "%p: passthrough mode %d", this, passthrough); if (!passthrough && this->convert == NULL) return -ENOTSUP; - if (this->passthrough != passthrough) { + if (old_passthrough != passthrough) { if (passthrough) { /* remove converter split/merge ports */ configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none); @@ -699,21 +765,22 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m if ((res = configure_format(this, SPA_NODE_PARAM_FLAG_NEAREST, format)) < 0) return res; - if (this->passthrough != passthrough) { - this->passthrough = passthrough; - if (passthrough) { - /* add follower ports */ - spa_zero(l); - spa_node_add_listener(this->follower, &l, &follower_node_events, this); - spa_hook_remove(&l); - } else { - /* add converter ports */ - configure_convert(this, mode); - } - link_io(this); + this->mode = mode; + + if (old_passthrough != passthrough && passthrough) { + /* add follower ports */ + spa_zero(l); + spa_node_add_listener(this->follower, &l, &follower_node_events, this); + spa_hook_remove(&l); + } else { + /* add converter ports */ + configure_convert(this, mode); } + link_io(this); + this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; - SPA_FLAG_CLEAR(this->info.flags, SPA_NODE_FLAG_NEED_CONFIGURE); + SPA_FLAG_UPDATE(this->info.flags, SPA_NODE_FLAG_NEED_CONFIGURE, + this->mode == SPA_PARAM_PORT_CONFIG_MODE_none); SPA_FLAG_UPDATE(this->info.flags, SPA_NODE_FLAG_ASYNC, this->async && this->follower == this->target); this->params[IDX_Props].user++; @@ -833,6 +900,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) switch (id) { case SPA_IO_Position: this->io_position = data; + this->recheck_format = true; break; default: break; @@ -847,6 +915,54 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) return res; } +static int update_param_peer_formats(struct impl *impl) +{ + uint8_t buffer[4096]; + spa_auto(spa_pod_dynamic_builder) b = { 0 }; + uint32_t state = 0; + struct spa_pod *param; + struct spa_pod_frame f; + int res; + + if (!impl->recheck_format) + return 0; + + spa_log_debug(impl->log, "updating peer formats"); + + spa_node_send_command(impl->follower, + &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin)); + + spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); + spa_pod_builder_push_struct(&b.b, &f); + + while (true) { + res = node_port_enum_params_sync(impl, impl->follower, + impl->direction, 0, + SPA_PARAM_EnumFormat, &state, + NULL, ¶m, &b.b); + if (res != 1) + break; + } + param = spa_pod_builder_pop(&b.b, &f); + + spa_node_send_command(impl->follower, + &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamEnd)); + + spa_pod_simplify(&b.b, ¶m, param); + spa_debug_log_pod(impl->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, param); + + res = spa_node_port_set_param(impl->target, + SPA_DIRECTION_REVERSE(impl->direction), 0, + SPA_PARAM_PeerFormats, 0, param); + + impl->recheck_format = false; + + spa_log_debug(impl->log, "done updating peer formats: %d", res); + + return 0; +} + + static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder *b, uint32_t id, struct spa_pod_object *o1, struct spa_pod_object *o2) { @@ -855,7 +971,7 @@ static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder * struct spa_pod_builder_state state; int res = 0; - if (o2 == NULL || SPA_POD_TYPE(o1) != SPA_POD_TYPE(o2)) + if (o2 == NULL || o1->pod.type != o2->pod.type) return (struct spa_pod*)o1; spa_pod_builder_push_object(b, &f, o1->body.type, o1->body.id); @@ -864,7 +980,7 @@ static struct spa_pod *merge_objects(struct impl *this, struct spa_pod_builder * p2 = spa_pod_object_find_prop(o2, p2, p1->key); if (p2 != NULL) { spa_pod_builder_get_state(b, &state); - res = spa_pod_filter_prop(b, p1, p2); + res = spa_pod_filter_prop(b, p2, p1); if (res < 0) spa_pod_builder_reset(b, &state); } @@ -898,35 +1014,21 @@ static int negotiate_format(struct impl *this) if (this->have_format && !this->recheck_format) return 0; - this->recheck_format = false; + update_param_peer_formats(this); spa_pod_builder_init(&b, buffer, sizeof(buffer)); spa_node_send_command(this->follower, &SPA_NODE_COMMAND_INIT(SPA_NODE_COMMAND_ParamBegin)); - /* first try the ideal converter format, which is likely passthrough */ - tstate = 0; - fres = spa_node_port_enum_params_sync(this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_EnumFormat, &tstate, - NULL, &format, &b); - if (fres == 1) { - fstate = 0; - res = spa_node_port_enum_params_sync(this->follower, - this->direction, 0, - SPA_PARAM_EnumFormat, &fstate, - format, &format, &b); - if (res == 1) - goto found; - } - - /* then try something the follower can accept */ - for (fstate = 0;;) { + /* The target has been negotiated on its other ports and so it can propose + * a passthrough format or an ideal conversion. We use the suggestions of the + * target to find the best follower format */ + for (tstate = 0;;) { format = NULL; - res = spa_node_port_enum_params_sync(this->follower, - this->direction, 0, - SPA_PARAM_EnumFormat, &fstate, + res = node_port_enum_params_sync(this, this->target, + SPA_DIRECTION_REVERSE(this->direction), 0, + SPA_PARAM_EnumFormat, &tstate, NULL, &format, &b); if (res == -ENOENT) @@ -934,18 +1036,23 @@ static int negotiate_format(struct impl *this) else if (res <= 0) break; - tstate = 0; - fres = spa_node_port_enum_params_sync(this->target, - SPA_DIRECTION_REVERSE(this->direction), 0, - SPA_PARAM_EnumFormat, &tstate, + if (format != NULL) + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, format); + + fstate = 0; + fres = node_port_enum_params_sync(this, this->follower, + this->direction, 0, + SPA_PARAM_EnumFormat, &fstate, format, &format, &b); if (fres == 0 && res == 1) continue; + if (format != NULL) + spa_debug_log_pod(this->log, SPA_LOG_LEVEL_DEBUG, 0, NULL, format); + res = fres; break; } -found: if (format == NULL) { debug_params(this, this->follower, this->direction, 0, SPA_PARAM_EnumFormat, format, "follower format", res); @@ -961,6 +1068,8 @@ found: format = merge_objects(this, &b, SPA_PARAM_Format, (struct spa_pod_object*)format, (struct spa_pod_object*)def); + if (format == NULL) + return -ENOSPC; spa_pod_fixate(format); @@ -986,8 +1095,6 @@ static int impl_node_send_command(void *object, const struct spa_command *comman switch (SPA_NODE_COMMAND_ID(command)) { case SPA_NODE_COMMAND_Start: spa_log_debug(this->log, "%p: starting %d", this, this->started); - if (this->started) - return 0; if ((res = negotiate_format(this)) < 0) return res; this->ready = true; @@ -1007,7 +1114,10 @@ static int impl_node_send_command(void *object, const struct spa_command *comman break; } - if ((res = spa_node_send_command(this->target, command)) < 0) { + res = spa_node_send_command(this->target, command); + if (res == -ENOTSUP && this->target != this->follower) + res = 0; + if (res < 0) { spa_log_error(this->log, "%p: can't send command %d: %s", this, SPA_NODE_COMMAND_ID(command), spa_strerror(res)); @@ -1176,7 +1286,7 @@ static void convert_port_info(void *data, port_id--; } else if (info) { pi = *info; - pi.flags = this->follower_port_flags & + pi.flags |= this->follower_port_flags & (SPA_PORT_FLAG_LIVE | SPA_PORT_FLAG_PHYSICAL | SPA_PORT_FLAG_TERMINAL); @@ -1194,7 +1304,7 @@ static void convert_result(void *data, int seq, int res, uint32_t type, const vo { struct impl *this = data; - if (this->target == this->follower) + if (this->target == this->follower || this->in_enum_sync) return; spa_log_trace(this->log, "%p: result %d %d", this, seq, res); @@ -1213,8 +1323,8 @@ static void follower_info(void *data, const struct spa_node_info *info) struct impl *this = data; uint32_t i; - spa_log_debug(this->log, "%p: info change:%08"PRIx64, this, - info->change_mask); + spa_log_debug(this->log, "%p: info change:%08"PRIx64" %d:%d", this, + info->change_mask, info->max_input_ports, info->max_output_ports); if (this->follower_removing) return; @@ -1223,7 +1333,7 @@ static void follower_info(void *data, const struct spa_node_info *info) if (info->max_input_ports > 0) this->direction = SPA_DIRECTION_INPUT; - else + else this->direction = SPA_DIRECTION_OUTPUT; if (this->direction == SPA_DIRECTION_INPUT) { @@ -1371,7 +1481,7 @@ static void follower_result(void *data, int seq, int res, uint32_t type, const v { struct impl *this = data; - if (this->target != this->follower) + if (this->target != this->follower || this->in_enum_sync) return; spa_log_trace(this->log, "%p: result %d %d", this, seq, res); @@ -1404,6 +1514,20 @@ static const struct spa_node_events follower_node_events = { .event = follower_event, }; +static void follower_probe_info(void *data, const struct spa_node_info *info) +{ + struct impl *this = data; + if (info->max_input_ports > 0) + this->direction = SPA_DIRECTION_INPUT; + else + this->direction = SPA_DIRECTION_OUTPUT; +} + +static const struct spa_node_events follower_probe_events = { + SPA_VERSION_NODE_EVENTS, + .info = follower_probe_info, +}; + static int follower_ready(void *data, int status) { struct impl *this = data; @@ -1551,33 +1675,6 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_ return spa_node_remove_port(this->target, direction, port_id); } -static int follower_port_enum_params(struct impl *this, - enum spa_direction direction, uint32_t port_id, - uint32_t id, uint32_t idx, - struct spa_result_node_params *result, - const struct spa_pod *filter, - struct spa_pod_builder *builder) -{ - int res; - if (result->next < 0x100000 && this->follower_params_flags[idx] & SPA_PARAM_INFO_READ) { - if ((res = spa_node_port_enum_params_sync(this->follower, direction, port_id, - id, &result->next, filter, &result->param, builder)) == 1) - return res; - result->next = 0x100000; - } - if (result->next < 0x200000 && - this->follower != this->target) { - result->next &= 0xfffff; - if ((res = spa_node_port_enum_params_sync(this->target, direction, port_id, - id, &result->next, filter, &result->param, builder)) == 1) { - result->next |= 0x100000; - return res; - } - result->next = 0x200000; - } - return 0; -} - static int impl_node_port_enum_params(void *object, int seq, enum spa_direction direction, uint32_t port_id, @@ -1585,12 +1682,6 @@ impl_node_port_enum_params(void *object, int seq, const struct spa_pod *filter) { struct impl *this = object; - uint8_t buffer[4096]; - spa_auto(spa_pod_dynamic_builder) b = { 0 }; - struct spa_pod_builder_state state; - struct spa_result_node_params result; - uint32_t count = 0; - int res; spa_return_val_if_fail(this != NULL, -EINVAL); spa_return_val_if_fail(num != 0, -EINVAL); @@ -1598,37 +1689,10 @@ impl_node_port_enum_params(void *object, int seq, if (direction != this->direction) port_id++; - spa_pod_dynamic_builder_init(&b, buffer, sizeof(buffer), 4096); - spa_pod_builder_get_state(&b.b, &state); + spa_log_debug(this->log, "%p: %d %u %u %u", this, seq, id, start, num); - result.id = id; - result.next = start; -next: - result.index = result.next; - - spa_log_debug(this->log, "%p: %d id:%u", this, seq, id); - - spa_pod_builder_reset(&b.b, &state); - - switch (id) { - case SPA_PARAM_EnumFormat: - res = follower_port_enum_params(this, direction, port_id, - id, IDX_EnumFormat, &result, filter, &b.b); - break; - default: - return spa_node_port_enum_params(this->target, seq, direction, port_id, id, - start, num, filter); - } - if (res != 1) - return res; - - spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); - count++; - - if (count != num) - goto next; - - return 0; + return spa_node_port_enum_params(this->target, seq, direction, port_id, id, + start, num, filter); } static int @@ -1819,14 +1883,38 @@ static const struct spa_node_methods impl_node = { .process = impl_node_process, }; -static int load_plugin_from(struct impl *this, const struct spa_dict *info, - const char *convertname, struct spa_handle **handle, struct spa_node **iface) +static int load_converter(struct impl *this, const struct spa_dict *info, + const struct spa_support *support, uint32_t n_support) { + const char* factory_name = NULL; struct spa_handle *hnd_convert = NULL; void *iface_conv = NULL; - hnd_convert = spa_plugin_loader_load(this->ploader, convertname, info); - if (!hnd_convert) - return -EINVAL; + struct spa_dict_item *items; + struct spa_dict cinfo; + char direction[16]; + uint32_t i; + + items = alloca((info->n_items + 1) * sizeof(struct spa_dict_item)); + cinfo = SPA_DICT(items, 0); + for (i = 0; i < info->n_items; i++) + items[cinfo.n_items++] = info->items[i]; + + snprintf(direction, sizeof(direction), "%s", + SPA_DIRECTION_REVERSE(this->direction) == SPA_DIRECTION_INPUT ? + "input" : "output"); + items[cinfo.n_items++] = SPA_DICT_ITEM("convert.direction", direction); + + factory_name = spa_dict_lookup(&cinfo, "video.adapt.converter"); + if (factory_name == NULL) + return 0; + + if (this->ploader) { + hnd_convert = spa_plugin_loader_load(this->ploader, factory_name, &cinfo); + if (!hnd_convert) + return -EINVAL; + } else { + return -ENOTSUP; + } spa_handle_get_interface(hnd_convert, SPA_TYPE_INTERFACE_Node, &iface_conv); if (iface_conv == NULL) { @@ -1834,30 +1922,13 @@ static int load_plugin_from(struct impl *this, const struct spa_dict *info, return -EINVAL; } - *handle = hnd_convert; - *iface = iface_conv; + this->hnd_convert = hnd_convert; + this->convert = iface_conv; + this->convertname = strdup(factory_name); return 0; } -static int load_converter(struct impl *this, const struct spa_dict *info) -{ - int ret; - if (!this->ploader || !info) - return -EINVAL; - - const char* factory_name = spa_dict_lookup(info, "video.adapt.converter"); - - if (factory_name) { - ret = load_plugin_from(this, info, factory_name, &this->hnd_convert, &this->convert); - if (ret >= 0) { - this->convertname = strdup(factory_name); - return ret; - } - } - return 0; -} - static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface) { struct impl *this; @@ -1915,6 +1986,7 @@ impl_init(const struct spa_handle_factory *factory, struct impl *this; const char *str; int ret; + struct spa_hook probe_listener; spa_return_val_if_fail(factory != NULL, -EINVAL); spa_return_val_if_fail(handle != NULL, -EINVAL); @@ -1949,7 +2021,12 @@ impl_init(const struct spa_handle_factory *factory, SPA_VERSION_NODE, &impl_node, this); - ret = load_converter(this, info); + /* just probe the ports to get the direction */ + spa_zero(probe_listener); + spa_node_add_listener(this->follower, &probe_listener, &follower_probe_events, this); + spa_hook_remove(&probe_listener); + + ret = load_converter(this, info, support, n_support); spa_log_info(this->log, "%p: loaded converter %s, hnd %p, convert %p", this, this->convertname, this->hnd_convert, this->convert); if (ret < 0) @@ -1957,10 +2034,11 @@ impl_init(const struct spa_handle_factory *factory, if (this->convert == NULL) { this->target = this->follower; - this->passthrough = true; + this->mode = SPA_PARAM_PORT_CONFIG_MODE_passthrough; } else { this->target = this->convert; - this->passthrough = false; + /* the actual mode is selected below */ + this->mode = SPA_PARAM_PORT_CONFIG_MODE_none; } this->info_all = SPA_NODE_CHANGE_MASK_FLAGS | @@ -1968,8 +2046,7 @@ impl_init(const struct spa_handle_factory *factory, SPA_NODE_CHANGE_MASK_PARAMS; this->info = SPA_NODE_INFO_INIT(); this->info.flags = SPA_NODE_FLAG_RT | - 0; - //SPA_NODE_FLAG_NEED_CONFIGURE; + SPA_NODE_FLAG_NEED_CONFIGURE; this->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); this->params[IDX_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); this->params[IDX_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); @@ -1986,16 +2063,18 @@ impl_init(const struct spa_handle_factory *factory, &this->follower_listener, &follower_node_events, this); spa_node_set_callbacks(this->follower, &follower_node_callbacks, this); + if (this->convert != NULL) + update_param_peer_formats(this); + // TODO: adapt port bootstrap for arbitrary converter (incl. dummy) if (this->convert) { spa_node_add_listener(this->convert, &this->convert_listener, &convert_node_events, this); if (strcmp(this->convertname, "video.convert.dummy") == 0) { - configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_none); reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_passthrough, this->direction, NULL); } else { - configure_convert(this, SPA_PARAM_PORT_CONFIG_MODE_convert); + reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_convert, this->direction, NULL); } } else { reconfigure_mode(this, SPA_PARAM_PORT_CONFIG_MODE_passthrough, this->direction, NULL); diff --git a/spa/plugins/videoconvert/videoconvert-ffmpeg.c b/spa/plugins/videoconvert/videoconvert-ffmpeg.c index 955bfa93a..525b2c933 100644 --- a/spa/plugins/videoconvert/videoconvert-ffmpeg.c +++ b/spa/plugins/videoconvert/videoconvert-ffmpeg.c @@ -9,9 +9,9 @@ #include #include -#include #include #include +#include #include #include @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include #undef SPA_LOG_TOPIC_DEFAULT @@ -42,7 +44,7 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.videoconvert.ffmpeg"); #define MAX_ALIGN 64u -#define MAX_BUFFERS 32 +#define MAX_BUFFERS 32u #define MAX_DATAS 4 #define MAX_PORTS (1+1) @@ -58,10 +60,12 @@ static void props_reset(struct props *props) struct buffer { uint32_t id; #define BUFFER_FLAG_QUEUED (1<<0) +#define BUFFER_FLAG_MAPPED (1<<1) uint32_t flags; struct spa_list link; struct spa_buffer *buf; void *datas[MAX_DATAS]; + struct spa_meta_header *h; }; struct port { @@ -72,6 +76,7 @@ struct port { uint64_t info_all; struct spa_port_info info; + #define IDX_EnumFormat 0 #define IDX_Meta 1 #define IDX_IO 2 @@ -79,9 +84,14 @@ struct port { #define IDX_Buffers 4 #define IDX_Latency 5 #define IDX_Tag 6 -#define N_PORT_PARAMS 7 +#define IDX_PeerFormats 7 +#define N_PORT_PARAMS 8 struct spa_param_info params[N_PORT_PARAMS]; + struct spa_pod *peer_format_pod; + const struct spa_pod **peer_formats; + uint32_t n_peer_formats; + struct buffer buffers[MAX_BUFFERS]; uint32_t n_buffers; @@ -97,6 +107,7 @@ struct port { uint32_t blocks; uint32_t stride; + uint32_t size; uint32_t maxsize; struct spa_list queue; @@ -114,8 +125,11 @@ struct dir { unsigned int have_profile:1; struct spa_pod *tag; enum AVPixelFormat pix_fmt; - int width; - int height; + struct spa_rectangle size; + struct spa_fraction framerate; + + ptrdiff_t linesizes[4]; + size_t plane_size[4]; unsigned int control:1; }; @@ -165,7 +179,6 @@ struct impl { char group_name[128]; struct { - const AVCodec *codec; AVCodecContext *context; AVPacket *packet; AVFrame *frame; @@ -175,7 +188,6 @@ struct impl { AVFrame *frame; } convert; struct { - const AVCodec *codec; AVCodecContext *context; AVFrame *frame; AVPacket *packet; @@ -206,8 +218,8 @@ static void emit_node_info(struct impl *this, bool full) } } spa_node_emit_info(&this->hooks, &this->info); - this->info.change_mask = old; } + this->info.change_mask = old; } static void emit_port_info(struct impl *this, struct port *port, bool full) @@ -228,7 +240,7 @@ static void emit_port_info(struct impl *this, struct port *port, bool full) items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_IGNORE_LATENCY, "true"); } else if (PORT_IS_CONTROL(this, port->direction, port->id)) { items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_NAME, "control"); - items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "32 bit raw UMP"); + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_FORMAT_DSP, "8 bit raw midi"); } if (this->group_name[0] != '\0') items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_PORT_GROUP, this->group_name); @@ -243,8 +255,203 @@ static void emit_port_info(struct impl *this, struct port *port, bool full) } } spa_node_emit_port_info(&this->hooks, port->direction, port->id, &port->info); - port->info.change_mask = old; } + port->info.change_mask = old; +} + +static void emit_info(struct impl *this, bool full) +{ + struct port *p; + uint32_t i; + + emit_node_info(this, full); + for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) { + if ((p = GET_IN_PORT(this, i)) && p->valid) + emit_port_info(this, p, full); + } + for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) { + if ((p = GET_OUT_PORT(this, i)) && p->valid) + emit_port_info(this, p, full); + } +} + +struct format_info { + enum AVPixelFormat pix_fmt; + uint32_t format; + uint32_t dsp_format; +#define FORMAT_DSP (1<<0) +#define FORMAT_COMMON (1<<1) + uint32_t flags; +}; + +#if defined AV_PIX_FMT_AYUV +#define VIDEO_FORMAT_DSP_AYUV SPA_VIDEO_FORMAT_AYUV +#else +#define VIDEO_FORMAT_DSP_AYUV SPA_VIDEO_FORMAT_Y444 +#endif +#define VIDEO_FORMAT_DSP_RGBA SPA_VIDEO_FORMAT_RGBA + +static struct format_info format_info[] = +{ +#if defined AV_PIX_FMT_AYUV + { AV_PIX_FMT_AYUV, SPA_VIDEO_FORMAT_AYUV, VIDEO_FORMAT_DSP_AYUV, FORMAT_DSP | FORMAT_COMMON }, +#else + { AV_PIX_FMT_YUV444P, SPA_VIDEO_FORMAT_Y444, VIDEO_FORMAT_DSP_AYUV, FORMAT_DSP | FORMAT_COMMON }, +#endif + { AV_PIX_FMT_RGBA, SPA_VIDEO_FORMAT_RGBA, VIDEO_FORMAT_DSP_RGBA, FORMAT_DSP | FORMAT_COMMON }, + + { AV_PIX_FMT_YUYV422, SPA_VIDEO_FORMAT_YUY2, VIDEO_FORMAT_DSP_AYUV, FORMAT_COMMON }, + { AV_PIX_FMT_UYVY422, SPA_VIDEO_FORMAT_UYVY, VIDEO_FORMAT_DSP_AYUV, FORMAT_COMMON }, + { AV_PIX_FMT_YVYU422, SPA_VIDEO_FORMAT_YVYU, VIDEO_FORMAT_DSP_AYUV, FORMAT_COMMON }, + { AV_PIX_FMT_YUV420P, SPA_VIDEO_FORMAT_I420, VIDEO_FORMAT_DSP_AYUV, FORMAT_COMMON }, + + { AV_PIX_FMT_BGR0, SPA_VIDEO_FORMAT_BGRx, VIDEO_FORMAT_DSP_RGBA, FORMAT_COMMON }, + { AV_PIX_FMT_BGRA, SPA_VIDEO_FORMAT_BGRA, VIDEO_FORMAT_DSP_RGBA, FORMAT_COMMON }, + { AV_PIX_FMT_ARGB, SPA_VIDEO_FORMAT_ARGB, VIDEO_FORMAT_DSP_RGBA, FORMAT_COMMON }, + { AV_PIX_FMT_ABGR, SPA_VIDEO_FORMAT_ABGR, VIDEO_FORMAT_DSP_RGBA, FORMAT_COMMON }, + + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_YV12 }, + + { AV_PIX_FMT_RGB0, SPA_VIDEO_FORMAT_RGBx, VIDEO_FORMAT_DSP_RGBA, FORMAT_COMMON }, + { AV_PIX_FMT_0RGB, SPA_VIDEO_FORMAT_xRGB, VIDEO_FORMAT_DSP_RGBA, FORMAT_COMMON }, + { AV_PIX_FMT_0BGR, SPA_VIDEO_FORMAT_xBGR, VIDEO_FORMAT_DSP_RGBA, FORMAT_COMMON }, + + //{ AV_PIX_FMT_RGB24, SPA_VIDEO_FORMAT_RGB }, + //{ AV_PIX_FMT_BGR24, SPA_VIDEO_FORMAT_BGR }, + //{ AV_PIX_FMT_YUV411P, SPA_VIDEO_FORMAT_Y41B }, + //{ AV_PIX_FMT_YUV422P, SPA_VIDEO_FORMAT_Y42B }, + + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_v210 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_v216 }, + + //{ AV_PIX_FMT_NV12, SPA_VIDEO_FORMAT_NV12 }, + //{ AV_PIX_FMT_NV21, SPA_VIDEO_FORMAT_NV21 }, + //{ AV_PIX_FMT_GRAY8, SPA_VIDEO_FORMAT_GRAY8 }, + //{ AV_PIX_FMT_GRAY16BE, SPA_VIDEO_FORMAT_GRAY16_BE }, + //{ AV_PIX_FMT_GRAY16LE, SPA_VIDEO_FORMAT_GRAY16_LE }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_v308 }, + //{ AV_PIX_FMT_RGB565, SPA_VIDEO_FORMAT_RGB16 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_BGR16 }, + //{ AV_PIX_FMT_RGB555, SPA_VIDEO_FORMAT_RGB15 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_BGR15 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_UYVP }, + //{ AV_PIX_FMT_YUVA420P, SPA_VIDEO_FORMAT_A420 }, + //{ AV_PIX_FMT_PAL8, SPA_VIDEO_FORMAT_RGB8P }, + //{ AV_PIX_FMT_YUV410P, SPA_VIDEO_FORMAT_YUV9 }, + + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_YVU9 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_IYU1 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_ARGB64 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_AYUV64 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_r210 }, + + //{ AV_PIX_FMT_YUV420P10BE, SPA_VIDEO_FORMAT_I420_10BE }, + //{ AV_PIX_FMT_YUV420P10LE, SPA_VIDEO_FORMAT_I420_10LE }, + //{ AV_PIX_FMT_YUV422P10BE, SPA_VIDEO_FORMAT_I422_10BE }, + //{ AV_PIX_FMT_YUV422P10LE, SPA_VIDEO_FORMAT_I422_10LE }, + //{ AV_PIX_FMT_YUV444P10BE, SPA_VIDEO_FORMAT_Y444_10BE }, + //{ AV_PIX_FMT_YUV444P10LE, SPA_VIDEO_FORMAT_Y444_10LE }, + //{ AV_PIX_FMT_GBRP, SPA_VIDEO_FORMAT_GBR }, + //{ AV_PIX_FMT_GBRP10BE, SPA_VIDEO_FORMAT_GBR_10BE }, + //{ AV_PIX_FMT_GBRP10LE, SPA_VIDEO_FORMAT_GBR_10LE }, + + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_NV16 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_NV24 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_NV12_64Z32 }, + + //{ AV_PIX_FMT_YUVA420P10BE, SPA_VIDEO_FORMAT_A420_10BE }, + //{ AV_PIX_FMT_YUVA420P10LE, SPA_VIDEO_FORMAT_A420_10LE }, + //{ AV_PIX_FMT_YUVA422P10BE, SPA_VIDEO_FORMAT_A422_10BE }, + //{ AV_PIX_FMT_YUVA422P10LE, SPA_VIDEO_FORMAT_A422_10LE }, + //{ AV_PIX_FMT_YUVA444P10BE, SPA_VIDEO_FORMAT_A444_10BE }, + //{ AV_PIX_FMT_YUVA444P10LE, SPA_VIDEO_FORMAT_A444_10LE }, + + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_NV61 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_P010_10BE }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_P010_10LE }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_IYU2 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_VYUY }, + + //{ AV_PIX_FMT_GBRAP, SPA_VIDEO_FORMAT_GBRA }, + //{ AV_PIX_FMT_GBRAP10BE, SPA_VIDEO_FORMAT_GBRA_10BE }, + //{ AV_PIX_FMT_GBRAP10LE, SPA_VIDEO_FORMAT_GBRA_10LE }, + //{ AV_PIX_FMT_GBRP12BE, SPA_VIDEO_FORMAT_GBR_12BE }, + //{ AV_PIX_FMT_GBRP12LE, SPA_VIDEO_FORMAT_GBR_12LE }, + //{ AV_PIX_FMT_GBRAP12BE, SPA_VIDEO_FORMAT_GBRA_12BE }, + //{ AV_PIX_FMT_GBRAP12LE, SPA_VIDEO_FORMAT_GBRA_12LE }, + //{ AV_PIX_FMT_YUV420P12BE, SPA_VIDEO_FORMAT_I420_12BE }, + //{ AV_PIX_FMT_YUV420P12LE, SPA_VIDEO_FORMAT_I420_12LE }, + //{ AV_PIX_FMT_YUV422P12BE, SPA_VIDEO_FORMAT_I422_12BE }, + //{ AV_PIX_FMT_YUV422P12LE, SPA_VIDEO_FORMAT_I422_12LE }, + //{ AV_PIX_FMT_YUV444P12BE, SPA_VIDEO_FORMAT_Y444_12BE }, + //{ AV_PIX_FMT_YUV444P12LE, SPA_VIDEO_FORMAT_Y444_12LE }, + + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_RGBA_F16 }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_RGBA_F32 }, + + //{ AV_PIX_FMT_X2RGB10LE, SPA_VIDEO_FORMAT_xRGB_210LE }, + //{ AV_PIX_FMT_X2BGR10LE, SPA_VIDEO_FORMAT_xBGR_210LE }, + + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_RGBx_102LE }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_BGRx_102LE }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_ARGB_210LE }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_ABGR_210LE }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_RGBA_102LE }, + //{ AV_PIX_FMT_NONE, SPA_VIDEO_FORMAT_BGRA_102LE }, +}; + +static struct format_info *format_info_for_format(uint32_t format) +{ + SPA_FOR_EACH_ELEMENT_VAR(format_info, i) { + if (i->format == format) + return i; + } + return NULL; +} + +static enum AVPixelFormat format_to_pix_fmt(uint32_t format) +{ + struct format_info *i = format_info_for_format(format); + return i ? i->pix_fmt : AV_PIX_FMT_NONE; +} + +static int get_format(struct dir *dir, uint32_t *format, struct spa_rectangle *size, + struct spa_fraction *framerate) +{ + if (dir->have_format) { + switch (dir->format.media_subtype) { + case SPA_MEDIA_SUBTYPE_dsp: + *format = dir->format.info.dsp.format; + *size = SPA_RECTANGLE(640, 480); + *framerate = SPA_FRACTION(30, 1); + break; + case SPA_MEDIA_SUBTYPE_raw: + *format = dir->format.info.raw.format; + *size = dir->format.info.raw.size; + *framerate = dir->format.info.raw.framerate; + break; + case SPA_MEDIA_SUBTYPE_mjpg: + *format = SPA_VIDEO_FORMAT_I420; + *size = dir->format.info.mjpg.size; + *framerate = dir->format.info.mjpg.framerate; + break; + case SPA_MEDIA_SUBTYPE_h264: + *format = SPA_VIDEO_FORMAT_I420; + *size = dir->format.info.h264.size; + *framerate = dir->format.info.h264.framerate; + break; + default: + *format = SPA_VIDEO_FORMAT_I420; + *size = SPA_RECTANGLE(640, 480); + *framerate = SPA_FRACTION(30, 1); + break; + } + } else { + *format = SPA_VIDEO_FORMAT_I420; + *size = SPA_RECTANGLE(640, 480); + *framerate = SPA_FRACTION(30, 1); + } + return 0; } static int init_port(struct impl *this, enum spa_direction direction, uint32_t port_id, @@ -265,10 +472,10 @@ static int init_port(struct impl *this, enum spa_direction direction, uint32_t p port->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); port->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); - port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | + port->info = SPA_PORT_INFO_INIT(); + port->info.change_mask = port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | SPA_PORT_CHANGE_MASK_PROPS | SPA_PORT_CHANGE_MASK_PARAMS; - port->info = SPA_PORT_INFO_INIT(); port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_DYNAMIC_DATA; port->params[IDX_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); @@ -278,6 +485,7 @@ static int init_port(struct impl *this, enum spa_direction direction, uint32_t p port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); port->params[IDX_Latency] = SPA_PARAM_INFO(SPA_PARAM_Latency, SPA_PARAM_INFO_READWRITE); port->params[IDX_Tag] = SPA_PARAM_INFO(SPA_PARAM_Tag, SPA_PARAM_INFO_READWRITE); + port->params[IDX_PeerFormats] = SPA_PARAM_INFO(SPA_PARAM_PeerFormats, SPA_PARAM_INFO_WRITE); port->info.params = port->params; port->info.n_params = N_PORT_PARAMS; @@ -304,7 +512,6 @@ static int init_port(struct impl *this, enum spa_direction direction, uint32_t p spa_log_debug(this->log, "%p: add port %d:%d %d %d %d", this, direction, port_id, is_dsp, is_monitor, is_control); - emit_port_info(this, port, true); return 0; } @@ -315,10 +522,66 @@ static int deinit_port(struct impl *this, enum spa_direction direction, uint32_t if (port == NULL || !port->valid) return -ENOENT; port->valid = false; + free(port->peer_formats); + port->peer_formats = NULL; + port->n_peer_formats = 0; + free(port->peer_format_pod); + port->peer_format_pod = NULL; spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); return 0; } +static int node_param_enum_port_config(struct impl *this, uint32_t id, uint32_t index, + struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0 ... 1: + { + struct dir *dir = &this->dir[index]; + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_ParamPortConfig, id, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_CHOICE_ENUM_Id(4, + SPA_PARAM_PORT_CONFIG_MODE_none, + SPA_PARAM_PORT_CONFIG_MODE_none, + SPA_PARAM_PORT_CONFIG_MODE_dsp, + SPA_PARAM_PORT_CONFIG_MODE_convert), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_CHOICE_Bool(false), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_CHOICE_Bool(false)); + return 1; + } + } + return 0; +} + +static int node_param_port_config(struct impl *this, uint32_t id, uint32_t index, + struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0 ... 1: + { + struct dir *dir = &this->dir[index];; + struct spa_pod_frame f[1]; + spa_pod_builder_push_object(b, &f[0], + SPA_TYPE_OBJECT_ParamPortConfig, id); + spa_pod_builder_add(b, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(dir->mode), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(this->monitor), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(dir->control), + 0); + + if (dir->have_format) { + spa_pod_builder_prop(b, SPA_PARAM_PORT_CONFIG_format, 0); + spa_format_video_build(b, id, &dir->format); + } + *param = spa_pod_builder_pop(b, &f[0]); + return 1; + } + } + return 0; +} + static int impl_node_enum_params(void *object, int seq, uint32_t id, uint32_t start, uint32_t num, const struct spa_pod *filter) @@ -329,6 +592,7 @@ static int impl_node_enum_params(void *object, int seq, uint8_t buffer[4096]; struct spa_result_node_params result; uint32_t count = 0; + int res = 0; spa_return_val_if_fail(this != NULL, -EINVAL); spa_return_val_if_fail(num != 0, -EINVAL); @@ -340,92 +604,27 @@ static int impl_node_enum_params(void *object, int seq, spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = NULL; switch (id) { case SPA_PARAM_EnumPortConfig: - { - struct dir *dir; - switch (result.index) { - case 0: - dir = &this->dir[SPA_DIRECTION_INPUT];; - break; - case 1: - dir = &this->dir[SPA_DIRECTION_OUTPUT];; - break; - default: - return 0; - } - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamPortConfig, id, - SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), - SPA_PARAM_PORT_CONFIG_mode, SPA_POD_CHOICE_ENUM_Id(4, - SPA_PARAM_PORT_CONFIG_MODE_none, - SPA_PARAM_PORT_CONFIG_MODE_none, - SPA_PARAM_PORT_CONFIG_MODE_dsp, - SPA_PARAM_PORT_CONFIG_MODE_convert), - SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_CHOICE_Bool(false), - SPA_PARAM_PORT_CONFIG_control, SPA_POD_CHOICE_Bool(false)); + res = node_param_enum_port_config(this, id, result.index, ¶m, &b); break; - } case SPA_PARAM_PortConfig: - { - struct dir *dir; - struct spa_pod_frame f[1]; - - switch (result.index) { - case 0: - dir = &this->dir[SPA_DIRECTION_INPUT];; - break; - case 1: - dir = &this->dir[SPA_DIRECTION_OUTPUT];; - break; - default: - return 0; - } - spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_ParamPortConfig, id); - spa_pod_builder_add(&b, - SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(dir->direction), - SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(dir->mode), - SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(this->monitor), - SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(dir->control), - 0); - - if (dir->have_format) { - spa_pod_builder_prop(&b, SPA_PARAM_PORT_CONFIG_format, 0); - spa_format_video_build(&b, SPA_PARAM_PORT_CONFIG_format, - &dir->format); - } - param = spa_pod_builder_pop(&b, &f[0]); + res = node_param_port_config(this, id, result.index, ¶m, &b); break; - } case SPA_PARAM_PropInfo: - { - switch (result.index) { - default: - return 0; - } + res = 0; break; - } - case SPA_PARAM_Props: - { - struct spa_pod_frame f[2]; - - switch (result.index) { - case 0: - spa_pod_builder_push_object(&b, &f[0], - SPA_TYPE_OBJECT_Props, id); - param = spa_pod_builder_pop(&b, &f[0]); - break; - default: - return 0; - } + res = 0; break; - } default: return 0; } + if (res <= 0) + return res; - if (spa_pod_filter(&b, &result.param, param, filter) < 0) + if (param == NULL || spa_pod_filter(&b, &result.param, param, filter) < 0) goto next; spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); @@ -556,14 +755,10 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m switch (mode) { case SPA_PARAM_PORT_CONFIG_MODE_dsp: { - if (info) { - dir->n_ports = 1; - dir->format = *info; - dir->format.info.dsp.format = SPA_VIDEO_FORMAT_DSP_F32; - dir->have_format = true; - } else { - dir->n_ports = 0; - } + dir->n_ports = 1; + dir->format.info.dsp = SPA_VIDEO_INFO_DSP_INIT( + .format = SPA_VIDEO_FORMAT_DSP_F32); + dir->have_format = true; if (this->monitor && direction == SPA_DIRECTION_INPUT) this->dir[SPA_DIRECTION_OUTPUT].n_ports = dir->n_ports + 1; @@ -591,272 +786,101 @@ static int reconfigure_mode(struct impl *this, enum spa_param_port_config_mode m i = dir->n_ports++; init_port(this, direction, i, false, false, true); } - /* when output is convert mode, we are in OUTPUT (merge) mode, we always output all - * the incoming data to output. When output is DSP, we need to output quantum size - * chunks. */ - this->direction = this->dir[SPA_DIRECTION_OUTPUT].mode == SPA_PARAM_PORT_CONFIG_MODE_convert ? - SPA_DIRECTION_OUTPUT : SPA_DIRECTION_INPUT; + + /* emit all port info first, then the node props and flags */ + emit_info(this, false); this->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS | SPA_NODE_CHANGE_MASK_PARAMS; this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; - this->params[IDX_Props].user++; this->params[IDX_PortConfig].user++; + return 0; } -static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, - const struct spa_pod *param) +static int node_set_param_port_config(struct impl *this, uint32_t flags, + const struct spa_pod *param) { - struct impl *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); + struct spa_video_info info = { 0, }, *infop = NULL; + struct spa_pod *format = NULL; + enum spa_direction direction; + enum spa_param_port_config_mode mode; + bool monitor = false, control = false; + int res; if (param == NULL) return 0; - switch (id) { - case SPA_PARAM_PortConfig: - { - struct spa_video_info info = { 0, }, *infop = NULL; - struct spa_pod *format = NULL; - enum spa_direction direction; - enum spa_param_port_config_mode mode; - bool monitor = false, control = false; - int res; + if (spa_pod_parse_object(param, + SPA_TYPE_OBJECT_ParamPortConfig, NULL, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_OPT_Bool(&monitor), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_OPT_Bool(&control), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + return -EINVAL; - if (spa_pod_parse_object(param, - SPA_TYPE_OBJECT_ParamPortConfig, NULL, - SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(&direction), - SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(&mode), - SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_OPT_Bool(&monitor), - SPA_PARAM_PORT_CONFIG_control, SPA_POD_OPT_Bool(&control), - SPA_PARAM_PORT_CONFIG_format, SPA_POD_OPT_Pod(&format)) < 0) + if (format) { + if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format)) return -EINVAL; - if (format) { - if (!spa_pod_is_object_type(format, SPA_TYPE_OBJECT_Format)) - return -EINVAL; - - if ((res = spa_format_video_parse(format, &info)) < 0) - return res; - - infop = &info; - } - - if ((res = reconfigure_mode(this, mode, direction, monitor, control, infop)) < 0) + if ((res = spa_format_video_parse(format, &info)) < 0) return res; - emit_node_info(this, false); - break; + infop = &info; } + return reconfigure_mode(this, mode, direction, monitor, control, infop); +} + +static int node_set_param_props(struct impl *this, uint32_t flags, + const struct spa_pod *param) +{ + if (param == NULL) + return 0; + + apply_props(this, param); + return 0; +} +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + int res; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_PortConfig: + res = node_set_param_port_config(this, flags, param); + break; case SPA_PARAM_Props: - if (apply_props(this, param) > 0) - emit_node_info(this, false); + res = node_set_param_props(this, flags, param); break; default: return -ENOENT; } - return 0; + emit_info(this, false); + return res; } -static enum AVPixelFormat format_to_pix_fmt(uint32_t format) +static inline void free_decoder(struct impl *this) { - switch (format) { - case SPA_VIDEO_FORMAT_I420: - return AV_PIX_FMT_YUV420P; - case SPA_VIDEO_FORMAT_YV12: - break; - case SPA_VIDEO_FORMAT_YUY2: - return AV_PIX_FMT_YUYV422; - case SPA_VIDEO_FORMAT_UYVY: - return AV_PIX_FMT_UYVY422; - case SPA_VIDEO_FORMAT_AYUV: - break; - case SPA_VIDEO_FORMAT_RGBx: - return AV_PIX_FMT_RGB0; - case SPA_VIDEO_FORMAT_BGRx: - return AV_PIX_FMT_BGR0; - case SPA_VIDEO_FORMAT_xRGB: - return AV_PIX_FMT_0RGB; - case SPA_VIDEO_FORMAT_xBGR: - return AV_PIX_FMT_0BGR; - case SPA_VIDEO_FORMAT_RGBA: - return AV_PIX_FMT_RGBA; - case SPA_VIDEO_FORMAT_BGRA: - return AV_PIX_FMT_BGRA; - case SPA_VIDEO_FORMAT_ARGB: - return AV_PIX_FMT_ARGB; - case SPA_VIDEO_FORMAT_ABGR: - return AV_PIX_FMT_ABGR; - case SPA_VIDEO_FORMAT_RGB: - return AV_PIX_FMT_RGB24; - case SPA_VIDEO_FORMAT_BGR: - return AV_PIX_FMT_BGR24; - case SPA_VIDEO_FORMAT_Y41B: - return AV_PIX_FMT_YUV411P; - case SPA_VIDEO_FORMAT_Y42B: - return AV_PIX_FMT_YUV422P; - case SPA_VIDEO_FORMAT_YVYU: - return AV_PIX_FMT_YVYU422; - case SPA_VIDEO_FORMAT_Y444: - return AV_PIX_FMT_YUV444P; - case SPA_VIDEO_FORMAT_v210: - case SPA_VIDEO_FORMAT_v216: - break; - case SPA_VIDEO_FORMAT_NV12: - return AV_PIX_FMT_NV12; - case SPA_VIDEO_FORMAT_NV21: - return AV_PIX_FMT_NV21; - case SPA_VIDEO_FORMAT_GRAY8: - return AV_PIX_FMT_GRAY8; - case SPA_VIDEO_FORMAT_GRAY16_BE: - return AV_PIX_FMT_GRAY16BE; - case SPA_VIDEO_FORMAT_GRAY16_LE: - return AV_PIX_FMT_GRAY16LE; - case SPA_VIDEO_FORMAT_v308: - break; - case SPA_VIDEO_FORMAT_RGB16: - return AV_PIX_FMT_RGB565; - case SPA_VIDEO_FORMAT_BGR16: - break; - case SPA_VIDEO_FORMAT_RGB15: - return AV_PIX_FMT_RGB555; - case SPA_VIDEO_FORMAT_BGR15: - case SPA_VIDEO_FORMAT_UYVP: - break; - case SPA_VIDEO_FORMAT_A420: - return AV_PIX_FMT_YUVA420P; - case SPA_VIDEO_FORMAT_RGB8P: - return AV_PIX_FMT_PAL8; - case SPA_VIDEO_FORMAT_YUV9: - return AV_PIX_FMT_YUV410P; - case SPA_VIDEO_FORMAT_YVU9: - case SPA_VIDEO_FORMAT_IYU1: - case SPA_VIDEO_FORMAT_ARGB64: - case SPA_VIDEO_FORMAT_AYUV64: - case SPA_VIDEO_FORMAT_r210: - break; - case SPA_VIDEO_FORMAT_I420_10BE: - return AV_PIX_FMT_YUV420P10BE; - case SPA_VIDEO_FORMAT_I420_10LE: - return AV_PIX_FMT_YUV420P10LE; - case SPA_VIDEO_FORMAT_I422_10BE: - return AV_PIX_FMT_YUV422P10BE; - case SPA_VIDEO_FORMAT_I422_10LE: - return AV_PIX_FMT_YUV422P10LE; - case SPA_VIDEO_FORMAT_Y444_10BE: - return AV_PIX_FMT_YUV444P10BE; - case SPA_VIDEO_FORMAT_Y444_10LE: - return AV_PIX_FMT_YUV444P10LE; - case SPA_VIDEO_FORMAT_GBR: - return AV_PIX_FMT_GBRP; - case SPA_VIDEO_FORMAT_GBR_10BE: - return AV_PIX_FMT_GBRP10BE; - case SPA_VIDEO_FORMAT_GBR_10LE: - return AV_PIX_FMT_GBRP10LE; - case SPA_VIDEO_FORMAT_NV16: - case SPA_VIDEO_FORMAT_NV24: - case SPA_VIDEO_FORMAT_NV12_64Z32: - break; - case SPA_VIDEO_FORMAT_A420_10BE: - return AV_PIX_FMT_YUVA420P10BE; - case SPA_VIDEO_FORMAT_A420_10LE: - return AV_PIX_FMT_YUVA420P10LE; - case SPA_VIDEO_FORMAT_A422_10BE: - return AV_PIX_FMT_YUVA422P10BE; - case SPA_VIDEO_FORMAT_A422_10LE: - return AV_PIX_FMT_YUVA422P10LE; - case SPA_VIDEO_FORMAT_A444_10BE: - return AV_PIX_FMT_YUVA444P10BE; - case SPA_VIDEO_FORMAT_A444_10LE: - return AV_PIX_FMT_YUVA444P10LE; - case SPA_VIDEO_FORMAT_NV61: - case SPA_VIDEO_FORMAT_P010_10BE: - case SPA_VIDEO_FORMAT_P010_10LE: - case SPA_VIDEO_FORMAT_IYU2: - case SPA_VIDEO_FORMAT_VYUY: - break; - case SPA_VIDEO_FORMAT_GBRA: - return AV_PIX_FMT_GBRAP; - case SPA_VIDEO_FORMAT_GBRA_10BE: - return AV_PIX_FMT_GBRAP10BE; - case SPA_VIDEO_FORMAT_GBRA_10LE: - return AV_PIX_FMT_GBRAP10LE; - case SPA_VIDEO_FORMAT_GBR_12BE: - return AV_PIX_FMT_GBRP12BE; - case SPA_VIDEO_FORMAT_GBR_12LE: - return AV_PIX_FMT_GBRP12LE; - case SPA_VIDEO_FORMAT_GBRA_12BE: - return AV_PIX_FMT_GBRAP12BE; - case SPA_VIDEO_FORMAT_GBRA_12LE: - return AV_PIX_FMT_GBRAP12LE; - case SPA_VIDEO_FORMAT_I420_12BE: - return AV_PIX_FMT_YUV420P12BE; - case SPA_VIDEO_FORMAT_I420_12LE: - return AV_PIX_FMT_YUV420P12LE; - case SPA_VIDEO_FORMAT_I422_12BE: - return AV_PIX_FMT_YUV422P12BE; - case SPA_VIDEO_FORMAT_I422_12LE: - return AV_PIX_FMT_YUV422P12LE; - case SPA_VIDEO_FORMAT_Y444_12BE: - return AV_PIX_FMT_YUV444P12BE; - case SPA_VIDEO_FORMAT_Y444_12LE: - return AV_PIX_FMT_YUV444P12LE; - - case SPA_VIDEO_FORMAT_RGBA_F16: - case SPA_VIDEO_FORMAT_RGBA_F32: - break; - - case SPA_VIDEO_FORMAT_xRGB_210LE: - return AV_PIX_FMT_X2RGB10LE; - case SPA_VIDEO_FORMAT_xBGR_210LE: - return AV_PIX_FMT_X2BGR10LE; - - case SPA_VIDEO_FORMAT_RGBx_102LE: - case SPA_VIDEO_FORMAT_BGRx_102LE: - case SPA_VIDEO_FORMAT_ARGB_210LE: - case SPA_VIDEO_FORMAT_ABGR_210LE: - case SPA_VIDEO_FORMAT_RGBA_102LE: - case SPA_VIDEO_FORMAT_BGRA_102LE: - break; - default: - break; - } - return AV_PIX_FMT_NONE; + avcodec_free_context(&this->decoder.context); + av_packet_free(&this->decoder.packet); } -static int get_format(struct dir *dir, int *width, int *height, uint32_t *format) +static inline void free_encoder(struct impl *this) { - if (dir->have_format) { - switch (dir->format.media_subtype) { - case SPA_MEDIA_SUBTYPE_raw: - *width = dir->format.info.raw.size.width; - *height = dir->format.info.raw.size.height; - *format = dir->format.info.raw.format; - break; - case SPA_MEDIA_SUBTYPE_mjpg: - *width = dir->format.info.mjpg.size.width; - *height = dir->format.info.mjpg.size.height; - break; - case SPA_MEDIA_SUBTYPE_h264: - *width = dir->format.info.h264.size.width; - *height = dir->format.info.h264.size.height; - break; - default: - *width = *height = 0; - break; - } - } else { - *width = *height = 0; - } - return 0; + avcodec_free_context(&this->encoder.context); + av_packet_free(&this->encoder.packet); + av_frame_free(&this->encoder.frame); } - static int setup_convert(struct impl *this) { struct dir *in, *out; - uint32_t format; + uint32_t in_format = 0, out_format = 0; + int decoder_id = 0, encoder_id = 0; + const AVCodec *codec; in = &this->dir[SPA_DIRECTION_INPUT]; out = &this->dir[SPA_DIRECTION_OUTPUT]; @@ -870,74 +894,96 @@ static int setup_convert(struct impl *this) if (!in->have_format || !out->have_format) return -EIO; + get_format(in, &in_format, &in->size, &in->framerate); + get_format(out, &out_format, &out->size, &out->framerate); + switch (in->format.media_subtype) { + case SPA_MEDIA_SUBTYPE_dsp: case SPA_MEDIA_SUBTYPE_raw: - in->pix_fmt = format_to_pix_fmt(in->format.info.raw.format); + in->pix_fmt = format_to_pix_fmt(in_format); switch (out->format.media_subtype) { case SPA_MEDIA_SUBTYPE_raw: - out->pix_fmt = format_to_pix_fmt(out->format.info.raw.format); + case SPA_MEDIA_SUBTYPE_dsp: + out->pix_fmt = format_to_pix_fmt(out_format); break; case SPA_MEDIA_SUBTYPE_mjpg: - if ((this->encoder.codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG)) == NULL) { - spa_log_error(this->log, "failed to find MJPEG encoder"); - return -ENOTSUP; - } + encoder_id = AV_CODEC_ID_MJPEG; out->format.media_subtype = SPA_MEDIA_SUBTYPE_raw; out->format.info.raw.format = SPA_VIDEO_FORMAT_I420; - out->format.info.raw.size = in->format.info.raw.size; + out->format.info.raw.size = in->size; + out->format.info.raw.framerate = in->framerate; out->pix_fmt = AV_PIX_FMT_YUVJ420P; break; case SPA_MEDIA_SUBTYPE_h264: - if ((this->encoder.codec = avcodec_find_encoder(AV_CODEC_ID_H264)) == NULL) { - spa_log_error(this->log, "failed to find H264 encoder"); - return -ENOTSUP; - } + encoder_id = AV_CODEC_ID_H264; + out->format.media_subtype = SPA_MEDIA_SUBTYPE_raw; + out->format.info.raw.format = SPA_VIDEO_FORMAT_I420; + out->format.info.raw.size = in->size; + out->format.info.raw.framerate = in->framerate; + out->pix_fmt = AV_PIX_FMT_YUVJ420P; break; default: + spa_log_warn(this->log, "%p: in_subtype:%d out_subtype:%d", this, + in->format.media_subtype, out->format.media_subtype); return -ENOTSUP; } break; case SPA_MEDIA_SUBTYPE_mjpg: switch (out->format.media_subtype) { case SPA_MEDIA_SUBTYPE_mjpg: - /* passthrough */ - break; - case SPA_MEDIA_SUBTYPE_raw: - out->pix_fmt = format_to_pix_fmt(out->format.info.raw.format); - if ((this->decoder.codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG)) == NULL) { - spa_log_error(this->log, "failed to find MJPEG decoder"); - return -ENOTSUP; + /* passthrough if same dimensions or else reencode */ + if (in->size.width != out->size.width || + in->size.height != out->size.height) { + encoder_id = decoder_id = AV_CODEC_ID_MJPEG; + out->pix_fmt = AV_PIX_FMT_YUVJ420P; } break; + case SPA_MEDIA_SUBTYPE_raw: + case SPA_MEDIA_SUBTYPE_dsp: + out->pix_fmt = format_to_pix_fmt(out_format); + decoder_id = AV_CODEC_ID_MJPEG; + break; default: + spa_log_warn(this->log, "%p: in_subtype:%d out_subtype:%d", this, + in->format.media_subtype, out->format.media_subtype); return -ENOTSUP; } break; case SPA_MEDIA_SUBTYPE_h264: switch (out->format.media_subtype) { case SPA_MEDIA_SUBTYPE_h264: - /* passthrough */ + /* passthrough if same dimensions or else reencode */ + if (in->size.width != out->size.width || + in->size.height != out->size.height) + encoder_id = decoder_id = AV_CODEC_ID_H264; break; case SPA_MEDIA_SUBTYPE_raw: - out->pix_fmt = format_to_pix_fmt(out->format.info.raw.format); - if ((this->decoder.codec = avcodec_find_decoder(AV_CODEC_ID_H264)) == NULL) { - spa_log_error(this->log, "failed to find H264 decoder"); - return -ENOTSUP; - } + case SPA_MEDIA_SUBTYPE_dsp: + out->pix_fmt = format_to_pix_fmt(out_format); + decoder_id = AV_CODEC_ID_H264; break; default: + spa_log_warn(this->log, "%p: in_subtype:%d out_subtype:%d", this, + in->format.media_subtype, out->format.media_subtype); return -ENOTSUP; } break; default: + spa_log_warn(this->log, "%p: in_subtype:%d out_subtype:%d", this, + in->format.media_subtype, out->format.media_subtype); return -ENOTSUP; } - get_format(in, &in->width, &in->height, &format); - get_format(out, &out->width, &out->height, &format); - - if (this->decoder.codec) { - if ((this->decoder.context = avcodec_alloc_context3(this->decoder.codec)) == NULL) + if (in->pix_fmt == AV_PIX_FMT_NONE || out->pix_fmt == AV_PIX_FMT_NONE) { + spa_log_warn(this->log, "%p: unsupported pixel format", this); + return -ENOTSUP; + } + if (decoder_id) { + if ((codec = avcodec_find_decoder(decoder_id)) == NULL) { + spa_log_error(this->log, "failed to find %d decoder", decoder_id); + return -ENOTSUP; + } + if ((this->decoder.context = avcodec_alloc_context3(codec)) == NULL) return -EIO; if ((this->decoder.packet = av_packet_alloc()) == NULL) @@ -945,15 +991,23 @@ static int setup_convert(struct impl *this) this->decoder.context->flags2 |= AV_CODEC_FLAG2_FAST; - if (avcodec_open2(this->decoder.context, this->decoder.codec, NULL) < 0) { + if (avcodec_open2(this->decoder.context, codec, NULL) < 0) { spa_log_error(this->log, "failed to open decoder codec"); return -EIO; } + spa_log_info(this->log, "%p: using decoder %s", this, codec->name); + } else { + free_decoder(this); } + av_frame_free(&this->decoder.frame); if ((this->decoder.frame = av_frame_alloc()) == NULL) return -EIO; - if (this->encoder.codec) { - if ((this->encoder.context = avcodec_alloc_context3(this->encoder.codec)) == NULL) + if (encoder_id) { + if ((codec = avcodec_find_encoder(encoder_id)) == NULL) { + spa_log_error(this->log, "failed to find %d encoder", encoder_id); + return -ENOTSUP; + } + if ((this->encoder.context = avcodec_alloc_context3(codec)) == NULL) return -EIO; if ((this->encoder.packet = av_packet_alloc()) == NULL) @@ -963,23 +1017,26 @@ static int setup_convert(struct impl *this) this->encoder.context->flags2 |= AV_CODEC_FLAG2_FAST; this->encoder.context->time_base.num = 1; - this->encoder.context->width = out->width; - this->encoder.context->height = out->height; + this->encoder.context->width = out->size.width; + this->encoder.context->height = out->size.height; this->encoder.context->pix_fmt = out->pix_fmt; - if (avcodec_open2(this->encoder.context, this->encoder.codec, NULL) < 0) { + if (avcodec_open2(this->encoder.context, codec, NULL) < 0) { spa_log_error(this->log, "failed to open encoder codec"); return -EIO; } + spa_log_info(this->log, "%p: using encoder %s", this, codec->name); + } else { + free_encoder(this); } + sws_freeContext(this->convert.context); + this->convert.context = NULL; + av_frame_free(&this->convert.frame); if ((this->convert.frame = av_frame_alloc()) == NULL) return -EIO; - this->setup = true; - emit_node_info(this, false); - return 0; } @@ -1025,24 +1082,15 @@ impl_node_add_listener(void *object, void *data) { struct impl *this = object; - uint32_t i; struct spa_hook_list save; - struct port *p; spa_return_val_if_fail(this != NULL, -EINVAL); spa_log_trace(this->log, "%p: add listener %p", this, listener); spa_hook_list_isolate(&this->hooks, &save, listener, events, data); - emit_node_info(this, true); - for (i = 0; i < this->dir[SPA_DIRECTION_INPUT].n_ports; i++) { - if ((p = GET_IN_PORT(this, i)) && p->valid) - emit_port_info(this, p, true); - } - for (i = 0; i < this->dir[SPA_DIRECTION_OUTPUT].n_ports; i++) { - if ((p = GET_OUT_PORT(this, i)) && p->valid) - emit_port_info(this, p, true); - } + emit_info(this, true); + spa_hook_list_join(&this->hooks, &save); return 0; @@ -1068,91 +1116,306 @@ impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_ return -ENOTSUP; } -static int port_enum_formats(void *object, - enum spa_direction direction, uint32_t port_id, - uint32_t index, - struct spa_pod **param, - struct spa_pod_builder *builder) +static void add_video_formats(struct spa_pod_builder *b, + uint32_t *vals, uint32_t n_vals, bool is_dsp) { - struct impl *this = object; - struct dir *other = &this->dir[SPA_DIRECTION_REVERSE(direction)]; struct spa_pod_frame f[1]; - int width, height; - uint32_t format = 0; + uint32_t ids[SPA_N_ELEMENTS(format_info) + 1], n_ids = 0, i, j, fmt; - get_format(other, &width, &height, &format); + spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_format, 0); + spa_pod_builder_push_choice(b, &f[0], SPA_CHOICE_Enum, 0); + + if (n_vals == 0) { + vals = &format_info[0].format; + n_vals = 1; + } + /* all supported formats */ + for (i = 0; i < n_vals; i++) { + struct format_info *fi = format_info_for_format(vals[i]); + if (fi == NULL) + continue; + + fmt = is_dsp ? fi->dsp_format : fi->format; + + for (j = 0; j < n_ids; j++) { + if (ids[j] == fmt) + break; + } + if (j == n_ids) + ids[n_ids++] = fmt; + } + /* then add all other supported formats */ + SPA_FOR_EACH_ELEMENT_VAR(format_info, fi) { + if (fi->pix_fmt == AV_PIX_FMT_NONE) + continue; + + fmt = is_dsp ? fi->dsp_format : fi->format; + + for (j = 0; j < n_ids; j++) { + if (fmt == ids[j]) + break; + } + if (j == n_ids) + ids[n_ids++] = fmt; + } + for (i = 0; i < n_ids; i++) { + spa_pod_builder_id(b, ids[i]); + if (i == 0) + spa_pod_builder_id(b, ids[i]); + } + spa_pod_builder_pop(b, &f[0]); +} + +static struct spa_pod *transform_format(struct impl *this, struct port *port, const struct spa_pod *format, + uint32_t id, struct spa_pod_builder *b) +{ + uint32_t media_type, media_subtype, fmt; + struct spa_pod_object *obj; + const struct spa_pod_prop *prop; + struct spa_pod_frame f[2]; + + if (!spa_format_parse(format, &media_type, &media_subtype) || + media_type != SPA_MEDIA_TYPE_video) { + return NULL; + } + + obj = (struct spa_pod_object*)format; + spa_pod_builder_push_object(b, &f[0], obj->body.type, id); + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_FORMAT_mediaType: + spa_pod_builder_prop(b, prop->key, prop->flags); + spa_pod_builder_id(b, SPA_MEDIA_TYPE_video); + break; + case SPA_FORMAT_mediaSubtype: + spa_pod_builder_prop(b, prop->key, prop->flags); + spa_pod_builder_id(b, SPA_MEDIA_SUBTYPE_raw); + fmt = SPA_VIDEO_FORMAT_I420; + switch (media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + break; + case SPA_MEDIA_SUBTYPE_mjpg: + add_video_formats(b, &fmt, 1, port->is_dsp); + break; + case SPA_MEDIA_SUBTYPE_h264: + add_video_formats(b, &fmt, 1, port->is_dsp); + break; + default: + return NULL; + } + break; + case SPA_FORMAT_VIDEO_format: + { + uint32_t n_vals, choice, *id_vals; + struct spa_pod *val = spa_pod_get_values(&prop->value, &n_vals, &choice); + + if (n_vals < 1) + return 0; + + if (!spa_pod_is_id(val)) + return 0; + + id_vals = SPA_POD_BODY(val); + + add_video_formats(b, id_vals, n_vals, port->is_dsp); + break; + } + default: + spa_pod_builder_raw_padded(b, prop, SPA_POD_PROP_SIZE(prop)); + break; + } + } + return spa_pod_builder_pop(b, &f[0]); +} + +static int diff_value(struct impl *impl, uint32_t type, uint32_t size, const void *v1, const void *v2) +{ + switch (type) { + case SPA_TYPE_None: + return INT_MAX; + case SPA_TYPE_Bool: + if (size < sizeof(int32_t)) + return INT_MAX; + return (!!*(int32_t *)v1) - (!!*(int32_t *)v2); + case SPA_TYPE_Id: + if (size < sizeof(uint32_t)) + return INT_MAX; + return (*(uint32_t *)v1) != (*(uint32_t *)v2); + case SPA_TYPE_Int: + if (size < sizeof(int32_t)) + return INT_MAX; + return *(int32_t *)v1 - *(int32_t *)v2; + case SPA_TYPE_Long: + if (size < sizeof(int64_t)) + return INT_MAX; + return *(int64_t *)v1 - *(int64_t *)v2; + case SPA_TYPE_Float: + if (size < sizeof(float)) + return INT_MAX; + return (int)(*(float *)v1 - *(float *)v2); + case SPA_TYPE_Double: + if (size < sizeof(double)) + return INT_MAX; + return (int)(*(double *)v1 - *(double *)v2); + case SPA_TYPE_String: + if (size < 1 || + ((const char *)v1)[size - 1] != 0 || + ((const char *)v2)[size - 1] != 0) + return INT_MAX; + return strcmp((char *)v1, (char *)v2); + case SPA_TYPE_Bytes: + return memcmp((char *)v1, (char *)v2, size); + case SPA_TYPE_Rectangle: + { + const struct spa_rectangle *rec1, *rec2; + uint64_t n1, n2; + + if (size < sizeof(*rec1)) + return INT_MAX; + rec1 = (struct spa_rectangle *) v1; + rec2 = (struct spa_rectangle *) v2; + n1 = ((uint64_t) rec1->width) * rec1->height; + n2 = ((uint64_t) rec2->width) * rec2->height; + if (rec1->width == rec2->width && rec1->height == rec2->height) + return 0; + else if (n1 < n2) + return -(n2 - n1); + else if (n1 > n2) + return n1 - n2; + else if (rec1->width == rec2->width) + return (int32_t)rec1->height - (int32_t)rec2->height; + else + return (int32_t)rec1->width - (int32_t)rec2->width; + } + case SPA_TYPE_Fraction: + { + const struct spa_fraction *f1, *f2; + uint64_t n1, n2; + + if (size < sizeof(*f1)) + return INT_MAX; + f1 = (struct spa_fraction *) v1; + f2 = (struct spa_fraction *) v2; + n1 = ((uint64_t) f1->num) * f2->denom; + n2 = ((uint64_t) f2->num) * f1->denom; + return (int) (n1 - n2); + } + default: + break; + } + return 0; +} + +static int diff_prop(struct impl *impl, struct spa_pod_prop *prop, + uint32_t type, const void *target, bool fix) +{ + uint32_t i, n_vals, choice, size; + struct spa_pod *val = spa_pod_get_values(&prop->value, &n_vals, &choice); + void *vals, *v, *best = NULL; + int res = INT_MAX; + + if (n_vals < 1 || val->type != type) + return -EINVAL; + + size = SPA_POD_BODY_SIZE(val); + vals = SPA_POD_BODY(val); + + switch (choice) { + case SPA_CHOICE_None: + case SPA_CHOICE_Enum: + for (i = 0, v = vals; i < n_vals; i++, v = SPA_PTROFF(v, size, void)) { + int diff = SPA_ABS(diff_value(impl, type, size, v, target)); + if (diff < res) { + res = diff; + best = v; + } + } + if (fix) { + if (best != NULL && best != vals) + memcpy(vals, best, size); + if (spa_pod_is_choice(&prop->value)) + SPA_POD_CHOICE_TYPE(&prop->value) = SPA_CHOICE_None; + } + break; + default: + return res; + } + return res; +} + +static int calc_diff(struct impl *impl, struct spa_pod *param, struct dir *dir, bool fix) +{ + struct spa_pod_object *obj = (struct spa_pod_object*)param; + struct spa_pod_prop *prop; + struct spa_rectangle size; + struct spa_fraction framerate; + uint32_t format; + int diff = 0; + + if (!dir->have_format) + return -1; + + get_format(dir, &format, &size, &framerate); + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_FORMAT_VIDEO_format: + diff += diff_prop(impl, prop, SPA_TYPE_Id, &format, fix); + break; + case SPA_FORMAT_VIDEO_size: + diff += diff_prop(impl, prop, SPA_TYPE_Rectangle, &size, fix); + break; + case SPA_FORMAT_VIDEO_framerate: + diff += diff_prop(impl, prop, SPA_TYPE_Fraction, &framerate, fix); + break; + default: + break; + } + } + return diff; +} + +static int all_formats(struct impl *this, struct port *port, uint32_t id, + uint32_t index, const struct spa_pod **param, struct spa_pod_builder *b) +{ + struct spa_pod_frame f[1]; switch (index) { case 0: - if (PORT_IS_DSP(this, direction, port_id)) { - struct spa_video_info_dsp info; - info.format = SPA_VIDEO_FORMAT_DSP_F32; - *param = spa_format_video_dsp_build(builder, - SPA_PARAM_EnumFormat, &info); - } else if (PORT_IS_CONTROL(this, direction, port_id)) { - *param = spa_pod_builder_add_object(builder, - SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, - SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), - SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int( - (1u<have_format) { - *param = spa_format_video_build(builder, SPA_PARAM_EnumFormat, &other->format); - } else { - *param = NULL; - } - } - break; - case 1: - if (PORT_IS_DSP(this, direction, port_id) || - PORT_IS_CONTROL(this, direction, port_id)) - return 0; - - spa_pod_builder_push_object(builder, &f[0], - SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); - spa_pod_builder_add(builder, + spa_pod_builder_push_object(b, &f[0], + SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), - SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(7, - format, - SPA_VIDEO_FORMAT_YUY2, - SPA_VIDEO_FORMAT_I420, - SPA_VIDEO_FORMAT_UYVY, - SPA_VIDEO_FORMAT_YVYU, - SPA_VIDEO_FORMAT_RGBA, - SPA_VIDEO_FORMAT_BGRx), 0); - if (width != 0 && height != 0) { - spa_pod_builder_add(builder, - SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( - &SPA_RECTANGLE(width, height), - &SPA_RECTANGLE(1, 1), - &SPA_RECTANGLE(INT32_MAX, INT32_MAX)), - 0); - } - *param = spa_pod_builder_pop(builder, &f[0]); + add_video_formats(b, NULL, 0, port->is_dsp); + *param = spa_pod_builder_pop(b, &f[0]); break; - case 2: - if (PORT_IS_DSP(this, direction, port_id) || - PORT_IS_CONTROL(this, direction, port_id)) + case 1: + if (this->direction != port->direction) return 0; - spa_pod_builder_push_object(builder, &f[0], - SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); - spa_pod_builder_add(builder, + /* JPEG */ + spa_pod_builder_push_object(b, &f[0], + SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_mjpg), 0); - if (width != 0 && height != 0) { - spa_pod_builder_add(builder, - SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( - &SPA_RECTANGLE(width, height), - &SPA_RECTANGLE(1, 1), - &SPA_RECTANGLE(INT32_MAX, INT32_MAX)), - 0); - } - *param = spa_pod_builder_pop(builder, &f[0]); + *param = spa_pod_builder_pop(b, &f[0]); + break; + case 2: + if (this->direction != port->direction) + return 0; + + /* H264 */ + spa_pod_builder_push_object(b, &f[0], + SPA_TYPE_OBJECT_Format, id); + spa_pod_builder_add(b, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_h264), + 0); + *param = spa_pod_builder_pop(b, &f[0]); break; default: return 0; @@ -1160,6 +1423,192 @@ static int port_enum_formats(void *object, return 1; } +static int port_param_enum_format(struct impl *this, struct port *port, uint32_t id, + uint32_t index, const struct spa_pod **param, struct spa_pod_builder *b) +{ + struct dir *other = &this->dir[SPA_DIRECTION_REVERSE(port->direction)]; + + spa_log_debug(this->log, "%p %d %d %d %d", port, port->valid, this->direction, port->direction, index); + if ((port->is_dsp || port->is_control) && index > 0) + return 0; + + if (index == 0) { + if (port->is_dsp) { + struct spa_video_info_dsp info = SPA_VIDEO_INFO_DSP_INIT( + .format = SPA_VIDEO_FORMAT_DSP_F32); + *param = spa_format_video_dsp_build(b, id, &info); + return 1; + } else if (port->is_control) { + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_Format, id, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + return 1; + } else if (other->have_format) { + /* peer format */ + *param = spa_format_video_build(b, id, &other->format); + } + return 1; + } else if (this->direction != port->direction) { + struct port *oport = GET_PORT(this, SPA_DIRECTION_REVERSE(port->direction), port->id); + if (oport != NULL && oport->valid && + index - 1 < oport->n_peer_formats) { + const struct spa_pod *p = oport->peer_formats[index-1]; + *param = transform_format(this, port, p, id, b); + } else { + return all_formats(this, port, id, index - 1 - + (oport ? oport->n_peer_formats : 0), param, b); + + } + } else if (index == 1) { + const struct spa_pod *best = NULL; + int best_diff = INT_MAX; + uint32_t i; + + for (i = 0; i < port->n_peer_formats; i++) { + const struct spa_pod *p = port->peer_formats[i]; + int diff = calc_diff(this, (struct spa_pod*)p, other, false); + if (diff < 0) + break; + if (diff < best_diff) { + best_diff = diff; + best = p; + } + } + if (best) { + uint32_t offset = b->state.offset; + struct spa_pod *p; + spa_pod_builder_primitive(b, best); + p = spa_pod_builder_deref(b, offset); + calc_diff(this, p, other, true); + *param = p; + } + } else { + return all_formats(this, port, id, index - 2, param, b); + } + return 1; +} + +static int port_param_format(struct impl *this, struct port *port, uint32_t id, + uint32_t index, const struct spa_pod **param, struct spa_pod_builder *b) +{ + if (!port->have_format) + return -EIO; + if (index != 0) + return 0; + + if (port->is_dsp) { + *param = spa_format_video_dsp_build(b, id, &port->format.info.dsp); + } else if (port->is_control) { + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_Format, id, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + } else { + *param = spa_format_video_build(b, id, &port->format); + } + return 1; +} + +static int port_param_buffers(struct impl *this, struct port *port, uint32_t id, + uint32_t index, const struct spa_pod **param, struct spa_pod_builder *b) +{ + uint32_t size, min, max, def; + struct port *other; + + if (!port->have_format) + return -EIO; + if (index != 0) + return 0; + + if (port->is_dsp) { + size = 1024 * 1024 * 16; + } else { + size = port->size; + } + + other = GET_PORT(this, SPA_DIRECTION_REVERSE(port->direction), port->id); + if (other->n_buffers > 0) { + min = other->n_buffers; + } else { + min = 2; + } + max = MAX_BUFFERS; + def = SPA_CLAMP(8u, min, MAX_BUFFERS); + + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_ParamBuffers, id, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(def, min, max), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(port->blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( + size, 16, INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int( + port->stride, 0, INT32_MAX)); + return 1; +} + +static int port_param_meta(struct impl *this, struct port *port, uint32_t id, + uint32_t index, const struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_ParamMeta, id, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); + return 1; + } + return 0; +} + +static int port_param_io(struct impl *this, struct port *port, uint32_t id, + uint32_t index, const struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_ParamIO, id, + SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), + SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); + return 1; + } + return 0; +} + +static int port_param_latency(struct impl *this, struct port *port, uint32_t id, + uint32_t index, const struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0 ... 1: + *param = spa_latency_build(b, id, &port->latency[index]); + return 1; + } + return 0; +} + +static int port_param_tag(struct impl *this, struct port *port, uint32_t id, + uint32_t index, const struct spa_pod **param, struct spa_pod_builder *b) +{ + switch (index) { + case 0 ... 1: + if (port->is_monitor) + index = index ^ 1; + *param = this->dir[index].tag; + return 1; + } + return 0; +} + +static int port_param_peer_formats(struct impl *this, struct port *port, uint32_t index, + const struct spa_pod **param, struct spa_pod_builder *b) +{ + if (index >= port->n_peer_formats) + return 0; + + *param = port->peer_formats[port->n_peer_formats - index]; + return 1; +} + static int impl_node_port_enum_params(void *object, int seq, enum spa_direction direction, uint32_t port_id, @@ -1167,8 +1616,8 @@ impl_node_port_enum_params(void *object, int seq, const struct spa_pod *filter) { struct impl *this = object; - struct port *port, *other; - struct spa_pod *param; + struct port *port; + const struct spa_pod *param; struct spa_pod_builder b = { 0 }; uint8_t buffer[4096]; struct spa_result_node_params result; @@ -1192,118 +1641,37 @@ impl_node_port_enum_params(void *object, int seq, spa_pod_builder_init(&b, buffer, sizeof(buffer)); + param = NULL; switch (id) { case SPA_PARAM_EnumFormat: - if ((res = port_enum_formats(object, direction, port_id, result.index, ¶m, &b)) <= 0) - return res; + res = port_param_enum_format(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_Format: - if (!port->have_format) - return -EIO; - if (result.index > 0) - return 0; - - if (PORT_IS_DSP(this, direction, port_id)) - param = spa_format_video_dsp_build(&b, id, &port->format.info.dsp); - else if (PORT_IS_CONTROL(this, direction, port_id)) - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_Format, id, - SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), - SPA_FORMAT_CONTROL_types, SPA_POD_Int( - (1u<format); + res = port_param_format(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_Buffers: - { - uint32_t size, min, max; - - if (!port->have_format) - return -EIO; - if (result.index > 0) - return 0; - - if (PORT_IS_DSP(this, direction, port_id)) { - size = 1024 * 1024 * 16; - } else { - size = 1024 * 1024 * 4; - } - - other = GET_PORT(this, SPA_DIRECTION_REVERSE(direction), port_id); - if (other->n_buffers > 0) { - min = max = other->n_buffers; - } else { - min = 2; - max = MAX_BUFFERS; - } - - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamBuffers, id, - SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, min, max), - SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(port->blocks), - SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( - size * port->stride, - 16 * port->stride, - INT32_MAX), - SPA_PARAM_BUFFERS_stride, SPA_POD_Int(port->stride)); + res = port_param_buffers(this, port, id, result.index, ¶m, &b); break; - } case SPA_PARAM_Meta: - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamMeta, id, - SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), - SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); - break; - default: - return 0; - } + res = port_param_meta(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_IO: - switch (result.index) { - case 0: - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_ParamIO, id, - SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers), - SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers))); - break; - default: - return 0; - } + res = port_param_io(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_Latency: - switch (result.index) { - case 0: case 1: - { - uint32_t idx = result.index; - param = spa_latency_build(&b, id, &port->latency[idx]); - break; - } - default: - return 0; - } + res = port_param_latency(this, port, id, result.index, ¶m, &b); break; case SPA_PARAM_Tag: - switch (result.index) { - case 0: case 1: - { - uint32_t idx = result.index; - if (port->is_monitor) - idx = idx ^ 1; - param = this->dir[idx].tag; - if (param == NULL) - goto next; - break; - } - default: - return 0; - } + res = port_param_tag(this, port, id, result.index, ¶m, &b); + break; + case SPA_PARAM_PeerFormats: + res = port_param_peer_formats(this, port, result.index, ¶m, &b); break; default: return -ENOENT; } + if (res <= 0) + return res; if (param == NULL || spa_pod_filter(&b, &result.param, param, filter) < 0) goto next; @@ -1318,11 +1686,25 @@ impl_node_port_enum_params(void *object, int seq, static int clear_buffers(struct impl *this, struct port *port) { - if (port->n_buffers > 0) { - spa_log_debug(this->log, "%p: clear buffers %p", this, port); - port->n_buffers = 0; - spa_list_init(&port->queue); + uint32_t i, j; + + spa_log_debug(this->log, "%p: clear buffers %p %d", this, port, port->n_buffers); + for (i = 0; i < port->n_buffers; i++) { + struct buffer *b = &port->buffers[i]; + if (SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_MAPPED)) { + for (j = 0; j < b->buf->n_datas; j++) { + if (b->datas[j]) { + spa_log_debug(this->log, "%p: unmap buffer %d data %d %p", + this, i, j, b->datas[j]); + munmap(b->datas[j], b->buf->datas[j].maxsize); + b->datas[j] = NULL; + } + } + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_MAPPED); + } } + port->n_buffers = 0; + spa_list_init(&port->queue); return 0; } @@ -1377,7 +1759,6 @@ static int port_set_latency(void *object, oport->latency[other] = info; oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; oport->params[IDX_Latency].user++; - emit_port_info(this, oport, false); } } else { spa_latency_info_combine_start(&info, other); @@ -1405,14 +1786,12 @@ static int port_set_latency(void *object, oport->latency[other] = info; oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; oport->params[IDX_Latency].user++; - emit_port_info(this, oport, false); } } } if (emit) { port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; port->params[IDX_Latency].user++; - emit_port_info(this, port, false); } return 0; } @@ -1450,12 +1829,10 @@ static int port_set_tag(void *object, oport = GET_PORT(this, other, i); oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; oport->params[IDX_Tag].user++; - emit_port_info(this, oport, false); } } port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; port->params[IDX_Tag].user++; - emit_port_info(this, port, false); return 0; } @@ -1475,10 +1852,13 @@ static int port_set_format(void *object, if (format == NULL) { port->have_format = false; + this->setup = false; + this->fmt_passthrough = false; clear_buffers(this, port); } else { + struct dir *dir = &this->dir[direction]; + struct dir *odir = &this->dir[SPA_DIRECTION_REVERSE(direction)]; struct spa_video_info info = { 0 }; - spa_debug_format(2, NULL, format); if ((res = spa_format_parse(format, &info.media_type, &info.media_subtype)) < 0) { spa_log_error(this->log, "can't parse format %s", spa_strerror(res)); @@ -1502,6 +1882,7 @@ static int port_set_format(void *object, } port->blocks = 1; port->stride = 16; + dir->have_format = true; } else if (PORT_IS_CONTROL(this, direction, port_id)) { if (info.media_type != SPA_MEDIA_TYPE_application || @@ -1512,10 +1893,10 @@ static int port_set_format(void *object, } port->blocks = 1; port->stride = 1; + dir->have_format = true; } else { - struct dir *dir = &this->dir[direction]; - struct dir *odir = &this->dir[SPA_DIRECTION_REVERSE(direction)]; + enum AVPixelFormat pix_fmt; if (info.media_type != SPA_MEDIA_TYPE_video) { spa_log_error(this->log, "unexpected types %d/%d", @@ -1526,22 +1907,62 @@ static int port_set_format(void *object, spa_log_error(this->log, "can't parse format %s", spa_strerror(res)); return res; } - port->stride = 2; - port->stride *= info.info.raw.size.width; - port->blocks = 1; + switch (info.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + { + int linesizes[4]; + + pix_fmt = format_to_pix_fmt(info.info.raw.format); + if (pix_fmt == AV_PIX_FMT_NONE) + return -EINVAL; + av_image_fill_linesizes(linesizes, pix_fmt, info.info.raw.size.width); + port->stride = linesizes[0]; + port->blocks = 0; + for (int i = 0; i < 4; i++) { + dir->linesizes[i] = linesizes[i]; + if (linesizes[i]) + port->blocks++; + } + av_image_fill_plane_sizes(dir->plane_size, + pix_fmt, info.info.raw.size.height, + dir->linesizes); + port->size = av_image_get_buffer_size(pix_fmt, + info.info.raw.size.width, + info.info.raw.size.height, this->max_align); + + break; + } + case SPA_MEDIA_SUBTYPE_h264: + port->stride = 0; + port->size = info.info.h264.size.width * info.info.h264.size.height; + port->blocks = 1; + break; + case SPA_MEDIA_SUBTYPE_mjpg: + port->stride = 0; + port->size = info.info.mjpg.size.width * info.info.mjpg.size.height; + port->blocks = 1; + break; + default: + spa_log_error(this->log, "unsupported subtype %d", info.media_subtype); + return -ENOTSUP; + } dir->format = info; dir->have_format = true; if (odir->have_format) { - if (memcmp(&odir->format, &dir->format, sizeof(dir->format)) == 0) - this->fmt_passthrough = true; + this->fmt_passthrough = + (memcmp(&odir->format, &dir->format, sizeof(dir->format)) == 0); } - this->setup = false; } port->format = info; port->have_format = true; + this->setup = false; spa_log_debug(this->log, "%p: %d %d %d", this, port_id, port->stride, port->blocks); + + if (dir->have_format && odir->have_format) + if ((res = setup_convert(this)) < 0) + return res; } port->info.change_mask |= SPA_PORT_CHANGE_MASK_FLAGS; @@ -1555,12 +1976,72 @@ static int port_set_format(void *object, port->params[IDX_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); port->params[IDX_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); } - emit_port_info(this, port, false); - return 0; } +static int port_set_peer_formats(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t flags, + const struct spa_pod *formats) +{ + struct impl *this = object; + struct port *port, *oport; + int res = 0; + uint32_t i; + const struct spa_pod *format; + enum spa_direction other = SPA_DIRECTION_REVERSE(direction); + static uint32_t subtypes[] = { + SPA_MEDIA_SUBTYPE_raw, + SPA_MEDIA_SUBTYPE_mjpg, + SPA_MEDIA_SUBTYPE_h264 }; + + spa_return_val_if_fail(spa_pod_is_struct(formats), -EINVAL); + + port = GET_PORT(this, direction, port_id); + oport = GET_PORT(this, other, port_id); + + free(port->peer_formats); + port->peer_formats = NULL; + free(port->peer_format_pod); + port->peer_format_pod = NULL; + port->n_peer_formats = 0; + + if (formats) { + uint32_t count = 0; + port->peer_format_pod = spa_pod_copy(formats); + + for (i = 0; i < SPA_N_ELEMENTS(subtypes); i++) { + SPA_POD_STRUCT_FOREACH(port->peer_format_pod, format) { + uint32_t media_type, media_subtype; + if (!spa_format_parse(format, &media_type, &media_subtype) || + media_type != SPA_MEDIA_TYPE_video || + media_subtype != subtypes[i]) + continue; + count++; + } + } + port->peer_formats = calloc(count, sizeof(struct spa_pod *)); + for (i = 0; i < SPA_N_ELEMENTS(subtypes); i++) { + SPA_POD_STRUCT_FOREACH(port->peer_format_pod, format) { + uint32_t media_type, media_subtype; + if (!spa_format_parse(format, &media_type, &media_subtype) || + media_type != SPA_MEDIA_TYPE_video || + media_subtype != subtypes[i]) + continue; + port->peer_formats[port->n_peer_formats++] = format; + } + } + } + oport->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + oport->params[IDX_EnumFormat].user++; + port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + port->params[IDX_EnumFormat].user++; + port->params[IDX_PeerFormats].user++; + return res; +} + static int impl_node_port_set_param(void *object, enum spa_direction direction, uint32_t port_id, @@ -1568,6 +2049,7 @@ impl_node_port_set_param(void *object, const struct spa_pod *param) { struct impl *this = object; + int res = 0; spa_return_val_if_fail(this != NULL, -EINVAL); @@ -1578,14 +2060,22 @@ impl_node_port_set_param(void *object, switch (id) { case SPA_PARAM_Latency: - return port_set_latency(this, direction, port_id, flags, param); + res = port_set_latency(this, direction, port_id, flags, param); + break; case SPA_PARAM_Tag: - return port_set_tag(this, direction, port_id, flags, param); + res = port_set_tag(this, direction, port_id, flags, param); + break; case SPA_PARAM_Format: - return port_set_format(this, direction, port_id, flags, param); + res = port_set_format(this, direction, port_id, flags, param); + break; + case SPA_PARAM_PeerFormats: + res = port_set_peer_formats(this, direction, port_id, flags, param); + break; default: return -ENOENT; } + emit_info(this, false); + return res; } static inline void queue_buffer(struct impl *this, struct port *port, uint32_t id) @@ -1642,8 +2132,8 @@ impl_node_port_use_buffers(void *object, port = GET_PORT(this, direction, port_id); - spa_log_debug(this->log, "%p: use buffers %d on port %d:%d", - this, n_buffers, direction, port_id); + spa_log_debug(this->log, "%p: use buffers %d on port %d:%d flags %08x", + this, n_buffers, direction, port_id, flags); clear_buffers(this, port); @@ -1663,10 +2153,12 @@ impl_node_port_use_buffers(void *object, b->id = i; b->flags = 0; b->buf = buffers[i]; + b->h = spa_buffer_find_meta_data(b->buf, + SPA_META_Header, sizeof(struct spa_meta_header)); if (n_datas != port->blocks) { - spa_log_error(this->log, "%p: invalid blocks %d on buffer %d", - this, n_datas, i); + spa_log_error(this->log, "%p: invalid blocks %d on buffer %d, expected %d", + this, n_datas, i, port->blocks); return -EINVAL; } if (SPA_FLAG_IS_SET(flags, SPA_NODE_BUFFERS_FLAG_ALLOC)) { @@ -1674,8 +2166,14 @@ impl_node_port_use_buffers(void *object, if (other->n_buffers <= 0) return -EIO; - *b->buf = *other->buffers[i % other->n_buffers].buf; - b->datas[0] = other->buffers[i % other->n_buffers].datas[0]; + + for (j = 0; j < n_datas; j++) { + b->buf->datas[j] = other->buffers[i % other->n_buffers].buf->datas[j]; + b->datas[j] = other->buffers[i % other->n_buffers].datas[j]; + maxsize = SPA_MAX(maxsize, d[j].maxsize); + spa_log_debug(this->log, "buffer %d: mem:%d passthrough:%p maxsize:%d", + i, j, b->datas[j], d[j].maxsize); + } } else { for (j = 0; j < n_datas; j++) { void *data = d[j].data; @@ -1688,12 +2186,17 @@ impl_node_port_use_buffers(void *object, this, j, i, d[j].type, data); return -EINVAL; } + SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); + spa_log_debug(this->log, "%p: mmap %d on buffer %d %d %p %p", + this, j, i, d[j].type, data, b); } if (data != NULL && !SPA_IS_ALIGNED(data, this->max_align)) { spa_log_warn(this->log, "%p: memory %d on buffer %d not aligned", this, j, i); } b->datas[j] = data; + spa_log_debug(this->log, "buffer %d: mem:%d data:%p maxsize:%d", + i, j, data, d[j].maxsize); maxsize = SPA_MAX(maxsize, d[j].maxsize); } } @@ -1741,7 +2244,7 @@ impl_node_port_set_io(void *object, case SPA_IO_Buffers: if (this->data_loop) { struct io_data d = { .port = port, .data = data, .size = size }; - spa_loop_invoke(this->data_loop, do_set_port_io, 0, NULL, 0, true, &d); + spa_loop_locked(this->data_loop, do_set_port_io, 0, NULL, 0, &d); } else port->io = data; @@ -1812,22 +2315,27 @@ static int impl_node_process(void *object) } sbuf = &in_port->buffers[input->buffer_id]; + input->status = SPA_STATUS_NEED_DATA; - if ((dbuf = peek_buffer(this, out_port)) == NULL) { - spa_log_error(this->log, "%p: out of buffers", this); + if (this->fmt_passthrough) { + dbuf = &out_port->buffers[input->buffer_id]; + } else if ((dbuf = peek_buffer(this, out_port)) == NULL) { + spa_log_error(this->log, "%p: out of buffers", this); return -EPIPE; } - dbuf = &out_port->buffers[input->buffer_id]; spa_log_trace(this->log, "%d %p:%p %d %d %d", input->buffer_id, sbuf->buf->datas[0].chunk, dbuf->buf->datas[0].chunk, sbuf->buf->datas[0].chunk->size, sbuf->id, dbuf->id); /* do decoding */ - if (this->decoder.codec) { + if (this->decoder.context) { this->decoder.packet->data = sbuf->datas[0]; this->decoder.packet->size = sbuf->buf->datas[0].chunk->size; + spa_log_trace(this->log, "decode %p:%d", this->decoder.packet->data, + this->decoder.packet->size); + if ((res = avcodec_send_packet(this->decoder.context, this->decoder.packet)) < 0) { spa_log_error(this->log, "failed to send frame to codec: %d %p:%d", res, this->decoder.packet->data, this->decoder.packet->size); @@ -1841,32 +2349,51 @@ static int impl_node_process(void *object) } in->pix_fmt = f->format; - in->width = f->width; - in->height = f->height; + in->size.width = f->width; + in->size.height = f->height; + for (uint32_t i = 0; i < 4; ++i) { + datas[i] = f->data[i]; + strides[i] = f->linesize[i]; + sizes[i] = out->plane_size[i]; + } } else { f = this->decoder.frame; f->format = in->pix_fmt; - f->width = in->width; - f->height = in->height; - f->data[0] = sbuf->datas[0]; - f->linesize[0] = sbuf->buf->datas[0].chunk->stride; + f->width = in->size.width; + f->height = in->size.height; + for (uint32_t i = 0; i < sbuf->buf->n_datas; ++i) { + datas[i] = f->data[i] = sbuf->datas[i]; + strides[i] = f->linesize[i] = sbuf->buf->datas[i].chunk->stride; + sizes[i] = sbuf->buf->datas[i].chunk->size; + } } /* do conversion */ if (f->format != out->pix_fmt || - f->width != out->width || - f->height != out->height) { + f->width != (int)out->size.width || + f->height != (int)out->size.height) { if (this->convert.context == NULL) { + const AVPixFmtDescriptor *in_fmt = av_pix_fmt_desc_get(f->format); + const AVPixFmtDescriptor *out_fmt = av_pix_fmt_desc_get(out->pix_fmt); this->convert.context = sws_getContext( f->width, f->height, f->format, - out->width, out->height, out->pix_fmt, + out->size.width, out->size.height, out->pix_fmt, 0, NULL, NULL, NULL); + spa_log_info(this->log, "%p: using convert %dx%d:%s -> %dx%d:%s", + this, f->width, f->height, in_fmt->name, + out->size.width, out->size.height, out_fmt->name); } + spa_log_trace(this->log, "convert"); sws_scale_frame(this->convert.context, this->convert.frame, f); f = this->convert.frame; + for (uint32_t i = 0; i < 4; ++i) { + datas[i] = f->data[i]; + strides[i] = f->linesize[i]; + sizes[i] = out->plane_size[i]; + } } /* do encoding */ - if (this->encoder.codec) { + if (this->encoder.context) { if ((res = avcodec_send_frame(this->encoder.context, f)) < 0) { spa_log_error(this->log, "failed to send frame to codec: %d", res); return -EIO; @@ -1878,15 +2405,11 @@ static int impl_node_process(void *object) datas[0] = this->encoder.packet->data; sizes[0] = this->encoder.packet->size; strides[0] = 1; - - } else { - datas[0] = f->data[0]; - strides[0] = f->linesize[0]; - sizes[0] = strides[0] * out->height; + spa_log_trace(this->log, "encode %p %d", datas[0], sizes[0]); } /* write to output */ - for (uint_fast32_t i = 0; i < dbuf->buf->n_datas; ++i) { + for (uint32_t i = 0; i < dbuf->buf->n_datas; ++i) { if (SPA_FLAG_IS_SET(dbuf->buf->datas[i].flags, SPA_DATA_FLAG_DYNAMIC)) dbuf->buf->datas[i].data = datas[i]; else if (datas[i] && dbuf->datas[i] && dbuf->datas[i] != datas[i]) @@ -1896,14 +2419,18 @@ static int impl_node_process(void *object) dbuf->buf->datas[i].chunk->stride = strides[i]; dbuf->buf->datas[i].chunk->size = sizes[i]; } + spa_log_trace(this->log, "out %u %u %p %d", dbuf->id, i, + dbuf->buf->datas[i].data, + dbuf->buf->datas[i].chunk->size); } - dequeue_buffer(this, out_port, dbuf); + + if (sbuf->h && dbuf->h) + *dbuf->h = *sbuf->h; + output->buffer_id = dbuf->id; output->status = SPA_STATUS_HAVE_DATA; - input->status = SPA_STATUS_NEED_DATA; - return SPA_STATUS_HAVE_DATA; } @@ -1958,6 +2485,11 @@ static int impl_clear(struct spa_handle *handle) this = (struct impl *) handle; + free_decoder(this); + free_encoder(this); + av_frame_free(&this->decoder.frame); + av_frame_free(&this->convert.frame); + free_dir(&this->dir[SPA_DIRECTION_INPUT]); free_dir(&this->dir[SPA_DIRECTION_OUTPUT]); return 0; @@ -2013,6 +2545,12 @@ impl_init(const struct spa_handle_factory *factory, spa_scnprintf(this->group_name, sizeof(this->group_name), "%s", s); else if (spa_streq(k, "monitor.passthrough")) this->monitor_passthrough = spa_atob(s); + else if (spa_streq(k, "convert.direction")) { + if (spa_streq(s, "output")) + this->direction = SPA_DIRECTION_OUTPUT; + else + this->direction = SPA_DIRECTION_INPUT; + } else videoconvert_set_param(this, k, s); } diff --git a/spa/plugins/videotestsrc/videotestsrc.c b/spa/plugins/videotestsrc/videotestsrc.c index aff3f5364..db5c90113 100644 --- a/spa/plugins/videotestsrc/videotestsrc.c +++ b/spa/plugins/videotestsrc/videotestsrc.c @@ -852,7 +852,7 @@ static int impl_clear(struct spa_handle *handle) this = (struct impl *) handle; if (this->data_loop) - spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_timer, 0, NULL, 0, this); spa_loop_utils_destroy_source(this->loop_utils, this->timer_source); return 0; diff --git a/spa/plugins/volume/volume.c b/spa/plugins/volume/volume.c index 164e46a6a..0c750a96a 100644 --- a/spa/plugins/volume/volume.c +++ b/spa/plugins/volume/volume.c @@ -454,8 +454,7 @@ static int port_set_format(void *object, return -EINVAL; if (info.info.raw.format != SPA_AUDIO_FORMAT_S16 || - info.info.raw.channels == 0 || - info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + info.info.raw.channels == 0) return -EINVAL; this->bpf = 2 * info.info.raw.channels; diff --git a/spa/plugins/vulkan/dmabuf_linux.c b/spa/plugins/vulkan/dmabuf_linux.c index f3b9c6f13..1efddb22b 100644 --- a/spa/plugins/vulkan/dmabuf_linux.c +++ b/spa/plugins/vulkan/dmabuf_linux.c @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -51,29 +52,9 @@ bool dmabuf_check_sync_file_import_export(struct spa_log *log) { return false; } - // Trim release suffix if any, e.g. "-arch1-1" - for (size_t i = 0; utsname.release[i] != '\0'; i++) { - char ch = utsname.release[i]; - if ((ch < '0' || ch > '9') && ch != '.') { - utsname.release[i] = '\0'; - break; - } - } - - char *rel = strtok(utsname.release, "."); - int major = atoi(rel); - - int minor = 0; - rel = strtok(NULL, "."); - if (rel != NULL) { - minor = atoi(rel); - } - - int patch = 0; - rel = strtok(NULL, "."); - if (rel != NULL) { - patch = atoi(rel); - } + unsigned int major, minor, patch; + if (sscanf(utsname.release, "%u.%u.%u", &major, &minor, &patch) != 3) + return false; return KERNEL_VERSION(major, minor, patch) >= KERNEL_VERSION(5, 20, 0); } diff --git a/spa/plugins/vulkan/vulkan-compute-source.c b/spa/plugins/vulkan/vulkan-compute-source.c index 8512c155e..1daf3b47c 100644 --- a/spa/plugins/vulkan/vulkan-compute-source.c +++ b/spa/plugins/vulkan/vulkan-compute-source.c @@ -933,7 +933,7 @@ static int impl_clear(struct spa_handle *handle) spa_vulkan_compute_deinit(&this->state); if (this->data_loop) - spa_loop_invoke(this->data_loop, do_remove_timer, 0, NULL, 0, true, this); + spa_loop_locked(this->data_loop, do_remove_timer, 0, NULL, 0, this); spa_system_close(this->data_system, this->timer_source.fd); return 0; diff --git a/spa/plugins/vulkan/vulkan-utils.c b/spa/plugins/vulkan/vulkan-utils.c index 64323135d..6a0f693dc 100644 --- a/spa/plugins/vulkan/vulkan-utils.c +++ b/spa/plugins/vulkan/vulkan-utils.c @@ -41,8 +41,9 @@ static int vkresult_to_errno(VkResult result) case VK_EVENT_SET: case VK_EVENT_RESET: return 0; - case VK_NOT_READY: case VK_INCOMPLETE: + return ENOSPC; + case VK_NOT_READY: case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: return EBUSY; case VK_TIMEOUT: diff --git a/spa/tests/benchmark-aec.c b/spa/tests/benchmark-aec.c new file mode 100644 index 000000000..3ac0fc62e --- /dev/null +++ b/spa/tests/benchmark-aec.c @@ -0,0 +1,391 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2025 Arun Raghavan */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static SPA_LOG_IMPL(default_log); + +struct data { + const char *plugin_dir; + + struct spa_log *log; + struct spa_system *system; + struct spa_loop *loop; + struct spa_loop_control *control; + struct spa_loop_utils *loop_utils; + struct spa_plugin_loader *plugin_loader; + + struct spa_support support[6]; + uint32_t n_support; + + struct spa_audio_aec *aec; + struct spa_handle *aec_handle; + uint32_t aec_samples; +}; + +static int load_handle(struct data *data, struct spa_handle **handle, const char *lib, const char *name) +{ + int res; + void *hnd; + spa_handle_factory_enum_func_t enum_func; + uint32_t i; + + char *path = NULL; + + if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL) { + return -ENOMEM; + } + if ((hnd = dlopen(path, RTLD_NOW)) == NULL) { + printf("can't load %s: %s\n", path, dlerror()); + free(path); + return -errno; + } + free(path); + if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) { + printf("can't find enum function\n"); + return -errno; + } + + for (i = 0;;) { + const struct spa_handle_factory *factory; + + if ((res = enum_func(&factory, &i)) <= 0) { + if (res != 0) + printf("can't enumerate factories: %s\n", spa_strerror(res)); + break; + } + if (!spa_streq(factory->name, name)) + continue; + + *handle = calloc(1, spa_handle_factory_get_size(factory, NULL)); + if ((res = spa_handle_factory_init(factory, *handle, + NULL, data->support, + data->n_support)) < 0) { + printf("can't make factory instance: %d\n", res); + return res; + } + return 0; + } + return -EBADF; +} + +static int init(struct data *data) +{ + int res; + const char *str; + struct spa_handle *handle = NULL; + void *iface; + + if ((str = getenv("SPA_PLUGIN_DIR")) == NULL) + str = PLUGINDIR; + data->plugin_dir = str; + + if ((res = load_handle(data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_SYSTEM)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) { + printf("can't get System interface %d\n", res); + return res; + } + data->system = iface; + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, data->system); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataSystem, data->system); + + if ((res = load_handle(data, &handle, + "support/libspa-support.so", + SPA_NAME_SUPPORT_LOOP)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->loop = iface; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopControl, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->control = iface; + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopUtils, &iface)) < 0) { + printf("can't get interface %d\n", res); + return res; + } + data->loop_utils = iface; + + data->log = &default_log.log; + + if ((str = getenv("SPA_DEBUG"))) + data->log->level = atoi(str); + + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data->log); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, data->loop); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, data->loop); + data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_LoopUtils, data->loop_utils); + + /* Use webrtc as default */ + if ((res = load_handle(data, &handle, + "aec/libspa-aec-webrtc.so", + SPA_NAME_AEC)) < 0) + return res; + + if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_AUDIO_AEC, &iface)) < 0) { + spa_log_error(data->log, "can't get %s interface %d", SPA_TYPE_INTERFACE_AUDIO_AEC, res); + return res; + } + + data->aec = iface; + data->aec_handle = handle; + + return 0; +} + +static int spa_dict_from_json(struct spa_dict_item *items, uint32_t n_items, const char *str) +{ + struct spa_json it; + int res; + char key[1024]; + const char *value; + uint32_t i, len; + struct spa_error_location loc; + + if ((res = spa_json_begin_object_relax(&it, str, strlen(str))) < 0) { + return res; + } + + i = 0; + while ((len = spa_json_object_next(&it, key, sizeof(key), &value)) > 0) { + if (i > n_items) + return -ENOSPC; + + char *k = malloc(strlen(key) + 1); + char *v = malloc(len + 1); + + memcpy(k, key, strlen(key) + 1); + spa_json_parse_stringn(value, len, v, len + 1); + + items[i++] = SPA_DICT_ITEM_INIT(k, v); + } + + if (spa_json_get_error(&it, str, &loc)) { + struct spa_debug_context *c = NULL; + spa_debugc(c, "Invalid JSON: %s", loc.reason); + spa_debugc_error_location(c, &loc); + return -EINVAL; + } + + return i; +} + +static const struct format_info { + const char *name; + int sf_format; + uint32_t spa_format; + uint32_t width; +} format_info[] = { + { "ulaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ULAW, 1 }, + { "alaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ALAW, 1 }, + { "s8", SF_FORMAT_PCM_S8, SPA_AUDIO_FORMAT_S8, 1 }, + { "u8", SF_FORMAT_PCM_U8, SPA_AUDIO_FORMAT_U8, 1 }, + { "s16", SF_FORMAT_PCM_16, SPA_AUDIO_FORMAT_S16, 2 }, + { "s24", SF_FORMAT_PCM_24, SPA_AUDIO_FORMAT_S24, 3 }, + { "s32", SF_FORMAT_PCM_32, SPA_AUDIO_FORMAT_S32, 4 }, + { "f32", SF_FORMAT_FLOAT, SPA_AUDIO_FORMAT_F32, 4 }, + { "f64", SF_FORMAT_DOUBLE, SPA_AUDIO_FORMAT_F32, 8 }, +}; + +static SNDFILE* open_file_read(const struct data *data, const char *name, struct spa_audio_info_raw *info) +{ + SF_INFO sf_info = { 0, }; + + SNDFILE *file = sf_open(name, SFM_READ, &sf_info); + + if (!file) { + spa_log_error(data->log, "Could not open file: %s", sf_strerror(NULL)); + exit(255); + } + + for (unsigned long i = 0; i < SPA_N_ELEMENTS(format_info); i++) { + if ((sf_info.format & SF_FORMAT_SUBMASK) == format_info[i].sf_format) { + info->format = format_info[i].spa_format; + break; + } + } + + info->rate = sf_info.samplerate; + info->channels = sf_info.channels; + + return file; +} + +static SNDFILE* open_file_write(const struct data *data, const char *name, struct spa_audio_info_raw *info) +{ + SF_INFO sf_info = { 0, }; + + for (unsigned long i = 0; i < SPA_N_ELEMENTS(format_info); i++) { + if (info->format == format_info[i].spa_format) { + sf_info.format = SF_FORMAT_WAV | format_info[i].sf_format; + break; + } + } + + sf_info.samplerate = info->rate; + sf_info.channels = info->channels; + + SNDFILE *file = sf_open(name, SFM_WRITE, &sf_info); + + if (!file) { + spa_log_error(data->log, "Could not open file: %s", sf_strerror(NULL)); + exit(255); + } + + return file; +} + +static void deinterleave(float *data, uint32_t channels, uint32_t samples) +{ + float temp[channels * samples]; + + for (uint32_t i = 0; i < channels; i++) { + for (uint32_t j = 0; j < samples; j++) { + temp[i * samples + j] = data[j * channels + i]; + } + } + + memcpy(data, temp, sizeof(temp)); +} + +static void interleave(float *data, uint32_t channels, uint32_t samples) +{ + float temp[channels * samples]; + + for (uint32_t i = 0; i < samples; i++) { + for (uint32_t j = 0; j < channels; j++) { + temp[i * channels + j] = data[j * samples + i]; + } + } + + memcpy(data, temp, sizeof(temp)); +} + +static void usage(char *exe) +{ + printf("Usage: %s rec_file play_file out_file <\"aec args\">\n", basename(exe)); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + struct spa_dict_item items[16] = { 0, }; + int n_items = 0, res; + + if ((res = init(&data)) < 0) + return res; + + if (argc < 4 || argc > 5) { + usage(argv[0]); + return -1; + } + + if (argc == 5) { + if ((res = spa_dict_from_json(items, SPA_N_ELEMENTS(items), argv[4])) < 0) + return res; + n_items = res; + } + + struct spa_dict aec_args = SPA_DICT(items, n_items); + + struct spa_audio_info_raw rec_info = { 0, }; + struct spa_audio_info_raw play_info = { 0, }; + + SNDFILE *rec_file = open_file_read(&data, argv[1], &rec_info); + SNDFILE *play_file = open_file_read(&data, argv[2], &play_info); + SNDFILE *out_file = open_file_write(&data, argv[3], &rec_info); + + if ((res = spa_audio_aec_init2(data.aec, &aec_args, &rec_info, &play_info, &rec_info)) < 0) { + spa_log_error(data.log, "Could not initialise AEC engine: %s", spa_strerror(res)); + return -1; + } + + if (data.aec->latency) { + unsigned int num, denom; + sscanf(data.aec->latency, "%u/%u", &num, &denom); + data.aec_samples = rec_info.rate * num / denom; + + } else { + /* Implementation doesn't care about the block size */ + data.aec_samples = 1024; + } + + float rec_data[rec_info.channels * data.aec_samples]; + float play_data[play_info.channels * data.aec_samples]; + float out_data[rec_info.channels * data.aec_samples]; + + const float *rec[rec_info.channels]; + const float *play[play_info.channels]; + float *out[rec_info.channels]; + + for (uint32_t i = 0; i < rec_info.channels; i++) { + rec[i] = &rec_data[i * data.aec_samples]; + out[i] = &out_data[i * data.aec_samples]; + } + + for (uint32_t i = 0; i < play_info.channels; i++) { + play[i] = &play_data[i * data.aec_samples]; + } + + while (1) { + res = sf_readf_float(rec_file, (float *)rec_data, data.aec_samples); + if (res != (int) data.aec_samples) + break; + + res = sf_readf_float(play_file, (float *)play_data, data.aec_samples); + if (res != (int) data.aec_samples) + break; + + deinterleave((float *)rec_data, rec_info.channels, data.aec_samples); + deinterleave((float *)play_data, play_info.channels, data.aec_samples); + + spa_audio_aec_run(data.aec, rec, play, out, data.aec_samples); + + interleave((float *)out_data, rec_info.channels, data.aec_samples); + + res = sf_writef_float(out_file, (const float *)out_data, data.aec_samples); + if (res != (int) data.aec_samples) { + spa_log_error(data.log, "Failed to write: %s", spa_strerror(res)); + break; + } + } + + sf_close(rec_file); + sf_close(play_file); + sf_close(out_file); + + return 0; +} diff --git a/spa/tests/benchmark-pod.c b/spa/tests/benchmark-pod.c index 9979228fe..f54452b2a 100644 --- a/spa/tests/benchmark-pod.c +++ b/spa/tests/benchmark-pod.c @@ -161,6 +161,7 @@ static void test_parse(void) uint32_t n_vals, choice; struct spa_pod *pod = spa_pod_get_values(&prop->value, &n_vals, &choice); + spa_assert_se(n_vals > 0); switch(prop->key) { case SPA_FORMAT_mediaType: spa_pod_get_id(pod, &vals.media_type); diff --git a/spa/tests/meson.build b/spa/tests/meson.build index c73c887f4..81635dea9 100644 --- a/spa/tests/meson.build +++ b/spa/tests/meson.build @@ -28,15 +28,22 @@ if find.found() endif benchmark_apps = [ - 'stress-ringbuffer', - 'benchmark-pod', - 'benchmark-dict', + ['stress-ringbuffer', []], + ['benchmark-pod', []], + ['benchmark-dict', []], ] +if sndfile_dep.found() + benchmark_apps += [ + ['benchmark-aec', [sndfile_dep]] + ] +endif + foreach a : benchmark_apps - benchmark('spa-' + a, - executable('spa-' + a, a + '.c', - dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib ], + benchmark('spa-' + a[0], + executable('spa-' + a[0], a[0] + '.c', + dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib ] + a[1], + include_directories : [configinc], install : installed_tests_enabled, install_dir : installed_tests_execdir, ), @@ -47,10 +54,10 @@ foreach a : benchmark_apps if installed_tests_enabled test_conf = configuration_data() - test_conf.set('exec', installed_tests_execdir / 'spa-' + a) + test_conf.set('exec', installed_tests_execdir / 'spa-' + a[0]) configure_file( input: installed_tests_template, - output: 'spa-' + a + '.test', + output: 'spa-' + a[0] + '.test', install_dir: installed_tests_metadir, configuration: test_conf, ) diff --git a/spa/tools/meson.build b/spa/tools/meson.build index 9508e65ca..60e66b55c 100644 --- a/spa/tools/meson.build +++ b/spa/tools/meson.build @@ -6,6 +6,12 @@ executable('spa-monitor', 'spa-monitor.c', dependencies : [ spa_dep, dl_lib ], install : true) -spa_json_dump_exe = executable('spa-json-dump', 'spa-json-dump.c', - dependencies : [ spa_dep, dl_lib, ], +spa_json_dump = executable('spa-json-dump', 'spa-json-dump.c', + dependencies : [ spa_dep ], install : true) + +spa_json_dump_exe = executable('spa-json-dump-native', 'spa-json-dump.c', + dependencies : [ spa_inc_dep ], + native : true) + +meson.override_find_program('spa-json-dump', spa_json_dump_exe) diff --git a/spa/tools/spa-inspect.c b/spa/tools/spa-inspect.c index fd562b5f0..dd3ff752f 100644 --- a/spa/tools/spa-inspect.c +++ b/spa/tools/spa-inspect.c @@ -277,11 +277,11 @@ int main(int argc, char *argv[]) data.n_support = 3; if ((handle = dlopen(argv[1], RTLD_NOW)) == NULL) { - printf("can't load %s\n", argv[1]); + printf("can't load %s: %s\n", argv[1], dlerror()); return -1; } if ((enum_func = dlsym(handle, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) { - printf("can't find function\n"); + printf("can't find \"%s\" function: %s\n", SPA_HANDLE_FACTORY_ENUM_FUNC_NAME, dlerror()); return -1; } diff --git a/spa/tools/spa-json-dump.c b/spa/tools/spa-json-dump.c index a1f2e619a..ee3b42da7 100644 --- a/spa/tools/spa-json-dump.c +++ b/spa/tools/spa-json-dump.c @@ -11,14 +11,74 @@ #include #include #include +#include #include #include #include -static void encode_string(FILE *f, const char *val, int len) +#define DEFAULT_INDENT 2 + +struct data { + const char *filename; + FILE *file; + + void *data; + size_t size; + + int indent; + bool simple_string; + const char *comma; + const char *key_sep; +}; + +#define OPTIONS "hi:s" +static const struct option long_options[] = { + { "help", no_argument, NULL, 'h'}, + + { "indent", required_argument, NULL, 'i' }, + { "spa", no_argument, NULL, 's' }, + + { NULL, 0, NULL, 0 } +}; + +static void show_usage(struct data *d, const char *name, bool is_error) +{ + FILE *fp; + + fp = is_error ? stderr : stdout; + + fprintf(fp, "%s [options] [spa-json-file]\n", name); + fprintf(fp, + " -h, --help Show this help\n" + "\n"); + fprintf(fp, + " -i --indent set indent (default %d)\n" + " -s --spa use simplified SPA JSON\n" + "\n", + DEFAULT_INDENT); +} + +#define REJECT "\"\\'=:,{}[]()#" + +static bool is_simple_string(const char *val, int len) { int i; + for (i = 0; i < len; i++) { + if (val[i] < 0x20 || strchr(REJECT, val[i]) != NULL) + return false; + } + return true; +} + +static void encode_string(struct data *d, const char *val, int len) +{ + FILE *f = d->file; + int i; + if (d->simple_string && is_simple_string(val, len)) { + fprintf(f, "%.*s", len, val); + return; + } fprintf(f, "\""); for (i = 0; i < len; i++) { char v = val[i]; @@ -52,8 +112,9 @@ static void encode_string(FILE *f, const char *val, int len) fprintf(f, "\""); } -static int dump(FILE *file, int indent, struct spa_json *it, const char *value, int len) +static int dump(struct data *d, int indent, struct spa_json *it, const char *value, int len) { + FILE *file = d->file; struct spa_json sub; bool toplevel = false; int count = 0, res; @@ -69,9 +130,9 @@ static int dump(FILE *file, int indent, struct spa_json *it, const char *value, fprintf(file, "["); spa_json_enter(it, &sub); while ((len = spa_json_next(&sub, &value)) > 0) { - fprintf(file, "%s\n%*s", count++ > 0 ? "," : "", - indent+2, ""); - if ((res = dump(file, indent+2, &sub, value, len)) < 0) + fprintf(file, "%s\n%*s", count++ > 0 ? d->comma : "", + indent+d->indent, ""); + if ((res = dump(d, indent+d->indent, &sub, value, len)) < 0) return res; } fprintf(file, "%s%*s]", count > 0 ? "\n" : "", @@ -84,11 +145,11 @@ static int dump(FILE *file, int indent, struct spa_json *it, const char *value, sub = *it; while ((len = spa_json_object_next(&sub, key, sizeof(key), &value)) > 0) { fprintf(file, "%s\n%*s", - count++ > 0 ? "," : "", - indent+2, ""); - encode_string(file, key, strlen(key)); - fprintf(file, ": "); - res = dump(file, indent+2, &sub, value, len); + count++ > 0 ? d->comma : "", + indent+d->indent, ""); + encode_string(d, key, strlen(key)); + fprintf(file, "%s ", d->key_sep); + res = dump(d, indent+d->indent, &sub, value, len); if (res < 0) { if (toplevel) *it = sub; @@ -106,7 +167,7 @@ static int dump(FILE *file, int indent, struct spa_json *it, const char *value, spa_json_is_float(value, len)) { fprintf(file, "%.*s", len, value); } else { - encode_string(file, value, len); + encode_string(d, value, len); } if (spa_json_get_error(it, NULL, NULL)) @@ -115,44 +176,46 @@ static int dump(FILE *file, int indent, struct spa_json *it, const char *value, return 0; } -static int process_json(const char *filename, void *buf, size_t size) +static int process_json(struct data *d) { int len, res; struct spa_json it; const char *value; - if ((len = spa_json_begin(&it, buf, size, &value)) <= 0) { - fprintf(stderr, "not a valid file '%s': %s\n", filename, spa_strerror(len)); + if ((len = spa_json_begin(&it, d->data, d->size, &value)) <= 0) { + fprintf(stderr, "not a valid file '%s': %s\n", d->filename, spa_strerror(len)); return -EINVAL; } if (!spa_json_is_container(value, len)) { - spa_json_init(&it, buf, size); + spa_json_init(&it, d->data, d->size); value = NULL; len = 0; } - res = dump(stdout, 0, &it, value, len); + + res = dump(d, 0, &it, value, len); if (spa_json_next(&it, &value) < 0) res = -EINVAL; - fprintf(stdout, "\n"); - fflush(stdout); + fprintf(d->file, "\n"); + fflush(d->file); if (res < 0) { struct spa_error_location loc; - if (spa_json_get_error(&it, buf, &loc)) + if (spa_json_get_error(&it, d->data, &loc)) spa_debug_file_error_location(stderr, &loc, "syntax error in file '%s': %s", - filename, loc.reason); + d->filename, loc.reason); else - fprintf(stderr, "error parsing file '%s': %s\n", filename, spa_strerror(res)); + fprintf(stderr, "error parsing file '%s': %s\n", + d->filename, spa_strerror(res)); return -EINVAL; } return 0; } -static int process_stdin(void) +static int process_stdin(struct data *d) { uint8_t *buf = NULL, *p; size_t alloc = 0, size = 0, read_size, res; @@ -176,8 +239,10 @@ static int process_stdin(void) fprintf(stderr, "error: %m\n"); goto error; } + d->data = buf; + d->size = size; - err = process_json("-", buf, size); + err = process_json(d); free(buf); return (err == 0) ? EXIT_SUCCESS : EXIT_FAILURE; @@ -189,38 +254,69 @@ error: int main(int argc, char *argv[]) { + int c; + int longopt_index = 0; int fd, res, exit_code = EXIT_FAILURE; - void *data; + struct data d; struct stat sbuf; - if (argc < 1) { - fprintf(stderr, "usage: %s [spa-json-file]\n", argv[0]); - goto error; - } - if (argc == 1) - return process_stdin(); - if ((fd = open(argv[1], O_CLOEXEC | O_RDONLY)) < 0) { - fprintf(stderr, "error opening file '%s': %m\n", argv[1]); - goto error; - } - if (fstat(fd, &sbuf) < 0) { - fprintf(stderr, "error statting file '%s': %m\n", argv[1]); - goto error_close; - } - if ((data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) { - fprintf(stderr, "error mmapping file '%s': %m\n", argv[1]); - goto error_close; + spa_zero(d); + d.file = stdout; + + d.filename = "-"; + d.simple_string = false; + d.comma = ","; + d.key_sep = ":"; + d.indent = DEFAULT_INDENT; + + while ((c = getopt_long(argc, argv, OPTIONS, long_options, &longopt_index)) != -1) { + switch (c) { + case 'h' : + show_usage(&d, argv[0], false); + return 0; + case 'i': + d.indent = atoi(optarg); + break; + case 's': + d.simple_string = true; + d.comma = ""; + d.key_sep = " ="; + break; + default: + show_usage(&d, argv[0], true); + return -1; + } } - res = process_json(argv[1], data, sbuf.st_size); + if (optind < argc) + d.filename = argv[optind++]; + + if (spa_streq(d.filename, "-")) + return process_stdin(&d); + + if ((fd = open(d.filename, O_CLOEXEC | O_RDONLY)) < 0) { + fprintf(stderr, "error opening file '%s': %m\n", d.filename); + goto error; + } + if (fstat(fd, &sbuf) < 0) { + fprintf(stderr, "error statting file '%s': %m\n", d.filename); + goto error_close; + } + if ((d.data = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) { + fprintf(stderr, "error mmapping file '%s': %m\n", d.filename); + goto error_close; + } + d.size = sbuf.st_size; + + res = process_json(&d); if (res < 0) exit_code = EXIT_FAILURE; else exit_code = EXIT_SUCCESS; - munmap(data, sbuf.st_size); + munmap(d.data, sbuf.st_size); error_close: - close(fd); + close(fd); error: return exit_code; } diff --git a/src/daemon/filter-chain/22-onnx-vad.conf b/src/daemon/filter-chain/22-onnx-vad.conf new file mode 100644 index 000000000..35ed8dde8 --- /dev/null +++ b/src/daemon/filter-chain/22-onnx-vad.conf @@ -0,0 +1,83 @@ +context.modules = [ +{ name = libpipewire-module-filter-chain + flags = [ nofail ] + args = { + node.description = "ONNX Example" + node.name = "neural.example" + audio.rate = 16000 + node.latency = "512/16000" + filter.graph = { + nodes = [ + { + type = builtin + name = copy + label = copy + } + { + type = onnx + name = onnx + label = { + #filename = "/home/wim/src/silero-vad/src/silero_vad/data/silero_vad_half.onnx" + filename = "/home/wim/src/silero-vad/src/silero_vad/data/silero_vad.onnx" + blocksize = 512 + input-tensors = { + "input" = { + dimensions = [ 1, 576 ] + retain = 64 + data = "port:input" + } + "state" = { + dimensions = [ 2, 1, 128 ] + data = "tensor:stateN" + } + "sr" = { + dimensions = [ 1 ] + data = "param:rate" + } + } + output-tensors = { + "output" = { + dimensions = [ 1, 1 ] + data = "control:speech" + } + "stateN" = { + dimensions = [ 2, 1, 128 ] + } + } + } + control = { + } + config = { + } + } + { + type = builtin + name = noisegate + label = noisegate + control = { + "Open Threshold" 0.1 + "Close Threshold" 0.02 + } + } + ] + links = [ + { output = "copy:Out" input="onnx:input" } + { output = "copy:Out" input="noisegate:In" } + { output = "onnx:speech" input="noisegate:Level" } + ] + inputs = [ "copy:In" ] + outputs = [ "noisegate:Out" ] + } + + capture.props = { + node.name = "capture.neural" + audio.position = [ MONO ] + } + + playback.props = { + node.name = "playback.neural" + audio.position = [ MONO ] + } + } +} +] diff --git a/src/daemon/filter-chain/meson.build b/src/daemon/filter-chain/meson.build index 0e6340643..774dcfd7f 100644 --- a/src/daemon/filter-chain/meson.build +++ b/src/daemon/filter-chain/meson.build @@ -6,6 +6,7 @@ conf_files = [ [ 'sink-virtual-surround-5.1-kemar.conf', 'sink-virtual-surround-5.1-kemar.conf' ], [ 'sink-virtual-surround-7.1-hesuvi.conf', 'sink-virtual-surround-7.1-hesuvi.conf' ], [ 'sink-dolby-surround.conf', 'sink-dolby-surround.conf' ], + [ 'sink-dolby-pro-logic-ii.conf', 'sink-dolby-pro-logic-ii.conf' ], [ 'sink-eq6.conf', 'sink-eq6.conf' ], [ 'sink-matrix-spatialiser.conf', 'sink-matrix-spatialiser.conf' ], [ 'source-rnnoise.conf', 'source-rnnoise.conf' ], diff --git a/src/daemon/filter-chain/sink-dolby-pro-logic-ii.conf b/src/daemon/filter-chain/sink-dolby-pro-logic-ii.conf new file mode 100644 index 000000000..036c69af4 --- /dev/null +++ b/src/daemon/filter-chain/sink-dolby-pro-logic-ii.conf @@ -0,0 +1,145 @@ +# Dolby Pro Logic II encoder sink +# +# Copy this file into a conf.d/ directory such as +# ~/.config/pipewire/filter-chain.conf.d/ +# +{ + "context.modules": [ + { + "name": "libpipewire-module-filter-chain", + "flags": [ + "nofail" + ], + "args": { + "node.description": "Dolby Pro Logic II Sink", + "media.name": "Dolby Pro Logic II Sink", + "filter.graph": { + "nodes": [ + { + "type": "builtin", + "name": "fc_copy", + "label": "copy" + }, + { + "type": "builtin", + "name": "lfe_copy", + "label": "copy" + }, + { + "type": "builtin", + "name": "sl_phased", + "label": "convolver", + "config": { + "filename": "/hilbert", + "length": 90 + } + }, + { + "type": "builtin", + "name": "sr_phased", + "label": "convolver", + "config": { + "filename": "/hilbert", + "length": 90 + } + }, + { + "type": "builtin", + "name": "mixer_lt", + "label": "mixer", + "control": { + "Gain 1": 1, + "Gain 2": 0, + "Gain 3": 0.7071067811865475, + "Gain 4": 0.7071067811865475, + "Gain 5": -0.8660254037844386, + "Gain 6": -0.5 + } + }, + { + "type": "builtin", + "name": "mixer_rt", + "label": "mixer", + "control": { + "Gain 1": 0, + "Gain 2": 1, + "Gain 3": 0.7071067811865475, + "Gain 4": 0.7071067811865475, + "Gain 5": 0.5, + "Gain 6": 0.8660254037844386 + } + } + ], + "links": [ + { + "output": "fc_copy:Out", + "input": "mixer_lt:In 3" + }, + { + "output": "fc_copy:Out", + "input": "mixer_rt:In 3" + }, + { + "output": "lfe_copy:Out", + "input": "mixer_lt:In 4" + }, + { + "output": "lfe_copy:Out", + "input": "mixer_rt:In 4" + }, + { + "output": "sl_phased:Out", + "input": "mixer_lt:In 5" + }, + { + "output": "sl_phased:Out", + "input": "mixer_rt:In 5" + }, + { + "output": "sr_phased:Out", + "input": "mixer_lt:In 6" + }, + { + "output": "sr_phased:Out", + "input": "mixer_rt:In 6" + } + ], + "inputs": [ + "mixer_lt:In 1", + "mixer_rt:In 2", + "fc_copy:In", + "lfe_copy:In", + "sl_phased:In", + "sr_phased:In" + ], + "outputs": [ + "mixer_lt:Out", + "mixer_rt:Out" + ] + }, + "capture.props": { + "node.name": "effect_input.dolby_pro_logic_ii", + "media.class": "Audio/Sink", + "audio.channels": 6, + "audio.position": [ + "FL", + "FR", + "FC", + "LFE", + "SL", + "SR" + ] + }, + "playback.props": { + "node.name": "effect_output.dolby_pro_logic_ii", + "node.passive": true, + "audio.channels": 2, + "audio.position": [ + "FL", + "FR" + ] + } + } + } + ] +} diff --git a/src/daemon/filter-chain/sink-dolby-surround.conf b/src/daemon/filter-chain/sink-dolby-surround.conf index f51395806..7eb66e6da 100644 --- a/src/daemon/filter-chain/sink-dolby-surround.conf +++ b/src/daemon/filter-chain/sink-dolby-surround.conf @@ -3,45 +3,118 @@ # Copy this file into a conf.d/ directory such as # ~/.config/pipewire/filter-chain.conf.d/ # -context.modules = [ - { name = libpipewire-module-filter-chain - flags = [ nofail ] - args = { - node.description = "Dolby Surround Sink" - media.name = "Dolby Surround Sink" - filter.graph = { - nodes = [ - { - type = builtin - name = mixer - label = mixer - control = { "Gain 1" = 0.5 "Gain 2" = 0.5 } - } - { - type = ladspa - name = enc - plugin = surround_encoder_1401 - label = surroundEncoder - } - ] - links = [ - { output = "mixer:Out" input = "enc:S" } - ] - inputs = [ "enc:L" "enc:R" "enc:C" null "mixer:In 1" "mixer:In 2" ] - outputs = [ "enc:Lt" "enc:Rt" ] +{ + "context.modules": [ + { + "name": "libpipewire-module-filter-chain", + "flags": [ + "nofail" + ], + "args": { + "node.description": "Dolby Surround Sink", + "media.name": "Dolby Surround Sink", + "filter.graph": { + "nodes": [ + { + "type": "builtin", + "name": "mixer_fc", + "label": "mixer" + }, + { + "type": "builtin", + "name": "mixer_s", + "label": "mixer" + }, + { + "type": "builtin", + "name": "s_phased", + "label": "convolver", + "config": { + "filename": "/hilbert", + "length": 90 + } + }, + { + "type": "builtin", + "name": "mixer_lt", + "label": "mixer", + "control": { + "Gain 1": 1, + "Gain 2": 0, + "Gain 3": 0.7071067811865475, + "Gain 4": -0.7071067811865475 + } + }, + { + "type": "builtin", + "name": "mixer_rt", + "label": "mixer", + "control": { + "Gain 1": 0, + "Gain 2": 1, + "Gain 3": 0.7071067811865475, + "Gain 4": 0.7071067811865475 + } } - capture.props = { - node.name = "effect_input.dolby_surround" - media.class = Audio/Sink - audio.channels = 6 - audio.position = [ FL FR FC LFE SL SR ] - } - playback.props = { - node.name = "effect_output.dolby_surround" - node.passive = true - audio.channels = 2 - audio.position = [ FL FR ] + ], + "links": [ + { + "output": "mixer_fc:Out", + "input": "mixer_lt:In 3" + }, + { + "output": "mixer_fc:Out", + "input": "mixer_rt:In 3" + }, + { + "output": "mixer_s:Out", + "input": "s_phased:In" + }, + { + "output": "s_phased:Out", + "input": "mixer_lt:In 4" + }, + { + "output": "s_phased:Out", + "input": "mixer_rt:In 4" } + ], + "inputs": [ + "mixer_lt:In 1", + "mixer_rt:In 2", + "mixer_fc:In 1", + "mixer_fc:In 2", + "mixer_s:In 1", + "mixer_s:In 2" + ], + "outputs": [ + "mixer_lt:Out", + "mixer_rt:Out" + ] + }, + "capture.props": { + "node.name": "effect_input.dolby_surround", + "media.class": "Audio/Sink", + "audio.channels": 6, + "audio.position": [ + "FL", + "FR", + "FC", + "LFE", + "SL", + "SR" + ] + }, + "playback.props": { + "node.name": "effect_output.dolby_surround", + "node.passive": true, + "audio.channels": 2, + "audio.position": [ + "FL", + "FR" + ] } + } } -] + ] +} diff --git a/src/daemon/filter-chain/sink-upmix-5.1-filter.conf b/src/daemon/filter-chain/sink-upmix-5.1-filter.conf index 197733045..5ac4e7650 100644 --- a/src/daemon/filter-chain/sink-upmix-5.1-filter.conf +++ b/src/daemon/filter-chain/sink-upmix-5.1-filter.conf @@ -87,6 +87,7 @@ context.modules = [ delay = 0.012 filename = "/hilbert" length = 33 + latency = 0.0 } } { @@ -101,6 +102,7 @@ context.modules = [ delay = 0.012 filename = "/hilbert" length = 33 + latency = 0.0 } } ] diff --git a/src/daemon/meson.build b/src/daemon/meson.build index b2ebb9375..e7e482f73 100644 --- a/src/daemon/meson.build +++ b/src/daemon/meson.build @@ -161,6 +161,4 @@ custom_target('pipewire-uninstalled', #endif subdir('filter-chain') -if systemd.found() - subdir('systemd') -endif +subdir('systemd') diff --git a/src/daemon/minimal.conf.in b/src/daemon/minimal.conf.in index cfaab1c1c..82647e9ca 100644 --- a/src/daemon/minimal.conf.in +++ b/src/daemon/minimal.conf.in @@ -47,6 +47,9 @@ context.properties = { # Load the pulseaudio emulation daemon minimal.use-pulse = true + + # Load the jack-tunnel as a backend + minimal.use-jack-tunnel = true } context.properties.rules = [ @@ -153,6 +156,42 @@ context.modules = [ { name = libpipewire-module-protocol-pulse condition = [ { minimal.use-pulse = true } ] } + + { name = libpipewire-module-jack-tunnel + args = { + #jack.library = libjack.so.0 + #jack.server = null + #jack.client-name = PipeWire + #jack.connect = true + #jack.connect-audio = [ ] + #jack.connect-midi = [ ] + #tunnel.mode = duplex + #midi.ports = 1 + #audio.channels = 2 + #audio.position = [ FL FR ] + source.props = { + # extra sink properties + #jack.connect-audio = [ "system:capture_1" ] + #jack.connect-midi = [ "system:midi_capture_2" ] + # jack-tunnel needs a PortConfig from the + # session manager so do this here. + node.param.PortConfig = { + direction = Output + mode = dsp + } + } + sink.props = { + # extra sink properties + #jack.connect-audio = [ "system:playback_1" ] + #jack.connect-midi = [ "system:midi_playback_2" ] + node.param.PortConfig = { + direction = Input + mode = dsp + } + } + } + condition = [ { minimal.use-jack-tunnel = true } ] + } ] pulse.properties = { diff --git a/src/daemon/pipewire-aes67.conf.in b/src/daemon/pipewire-aes67.conf.in index 479c12c9b..167b89a6f 100644 --- a/src/daemon/pipewire-aes67.conf.in +++ b/src/daemon/pipewire-aes67.conf.in @@ -128,6 +128,11 @@ context.modules = [ # This property is used if you aren't using ptp4l 4 sess.ts-refclk = "ptp=traceable" sess.ts-offset = 0 + # Directly synchronize output against the PTP-synced driver using the RTP timestamps + # This can be set to true if the reference clocks are the same; it then makes the + # synchronization more robust against transport delay variations and can help lower + # latency + sess.ts-direct = false # You can adjust the latency buffering here. Use integer values only sess.latency.msec = 3 audio.format = "S24BE" diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in index affb993dd..8d018606d 100644 --- a/src/daemon/pipewire-pulse.conf.in +++ b/src/daemon/pipewire-pulse.conf.in @@ -14,6 +14,7 @@ context.properties = { #mem.allow-mlock = true #mem.mlock-all = false #log.level = 2 + #rlimit.nofile = -1 #default.clock.quantum-limit = 8192 } diff --git a/src/daemon/pipewire.c b/src/daemon/pipewire.c index 9193d817b..7d8f185c7 100644 --- a/src/daemon/pipewire.c +++ b/src/daemon/pipewire.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -16,8 +18,6 @@ #include -#include "config.h" - static void do_quit(void *data, int signal_number) { struct pw_main_loop *loop = data; diff --git a/src/daemon/pipewire.conf.avail/50-raop.conf.in b/src/daemon/pipewire.conf.avail/50-raop.conf.in new file mode 100644 index 000000000..22050b2a8 --- /dev/null +++ b/src/daemon/pipewire.conf.avail/50-raop.conf.in @@ -0,0 +1,6 @@ +context.modules = [ + # Use mDNS to detect and load module-raop-sink + { name = libpipewire-module-raop-discover + condition = [ { module.raop = !false } ] + } +] diff --git a/src/daemon/pipewire.conf.avail/meson.build b/src/daemon/pipewire.conf.avail/meson.build index 8b071ba42..b936a217b 100644 --- a/src/daemon/pipewire.conf.avail/meson.build +++ b/src/daemon/pipewire.conf.avail/meson.build @@ -1,6 +1,7 @@ conf_files = [ '10-rates.conf', '20-upmix.conf', + '50-raop.conf', ] foreach c : conf_files diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in index 14f7c6ea0..c3eb7120f 100644 --- a/src/daemon/pipewire.conf.in +++ b/src/daemon/pipewire.conf.in @@ -21,6 +21,7 @@ context.properties = { #clock.power-of-two-quantum = true #log.level = 2 #cpu.zero.denormals = false + #rlimit.nofile = -1 #loop.rt-prio = -1 # -1 = use module-rt prio, 0 disable rt #loop.class = data.rt diff --git a/src/daemon/systemd/meson.build b/src/daemon/systemd/meson.build index 482a44c4b..e07449c00 100644 --- a/src/daemon/systemd/meson.build +++ b/src/daemon/systemd/meson.build @@ -1,6 +1,2 @@ -if get_option('systemd-system-service').allowed() - subdir('system') -endif -if get_option('systemd-user-service').allowed() - subdir('user') -endif +subdir('system') +subdir('user') diff --git a/src/daemon/systemd/system/meson.build b/src/daemon/systemd/system/meson.build index 0cc17670e..02efc7a41 100644 --- a/src/daemon/systemd/system/meson.build +++ b/src/daemon/systemd/system/meson.build @@ -1,3 +1,8 @@ +systemd = dependency('systemd', required : get_option('systemd-system-service')) +if not systemd.found() + subdir_done() +endif + systemd_system_services_dir = systemd.get_variable('systemdsystemunitdir', pkgconfig_define : [ 'rootprefix', prefix]) if get_option('systemd-system-unit-dir') != '' systemd_system_services_dir = get_option('systemd-system-unit-dir') diff --git a/src/daemon/systemd/system/pipewire.service.in b/src/daemon/systemd/system/pipewire.service.in index dc8db3f8f..aeddea300 100644 --- a/src/daemon/systemd/system/pipewire.service.in +++ b/src/daemon/systemd/system/pipewire.service.in @@ -18,7 +18,6 @@ Requires=pipewire.socket LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes -RestrictNamespaces=yes SystemCallArchitectures=native SystemCallFilter=@system-service Type=simple diff --git a/src/daemon/systemd/user/meson.build b/src/daemon/systemd/user/meson.build index 10227629d..1b65d5f7a 100644 --- a/src/daemon/systemd/user/meson.build +++ b/src/daemon/systemd/user/meson.build @@ -1,3 +1,8 @@ +systemd = dependency('systemd', required : get_option('systemd-user-service')) +if not systemd.found() + subdir_done() +endif + systemd_user_services_dir = systemd.get_variable('systemduserunitdir', pkgconfig_define : [ 'prefix', prefix]) if get_option('systemd-user-unit-dir') != '' systemd_user_services_dir = get_option('systemd-user-unit-dir') @@ -11,6 +16,12 @@ systemd_config = configuration_data() systemd_config.set('PW_BINARY', pipewire_bindir / 'pipewire') systemd_config.set('PW_PULSE_BINARY', pipewire_bindir / 'pipewire-pulse') +pw_service_reqs = '' +if get_option('dbus').enabled() + pw_service_reqs += 'dbus.service ' +endif +systemd_config.set('PW_SERVICE_REQS', pw_service_reqs) + configure_file(input : 'pipewire.service.in', output : 'pipewire.service', configuration : systemd_config, diff --git a/src/daemon/systemd/user/pipewire.service.in b/src/daemon/systemd/user/pipewire.service.in index b9b137351..c2621e421 100644 --- a/src/daemon/systemd/user/pipewire.service.in +++ b/src/daemon/systemd/user/pipewire.service.in @@ -13,15 +13,15 @@ Description=PipeWire Multimedia Service # # After=pipewire.socket is not needed, as it is already implicit in the # socket-service relationship, see systemd.socket(5). -Requires=pipewire.socket +Requires=pipewire.socket @PW_SERVICE_REQS@ +ConditionUser=!root [Service] LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes -RestrictNamespaces=yes SystemCallArchitectures=native -SystemCallFilter=@system-service +SystemCallFilter=@system-service mincore Type=simple ExecStart=@PW_BINARY@ Restart=on-failure diff --git a/src/daemon/systemd/user/pipewire.socket b/src/daemon/systemd/user/pipewire.socket index 16e23a7b6..890342abb 100644 --- a/src/daemon/systemd/user/pipewire.socket +++ b/src/daemon/systemd/user/pipewire.socket @@ -1,5 +1,6 @@ [Unit] Description=PipeWire Multimedia System Sockets +ConditionUser=!root [Socket] Priority=6 diff --git a/src/examples/audio-capture.c b/src/examples/audio-capture.c index c44f905f6..2429a2d34 100644 --- a/src/examples/audio-capture.c +++ b/src/examples/audio-capture.c @@ -118,6 +118,7 @@ int main(int argc, char *argv[]) { struct data data = { 0, }; const struct spa_pod *params[1]; + uint32_t n_params = 0; uint8_t buffer[1024]; struct pw_properties *props; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); @@ -164,7 +165,7 @@ int main(int argc, char *argv[]) * id means that this is a format enumeration (of 1 value). * We leave the channels and rate empty to accept the native graph * rate and channels. */ - params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &SPA_AUDIO_INFO_RAW_INIT( .format = SPA_AUDIO_FORMAT_F32)); @@ -176,7 +177,7 @@ int main(int argc, char *argv[]) PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS, - params, 1); + params, n_params); /* and wait while we let things run */ pw_main_loop_run(data.loop); diff --git a/src/examples/audio-dsp-filter.c b/src/examples/audio-dsp-filter.c index f5bda85c9..8a43147a6 100644 --- a/src/examples/audio-dsp-filter.c +++ b/src/examples/audio-dsp-filter.c @@ -80,6 +80,7 @@ int main(int argc, char *argv[]) { struct data data = { 0, }; const struct spa_pod *params[1]; + uint32_t n_params = 0; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); @@ -133,7 +134,7 @@ int main(int argc, char *argv[]) NULL), NULL, 0); - params[0] = spa_process_latency_build(&b, + params[n_params++] = spa_process_latency_build(&b, SPA_PARAM_ProcessLatency, &SPA_PROCESS_LATENCY_INFO_INIT( .ns = 10 * SPA_NSEC_PER_MSEC @@ -144,7 +145,7 @@ int main(int argc, char *argv[]) * called in a realtime thread. */ if (pw_filter_connect(data.filter, PW_FILTER_FLAG_RT_PROCESS, - params, 1) < 0) { + params, n_params) < 0) { fprintf(stderr, "can't connect\n"); return -1; } diff --git a/src/examples/audio-dsp-sink.c b/src/examples/audio-dsp-sink.c new file mode 100644 index 000000000..caaac3b66 --- /dev/null +++ b/src/examples/audio-dsp-sink.c @@ -0,0 +1,217 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Audio sink using \ref pw_filter "pw_filter" + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +/* define to make this filter allocate buffer memory */ +#define ALLOC_BUFFERS + +struct data; + +struct port { + struct data *data; +}; + +struct data { + struct pw_main_loop *loop; + struct pw_filter *filter; + struct port *in_port; + bool move; + uint32_t quantum_limit; +}; + +/* our data processing function is in general: + * + * struct pw_buffer *b; + * out = pw_filter_dequeue_buffer(filter, in_port); + * + * .. consume data in the buffer ... + * + * pw_filter_queue_buffer(filter, in_port, out); + * + * For DSP ports, there is a shortcut to directly dequeue, get + * the data and requeue the buffer with pw_filter_get_dsp_buffer(). + */ +static void on_process(void *userdata, struct spa_io_position *position) +{ + struct data *data = userdata; + float *in, max; + struct port *in_port = data->in_port; + uint32_t i, n_samples = position->clock.duration, peak; + + pw_log_trace("do process %d", n_samples); + + in = pw_filter_get_dsp_buffer(in_port, n_samples); + if (in == NULL) + return; + + /* move cursor up */ + if (data->move) + fprintf(stdout, "%c[%dA", 0x1b, 2); + fprintf(stdout, "captured %d samples\n", n_samples); + max = 0.0f; + for (i = 0; i < n_samples; i++) + max = fmaxf(max, fabsf(in[i])); + + peak = (uint32_t)SPA_CLAMPF(max * 30, 0.f, 39.f); + + fprintf(stdout, "input: |%*s%*s| peak:%f\n", peak+1, "*", 40 - peak, "", max); + data->move = true; + fflush(stdout); +} + +#ifdef ALLOC_BUFFERS +/* close the memfd we set on the buffers here */ +static void on_remove_buffer(void *_data, void *_port_data, struct pw_buffer *buffer) +{ + struct spa_buffer *buf = buffer->buffer; + struct spa_data *d; + + d = buf->datas; + pw_log_info("remove buffer %p", buffer); + + close(d[0].fd); +} + +/* we set the PW_STREAM_FLAG_ALLOC_BUFFERS flag when connecting so we need + * to provide buffer memory. */ +static void on_add_buffer(void *_data, void *_port_data, struct pw_buffer *buffer) +{ + struct data *data = _data; + struct spa_buffer *buf = buffer->buffer; + struct spa_data *d; + + pw_log_info("add buffer %p", buffer); + d = buf->datas; + + if ((d[0].type & (1<quantum_limit * sizeof(float); + + /* truncate to the right size */ + if (ftruncate(d[0].fd, d[0].maxsize) < 0) { + pw_log_error("can't truncate to %d: %m", d[0].maxsize); + return; + } +} +#endif + +static const struct pw_filter_events filter_events = { + PW_VERSION_FILTER_EVENTS, + .process = on_process, +#ifdef ALLOC_BUFFERS + .add_buffer = on_add_buffer, + .remove_buffer = on_remove_buffer, +#endif +}; + +static void do_quit(void *userdata, int signal_number) +{ + struct data *data = userdata; + pw_main_loop_quit(data->loop); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + uint32_t flags; + + pw_init(&argc, &argv); + + data.quantum_limit= 8192; + + /* make a main loop. If you already have another main loop, you can add + * the fd of this pipewire mainloop to it. */ + data.loop = pw_main_loop_new(NULL); + + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data); + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data); + + /* Create a simple filter, the simple filter manages the core and remote + * objects for you if you don't need to deal with them. + * + * Pass your events and a user_data pointer as the last arguments. This + * will inform you about the filter state. The most important event + * you need to listen to is the process event where you need to process + * the data. + */ + data.filter = pw_filter_new_simple( + pw_main_loop_get_loop(data.loop), + "audio-dsp-sink", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Sink", + PW_KEY_MEDIA_ROLE, "DSP", + PW_KEY_MEDIA_CLASS, "Stream/Input/Audio", + PW_KEY_NODE_AUTOCONNECT, "true", + NULL), + &filter_events, + &data); + + flags = PW_FILTER_PORT_FLAG_MAP_BUFFERS; +#ifdef ALLOC_BUFFERS + flags |= PW_FILTER_PORT_FLAG_ALLOC_BUFFERS; +#endif + + /* make an audio DSP output port */ + data.in_port = pw_filter_add_port(data.filter, + PW_DIRECTION_INPUT, + flags, + sizeof(struct port), + pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", + PW_KEY_PORT_NAME, "input", + NULL), + NULL, 0); + + /* Now connect this filter. We ask that our process function is + * called in a realtime thread. */ + if (pw_filter_connect(data.filter, + PW_FILTER_FLAG_RT_PROCESS, + NULL, 0) < 0) { + fprintf(stderr, "can't connect\n"); + return -1; + } + + /* and wait while we let things run */ + pw_main_loop_run(data.loop); + + pw_filter_destroy(data.filter); + pw_main_loop_destroy(data.loop); + pw_deinit(); + + return 0; +} diff --git a/src/examples/audio-dsp-sink2.c b/src/examples/audio-dsp-sink2.c new file mode 100644 index 000000000..7ced1a553 --- /dev/null +++ b/src/examples/audio-dsp-sink2.c @@ -0,0 +1,186 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Audio sink using \ref pw_filter "pw_filter" + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +struct data; + +struct port { + struct data *data; +}; + +struct data { + struct pw_main_loop *loop; + struct pw_filter *filter; + struct port *in_port; + bool move; + uint32_t quantum_limit; +}; + +/* our data processing function is in general: + * + * struct pw_buffer *b; + * out = pw_filter_dequeue_buffer(filter, in_port); + * + * .. consume data in the buffer ... + * + * pw_filter_queue_buffer(filter, in_port, out); + * + * For DSP ports, there is a shortcut to directly dequeue, get + * the data and requeue the buffer with pw_filter_get_dsp_buffer(). + */ +static void on_process(void *userdata, struct spa_io_position *position) +{ + struct data *data = userdata; + float *in, max; + struct port *in_port = data->in_port; + uint32_t i, n_samples = position->clock.duration, peak; + + pw_log_trace("do process %d", n_samples); + + in = pw_filter_get_dsp_buffer(in_port, n_samples); + if (in == NULL) + return; + + /* move cursor up */ + if (data->move) + fprintf(stdout, "%c[%dA", 0x1b, 2); + fprintf(stdout, "captured %d samples\n", n_samples); + max = 0.0f; + for (i = 0; i < n_samples; i++) + max = fmaxf(max, fabsf(in[i])); + + peak = (uint32_t)SPA_CLAMPF(max * 30, 0.f, 39.f); + + fprintf(stdout, "input: |%*s%*s| peak:%f\n", peak+1, "*", 40 - peak, "", max); + data->move = true; + fflush(stdout); +} + +/* Check the buffer memory */ +static void on_add_buffer(void *_data, void *_port_data, struct pw_buffer *buffer) +{ + struct spa_buffer *buf = buffer->buffer; + struct spa_data *d; + + pw_log_info("add buffer %p", buffer); + d = buf->datas; + + if ((d[0].type != SPA_DATA_MemFd)) { + pw_log_error("unsupported data type %08x", d[0].type); + return; + } +} + +static const struct pw_filter_events filter_events = { + PW_VERSION_FILTER_EVENTS, + .process = on_process, + .add_buffer = on_add_buffer, +}; + +static void do_quit(void *userdata, int signal_number) +{ + struct data *data = userdata; + pw_main_loop_quit(data->loop); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + uint32_t flags, n_params = 0; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + + pw_init(&argc, &argv); + + data.quantum_limit = 8192; + + /* make a main loop. If you already have another main loop, you can add + * the fd of this pipewire mainloop to it. */ + data.loop = pw_main_loop_new(NULL); + + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data); + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data); + + /* Create a simple filter, the simple filter manages the core and remote + * objects for you if you don't need to deal with them. + * + * Pass your events and a user_data pointer as the last arguments. This + * will inform you about the filter state. The most important event + * you need to listen to is the process event where you need to process + * the data. + */ + data.filter = pw_filter_new_simple( + pw_main_loop_get_loop(data.loop), + "audio-dsp-sink2", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Sink", + PW_KEY_MEDIA_ROLE, "DSP", + PW_KEY_MEDIA_CLASS, "Stream/Input/Audio", + PW_KEY_NODE_AUTOCONNECT, "true", + NULL), + &filter_events, + &data); + + flags = PW_FILTER_PORT_FLAG_MAP_BUFFERS; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[n_params++] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, 16), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(sizeof(float) * data.quantum_limit), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(sizeof(float)), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<loop->system, data->eventfd, &count); + if (!data->running) + return; } if (avail > n_samples) avail = n_samples; @@ -189,6 +191,7 @@ int main(int argc, char *argv[]) { struct data data = { 0, }; const struct spa_pod *params[1]; + uint32_t n_params = 0; uint8_t buffer[1024]; struct pw_properties *props; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); @@ -226,7 +229,7 @@ int main(int argc, char *argv[]) /* Make one parameter with the supported formats. The SPA_PARAM_EnumFormat * id means that this is a format enumeration (of 1 value). */ - params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &SPA_AUDIO_INFO_RAW_INIT( .format = SPA_AUDIO_FORMAT_F32, .channels = DEFAULT_CHANNELS, @@ -240,7 +243,7 @@ int main(int argc, char *argv[]) PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS, - params, 1); + params, n_params); /* prefill the ringbuffer */ fill_f32(&data, samples, BUFFER_SIZE); diff --git a/src/examples/audio-src.c b/src/examples/audio-src.c index 08c93421e..914d18fe9 100644 --- a/src/examples/audio-src.c +++ b/src/examples/audio-src.c @@ -101,6 +101,7 @@ int main(int argc, char *argv[]) { struct data data = { 0, }; const struct spa_pod *params[1]; + uint32_t n_params = 0; uint8_t buffer[1024]; struct pw_properties *props; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); @@ -141,7 +142,7 @@ int main(int argc, char *argv[]) /* Make one parameter with the supported formats. The SPA_PARAM_EnumFormat * id means that this is a format enumeration (of 1 value). */ - params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &SPA_AUDIO_INFO_RAW_INIT( .format = SPA_AUDIO_FORMAT_F32, .channels = DEFAULT_CHANNELS, @@ -155,7 +156,7 @@ int main(int argc, char *argv[]) PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS, - params, 1); + params, n_params); /* and wait while we let things run */ pw_main_loop_run(data.loop); diff --git a/src/examples/bluez-session.c b/src/examples/bluez-session.c index 81f9926a6..2183071ce 100644 --- a/src/examples/bluez-session.c +++ b/src/examples/bluez-session.c @@ -8,14 +8,14 @@ [title] */ +#include "config.h" + #include #include #include #include #include -#include "config.h" - #include #include #include diff --git a/src/examples/export-sink.c b/src/examples/export-sink.c index 0a5644f8d..fe79c3cfa 100644 --- a/src/examples/export-sink.c +++ b/src/examples/export-sink.c @@ -26,6 +26,7 @@ #define WIDTH 640 #define HEIGHT 480 #define BPP 3 +#define RATE 30 #include "sdl.h" @@ -304,7 +305,8 @@ static int port_set_format(void *object, SDL_TEXTUREACCESS_STREAMING, d->format.size.width, d->format.size.height); - SDL_LockTexture(d->texture, NULL, &dest, &d->stride); + if (SDL_LockTexture(d->texture, NULL, &dest, &d->stride) < 0) + return -EINVAL; SDL_UnlockTexture(d->texture); } diff --git a/src/examples/export-source.c b/src/examples/export-source.c index 85af5b727..a0e983de1 100644 --- a/src/examples/export-source.c +++ b/src/examples/export-source.c @@ -268,8 +268,7 @@ static int port_set_format(void *object, d->format.format != SPA_AUDIO_FORMAT_F32) return -EINVAL; if (d->format.rate == 0 || - d->format.channels == 0 || - d->format.channels > SPA_AUDIO_MAX_CHANNELS) + d->format.channels == 0) return -EINVAL; } diff --git a/src/examples/local-v4l2.c b/src/examples/local-v4l2.c index 2093a9b48..3e00b979a 100644 --- a/src/examples/local-v4l2.c +++ b/src/examples/local-v4l2.c @@ -13,6 +13,7 @@ #define WIDTH 640 #define HEIGHT 480 +#define RATE 30 #define BPP 3 #define MAX_BUFFERS 32 @@ -210,7 +211,8 @@ static int port_set_format(void *object, enum spa_direction direction, uint32_t SDL_TEXTUREACCESS_STREAMING, d->format.size.width, d->format.size.height); - SDL_LockTexture(d->texture, NULL, &dest, &d->stride); + if (SDL_LockTexture(d->texture, NULL, &dest, &d->stride) < 0) + return -EINVAL; SDL_UnlockTexture(d->texture); } diff --git a/src/examples/meson.build b/src/examples/meson.build index 7d45a34f9..ad40f0069 100644 --- a/src/examples/meson.build +++ b/src/examples/meson.build @@ -5,13 +5,17 @@ examples = [ 'audio-src-ring2', 'audio-dsp-src', 'audio-dsp-filter', + 'audio-dsp-sink', + 'audio-dsp-sink2', 'audio-capture', 'video-play', 'video-src', + 'video-src-sync', 'video-dsp-play', 'video-dsp-src', 'video-play-pull', 'video-play-reneg', + 'video-play-sync', 'video-src-alloc', 'video-src-reneg', 'video-src-fixate', @@ -37,6 +41,7 @@ examples_extra_deps = { 'video-play-reneg': [sdl_dep], 'video-play-fixate': [sdl_dep, drm_dep], 'video-play-pull': [sdl_dep], + 'video-play-sync': [sdl_dep], 'video-dsp-play': [sdl_dep], 'local-v4l2': [sdl_dep], 'export-sink': [sdl_dep], diff --git a/src/examples/midi-src.c b/src/examples/midi-src.c index edcaa0f08..90fd36c3d 100644 --- a/src/examples/midi-src.c +++ b/src/examples/midi-src.c @@ -178,6 +178,7 @@ int main(int argc, char *argv[]) uint8_t buffer[1024]; struct spa_pod_builder builder; struct spa_pod *params[1]; + uint32_t n_params = 0; pw_init(&argc, &argv); @@ -226,7 +227,7 @@ int main(int argc, char *argv[]) */ spa_pod_builder_init(&builder, buffer, sizeof(buffer)); - params[0] = spa_pod_builder_add_object(&builder, + params[n_params++] = spa_pod_builder_add_object(&builder, /* POD Object for the buffer parameter */ SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, /* Default 1 buffer, minimum of 1, max of 32 buffers. @@ -242,7 +243,7 @@ int main(int argc, char *argv[]) SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1)); pw_filter_update_params(data.filter, data.port, - (const struct spa_pod **)params, SPA_N_ELEMENTS(params)); + (const struct spa_pod **)params, n_params); /* Now connect this filter. We ask that our process function is * called in a realtime thread. */ diff --git a/src/examples/sdl.h b/src/examples/sdl.h index f96ed26ea..27b768bed 100644 --- a/src/examples/sdl.h +++ b/src/examples/sdl.h @@ -151,7 +151,7 @@ static inline struct spa_pod *sdl_build_formats(SDL_RendererInfo *info, struct s if (id == 0) continue; if (c++ == 0) - spa_pod_builder_id(b, id); + spa_pod_builder_id(b, SPA_VIDEO_FORMAT_UNKNOWN); spa_pod_builder_id(b, id); } /* then all the other ones SDL can convert from/to */ @@ -170,7 +170,7 @@ static inline struct spa_pod *sdl_build_formats(SDL_RendererInfo *info, struct s &SPA_RECTANGLE(info->max_texture_width, info->max_texture_height)), SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction( - &SPA_FRACTION(25,1), + &SPA_FRACTION(RATE,1), &SPA_FRACTION(0,1), &SPA_FRACTION(30,1)), 0); diff --git a/src/examples/video-dsp-play.c b/src/examples/video-dsp-play.c index ad645d7a0..40deb8500 100644 --- a/src/examples/video-dsp-play.c +++ b/src/examples/video-dsp-play.c @@ -22,6 +22,7 @@ #define WIDTH 640 #define HEIGHT 480 #define BPP 3 +#define RATE 30 #define MAX_BUFFERS 64 diff --git a/src/examples/video-dsp-src.c b/src/examples/video-dsp-src.c index 5992088e7..61a189cfd 100644 --- a/src/examples/video-dsp-src.c +++ b/src/examples/video-dsp-src.c @@ -238,6 +238,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[5]; + uint32_t n_params = 0; if (param != NULL && id == SPA_PARAM_Tag) { spa_debug_pod(0, NULL, param); @@ -250,38 +251,38 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) data->stride = SPA_ROUND_UP_N(data->position->video.size.width * BPP, 4); - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->position->video.size.height), SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride)); - params[1] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); - params[2] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage), SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int( sizeof(struct spa_meta_region) * 16, sizeof(struct spa_meta_region) * 1, sizeof(struct spa_meta_region) * 16)); - params[3] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region))); #define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \ sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP) - params[4] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor), SPA_PARAM_META_size, SPA_POD_Int( CURSOR_META_SIZE(CURSOR_WIDTH,CURSOR_HEIGHT))); - pw_stream_update_params(stream, params, 5); + pw_stream_update_params(stream, params, n_params); } static void @@ -309,6 +310,7 @@ int main(int argc, char *argv[]) { struct data data = { 0, }; const struct spa_pod *params[2]; + uint32_t n_params = 0; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); @@ -335,7 +337,7 @@ int main(int argc, char *argv[]) PW_KEY_MEDIA_CLASS, "Video/Source", NULL)); - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), @@ -348,7 +350,7 @@ int main(int argc, char *argv[]) spa_tag_build_start(&b, &f, SPA_PARAM_Tag, SPA_DIRECTION_OUTPUT); items[0] = SPA_DICT_ITEM_INIT("my-tag-key", "my-special-tag-value"); spa_tag_build_add_dict(&b, &SPA_DICT_INIT(items, 1)); - params[1] = spa_tag_build_end(&b, &f); + params[n_params++] = spa_tag_build_end(&b, &f); } pw_stream_add_listener(data.stream, @@ -361,7 +363,7 @@ int main(int argc, char *argv[]) PW_ID_ANY, PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_MAP_BUFFERS, - params, 2); + params, n_params); pw_main_loop_run(data.loop); diff --git a/src/examples/video-play-fixate.c b/src/examples/video-play-fixate.c index b59be31b5..feb97b23c 100644 --- a/src/examples/video-play-fixate.c +++ b/src/examples/video-play-fixate.c @@ -23,6 +23,7 @@ #define WIDTH 640 #define HEIGHT 480 +#define RATE 30 #define MAX_BUFFERS 64 #define MAX_MOD 8 @@ -296,6 +297,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[1]; + uint32_t n_params = 0; Uint32 sdl_format; void *d; @@ -332,12 +334,15 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) SDL_TEXTUREACCESS_STREAMING, data->size.width, data->size.height); - SDL_LockTexture(data->texture, NULL, &d, &data->stride); + if (SDL_LockTexture(data->texture, NULL, &d, &data->stride) < 0) { + pw_stream_set_error(stream, -EINVAL, "invalid texture format"); + return; + } SDL_UnlockTexture(data->texture); /* a SPA_TYPE_OBJECT_ParamBuffers object defines the acceptable size, * number, stride etc of the buffers */ - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), @@ -346,7 +351,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<renderer, &info); if (data->mod_info[0].n_modifiers > 0) { - params[n_params++] = build_format(b, &info, SPA_VIDEO_FORMAT_RGB, data->mod_info[0].modifiers, data->mod_info[0].n_modifiers); + params[n_params++] = build_format(b, + &info, SPA_VIDEO_FORMAT_RGB, + data->mod_info[0].modifiers, + data->mod_info[0].n_modifiers); } params[n_params++] = build_format(b, &info, SPA_VIDEO_FORMAT_RGB, NULL, 0); for (int i=0; i < n_params; i++) { spa_debug_format(2, NULL, params[i]); } - return n_params; } diff --git a/src/examples/video-play-pull.c b/src/examples/video-play-pull.c index f9492edea..13cc21cf8 100644 --- a/src/examples/video-play-pull.c +++ b/src/examples/video-play-pull.c @@ -21,6 +21,7 @@ #define WIDTH 640 #define HEIGHT 480 +#define RATE 30 #define MAX_BUFFERS 64 @@ -170,14 +171,15 @@ on_process(void *_data) /* copy video image in texture */ if (data->is_yuv) { sstride = data->stride; - SDL_UpdateYUVTexture(data->texture, - NULL, - sdata, - sstride, - SPA_PTROFF(sdata, sstride * data->size.height, void), - sstride / 2, - SPA_PTROFF(sdata, 5 * (sstride * data->size.height) / 4, void), - sstride / 2); + if (buf->n_datas == 1) { + SDL_UpdateTexture(data->texture, NULL, + sdata, sstride); + } else { + SDL_UpdateYUVTexture(data->texture, NULL, + sdata, sstride, + buf->datas[1].data, sstride / 2, + buf->datas[2].data, sstride / 2); + } } else { if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) { @@ -328,9 +330,10 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[5]; + uint32_t n_params = 0; Uint32 sdl_format; void *d; - int32_t mult, size; + int32_t mult, size, blocks; /* NULL means to clear the format */ if (param == NULL || id != SPA_PARAM_Format) @@ -382,17 +385,29 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) SDL_TEXTUREACCESS_STREAMING, data->size.width, data->size.height); - SDL_LockTexture(data->texture, NULL, &d, &data->stride); - SDL_UnlockTexture(data->texture); switch(sdl_format) { case SDL_PIXELFORMAT_YV12: case SDL_PIXELFORMAT_IYUV: + data->stride = data->size.width; size = (data->stride * data->size.height) * 3 / 2; data->is_yuv = true; + blocks = 3; + break; + case SDL_PIXELFORMAT_YUY2: + data->stride = data->size.width * 2; + size = data->stride * data->size.height; + data->is_yuv = true; + blocks = 1; break; default: + if (SDL_LockTexture(data->texture, NULL, &d, &data->stride) < 0) { + data->stride = data->size.width * 2; + } + else + SDL_UnlockTexture(data->texture); size = data->stride * data->size.height; + blocks = 1; break; } @@ -403,28 +418,28 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) /* a SPA_TYPE_OBJECT_ParamBuffers object defines the acceptable size, * number, stride etc of the buffers */ - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), - SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(blocks), SPA_PARAM_BUFFERS_size, SPA_POD_Int(size * mult), SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride * mult), SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<renderer, &info); - params[0] = sdl_build_formats(&info, b); + params[n_params++] = sdl_build_formats(&info, b); fprintf(stderr, "supported SDL formats:\n"); spa_debug_format(2, NULL, params[0]); - params[1] = spa_pod_builder_add_object(b, + params[n_params++] = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), @@ -466,7 +482,7 @@ static int build_format(struct data *data, struct spa_pod_builder *b, const stru fprintf(stderr, "supported DSP formats:\n"); spa_debug_format(2, NULL, params[1]); - return 2; + return n_params; } static void do_quit(void *userdata, int signal_number) diff --git a/src/examples/video-play-reneg.c b/src/examples/video-play-reneg.c index c93ed24de..3c9a1e984 100644 --- a/src/examples/video-play-reneg.c +++ b/src/examples/video-play-reneg.c @@ -21,6 +21,7 @@ #define WIDTH 640 #define HEIGHT 480 +#define RATE 30 #define MAX_BUFFERS 64 @@ -189,6 +190,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[1]; + uint32_t n_params = 0; Uint32 sdl_format; void *d; @@ -225,12 +227,15 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) SDL_TEXTUREACCESS_STREAMING, data->size.width, data->size.height); - SDL_LockTexture(data->texture, NULL, &d, &data->stride); + if (SDL_LockTexture(data->texture, NULL, &d, &data->stride) < 0) { + pw_stream_set_error(stream, -EINVAL, "invalid texture format"); + return; + } SDL_UnlockTexture(data->texture); /* a SPA_TYPE_OBJECT_ParamBuffers object defines the acceptable size, * number, stride etc of the buffers */ - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), @@ -239,7 +244,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<renderer, &info); - params[0] = sdl_build_formats(&info, b); + params[n_params++] = sdl_build_formats(&info, b); fprintf(stderr, "supported SDL formats:\n"); spa_debug_format(2, NULL, params[0]); - return 1; + return n_params; } static int reneg_format(struct data *data) @@ -268,6 +274,7 @@ static int reneg_format(struct data *data) uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); const struct spa_pod *params[2]; + uint32_t n_params = 0; int32_t width, height; if (data->format.info.raw.format == 0) @@ -277,7 +284,7 @@ static int reneg_format(struct data *data) height = data->counter & 1 ? 240 : 480; fprintf(stderr, "renegotiate to %dx%d:\n", width, height); - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), @@ -285,7 +292,7 @@ static int reneg_format(struct data *data) SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(width, height)), SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&data->format.info.raw.framerate)); - pw_stream_update_params(data->stream, params, 1); + pw_stream_update_params(data->stream, params, n_params); data->counter++; return 0; @@ -296,16 +303,17 @@ static int reneg_buffers(struct data *data) uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); const struct spa_pod *params[2]; + uint32_t n_params = 0; fprintf(stderr, "renegotiate buffers\n"); - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->size.height), SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride)); - pw_stream_update_params(data->stream, params, 1); + pw_stream_update_params(data->stream, params, n_params); data->counter++; return 0; diff --git a/src/examples/video-play-sync.c b/src/examples/video-play-sync.c new file mode 100644 index 000000000..3ef4ed619 --- /dev/null +++ b/src/examples/video-play-sync.c @@ -0,0 +1,590 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Video input stream using \ref pw_stream "pw_stream" and sync timeline. + [title] + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#define WIDTH 1920 +#define HEIGHT 1080 +#define RATE 30 + +#define MAX_BUFFERS 64 + +#include "sdl.h" + +struct pixel { + float r, g, b, a; +}; + +struct data { + const char *path; + + SDL_Renderer *renderer; + SDL_Window *window; + SDL_Texture *texture; + + struct pw_main_loop *loop; + + struct pw_stream *stream; + struct spa_hook stream_listener; + + struct spa_io_position *position; + + struct spa_video_info format; + int32_t stride; + struct spa_rectangle size; + + int counter; + SDL_Rect rect; + bool is_yuv; + + bool with_synctimeline; + bool with_synctimeline_release; + bool force_synctimeline_release; +}; + +static void handle_events(struct data *data) +{ + SDL_Event event; + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + pw_main_loop_quit(data->loop); + break; + } + } +} + +/* our data processing function is in general: + * + * struct pw_buffer *b; + * b = pw_stream_dequeue_buffer(stream); + * + * .. do stuff with buffer ... + * + * pw_stream_queue_buffer(stream, b); + */ +static void +on_process(void *_data) +{ + struct data *data = _data; + struct pw_stream *stream = data->stream; + struct pw_buffer *b; + struct spa_buffer *buf; + void *sdata, *ddata; + int sstride, dstride, ostride; + struct spa_meta_header *h; + struct spa_meta_sync_timeline *stl = NULL; + uint32_t i, j; + uint8_t *src, *dst; + uint64_t cmd; + + b = NULL; + while (true) { + struct pw_buffer *t; + if ((t = pw_stream_dequeue_buffer(stream)) == NULL) + break; + if (b) + pw_stream_queue_buffer(stream, b); + b = t; + } + if (b == NULL) { + pw_log_warn("out of buffers: %m"); + return; + } + + buf = b->buffer; + + pw_log_trace("new buffer %p", buf); + + handle_events(data); + + if ((sdata = buf->datas[0].data) == NULL) + goto done; + + if ((h = spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h)))) { + uint64_t now = pw_stream_get_nsec(stream); + pw_log_debug("now:%"PRIu64" pts:%"PRIu64" diff:%"PRIi64, + now, h->pts, now - h->pts); + } + if ((stl = spa_buffer_find_meta_data(buf, SPA_META_SyncTimeline, sizeof(*stl))) && + stl->acquire_point) { + /* wait before we can use the buffer */ + if (read(buf->datas[1].fd, &cmd, sizeof(cmd)) < 0) + pw_log_warn("acquire_point wait error %m"); + pw_log_debug("acquire_point:%"PRIu64, stl->acquire_point); + } + + /* copy video image in texture */ + if (data->is_yuv) { + void *datas[4]; + sstride = data->stride; + if (buf->n_datas == 1) { + SDL_UpdateTexture(data->texture, NULL, + sdata, sstride); + } else { + datas[0] = sdata; + datas[1] = buf->datas[1].data; + datas[2] = buf->datas[2].data; + SDL_UpdateYUVTexture(data->texture, NULL, + datas[0], sstride, + datas[1], sstride / 2, + datas[2], sstride / 2); + } + } + else { + if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + } + + sstride = buf->datas[0].chunk->stride; + if (sstride == 0) + sstride = buf->datas[0].chunk->size / data->size.height; + ostride = SPA_MIN(sstride, dstride); + + src = sdata; + dst = ddata; + + if (data->format.media_subtype == SPA_MEDIA_SUBTYPE_dsp) { + for (i = 0; i < data->size.height; i++) { + struct pixel *p = (struct pixel *) src; + for (j = 0; j < data->size.width; j++) { + dst[j * 4 + 0] = SPA_CLAMP((uint8_t)(p[j].r * 255.0f), 0u, 255u); + dst[j * 4 + 1] = SPA_CLAMP((uint8_t)(p[j].g * 255.0f), 0u, 255u); + dst[j * 4 + 2] = SPA_CLAMP((uint8_t)(p[j].b * 255.0f), 0u, 255u); + dst[j * 4 + 3] = SPA_CLAMP((uint8_t)(p[j].a * 255.0f), 0u, 255u); + } + src += sstride; + dst += dstride; + } + } else { + for (i = 0; i < data->size.height; i++) { + memcpy(dst, src, ostride); + src += sstride; + dst += dstride; + } + } + SDL_UnlockTexture(data->texture); + } + + SDL_RenderClear(data->renderer); + /* now render the video */ + SDL_RenderCopy(data->renderer, data->texture, &data->rect, NULL); + SDL_RenderPresent(data->renderer); + + done: + pw_stream_queue_buffer(stream, b); + + if (stl != NULL && stl->release_point) { + /* we promise to signal the release point */ + if (data->with_synctimeline_release) + SPA_FLAG_CLEAR(stl->flags, SPA_META_SYNC_TIMELINE_UNSCHEDULED_RELEASE); + cmd = 1; + /* signal buffer release point */ + write(buf->datas[2].fd, &cmd, sizeof(cmd)); + pw_log_debug("release:%"PRIu64, stl->release_point); + } +} + +static void on_stream_state_changed(void *_data, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct data *data = _data; + fprintf(stderr, "stream state: \"%s\"\n", pw_stream_state_as_string(state)); + switch (state) { + case PW_STREAM_STATE_UNCONNECTED: + pw_main_loop_quit(data->loop); + break; + case PW_STREAM_STATE_PAUSED: + /* because we started inactive, activate ourselves now */ + pw_stream_set_active(data->stream, true); + break; + default: + break; + } +} + +static void +on_stream_io_changed(void *_data, uint32_t id, void *area, uint32_t size) +{ + struct data *data = _data; + + switch (id) { + case SPA_IO_Position: + data->position = area; + break; + } +} + +/* Be notified when the stream param changes. We're only looking at the + * format changes. + * + * We are now supposed to call pw_stream_finish_format() with success or + * failure, depending on if we can support the format. Because we gave + * a list of supported formats, this should be ok. + * + * As part of pw_stream_finish_format() we can provide parameters that + * will control the buffer memory allocation. This includes the metadata + * that we would like on our buffer, the size, alignment, etc. + */ +static void +on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) +{ + struct data *data = _data; + struct pw_stream *stream = data->stream; + uint8_t params_buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + struct spa_pod_frame f; + const struct spa_pod *params[5]; + uint32_t n_params = 0; + Uint32 sdl_format; + void *d; + int32_t mult, size, blocks; + + if (param != NULL && id == SPA_PARAM_Tag) { + spa_debug_pod(0, NULL, param); + return; + } + if (param != NULL && id == SPA_PARAM_Latency) { + struct spa_latency_info info; + if (spa_latency_parse(param, &info) >= 0) + fprintf(stderr, "got latency: %"PRIu64"\n", (info.min_ns + info.max_ns) / 2); + return; + } + /* NULL means to clear the format */ + if (param == NULL || id != SPA_PARAM_Format) + return; + + fprintf(stderr, "got format:\n"); + spa_debug_format(2, NULL, param); + + if (spa_format_parse(param, &data->format.media_type, &data->format.media_subtype) < 0) + return; + + if (data->format.media_type != SPA_MEDIA_TYPE_video) + return; + + switch (data->format.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + /* call a helper function to parse the format for us. */ + spa_format_video_raw_parse(param, &data->format.info.raw); + sdl_format = id_to_sdl_format(data->format.info.raw.format); + data->size = SPA_RECTANGLE(data->format.info.raw.size.width, + data->format.info.raw.size.height); + mult = 1; + break; + case SPA_MEDIA_SUBTYPE_dsp: + spa_format_video_dsp_parse(param, &data->format.info.dsp); + if (data->format.info.dsp.format != SPA_VIDEO_FORMAT_DSP_F32) + return; + sdl_format = SDL_PIXELFORMAT_RGBA32; + data->size = SPA_RECTANGLE(data->position->video.size.width, + data->position->video.size.height); + mult = 4; + break; + default: + sdl_format = SDL_PIXELFORMAT_UNKNOWN; + break; + } + + if (sdl_format == SDL_PIXELFORMAT_UNKNOWN) { + pw_stream_set_error(stream, -EINVAL, "unknown pixel format"); + return; + } + if (data->size.width == 0 || data->size.height == 0) { + pw_stream_set_error(stream, -EINVAL, "invalid size"); + return; + } + + data->texture = SDL_CreateTexture(data->renderer, + sdl_format, + SDL_TEXTUREACCESS_STREAMING, + data->size.width, + data->size.height); + switch(sdl_format) { + case SDL_PIXELFORMAT_YV12: + case SDL_PIXELFORMAT_IYUV: + data->stride = data->size.width; + size = (data->stride * data->size.height) * 3 / 2; + data->is_yuv = true; + blocks = 3; + break; + case SDL_PIXELFORMAT_YUY2: + data->is_yuv = true; + data->stride = data->size.width * 2; + size = (data->stride * data->size.height); + blocks = 1; + break; + default: + if (SDL_LockTexture(data->texture, NULL, &d, &data->stride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + data->stride = data->size.width * 2; + } else + SDL_UnlockTexture(data->texture); + size = data->stride * data->size.height; + blocks = 1; + break; + } + + data->rect.x = 0; + data->rect.y = 0; + data->rect.w = data->size.width; + data->rect.h = data->size.height; + + if (data->with_synctimeline) { + /* first add Buffer with 3 blocks (1 data, 2 sync fds). */ + spa_pod_builder_push_object(&b, &f, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); + spa_pod_builder_add(&b, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(3), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(size * mult), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride * mult), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<with_synctimeline_release) { + uint32_t flags = data->force_synctimeline_release ? + /* both sides need compatible features */ + SPA_POD_PROP_FLAG_MANDATORY : + /* drop features flags if not provided by both sides */ + SPA_POD_PROP_FLAG_DROP; + + spa_pod_builder_prop(&b, SPA_PARAM_META_features, flags); + spa_pod_builder_int(&b, SPA_META_FEATURE_SYNC_TIMELINE_RELEASE); + } + params[n_params++] = spa_pod_builder_pop(&b, &f); + } + + /* fallback for when the synctimeline is not negotiated */ + params[n_params++] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(size * mult), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride * mult), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<renderer, &info); + params[n_params++] = sdl_build_formats(&info, b); + + fprintf(stderr, "supported SDL formats:\n"); + spa_debug_format(2, NULL, params[0]); + + params[n_params++] = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_DSP_F32)); + + fprintf(stderr, "supported DSP formats:\n"); + spa_debug_format(2, NULL, params[1]); + + return n_params; +} + +static void do_quit(void *userdata, int signal_number) +{ + struct data *data = userdata; + pw_main_loop_quit(data->loop); +} + +static void show_help(struct data *data, const char *name, bool is_error) +{ + FILE *fp; + + fp = is_error ? stderr : stdout; + + fprintf(fp, + "%s [options]\n" + " -h, --help Show this help\n" + " --version Show version\n" + " -r, --remote Remote daemon name\n" + " -S, --sync Enable SyncTimeline\n" + " -R, --release Enable RELEASE feature\n" + " -F, --force-release RELEASE feature needs to be present\n" + "\n", name); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + const struct spa_pod *params[3]; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + struct pw_properties *props; + int res, n_params; + static const struct option long_options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "remote", required_argument, NULL, 'r' }, + { "sync", no_argument, NULL, 'S' }, + { "release", no_argument, NULL, 'R' }, + { "force-release", no_argument, NULL, 'F' }, + { NULL, 0, NULL, 0} + }; + char *opt_remote = NULL; + int c; + + pw_init(&argc, &argv); + + while ((c = getopt_long(argc, argv, "hVr:SRF", long_options, NULL)) != -1) { + switch (c) { + case 'h': + show_help(&data, argv[0], false); + return 0; + case 'V': + printf("%s\n" + "Compiled with libpipewire %s\n" + "Linked with libpipewire %s\n", + argv[0], + pw_get_headers_version(), + pw_get_library_version()); + return 0; + case 'r': + opt_remote = optarg; + break; + case 'F': + data.force_synctimeline_release = true; + SPA_FALLTHROUGH; + case 'R': + data.with_synctimeline_release = true; + SPA_FALLTHROUGH; + case 'S': + data.with_synctimeline = true; + break; + default: + show_help(&data, argv[0], true); + return -1; + } + } + + /* create a main loop */ + data.loop = pw_main_loop_new(NULL); + + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data); + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data); + + /* create a simple stream, the simple stream manages to core and remote + * objects for you if you don't need to deal with them + * + * If you plan to autoconnect your stream, you need to provide at least + * media, category and role properties + * + * Pass your events and a user_data pointer as the last arguments. This + * will inform you about the stream state. The most important event + * you need to listen to is the process event where you need to consume + * the data provided to you. + */ + props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Camera", + PW_KEY_REMOTE_NAME, opt_remote, + NULL), + + data.path = optind < argc ? argv[optind++] : "video-src-sync"; + if (data.path) + pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path); + + data.stream = pw_stream_new_simple( + pw_main_loop_get_loop(data.loop), + "video-play", + props, + &stream_events, + &data); + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + fprintf(stderr, "can't initialize SDL: %s\n", SDL_GetError()); + return -1; + } + + if (SDL_CreateWindowAndRenderer + (WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE, &data.window, &data.renderer)) { + fprintf(stderr, "can't create window: %s\n", SDL_GetError()); + return -1; + } + + /* build the extra parameters to connect with. To connect, we can provide + * a list of supported formats. We use a builder that writes the param + * object to the stack. */ + n_params = build_format(&data, &b, params); + + /* now connect the stream, we need a direction (input/output), + * an optional target node to connect to, some flags and parameters + */ + if ((res = pw_stream_connect(data.stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | /* try to automatically connect this stream */ + PW_STREAM_FLAG_INACTIVE | /* we will activate ourselves */ + PW_STREAM_FLAG_MAP_BUFFERS, /* mmap the buffer data for us */ + params, n_params)) /* extra parameters, see above */ < 0) { + fprintf(stderr, "can't connect: %s\n", spa_strerror(res)); + return -1; + } + + /* do things until we quit the mainloop */ + pw_main_loop_run(data.loop); + + pw_stream_destroy(data.stream); + pw_main_loop_destroy(data.loop); + + SDL_DestroyTexture(data.texture); + SDL_DestroyRenderer(data.renderer); + SDL_DestroyWindow(data.window); + pw_deinit(); + + return 0; +} diff --git a/src/examples/video-play.c b/src/examples/video-play.c index 53041f284..f4f65bb8f 100644 --- a/src/examples/video-play.c +++ b/src/examples/video-play.c @@ -22,8 +22,9 @@ #include -#define WIDTH 640 -#define HEIGHT 480 +#define WIDTH 1920 +#define HEIGHT 1080 +#define RATE 30 #define MAX_BUFFERS 64 @@ -177,20 +178,24 @@ on_process(void *_data) /* copy video image in texture */ if (data->is_yuv) { + void *datas[4]; sstride = data->stride; - SDL_UpdateYUVTexture(data->texture, - NULL, - sdata, - sstride, - SPA_PTROFF(sdata, sstride * data->size.height, void), - sstride / 2, - SPA_PTROFF(sdata, 5 * (sstride * data->size.height) / 4, void), - sstride / 2); + if (buf->n_datas == 1) { + SDL_UpdateTexture(data->texture, NULL, + sdata, sstride); + } else { + datas[0] = sdata; + datas[1] = buf->datas[1].data; + datas[2] = buf->datas[2].data; + SDL_UpdateYUVTexture(data->texture, NULL, + datas[0], sstride, + datas[1], sstride / 2, + datas[2], sstride / 2); + } } else { if (SDL_LockTexture(data->texture, NULL, &ddata, &dstride) < 0) { fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); - goto done; } sstride = buf->datas[0].chunk->stride; @@ -284,9 +289,10 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[5]; + uint32_t n_params = 0; Uint32 sdl_format; void *d; - int32_t mult, size; + int32_t mult, size, blocks; if (param != NULL && id == SPA_PARAM_Tag) { spa_debug_pod(0, NULL, param); @@ -348,17 +354,28 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) SDL_TEXTUREACCESS_STREAMING, data->size.width, data->size.height); - SDL_LockTexture(data->texture, NULL, &d, &data->stride); - SDL_UnlockTexture(data->texture); - switch(sdl_format) { case SDL_PIXELFORMAT_YV12: case SDL_PIXELFORMAT_IYUV: + data->stride = data->size.width; size = (data->stride * data->size.height) * 3 / 2; data->is_yuv = true; + blocks = 3; + break; + case SDL_PIXELFORMAT_YUY2: + data->is_yuv = true; + data->stride = data->size.width * 2; + size = (data->stride * data->size.height); + blocks = 1; break; default: + if (SDL_LockTexture(data->texture, NULL, &d, &data->stride) < 0) { + fprintf(stderr, "Couldn't lock texture: %s\n", SDL_GetError()); + data->stride = data->size.width * 2; + } else + SDL_UnlockTexture(data->texture); size = data->stride * data->size.height; + blocks = 1; break; } @@ -369,28 +386,28 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) /* a SPA_TYPE_OBJECT_ParamBuffers object defines the acceptable size, * number, stride etc of the buffers */ - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), - SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(blocks), SPA_PARAM_BUFFERS_size, SPA_POD_Int(size * mult), SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride * mult), SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<renderer, &info); - params[0] = sdl_build_formats(&info, b); + params[n_params++] = sdl_build_formats(&info, b); fprintf(stderr, "supported SDL formats:\n"); spa_debug_format(2, NULL, params[0]); - params[1] = spa_pod_builder_add_object(b, + params[n_params++] = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp), @@ -430,7 +448,7 @@ static int build_format(struct data *data, struct spa_pod_builder *b, const stru fprintf(stderr, "supported DSP formats:\n"); spa_debug_format(2, NULL, params[1]); - return 2; + return n_params; } static void do_quit(void *userdata, int signal_number) diff --git a/src/examples/video-src-alloc.c b/src/examples/video-src-alloc.c index 1165183c4..add9597ed 100644 --- a/src/examples/video-src-alloc.c +++ b/src/examples/video-src-alloc.c @@ -22,7 +22,7 @@ #include -#define BPP 3 +#define BPP 4 #define CURSOR_WIDTH 64 #define CURSOR_HEIGHT 64 #define CURSOR_BPP 4 @@ -290,6 +290,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[5]; + uint32_t n_params = 0; if (param == NULL || id != SPA_PARAM_Format) return; @@ -298,7 +299,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) data->stride = SPA_ROUND_UP_N(data->format.size.width * BPP, 4); - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), @@ -306,31 +307,31 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride), SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1< -#define BPP 3 +#define BPP 4 #define CURSOR_WIDTH 64 #define CURSOR_HEIGHT 64 #define CURSOR_BPP 4 @@ -378,6 +378,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[5]; + uint32_t n_params = 0; int blocks, size, stride, buffertypes; if (param == NULL || id != SPA_PARAM_Format) @@ -414,11 +415,10 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) modifier = modifiers[rand()%n_modifiers]; } - params[0] = fixate_format(&b, SPA_VIDEO_FORMAT_RGB, &modifier); - - params[1] = build_format(&b, SPA_VIDEO_FORMAT_RGB, - supported_modifiers, sizeof(supported_modifiers)/sizeof(supported_modifiers[0])); - params[2] = build_format(&b, SPA_VIDEO_FORMAT_RGB, + params[n_params++] = fixate_format(&b, SPA_VIDEO_FORMAT_RGBA, &modifier); + params[n_params++] = build_format(&b, SPA_VIDEO_FORMAT_RGBA, + supported_modifiers, SPA_N_ELEMENTS(supported_modifiers)); + params[n_params++] = build_format(&b, SPA_VIDEO_FORMAT_RGBA, NULL, 0); printf("announcing fixated EnumFormats\n"); @@ -426,7 +426,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) spa_debug_format(4, NULL, params[i]); } - pw_stream_update_params(stream, params, 3); + pw_stream_update_params(stream, params, n_params); return; } printf("no fixation required\n"); @@ -436,7 +436,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) buffertypes = (1< -#define BPP 3 +#define BPP 4 #define CURSOR_WIDTH 64 #define CURSOR_HEIGHT 64 #define CURSOR_BPP 4 @@ -304,6 +304,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[5]; + uint32_t n_params = 0; if (param == NULL || id != SPA_PARAM_Format) return; @@ -313,7 +314,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) data->stride = SPA_ROUND_UP_N(data->format.size.width * BPP, 4); - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), @@ -321,31 +322,31 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride), SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1<cycle & 1 ? 320 : 640; height = data->cycle & 1 ? 240 : 480; fprintf(stderr, "renegotiate to %dx%d:\n", width, height); - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), - SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_RGB), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_RGBA), SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(width, height)), SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(25, 1))); - pw_stream_update_params(data->stream, params, 1); + pw_stream_update_params(data->stream, params, n_params); data->cycle++; } @@ -392,13 +394,14 @@ int main(int argc, char *argv[]) { struct data data = { 0, }; const struct spa_pod *params[1]; + uint32_t n_params = 0; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); pw_init(&argc, &argv); /* create a thread loop and start it */ - data.loop = pw_thread_loop_new("video-src-alloc", NULL); + data.loop = pw_thread_loop_new("video-src-reneg", NULL); /* take the lock around all PipeWire functions. In callbacks, the lock * is already taken for you but it's ok to lock again because the lock is @@ -445,11 +448,11 @@ int main(int argc, char *argv[]) * The server will select a format that matches and informs us about this * in the stream param_changed event. */ - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), - SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_RGB), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_RGBA), SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( &SPA_RECTANGLE(320, 240), &SPA_RECTANGLE(1, 1), @@ -468,7 +471,7 @@ int main(int argc, char *argv[]) PW_ID_ANY, PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_ALLOC_BUFFERS, - params, 1); + params, n_params); /* unlock, run the loop and wait, this will trigger the callbacks */ pw_thread_loop_wait(data.loop); diff --git a/src/examples/video-src-sync.c b/src/examples/video-src-sync.c new file mode 100644 index 000000000..873a27407 --- /dev/null +++ b/src/examples/video-src-sync.c @@ -0,0 +1,529 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +/* + [title] + Video source using \ref pw_stream and sync_timeline. + [title] + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define BPP 4 +#define CURSOR_WIDTH 64 +#define CURSOR_HEIGHT 64 +#define CURSOR_BPP 4 + +#define MAX_BUFFERS 64 + +#define M_PI_M2 ( M_PI + M_PI ) + +struct data { + struct pw_main_loop *loop; + struct spa_source *timer; + + struct pw_context *context; + struct pw_core *core; + + struct pw_stream *stream; + struct spa_hook stream_listener; + + struct spa_video_info_raw format; + int32_t stride; + + int counter; + uint32_t seq; + uint32_t n_buffers; + + int res; + + bool with_synctimeline; + bool with_synctimeline_release; + bool force_synctimeline_release; +}; + +static void on_process(void *userdata) +{ + struct data *data = userdata; + struct pw_buffer *b; + struct spa_buffer *buf; + uint32_t i, j; + uint8_t *p; + struct spa_meta_header *h; + struct spa_meta_sync_timeline *stl; + uint64_t cmd; + + if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) { + pw_log_warn("out of buffers: %m"); + return; + } + + buf = b->buffer; + if ((p = buf->datas[0].data) == NULL) + return; + + if ((h = spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h)))) { +#if 0 + h->pts = pw_stream_get_nsec(data->stream); +#else + h->pts = -1; +#endif + h->flags = 0; + h->seq = data->seq++; + h->dts_offset = 0; + } + if ((stl = spa_buffer_find_meta_data(buf, SPA_META_SyncTimeline, sizeof(*stl))) && + stl->release_point) { + if (!SPA_FLAG_IS_SET(stl->flags, SPA_META_SYNC_TIMELINE_UNSCHEDULED_RELEASE)) { + /* The other end promised to schedule the release point, wait before we + * can use the buffer */ + if (read(buf->datas[2].fd, &cmd, sizeof(cmd)) < 0) + pw_log_warn("release_point wait error %m"); + pw_log_debug("release_point:%"PRIu64, stl->release_point); + } else if (spa_buffer_has_meta_features(buf, SPA_META_SyncTimeline, + SPA_META_FEATURE_SYNC_TIMELINE_RELEASE)) { + /* this happens when the other end did not get the buffer or + * will not trigger the release point, There is no point waiting, + * we can use the buffer right away */ + pw_log_warn("release_point not scheduled:%"PRIu64, stl->release_point); + } else { + /* The other end does not support the RELEASE flag, we don't + * know if the buffer was used or not or if the release point will + * ever be scheduled, we must assume we can reuse the buffer */ + pw_log_debug("assume buffer was released:%"PRIu64, stl->release_point); + } + } + + for (i = 0; i < data->format.size.height; i++) { + for (j = 0; j < data->format.size.width * BPP; j++) + p[j] = data->counter + j * i; + p += data->stride; + data->counter += 13; + } + + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->size = data->format.size.height * data->stride; + buf->datas[0].chunk->stride = data->stride; + + if (stl) { + /* set the UNSCHEDULED_RELEASE flag, the consumer will clear this if + * it promises to signal the release point */ + SPA_FLAG_SET(stl->flags, SPA_META_SYNC_TIMELINE_UNSCHEDULED_RELEASE); + cmd = 1; + stl->acquire_point = data->seq; + stl->release_point = data->seq; + /* write the acquire point */ + write(buf->datas[1].fd, &cmd, sizeof(cmd)); + } + pw_stream_queue_buffer(data->stream, b); +} + +static void on_timeout(void *userdata, uint64_t expirations) +{ + struct data *data = userdata; + pw_log_trace("timeout"); + pw_stream_trigger_process(data->stream); +} + +static void on_stream_state_changed(void *_data, enum pw_stream_state old, enum pw_stream_state state, + const char *error) +{ + struct data *data = _data; + + printf("stream state: \"%s\" %s\n", pw_stream_state_as_string(state), error ? error : ""); + + switch (state) { + case PW_STREAM_STATE_ERROR: + case PW_STREAM_STATE_UNCONNECTED: + pw_main_loop_quit(data->loop); + break; + + case PW_STREAM_STATE_PAUSED: + printf("node id: %d\n", pw_stream_get_node_id(data->stream)); + pw_loop_update_timer(pw_main_loop_get_loop(data->loop), + data->timer, NULL, NULL, false); + break; + case PW_STREAM_STATE_STREAMING: + { + struct timespec timeout, interval; + + timeout.tv_sec = 0; + timeout.tv_nsec = 1; + interval.tv_sec = 0; + interval.tv_nsec = 40 * SPA_NSEC_PER_MSEC; + + printf("driving:%d\n", pw_stream_is_driving(data->stream)); + + if (pw_stream_is_driving(data->stream)) + pw_loop_update_timer(pw_main_loop_get_loop(data->loop), + data->timer, &timeout, &interval, false); + break; + } + default: + break; + } +} + +static void +on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) +{ + struct data *data = _data; + struct pw_stream *stream = data->stream; + uint8_t params_buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + const struct spa_pod *params[5]; + uint32_t n_params = 0; + struct spa_pod_frame f; + + if (param != NULL && id == SPA_PARAM_Tag) { + spa_debug_pod(0, NULL, param); + return; + } + if (param == NULL || id != SPA_PARAM_Format) + return; + + fprintf(stderr, "got format:\n"); + spa_debug_format(2, NULL, param); + + spa_format_video_raw_parse(param, &data->format); + + data->stride = SPA_ROUND_UP_N(data->format.size.width * BPP, 4); + + /* first add Buffer with 3 blocks (1 data, 2 sync fds). */ + if (data->with_synctimeline) { + spa_pod_builder_push_object(&b, &f, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); + spa_pod_builder_add(&b, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(3), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->format.size.height), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<with_synctimeline_release) { + uint32_t flags = data->force_synctimeline_release ? + /* both sides need compatible features */ + SPA_POD_PROP_FLAG_MANDATORY : + /* drop features flags if not provided by both sides */ + SPA_POD_PROP_FLAG_DROP; + + spa_pod_builder_prop(&b, SPA_PARAM_META_features, flags); + spa_pod_builder_int(&b, SPA_META_FEATURE_SYNC_TIMELINE_RELEASE); + } + params[n_params++] = spa_pod_builder_pop(&b, &f); + } + + /* fallback for when the synctimeline is not negotiated */ + params[n_params++] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->format.size.height), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<buffer; + struct spa_data *d; +#ifdef HAVE_MEMFD_CREATE + unsigned int seals; +#endif + struct spa_meta_sync_timeline *s; + + d = buf->datas; + + pw_log_debug("add buffer %p", buffer); + if ((d[0].type & (1<stride * data->format.size.height; + + /* truncate to the right size before we set seals */ + if (ftruncate(d[0].fd, d[0].maxsize) < 0) { + pw_log_error("can't truncate to %d: %m", d[0].maxsize); + return; + } +#ifdef HAVE_MEMFD_CREATE + /* not enforced yet but server might require SEAL_SHRINK later */ + seals = F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL; + if (fcntl(d[0].fd, F_ADD_SEALS, seals) == -1) { + pw_log_warn("Failed to add seals: %m"); + } +#endif + + /* now mmap so we can write to it in the process function above */ + d[0].data = mmap(NULL, d[0].maxsize, PROT_READ|PROT_WRITE, + MAP_SHARED, d[0].fd, d[0].mapoffset); + if (d[0].data == MAP_FAILED) { + pw_log_error("can't mmap memory: %m"); + return; + } + + if ((s = spa_buffer_find_meta_data(buf, SPA_META_SyncTimeline, sizeof(*s))) && buf->n_datas >= 3) { + pw_log_debug("got sync timeline"); + /* acquire fd (just an example, not really syncobj here) */ + d[1].type = SPA_DATA_SyncObj; + d[1].flags = SPA_DATA_FLAG_READWRITE; + d[1].fd = eventfd(0, EFD_CLOEXEC); + d[1].mapoffset = 0; + d[1].maxsize = 0; + if (d[1].fd == -1) { + pw_log_error("can't create acquire fd: %m"); + return; + } + /* release fd (just an example, not really syncobj here) */ + d[2].type = SPA_DATA_SyncObj; + d[2].flags = SPA_DATA_FLAG_READWRITE; + d[2].fd = eventfd(0, EFD_CLOEXEC); + d[2].mapoffset = 0; + d[2].maxsize = 0; + if (d[2].fd == -1) { + pw_log_error("can't create release fd: %m"); + return; + } + } + + if (data->n_buffers++ == 0) { + struct spa_dict_item items[2]; + uint32_t n_items = 0; + bool reliable = false, exclusive = false; + + if (s != NULL) { + /* sync timeline is always exclusive */ + exclusive = true; + if (spa_buffer_has_meta_features(buf, SPA_META_SyncTimeline, + SPA_META_FEATURE_SYNC_TIMELINE_RELEASE)) { + pw_log_info("got sync timeline with release"); + } else { + pw_log_info("got sync timeline"); + /* we need reliable transport without release */ + reliable = true; + } + } + else { + pw_log_info("did not get sync timeline"); + } + items[n_items++] = SPA_DICT_ITEM(PW_KEY_NODE_EXCLUSIVE, exclusive ? "true" : "false"); + items[n_items++] = SPA_DICT_ITEM(PW_KEY_NODE_RELIABLE, reliable ? "true" : "false"); + + pw_stream_update_properties(data->stream, &SPA_DICT(items, n_items)); + } +} + +/* close the memfd we set on the buffers here */ +static void on_stream_remove_buffer(void *_data, struct pw_buffer *buffer) +{ + struct data *data = _data; + struct spa_buffer *buf = buffer->buffer; + struct spa_data *d; + + d = buf->datas; + pw_log_debug("remove buffer %p", buffer); + + munmap(d[0].data, d[0].maxsize); + close(d[0].fd); + if (buf->n_datas >= 3) { + close(d[1].fd); + close(d[2].fd); + } + data->n_buffers--; + } + + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .process = on_process, + .state_changed = on_stream_state_changed, + .param_changed = on_stream_param_changed, + .add_buffer = on_stream_add_buffer, + .remove_buffer = on_stream_remove_buffer, +}; + +static void do_quit(void *userdata, int signal_number) +{ + struct data *data = userdata; + pw_main_loop_quit(data->loop); +} + +static void show_help(struct data *data, const char *name, bool is_error) +{ + FILE *fp; + + fp = is_error ? stderr : stdout; + + fprintf(fp, + "%s [options]\n" + " -h, --help Show this help\n" + " --version Show version\n" + " -r, --remote Remote daemon name\n" + " -S, --sync Enable SyncTimeline\n" + " -R, --release Enable RELEASE feature\n" + " -F, --force-release RELEASE feature needs to be present\n" + "\n", name); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + const struct spa_pod *params[2]; + uint32_t n_params = 0; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + static const struct option long_options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "remote", required_argument, NULL, 'r' }, + { "sync", no_argument, NULL, 'S' }, + { "release", no_argument, NULL, 'R' }, + { "force-release", no_argument, NULL, 'F' }, + { NULL, 0, NULL, 0} + }; + char *opt_remote = NULL; + int c; + + pw_init(&argc, &argv); + + while ((c = getopt_long(argc, argv, "hVr:SRF", long_options, NULL)) != -1) { + switch (c) { + case 'h': + show_help(&data, argv[0], false); + return 0; + case 'V': + printf("%s\n" + "Compiled with libpipewire %s\n" + "Linked with libpipewire %s\n", + argv[0], + pw_get_headers_version(), + pw_get_library_version()); + return 0; + case 'r': + opt_remote = optarg; + break; + case 'F': + data.force_synctimeline_release = true; + SPA_FALLTHROUGH; + case 'R': + data.with_synctimeline_release = true; + SPA_FALLTHROUGH; + case 'S': + data.with_synctimeline = true; + break; + default: + show_help(&data, argv[0], true); + return -1; + } + } + + data.loop = pw_main_loop_new(NULL); + + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data); + pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data); + + data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0); + + data.timer = pw_loop_add_timer(pw_main_loop_get_loop(data.loop), on_timeout, &data); + + data.core = pw_context_connect(data.context, + pw_properties_new( + PW_KEY_REMOTE_NAME, opt_remote, + NULL), + 0); + if (data.core == NULL) { + fprintf(stderr, "can't connect: %m\n"); + data.res = -errno; + goto cleanup; + } + + data.stream = pw_stream_new(data.core, "video-src-sync", + pw_properties_new( + PW_KEY_MEDIA_CLASS, "Video/Source", + NULL)); + + params[n_params++] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_BGRA), + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(320, 240), + &SPA_RECTANGLE(1, 1), + &SPA_RECTANGLE(4096, 4096)), + SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&SPA_FRACTION(25, 1))); + + pw_stream_add_listener(data.stream, + &data.stream_listener, + &stream_events, + &data); + + pw_stream_connect(data.stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_DRIVER | + PW_STREAM_FLAG_ALLOC_BUFFERS | + PW_STREAM_FLAG_MAP_BUFFERS, + params, n_params); + + pw_main_loop_run(data.loop); + +cleanup: + pw_context_destroy(data.context); + pw_main_loop_destroy(data.loop); + pw_deinit(); + + return data.res; +} diff --git a/src/examples/video-src.c b/src/examples/video-src.c index f439f168e..452924e59 100644 --- a/src/examples/video-src.c +++ b/src/examples/video-src.c @@ -16,10 +16,11 @@ #include #include #include +#include #include -#define BPP 3 +#define BPP 4 #define CURSOR_WIDTH 64 #define CURSOR_HEIGHT 64 #define CURSOR_BPP 4 @@ -172,7 +173,7 @@ static void on_stream_state_changed(void *_data, enum pw_stream_state old, enum { struct data *data = _data; - printf("stream state: \"%s\"\n", pw_stream_state_as_string(state)); + printf("stream state: \"%s\" %s\n", pw_stream_state_as_string(state), error ? error : ""); switch (state) { case PW_STREAM_STATE_ERROR: @@ -216,6 +217,7 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[5]; + uint32_t n_params = 0; if (param != NULL && id == SPA_PARAM_Tag) { spa_debug_pod(0, NULL, param); @@ -224,42 +226,45 @@ on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) if (param == NULL || id != SPA_PARAM_Format) return; + fprintf(stderr, "got format:\n"); + spa_debug_format(2, NULL, param); + spa_format_video_raw_parse(param, &data->format); data->stride = SPA_ROUND_UP_N(data->format.size.width * BPP, 4); - params[0] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->format.size.height), SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride)); - params[1] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); - params[2] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage), SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int( sizeof(struct spa_meta_region) * 16, sizeof(struct spa_meta_region) * 1, sizeof(struct spa_meta_region) * 16)); - params[3] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region))); #define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + \ sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP) - params[4] = spa_pod_builder_add_object(&b, + params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor), SPA_PARAM_META_size, SPA_POD_Int( CURSOR_META_SIZE(CURSOR_WIDTH,CURSOR_HEIGHT))); - pw_stream_update_params(stream, params, 5); + pw_stream_update_params(stream, params, n_params); } static void @@ -317,7 +322,7 @@ int main(int argc, char *argv[]) SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), - SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_RGB), + SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_BGRA), SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( &SPA_RECTANGLE(320, 240), &SPA_RECTANGLE(1, 1), diff --git a/src/gst/gstpipewireclock.c b/src/gst/gstpipewireclock.c index 701bb6ff0..10607c3e7 100644 --- a/src/gst/gstpipewireclock.c +++ b/src/gst/gstpipewireclock.c @@ -45,7 +45,7 @@ gst_pipewire_clock_get_internal_time (GstClock * clock) t.rate.denom == 0) return pclock->last_time; - result = gst_util_uint64_scale_int (t.ticks, GST_SECOND * t.rate.num, t.rate.denom); + result = gst_util_uint64_scale (t.ticks, GST_SECOND * t.rate.num, t.rate.denom); result += now - t.now; result += pclock->time_offset; diff --git a/src/gst/gstpipewiredeviceprovider.c b/src/gst/gstpipewiredeviceprovider.c index c9d0d7ad7..744cf2752 100644 --- a/src/gst/gstpipewiredeviceprovider.c +++ b/src/gst/gstpipewiredeviceprovider.c @@ -324,9 +324,10 @@ static void do_add_nodes(GstPipeWireDeviceProvider *self) if(self->list_only) { self->devices = g_list_insert_sorted (self->devices, - gst_object_ref_sink (device), + gst_object_ref (device), compare_device_session_priority); } else { + gst_object_ref (device); gst_device_provider_device_add (GST_DEVICE_PROVIDER (self), device); } } @@ -484,7 +485,8 @@ destroy_node (void *data) } if (nd->dev != NULL) { - gst_device_provider_device_remove (provider, GST_DEVICE (nd->dev)); + gst_device_provider_device_remove (provider, nd->dev); + gst_clear_object (&nd->dev); } if (nd->caps) gst_caps_unref(nd->caps); diff --git a/src/gst/gstpipewireformat.c b/src/gst/gstpipewireformat.c index 24115325e..fa97d3e01 100644 --- a/src/gst/gstpipewireformat.c +++ b/src/gst/gstpipewireformat.c @@ -43,6 +43,7 @@ static const struct media_type media_type_map[] = { { "image/jpeg", SPA_MEDIA_TYPE_video, SPA_MEDIA_SUBTYPE_mjpg }, { "video/x-jpeg", SPA_MEDIA_TYPE_video, SPA_MEDIA_SUBTYPE_mjpg }, { "video/x-h264", SPA_MEDIA_TYPE_video, SPA_MEDIA_SUBTYPE_h264 }, + { "video/x-h265", SPA_MEDIA_TYPE_video, SPA_MEDIA_SUBTYPE_h265 }, { "audio/x-mulaw", SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_raw }, { "audio/x-alaw", SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_raw }, { "audio/mpeg", SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_mp3 }, @@ -131,6 +132,58 @@ static const uint32_t video_format_map[] = { SPA_VIDEO_FORMAT_Y444_12LE, }; +static const uint32_t color_range_map[] = { + SPA_VIDEO_COLOR_RANGE_UNKNOWN, + SPA_VIDEO_COLOR_RANGE_0_255, + SPA_VIDEO_COLOR_RANGE_16_235, +}; + +static const uint32_t color_matrix_map[] = { + SPA_VIDEO_COLOR_MATRIX_UNKNOWN, + SPA_VIDEO_COLOR_MATRIX_RGB, + SPA_VIDEO_COLOR_MATRIX_FCC, + SPA_VIDEO_COLOR_MATRIX_BT709, + SPA_VIDEO_COLOR_MATRIX_BT601, + SPA_VIDEO_COLOR_MATRIX_SMPTE240M, + SPA_VIDEO_COLOR_MATRIX_BT2020, +}; + +static const uint32_t transfer_function_map[] = { + SPA_VIDEO_TRANSFER_UNKNOWN, + SPA_VIDEO_TRANSFER_GAMMA10, + SPA_VIDEO_TRANSFER_GAMMA18, + SPA_VIDEO_TRANSFER_GAMMA20, + SPA_VIDEO_TRANSFER_GAMMA22, + SPA_VIDEO_TRANSFER_BT709, + SPA_VIDEO_TRANSFER_SMPTE240M, + SPA_VIDEO_TRANSFER_SRGB, + SPA_VIDEO_TRANSFER_GAMMA28, + SPA_VIDEO_TRANSFER_LOG100, + SPA_VIDEO_TRANSFER_LOG316, + SPA_VIDEO_TRANSFER_BT2020_12, + SPA_VIDEO_TRANSFER_ADOBERGB, + SPA_VIDEO_TRANSFER_BT2020_10, + SPA_VIDEO_TRANSFER_SMPTE2084, + SPA_VIDEO_TRANSFER_ARIB_STD_B67, + SPA_VIDEO_TRANSFER_BT601, +}; + +static const uint32_t color_primaries_map[] = { + SPA_VIDEO_COLOR_PRIMARIES_UNKNOWN, + SPA_VIDEO_COLOR_PRIMARIES_BT709, + SPA_VIDEO_COLOR_PRIMARIES_BT470M, + SPA_VIDEO_COLOR_PRIMARIES_BT470BG, + SPA_VIDEO_COLOR_PRIMARIES_SMPTE170M, + SPA_VIDEO_COLOR_PRIMARIES_SMPTE240M, + SPA_VIDEO_COLOR_PRIMARIES_FILM, + SPA_VIDEO_COLOR_PRIMARIES_BT2020, + SPA_VIDEO_COLOR_PRIMARIES_ADOBERGB, + SPA_VIDEO_COLOR_PRIMARIES_SMPTEST428, + SPA_VIDEO_COLOR_PRIMARIES_SMPTERP431, + SPA_VIDEO_COLOR_PRIMARIES_SMPTEEG432, + SPA_VIDEO_COLOR_PRIMARIES_EBU3213, +}; + static const uint32_t interlace_mode_map[] = { SPA_VIDEO_INTERLACE_MODE_PROGRESSIVE, SPA_VIDEO_INTERLACE_MODE_INTERLEAVED, @@ -596,7 +649,7 @@ handle_video_fields (ConvertData *d) static void set_default_channels (struct spa_pod_builder *b, uint32_t channels) { - uint32_t position[SPA_AUDIO_MAX_CHANNELS] = {0}; + uint32_t position[8] = {0}; gboolean ok = TRUE; switch (channels) { @@ -828,6 +881,8 @@ static char *video_id_to_dma_drm_fourcc(uint32_t id, uint64_t mod) if ((idx = find_index(video_format_map, SPA_N_ELEMENTS(video_format_map), id)) == -1) return NULL; fourcc = gst_video_dma_drm_fourcc_from_format(idx); + if (fourcc == DRM_FORMAT_INVALID) + return NULL; return gst_video_dma_drm_fourcc_to_string(fourcc, mod); } #endif @@ -848,6 +903,46 @@ static const char *audio_id_to_string(uint32_t id) return gst_audio_format_to_string(idx); } +static GstVideoColorRange color_range_to_gst(uint32_t id) +{ + int idx; + if ((idx = find_index(color_range_map, SPA_N_ELEMENTS(color_range_map), id)) == -1) + return GST_VIDEO_COLOR_RANGE_UNKNOWN; + return idx; +} + +static GstVideoColorMatrix color_matrix_to_gst(uint32_t id) +{ + int idx; + if ((idx = find_index(color_matrix_map, SPA_N_ELEMENTS(color_matrix_map), id)) == -1) + return GST_VIDEO_COLOR_MATRIX_UNKNOWN; + return idx; +} + +static GstVideoTransferFunction transfer_function_to_gst(uint32_t id) +{ + int idx; + if ((idx = find_index(transfer_function_map, SPA_N_ELEMENTS(transfer_function_map), id)) == -1) + return GST_VIDEO_TRANSFER_UNKNOWN; + return idx; +} + +static GstVideoColorPrimaries color_primaries_to_gst(uint32_t id) +{ + int idx; + if ((idx = find_index(color_primaries_map, SPA_N_ELEMENTS(color_primaries_map), id)) == -1) + return GST_VIDEO_COLOR_PRIMARIES_UNKNOWN; + return idx; +} + +static void colorimetry_to_gst_colorimetry(struct spa_video_colorimetry *colorimetry, GstVideoColorimetry *gst_colorimetry) +{ + gst_colorimetry->range = color_range_to_gst(colorimetry->range); + gst_colorimetry->matrix = color_matrix_to_gst(colorimetry->matrix); + gst_colorimetry->transfer = transfer_function_to_gst(colorimetry->transfer); + gst_colorimetry->primaries = color_primaries_to_gst(colorimetry->primaries); +} + static void handle_id_prop (const struct spa_pod_prop *prop, const char *key, id_to_string_func func, GstCaps *res) { @@ -930,21 +1025,25 @@ handle_dmabuf_prop (const struct spa_pod_prop *prop, for (i = 0; i < n_fmts; i++) { for (j = 0; j < n_mods; j++) { + gboolean as_drm = FALSE; const char *fmt_str; - if ((mods[j] == DRM_FORMAT_MOD_LINEAR || - mods[j] == DRM_FORMAT_MOD_INVALID) && - (fmt_str = video_id_to_string(id[i]))) - g_ptr_array_add(fmt_array, g_strdup_printf ("%s", fmt_str)); - #ifdef HAVE_GSTREAMER_DMA_DRM if (mods[j] != DRM_FORMAT_MOD_INVALID) { char *drm_str; - if ((drm_str = video_id_to_dma_drm_fourcc(id[i], mods[j]))) + if ((drm_str = video_id_to_dma_drm_fourcc(id[i], mods[j]))) { g_ptr_array_add(drm_fmt_array, drm_str); + as_drm = TRUE; + } } #endif + + if (!as_drm && + (mods[j] == DRM_FORMAT_MOD_LINEAR || + mods[j] == DRM_FORMAT_MOD_INVALID) && + (fmt_str = video_id_to_string(id[i]))) + g_ptr_array_add(fmt_array, g_strdup_printf ("%s", fmt_str)); } } @@ -1065,8 +1164,19 @@ handle_rect_prop (const struct spa_pod_prop *prop, const char *width, const char { if (n_items < 3) return; - gst_caps_set_simple (res, width, GST_TYPE_INT_RANGE, rect[1].width, rect[2].width, - height, GST_TYPE_INT_RANGE, rect[1].height, rect[2].height, NULL); + + if (rect[1].width == rect[2].width && + rect[1].height == rect[2].height) { + gst_caps_set_simple (res, + width, G_TYPE_INT, rect[1].width, + height, G_TYPE_INT, rect[1].height, + NULL); + } else { + gst_caps_set_simple (res, + width, GST_TYPE_INT_RANGE, rect[1].width, rect[2].width, + height, GST_TYPE_INT_RANGE, rect[1].height, rect[2].height, + NULL); + } break; } case SPA_CHOICE_Enum: @@ -1117,8 +1227,17 @@ handle_fraction_prop (const struct spa_pod_prop *prop, const char *key, GstCaps { if (n_items < 3) return; - gst_caps_set_simple (res, key, GST_TYPE_FRACTION_RANGE, fract[1].num, fract[1].denom, - fract[2].num, fract[2].denom, NULL); + + if (fract[1].num == fract[2].num && + fract[1].denom == fract[2].denom) { + gst_caps_set_simple (res, key, GST_TYPE_FRACTION, + fract[1].num, fract[1].denom, NULL); + } else { + gst_caps_set_simple (res, key, GST_TYPE_FRACTION_RANGE, + fract[1].num, fract[1].denom, + fract[2].num, fract[2].denom, + NULL); + } break; } case SPA_CHOICE_Enum: @@ -1144,6 +1263,7 @@ gst_caps_from_format (const struct spa_pod *format) { GstCaps *res = NULL; uint32_t media_type, media_subtype; + struct spa_video_colorimetry colorimetry = { 0 }; const struct spa_pod_prop *prop = NULL; const struct spa_pod_object *obj = (const struct spa_pod_object *) format; @@ -1178,8 +1298,14 @@ gst_caps_from_format (const struct spa_pod *format) "stream-format", G_TYPE_STRING, "byte-stream", "alignment", G_TYPE_STRING, "au", NULL); + } + else if (media_subtype == SPA_MEDIA_SUBTYPE_h265) { + res = gst_caps_new_simple ("video/x-h265", + "stream-format", G_TYPE_STRING, "byte-stream", + "alignment", G_TYPE_STRING, "au", + NULL); } else { - return NULL; + return NULL; } if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_size))) { handle_rect_prop (prop, "width", "height", res); @@ -1190,6 +1316,20 @@ gst_caps_from_format (const struct spa_pod *format) if ((prop = spa_pod_object_find_prop (obj, prop, SPA_FORMAT_VIDEO_maxFramerate))) { handle_fraction_prop (prop, "max-framerate", res); } + if (spa_pod_parse_object(format, + SPA_TYPE_OBJECT_Format, NULL, + SPA_FORMAT_VIDEO_colorRange, SPA_POD_OPT_Id(&colorimetry.range), + SPA_FORMAT_VIDEO_colorMatrix, SPA_POD_OPT_Id(&colorimetry.matrix), + SPA_FORMAT_VIDEO_transferFunction, SPA_POD_OPT_Id(&colorimetry.transfer), + SPA_FORMAT_VIDEO_colorPrimaries, SPA_POD_OPT_Id(&colorimetry.primaries)) > 0) { + GstVideoColorimetry gst_colorimetry; + char *color; + colorimetry_to_gst_colorimetry(&colorimetry, &gst_colorimetry); + color = gst_video_colorimetry_to_string(&gst_colorimetry); + gst_caps_set_simple(res, "colorimetry", G_TYPE_STRING, color, NULL); + g_free(color); + + } } else if (media_type == SPA_MEDIA_TYPE_audio) { if (media_subtype == SPA_MEDIA_SUBTYPE_raw) { res = gst_caps_new_simple ("audio/x-raw", diff --git a/src/gst/gstpipewirepool.c b/src/gst/gstpipewirepool.c index 78869e163..5581c64ac 100644 --- a/src/gst/gstpipewirepool.c +++ b/src/gst/gstpipewirepool.c @@ -10,8 +10,12 @@ #include #include +#ifdef HAVE_GSTREAMER_SHM_ALLOCATOR +#include +#endif #include +#include #include "gstpipewirepool.h" @@ -53,7 +57,7 @@ pool_data_destroy (gpointer user_data) GstPipeWirePoolData *data = user_data; gst_object_unref (data->pool); - g_slice_free (GstPipeWirePoolData, data); + g_free (data); } void gst_pipewire_pool_wrap_buffer (GstPipeWirePool *pool, struct pw_buffer *b) @@ -61,13 +65,42 @@ void gst_pipewire_pool_wrap_buffer (GstPipeWirePool *pool, struct pw_buffer *b) GstBuffer *buf; uint32_t i; GstPipeWirePoolData *data; + /* Default to a large enough value */ + gsize plane_0_size = pool->has_rawvideo ? + pool->video_info.size : + (gsize) pool->video_info.width * pool->video_info.height; + gsize plane_sizes[GST_VIDEO_MAX_PLANES] = { plane_0_size, }; - GST_DEBUG_OBJECT (pool, "wrap buffer"); + GST_DEBUG_OBJECT (pool, "wrap buffer, datas:%d", b->buffer->n_datas); - data = g_slice_new (GstPipeWirePoolData); + data = g_new0 (GstPipeWirePoolData, 1); buf = gst_buffer_new (); + if (pool->add_metavideo) { + GstVideoMeta *meta = gst_buffer_add_video_meta_full (buf, + GST_VIDEO_FRAME_FLAG_NONE, + GST_VIDEO_INFO_FORMAT (&pool->video_info), + GST_VIDEO_INFO_WIDTH (&pool->video_info), + GST_VIDEO_INFO_HEIGHT (&pool->video_info), + GST_VIDEO_INFO_N_PLANES (&pool->video_info), + pool->video_info.offset, + pool->video_info.stride); + + gst_video_meta_set_alignment (meta, pool->video_align); + + if (!gst_video_meta_get_plane_size (meta, plane_sizes)) { + GST_ERROR_OBJECT (pool, "could not compute plane sizes"); + } + + /* + * We need to set the video meta as pooled, else gst_buffer_pool_release_buffer + * will call reset_buffer and the default_reset_buffer implementation for + * GstBufferPool removes all metadata without the POOLED flag. + */ + GST_META_FLAG_SET (meta, GST_META_FLAG_POOLED); + } + for (i = 0; i < b->buffer->n_datas; i++) { struct spa_data *d = &b->buffer->datas[i]; GstMemory *gmem = NULL; @@ -75,32 +108,98 @@ void gst_pipewire_pool_wrap_buffer (GstPipeWirePool *pool, struct pw_buffer *b) GST_DEBUG_OBJECT (pool, "wrap data (%s %d) %d %d", spa_debug_type_find_short_name(spa_type_data_type, d->type), d->type, d->mapoffset, d->maxsize); - if (d->type == SPA_DATA_MemFd) { + + if (pool->allocate_memory) { +#ifdef HAVE_GSTREAMER_SHM_ALLOCATOR + gsize block_size = d->maxsize; + + if (pool->has_video) { + /* For video, we know block sizes from the video info already */ + block_size = plane_sizes[i]; + } else { + /* For audio, reserve space based on the quantum limit and channel count */ + g_autoptr (GstPipeWireStream) s = g_weak_ref_get (&pool->stream); + + struct pw_context *context = pw_core_get_context(pw_stream_get_core(s->pwstream)); + const struct pw_properties *props = pw_context_get_properties(context); + uint32_t quantum_limit = 8192; /* "reasonable" default */ + + const char *quantum = spa_dict_lookup(&props->dict, "clock.quantum-limit"); + if (!quantum) { + quantum = spa_dict_lookup(&props->dict, "default.clock.quantum-limit"); + GST_DEBUG_OBJECT (pool, "using default quantum limit %s", quantum); + } + + if (quantum) + spa_atou32(quantum, &quantum_limit, 0); + GST_DEBUG_OBJECT (pool, "quantum limit %s", quantum); + + block_size = quantum_limit * pool->audio_info.bpf; + } + + GST_DEBUG_OBJECT (pool, "setting block size %zu", block_size); + + if (!pool->shm_allocator) + pool->shm_allocator = gst_shm_allocator_get(); + + /* use MemFd only. That is the only supported data type when memory is remote i.e. allocated by the client */ + gmem = gst_allocator_alloc (pool->shm_allocator, block_size, NULL); + d->fd = gst_fd_memory_get_fd (gmem); + d->mapoffset = 0; + d->flags = SPA_DATA_FLAG_READWRITE | SPA_DATA_FLAG_MAPPABLE; + + d->type = SPA_DATA_MemFd; + d->maxsize = block_size; + d->data = NULL; +#endif + } else if (d->type == SPA_DATA_MemFd) { gmem = gst_fd_allocator_alloc (pool->fd_allocator, dup(d->fd), d->mapoffset + d->maxsize, GST_FD_MEMORY_FLAG_NONE); gst_memory_resize (gmem, d->mapoffset, d->maxsize); } else if(d->type == SPA_DATA_DmaBuf) { + GstMapInfo info = { 0 }; + GstFdMemoryFlags fd_flags = GST_FD_MEMORY_FLAG_NONE; + + if (d->flags & SPA_DATA_FLAG_MAPPABLE && d->flags & SPA_DATA_FLAG_READABLE) + fd_flags |= GST_FD_MEMORY_FLAG_KEEP_MAPPED; + gmem = gst_fd_allocator_alloc (pool->dmabuf_allocator, dup(d->fd), - d->mapoffset + d->maxsize, GST_FD_MEMORY_FLAG_NONE); + d->mapoffset + d->maxsize, fd_flags); gst_memory_resize (gmem, d->mapoffset, d->maxsize); + + if (fd_flags & GST_FD_MEMORY_FLAG_KEEP_MAPPED) { + GstMapFlags map_flags = GST_MAP_READ; + + if (d->flags & SPA_DATA_FLAG_WRITABLE) + map_flags |= GST_MAP_WRITE; + + if (gst_memory_map (gmem, &info, map_flags)) { + gst_memory_unmap (gmem, &info); + } else { + GST_ERROR_OBJECT (pool, "mmaping buffer failed"); + } + } } else if (d->type == SPA_DATA_MemPtr) { gmem = gst_memory_new_wrapped (0, d->data, d->maxsize, 0, d->maxsize, NULL, NULL); } + else { + GST_WARNING_OBJECT (pool, "unknown data type (%s %d)", + spa_debug_type_find_short_name(spa_type_data_type, d->type), d->type); + } if (gmem) gst_buffer_insert_memory (buf, i, gmem); } - if (pool->add_metavideo) { - gst_buffer_add_video_meta_full (buf, GST_VIDEO_FRAME_FLAG_NONE, - GST_VIDEO_INFO_FORMAT (&pool->video_info), - GST_VIDEO_INFO_WIDTH (&pool->video_info), - GST_VIDEO_INFO_HEIGHT (&pool->video_info), - GST_VIDEO_INFO_N_PLANES (&pool->video_info), - pool->video_info.offset, - pool->video_info.stride); + if (pool->add_metavideo && !pool->allocate_memory) { + /* Set memory sizes to expected plane sizes, so we know the valid size, + * and the offsets in the meta make sense */ + for (i = 0; i < gst_buffer_n_memory (buf); i++) { + GstMemory *mem = gst_buffer_peek_memory (buf, i); + gst_memory_resize (mem, 0, plane_sizes[i]); + } } data->pool = gst_object_ref (pool); @@ -114,6 +213,7 @@ void gst_pipewire_pool_wrap_buffer (GstPipeWirePool *pool, struct pw_buffer *b) gst_buffer_add_video_crop_meta(buf); data->videotransform = spa_buffer_find_meta_data (b->buffer, SPA_META_VideoTransform, sizeof(*data->videotransform)); + data->cursor = spa_buffer_find_meta_data (b->buffer, SPA_META_Cursor, sizeof(*data->cursor)); gst_mini_object_set_qdata (GST_MINI_OBJECT_CAST (buf), pool_data_quark, @@ -133,7 +233,8 @@ void gst_pipewire_pool_remove_buffer (GstPipeWirePool *pool, struct pw_buffer *b data->crop = NULL; data->videotransform = NULL; - gst_buffer_remove_all_memory (data->buf); + if (!pool->allocate_memory) + gst_buffer_remove_all_memory (data->buf); /* this will also destroy the pool data, if this is the last reference */ gst_clear_buffer (&data->buf); @@ -212,7 +313,13 @@ no_more_buffers: static const gchar ** get_options (GstBufferPool * pool) { - static const gchar *options[] = { GST_BUFFER_POOL_OPTION_VIDEO_META, NULL }; + static const gchar *options[] = { + GST_BUFFER_POOL_OPTION_VIDEO_META, +#ifdef HAVE_GSTREAMER_SHM_ALLOCATOR + GST_BUFFER_POOL_OPTION_VIDEO_ALIGNMENT, +#endif + NULL + }; return options; } @@ -223,7 +330,7 @@ set_config (GstBufferPool * pool, GstStructure * config) GstCaps *caps; GstStructure *structure; guint size, min_buffers, max_buffers; - gboolean has_video; + gboolean has_videoalign; if (!gst_buffer_pool_config_get_params (config, &caps, &size, &min_buffers, &max_buffers)) { GST_WARNING_OBJECT (pool, "invalid config"); @@ -235,18 +342,63 @@ set_config (GstBufferPool * pool, GstStructure * config) return FALSE; } + /* We don't support unlimited buffers */ + if (max_buffers == 0) + max_buffers = PIPEWIRE_POOL_MAX_BUFFERS; + /* Pick a sensible min to avoid starvation */ + if (min_buffers == 0) + min_buffers = PIPEWIRE_POOL_MIN_BUFFERS; + + if (min_buffers < PIPEWIRE_POOL_MIN_BUFFERS || max_buffers > PIPEWIRE_POOL_MAX_BUFFERS) + return FALSE; + structure = gst_caps_get_structure (caps, 0); if (g_str_has_prefix (gst_structure_get_name (structure), "video/") || g_str_has_prefix (gst_structure_get_name (structure), "image/")) { - has_video = TRUE; + p->has_video = TRUE; + gst_video_info_from_caps (&p->video_info, caps); + + if (GST_VIDEO_FORMAT_INFO_IS_VALID_RAW (p->video_info.finfo) +#ifdef HAVE_GSTREAMER_DMA_DRM + && GST_VIDEO_FORMAT_INFO_FORMAT (p->video_info.finfo) != GST_VIDEO_FORMAT_DMA_DRM +#endif + ) + p->has_rawvideo = TRUE; + else + p->has_rawvideo = FALSE; + +#ifdef HAVE_GSTREAMER_SHM_ALLOCATOR + if (p->has_rawvideo) { + gst_video_alignment_reset (&p->video_align); + gst_video_info_align (&p->video_info, &p->video_align); + } +#endif + } else if (g_str_has_prefix(gst_structure_get_name(structure), "audio/")) { + p->has_video = FALSE; + gst_audio_info_from_caps(&p->audio_info, caps); } else { - has_video = FALSE; + g_assert_not_reached (); } - p->add_metavideo = has_video && gst_buffer_pool_config_has_option (config, + p->add_metavideo = p->has_rawvideo && gst_buffer_pool_config_has_option (config, GST_BUFFER_POOL_OPTION_VIDEO_META); +#ifdef HAVE_GSTREAMER_SHM_ALLOCATOR + has_videoalign = p->has_rawvideo && gst_buffer_pool_config_has_option (config, + GST_BUFFER_POOL_OPTION_VIDEO_ALIGNMENT); + + if (has_videoalign) { + gst_buffer_pool_config_get_video_alignment (config, &p->video_align); + gst_video_info_align (&p->video_info, &p->video_align); + gst_buffer_pool_config_set_video_alignment (config, &p->video_align); + + GST_LOG_OBJECT (pool, "Set alignment: %u-%ux%u-%u", + p->video_align.padding_left, p->video_align.padding_right, + p->video_align.padding_top, p->video_align.padding_bottom); + } +#endif + if (p->video_info.size != 0) size = p->video_info.size; @@ -321,6 +473,10 @@ gst_pipewire_pool_finalize (GObject * object) g_weak_ref_set (&pool->stream, NULL); g_object_unref (pool->fd_allocator); g_object_unref (pool->dmabuf_allocator); +#ifdef HAVE_GSTREAMER_SHM_ALLOCATOR + if (pool->shm_allocator) + g_object_unref (pool->shm_allocator); +#endif G_OBJECT_CLASS (gst_pipewire_pool_parent_class)->finalize (object); } @@ -355,5 +511,8 @@ gst_pipewire_pool_init (GstPipeWirePool * pool) { pool->fd_allocator = gst_fd_allocator_new (); pool->dmabuf_allocator = gst_dmabuf_allocator_new (); +#ifdef HAVE_GSTREAMER_SHM_ALLOCATOR + gst_shm_allocator_init_once(); +#endif g_cond_init (&pool->cond); } diff --git a/src/gst/gstpipewirepool.h b/src/gst/gstpipewirepool.h index fb00a100c..4953b689c 100644 --- a/src/gst/gstpipewirepool.h +++ b/src/gst/gstpipewirepool.h @@ -9,6 +9,7 @@ #include +#include #include #include @@ -18,6 +19,15 @@ G_BEGIN_DECLS #define GST_TYPE_PIPEWIRE_POOL (gst_pipewire_pool_get_type()) G_DECLARE_FINAL_TYPE (GstPipeWirePool, gst_pipewire_pool, GST, PIPEWIRE_POOL, GstBufferPool) +#define PIPEWIRE_POOL_MIN_BUFFERS 2u +#define PIPEWIRE_POOL_MAX_BUFFERS 16u + +/* Only available in GStreamer 1.22+ */ +#ifndef GST_VIDEO_FORMAT_INFO_IS_VALID_RAW +#define GST_VIDEO_FORMAT_INFO_IS_VALID_RAW(info) \ + (info != NULL && (info)->format > GST_VIDEO_FORMAT_ENCODED) +#endif + typedef struct _GstPipeWirePoolData GstPipeWirePoolData; struct _GstPipeWirePoolData { GstPipeWirePool *pool; @@ -29,6 +39,7 @@ struct _GstPipeWirePoolData { gboolean queued; struct spa_meta_region *crop; struct spa_meta_videotransform *videotransform; + struct spa_meta_cursor *cursor; }; struct _GstPipeWirePool { @@ -37,14 +48,20 @@ struct _GstPipeWirePool { GWeakRef stream; guint n_buffers; + gboolean has_video; + gboolean has_rawvideo; gboolean add_metavideo; + GstAudioInfo audio_info; GstVideoInfo video_info; + GstVideoAlignment video_align; GstAllocator *fd_allocator; GstAllocator *dmabuf_allocator; + GstAllocator *shm_allocator; GCond cond; gboolean paused; + gboolean allocate_memory; }; enum GstPipeWirePoolMode { diff --git a/src/gst/gstpipewiresink.c b/src/gst/gstpipewiresink.c index aa9d94b99..58a6cd8db 100644 --- a/src/gst/gstpipewiresink.c +++ b/src/gst/gstpipewiresink.c @@ -40,7 +40,8 @@ GST_DEBUG_CATEGORY_STATIC (pipewire_sink_debug); #define DEFAULT_PROP_SLAVE_METHOD GST_PIPEWIRE_SINK_SLAVE_METHOD_NONE #define DEFAULT_PROP_USE_BUFFERPOOL USE_BUFFERPOOL_AUTO -#define MIN_BUFFERS 8u +#define MAX_ERROR_MS 1 +#define RESYNC_TIMEOUT_MS 10 enum { @@ -167,7 +168,8 @@ gst_pipewire_sink_propose_allocation (GstBaseSink * bsink, GstQuery * query) GstPipeWireSink *pwsink = GST_PIPEWIRE_SINK (bsink); if (pwsink->use_bufferpool != USE_BUFFERPOOL_NO) - gst_query_add_allocation_pool (query, GST_BUFFER_POOL_CAST (pwsink->stream->pool), 0, 0, 0); + gst_query_add_allocation_pool (query, GST_BUFFER_POOL_CAST (pwsink->stream->pool), 0, + PIPEWIRE_POOL_MIN_BUFFERS, PIPEWIRE_POOL_MAX_BUFFERS); gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, NULL); return TRUE; @@ -242,7 +244,8 @@ gst_pipewire_sink_class_init (GstPipeWireSinkClass * klass) GST_TYPE_PIPEWIRE_SINK_MODE, DEFAULT_PROP_MODE, G_PARAM_READWRITE | - G_PARAM_STATIC_STRINGS)); + G_PARAM_STATIC_STRINGS | + GST_PARAM_MUTABLE_READY)); g_object_class_install_property (gobject_class, PROP_FD, @@ -305,41 +308,63 @@ gst_pipewire_sink_update_params (GstPipeWireSink *sink) struct spa_pod_builder b = { NULL }; uint8_t buffer[1024]; struct spa_pod_frame f; + guint n_params = 0; config = gst_buffer_pool_get_config (GST_BUFFER_POOL (pool)); gst_buffer_pool_config_get_params (config, &caps, &size, &min_buffers, &max_buffers); + /* We cannot dynamically grow the pool */ + if (max_buffers == 0) { + GST_WARNING_OBJECT (sink, "cannot support unlimited buffers in pool"); + max_buffers = PIPEWIRE_POOL_MAX_BUFFERS; + } + spa_pod_builder_init (&b, buffer, sizeof (buffer)); spa_pod_builder_push_object (&b, &f, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); spa_pod_builder_add (&b, SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(size, size, INT32_MAX), 0); + if (sink->is_rawvideo) { + /* MUST have n_datas == n_planes */ + spa_pod_builder_add (&b, + SPA_PARAM_BUFFERS_blocks, + SPA_POD_Int(GST_VIDEO_INFO_N_PLANES (&pool->video_info)), + 0); + } else { + /* Non-planar data, get a single block */ + spa_pod_builder_add (&b, + SPA_PARAM_BUFFERS_blocks, + SPA_POD_Int(1), + 0); + } spa_pod_builder_add (&b, SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int(0, 0, INT32_MAX), + /* At this stage, we will request as many buffers as we _might_ need as + * the default, since we can't grow the pool once this is set */ SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int( - SPA_MAX(MIN_BUFFERS, min_buffers), - SPA_MAX(MIN_BUFFERS, min_buffers), - max_buffers ? max_buffers : INT32_MAX), - SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int( - (1<is_rawvideo) { + port_params[n_params++] = spa_pod_builder_add_object (&b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop), + SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_region))); + } pw_thread_loop_lock (sink->stream->core->loop); - pw_stream_update_params (sink->stream->pwstream, port_params, 3); + pw_stream_update_params (sink->stream->pwstream, port_params, n_params); pw_thread_loop_unlock (sink->stream->core->loop); + + gst_structure_free (config); } static void @@ -356,7 +381,8 @@ gst_pipewire_sink_init (GstPipeWireSink * sink) sink->mode = DEFAULT_PROP_MODE; sink->use_bufferpool = DEFAULT_PROP_USE_BUFFERPOOL; - sink->is_video = false; + sink->is_rawvideo = false; + sink->first_buffer = true; GST_OBJECT_FLAG_SET (sink, GST_ELEMENT_FLAG_PROVIDE_CLOCK); @@ -374,7 +400,7 @@ gst_pipewire_sink_sink_fixate (GstBaseSink * bsink, GstCaps * caps) structure = gst_caps_get_structure (caps, 0); if (gst_structure_has_name (structure, "video/x-raw")) { - pwsink->is_video = true; + pwsink->is_rawvideo = true; gst_structure_fixate_field_nearest_int (structure, "width", 320); gst_structure_fixate_field_nearest_int (structure, "height", 240); gst_structure_fixate_field_nearest_fraction (structure, "framerate", 30, 1); @@ -588,14 +614,17 @@ static void on_remove_buffer (void *_data, struct pw_buffer *b) { GstPipeWireSink *pwsink = _data; + GST_DEBUG_OBJECT (pwsink, "remove pw_buffer %p", b); gst_pipewire_pool_remove_buffer (pwsink->stream->pool, b); if (!gst_pipewire_pool_has_buffers (pwsink->stream->pool) && !GST_BUFFER_POOL_IS_FLUSHING (GST_BUFFER_POOL_CAST (pwsink->stream->pool))) { - GST_ELEMENT_ERROR (pwsink, RESOURCE, NOT_FOUND, - ("all buffers have been removed"), - ("PipeWire link to remote node was destroyed")); + if (pwsink->mode != GST_PIPEWIRE_SINK_MODE_PROVIDE) { + GST_ELEMENT_ERROR (pwsink, RESOURCE, NOT_FOUND, + ("all buffers have been removed"), + ("PipeWire link to remote node was destroyed")); + } } } @@ -630,6 +659,9 @@ do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer) } } data->b->size = 0; + + spa_assert(b->n_datas == gst_buffer_n_memory(buffer)); + for (i = 0; i < b->n_datas; i++) { struct spa_data *d = &b->datas[i]; GstMemory *mem = gst_buffer_peek_memory (buffer, i); @@ -643,16 +675,20 @@ do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer) GstVideoMeta *meta = gst_buffer_get_video_meta (buffer); if (meta) { if (meta->n_planes == b->n_datas) { + uint32_t n_planes = GST_VIDEO_INFO_N_PLANES (&data->pool->video_info); gsize video_size = 0; - for (i = 0; i < meta->n_planes; i++) { + + for (i = 0; i < n_planes; i++) { struct spa_data *d = &b->datas[i]; - d->chunk->offset += meta->offset[i] - video_size; + d->chunk->stride = meta->stride[i]; + d->chunk->offset = meta->offset[i] - video_size; video_size += d->chunk->size; } } else { - GST_ERROR_OBJECT (pwsink, "plane num not matching, meta:%u buffer:%u", meta->n_planes, b->n_datas); + GST_ERROR_OBJECT (pwsink, "plane num not matching, meta:%u buffer:%u", + meta->n_planes, b->n_datas); } } @@ -660,7 +696,18 @@ do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer) GST_WARNING_OBJECT (pwsink, "can't send buffer %s", spa_strerror(res)); } else { data->queued = TRUE; - GST_LOG_OBJECT(pwsink, "queued pwbuffer: %p; gstbuffer %p ",data->b, buffer); + GST_LOG_OBJECT(pwsink, "queued pwbuffer: %p size: %"PRIu64"; gstbuffer %p", + data->b, data->b->size, buffer); + if (pwsink->first_buffer) { + pwsink->first_buffer = false; + pwsink->first_buffer_pts = GST_BUFFER_PTS(buffer); + } + stream->position = gst_util_uint64_scale_int(GST_BUFFER_PTS(buffer) - pwsink->first_buffer_pts, + pwsink->rate, 1 * GST_SECOND); + + // have the buffer duration value minimum as 1, in case of video where rate is 0 (not applicable) + stream->buf_duration = SPA_MAX((uint64_t)1, gst_util_uint64_scale_int(GST_BUFFER_DURATION(buffer), + pwsink->rate, 1 * GST_SECOND)); } switch (pwsink->slave_method) { @@ -672,6 +719,56 @@ do_send_buffer (GstPipeWireSink *pwsink, GstBuffer *buffer) } } +static void update_time (GstPipeWireSink *pwsink) +{ + struct spa_io_position *p = pwsink->stream->io_position; + double err = 0.0, corr = 1.0; + guint64 now; + double max_err = pwsink->rate * MAX_ERROR_MS/1000.0; + double resync_timeout = pwsink->rate * RESYNC_TIMEOUT_MS/1000.0; + + if (pwsink->first_buffer) { + // use the target duration before the first buffer + pwsink->stream->buf_duration = p->clock.target_duration; + spa_dll_set_bw(&pwsink->stream->dll, SPA_DLL_BW_MIN, pwsink->stream->buf_duration, + pwsink->rate); + } + + now = pw_stream_get_nsec(pwsink->stream->pwstream); + err = (double)gst_util_uint64_scale(now, pwsink->rate, 1 * GST_SECOND) - + (double)gst_util_uint64_scale(p->clock.next_nsec, pwsink->rate, 1 * GST_SECOND); + + GST_LOG_OBJECT(pwsink, "err is %f max err is %f now %"PRIu64" next is %"PRIu64"", err, max_err, now, + p->clock.next_nsec); + + if (fabs(err) > max_err) { + if (fabs(err) > resync_timeout) { + GST_WARNING_OBJECT(pwsink, "err %f exceeds resync timeout, resetting", err); + spa_dll_set_bw(&pwsink->stream->dll, SPA_DLL_BW_MIN, pwsink->stream->buf_duration, + pwsink->rate); + err = 0.0; + } else { + err = SPA_CLAMPD(err, -max_err, max_err); + } + } + corr = spa_dll_update(&pwsink->stream->dll, err); + + p->clock.nsec = now; + p->clock.position = pwsink->stream->position; + p->clock.duration = pwsink->stream->buf_duration; + /* we don't have a way to estimate the target (next cycle) buffer duration + * so use the current buffer duration + */ + p->clock.target_duration = pwsink->stream->buf_duration; + p->clock.rate = SPA_FRACTION(1, pwsink->rate); + // current time plus duration scaled with correlation + p->clock.next_nsec = now + (uint64_t)(p->clock.duration / corr * GST_SECOND / pwsink->rate); + p->clock.rate_diff = corr; + + GST_DEBUG_OBJECT(pwsink, "now %"PRIu64", position %"PRIu64", duration %"PRIu64", rate :%d," + "next : %"PRIu64", delay is %"PRIi64", rate_diff is %f", p->clock.nsec, p->clock.position, + p->clock.duration, pwsink->rate, p->clock.next_nsec, p->clock.delay,p->clock.rate_diff); +} static void on_process (void *data) @@ -681,6 +778,23 @@ on_process (void *data) g_cond_signal (&pwsink->stream->pool->cond); } +static int invoke_trigger_process(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + + GstPipeWireSink *pwsink = user_data; + + /* Note: We cannot use the rate for computation of other clock params + * in case of video because the rate for video is set as 0 in the _setcaps. + * So skip update time for video (i.e. when rate is 0). The video buffers + * get timestamp from the SPA_META_Header anyway + */ + + if (pwsink->rate) + update_time(pwsink); + return pw_stream_trigger_process(pwsink->stream->pwstream); +} + static void on_state_changed (void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error) { @@ -696,7 +810,8 @@ on_state_changed (void *data, enum pw_stream_state old, enum pw_stream_state sta break; case PW_STREAM_STATE_STREAMING: if (pw_stream_is_driving (pwsink->stream->pwstream)) - pw_stream_trigger_process (pwsink->stream->pwstream); + pw_loop_invoke(pw_stream_get_data_loop(pwsink->stream->pwstream), + invoke_trigger_process, 1, NULL, 0 , false, pwsink); break; case PW_STREAM_STATE_ERROR: /* make the error permanent, if it is not already; @@ -756,9 +871,21 @@ gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps) if (pwsink->use_bufferpool != USE_BUFFERPOOL_YES) pwsink->use_bufferpool = USE_BUFFERPOOL_NO; } else { + GstVideoInfo video_info; + pwsink->rate = rate = 0; pwsink->rate_match = false; - pwsink->is_video = true; + + gst_video_info_from_caps (&video_info, caps); + + if (GST_VIDEO_FORMAT_INFO_IS_VALID_RAW (video_info.finfo) +#ifdef HAVE_GSTREAMER_DMA_DRM + && GST_VIDEO_FORMAT_INFO_FORMAT (video_info.finfo) != GST_VIDEO_FORMAT_DMA_DRM +#endif + ) + pwsink->is_rawvideo = TRUE; + else + pwsink->is_rawvideo = FALSE; } spa_dll_set_bw(&pwsink->stream->dll, SPA_DLL_BW_MIN, 4096, rate); @@ -785,6 +912,11 @@ gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps) else flags |= PW_STREAM_FLAG_DRIVER; +#ifdef HAVE_GSTREAMER_SHM_ALLOCATOR + flags |= PW_STREAM_FLAG_ALLOC_BUFFERS; + pwsink->stream->pool->allocate_memory = true; +#endif + target_id = pwsink->stream->path ? (uint32_t)atoi(pwsink->stream->path) : PW_ID_ANY; if (pwsink->stream->target_object) { @@ -838,8 +970,12 @@ gst_pipewire_sink_setcaps (GstBaseSink * bsink, GstCaps * caps) config = gst_buffer_pool_get_config (GST_BUFFER_POOL_CAST (pwsink->stream->pool)); gst_buffer_pool_config_get_params (config, NULL, &size, &min_buffers, &max_buffers); gst_buffer_pool_config_set_params (config, caps, size, min_buffers, max_buffers); - if(pwsink->is_video) - gst_buffer_pool_config_add_option(config, GST_BUFFER_POOL_OPTION_VIDEO_META); + if (pwsink->is_rawvideo) { + gst_buffer_pool_config_add_option (config, GST_BUFFER_POOL_OPTION_VIDEO_META); +#ifdef HAVE_GSTREAMER_SHM_ALLOCATOR + gst_buffer_pool_config_add_option (config, GST_BUFFER_POOL_OPTION_VIDEO_ALIGNMENT); +#endif + } gst_buffer_pool_set_config (GST_BUFFER_POOL_CAST (pwsink->stream->pool), config); pw_thread_loop_unlock (pwsink->stream->core->loop); @@ -921,20 +1057,17 @@ gst_pipewire_sink_render (GstBaseSink * bsink, GstBuffer * buffer) if (res != GST_FLOW_OK) goto done; - if (pwsink->is_video) { + if (pwsink->is_rawvideo) { GstVideoFrame src, dst; gboolean copied = FALSE; buf_size = 0; // to break from the loop - /* - splitting of buffers in the case of video might break the frame layout - and that seems to be causing issues while retrieving the buffers on the receiver - side. Hence use the video_frame_map to copy the buffer of bigger size into the - pipewirepool's buffer - */ + /* splitting of buffers in the case of video might break the frame layout + * and that seems to be causing issues while retrieving the buffers on the receiver + * side. Hence use the video_frame_map to copy the buffer of bigger size into the + * pipewirepool's buffer */ - if (!gst_video_frame_map (&dst, &pwsink->stream->pool->video_info, b, - GST_MAP_WRITE)) { + if (!gst_video_frame_map (&dst, &pwsink->stream->pool->video_info, b, GST_MAP_WRITE)) { GST_ERROR_OBJECT(pwsink, "Failed to map dest buffer"); return GST_FLOW_ERROR; } @@ -954,8 +1087,6 @@ gst_pipewire_sink_render (GstBaseSink * bsink, GstBuffer * buffer) GST_ERROR_OBJECT(pwsink, "Failed to copy the frame"); return GST_FLOW_ERROR; } - - gst_buffer_copy_into(b, buffer, GST_BUFFER_COPY_METADATA, 0, -1); } else { gst_buffer_map (b, &info, GST_MAP_WRITE); gsize extract_size = (buf_size <= info.maxsize) ? buf_size: info.maxsize; @@ -977,7 +1108,8 @@ gst_pipewire_sink_render (GstBaseSink * bsink, GstBuffer * buffer) gst_buffer_unref (b); if (pw_stream_is_driving (pwsink->stream->pwstream)) - pw_stream_trigger_process (pwsink->stream->pwstream); + pw_loop_invoke(pw_stream_get_data_loop(pwsink->stream->pwstream), + invoke_trigger_process, 1, NULL, 0 , false, pwsink); } } else { GST_TRACE_OBJECT(pwsink, "Buffer is from pipewirepool"); @@ -985,7 +1117,8 @@ gst_pipewire_sink_render (GstBaseSink * bsink, GstBuffer * buffer) do_send_buffer (pwsink, buffer); if (pw_stream_is_driving (pwsink->stream->pwstream)) - pw_stream_trigger_process (pwsink->stream->pwstream); + pw_loop_invoke(pw_stream_get_data_loop(pwsink->stream->pwstream), + invoke_trigger_process, 1, NULL, 0 , false, pwsink); } done_unlock: @@ -999,6 +1132,18 @@ not_negotiated: } } +static void +on_io_changed(void *data, uint32_t id, void *area, uint32_t size) +{ + GstPipeWireSink *pwsink = data; + + switch (id) { + case SPA_IO_Position: + pwsink->stream->io_position = area; + break; + } +} + static const struct pw_stream_events stream_events = { PW_VERSION_STREAM_EVENTS, .state_changed = on_state_changed, @@ -1006,6 +1151,7 @@ static const struct pw_stream_events stream_events = { .add_buffer = on_add_buffer, .remove_buffer = on_remove_buffer, .process = on_process, + .io_changed = on_io_changed, }; static GstStateChangeReturn @@ -1020,6 +1166,14 @@ gst_pipewire_sink_change_state (GstElement * element, GstStateChange transition) goto open_failed; break; case GST_STATE_CHANGE_READY_TO_PAUSED: + /* If we are a driver, we shouldn't try to also provide the clock, as we + * _are_ the clock for the graph. For that case, we rely on the pipeline + * clock to drive the pipeline (and thus the graph). */ + if (this->mode == GST_PIPEWIRE_SINK_MODE_PROVIDE) + GST_OBJECT_FLAG_UNSET (this, GST_ELEMENT_FLAG_PROVIDE_CLOCK); + else + GST_OBJECT_FLAG_SET (this, GST_ELEMENT_FLAG_PROVIDE_CLOCK); + /* the initial stream state is active, which is needed for linking and * negotiation to happen and the bufferpool to be set up. We don't know * if we'll go to plaing, so we deactivate the stream until that diff --git a/src/gst/gstpipewiresink.h b/src/gst/gstpipewiresink.h index 60eb3b79f..5816f7a15 100644 --- a/src/gst/gstpipewiresink.h +++ b/src/gst/gstpipewiresink.h @@ -69,7 +69,9 @@ struct _GstPipeWireSink { gboolean negotiated; gboolean rate_match; gint rate; - gboolean is_video; + gboolean is_rawvideo; + gboolean first_buffer; + GstClockTime first_buffer_pts; GstPipeWireSinkMode mode; GstPipeWireSinkSlaveMethod slave_method; diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c index 1cd049418..ae779bd11 100644 --- a/src/gst/gstpipewiresrc.c +++ b/src/gst/gstpipewiresrc.c @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -41,12 +42,14 @@ GST_DEBUG_CATEGORY_STATIC (pipewire_src_debug); #define GST_CAT_DEFAULT pipewire_src_debug #define DEFAULT_ALWAYS_COPY false -#define DEFAULT_MIN_BUFFERS 8 +#define DEFAULT_MIN_BUFFERS 1 #define DEFAULT_MAX_BUFFERS INT32_MAX #define DEFAULT_RESEND_LAST false #define DEFAULT_KEEPALIVE_TIME 0 #define DEFAULT_AUTOCONNECT true -#define DEFAULT_USE_BUFFERPOOL USE_BUFFERPOOL_AUTO +#define DEFAULT_USE_BUFFERPOOL USE_BUFFERPOOL_AUTO +#define DEFAULT_ON_DISCONNECT GST_PIPEWIRE_SRC_ON_DISCONNECT_NONE +#define DEFAULT_PROVIDE_CLOCK TRUE enum { @@ -64,8 +67,29 @@ enum PROP_KEEPALIVE_TIME, PROP_AUTOCONNECT, PROP_USE_BUFFERPOOL, + PROP_ON_DISCONNECT, + PROP_PROVIDE_CLOCK, }; +GType +gst_pipewire_src_on_disconnect_get_type (void) +{ + static gsize on_disconnect_type = 0; + static const GEnumValue on_disconnect[] = { + {GST_PIPEWIRE_SRC_ON_DISCONNECT_NONE, "GST_PIPEWIRE_SRC_ON_DISCONNECT_NONE", "none"}, + {GST_PIPEWIRE_SRC_ON_DISCONNECT_EOS, "GST_PIPEWIRE_SRC_ON_DISCONNECT_EOS", "eos"}, + {GST_PIPEWIRE_SRC_ON_DISCONNECT_ERROR, "GST_PIPEWIRE_SRC_ON_DISCONNECT_ERROR", "error"}, + {0, NULL, NULL}, + }; + + if (g_once_init_enter (&on_disconnect_type)) { + GType tmp = + g_enum_register_static ("GstPipeWireSrcOnDisconnect", on_disconnect); + g_once_init_leave (&on_disconnect_type, tmp); + } + + return (GType) on_disconnect_type; +} static GstStaticPadTemplate gst_pipewire_src_template = GST_STATIC_PAD_TEMPLATE ("src", @@ -170,6 +194,20 @@ gst_pipewire_src_set_property (GObject * object, guint prop_id, pwsrc->use_bufferpool = USE_BUFFERPOOL_NO; break; + case PROP_ON_DISCONNECT: + pwsrc->on_disconnect = g_value_get_enum (value); + break; + + case PROP_PROVIDE_CLOCK: + gboolean provide = g_value_get_boolean (value); + GST_OBJECT_LOCK (pwsrc); + if (provide) + GST_OBJECT_FLAG_SET (pwsrc, GST_ELEMENT_FLAG_PROVIDE_CLOCK); + else + GST_OBJECT_FLAG_UNSET (pwsrc, GST_ELEMENT_FLAG_PROVIDE_CLOCK); + GST_OBJECT_UNLOCK (pwsrc); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -235,6 +273,18 @@ gst_pipewire_src_get_property (GObject * object, guint prop_id, g_value_set_boolean (value, !!pwsrc->use_bufferpool); break; + case PROP_ON_DISCONNECT: + g_value_set_enum (value, pwsrc->on_disconnect); + break; + + case PROP_PROVIDE_CLOCK: + gboolean result; + GST_OBJECT_LOCK (pwsrc); + result = GST_OBJECT_FLAG_IS_SET (pwsrc, GST_ELEMENT_FLAG_PROVIDE_CLOCK); + GST_OBJECT_UNLOCK (pwsrc); + g_value_set_boolean (value, result); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -414,6 +464,25 @@ gst_pipewire_src_class_init (GstPipeWireSrcClass * klass) G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, + PROP_ON_DISCONNECT, + g_param_spec_enum ("on-disconnect", + "On disconnect", + "Action to take on disconnect", + GST_TYPE_PIPEWIRE_SRC_ON_DISCONNECT, + DEFAULT_ON_DISCONNECT, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, + PROP_PROVIDE_CLOCK, + g_param_spec_boolean ("provide-clock", + "Provide Clock", + "Provide a clock to be used as the global pipeline clock", + DEFAULT_PROVIDE_CLOCK, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + gstelement_class->provide_clock = gst_pipewire_src_provide_clock; gstelement_class->change_state = gst_pipewire_src_change_state; gstelement_class->send_event = gst_pipewire_src_send_event; @@ -462,6 +531,9 @@ gst_pipewire_src_init (GstPipeWireSrc * src) src->autoconnect = DEFAULT_AUTOCONNECT; src->min_latency = 0; src->max_latency = GST_CLOCK_TIME_NONE; + src->n_buffers = 0; + src->flushing_on_remove_buffer = FALSE; + src->on_disconnect = DEFAULT_ON_DISCONNECT; src->transform_value = UINT32_MAX; } @@ -469,11 +541,26 @@ gst_pipewire_src_init (GstPipeWireSrc * src) static gboolean buffer_recycle (GstMiniObject *obj) { - GstPipeWireSrc *src; - GstPipeWirePoolData *data; + GstPipeWirePoolData *data = gst_pipewire_pool_get_data (GST_BUFFER_CAST(obj)); + GstPipeWireSrc *src = data->owner; int res; - data = gst_pipewire_pool_get_data (GST_BUFFER_CAST(obj)); + if (src->flushing_on_remove_buffer) { + /* + * If a flush-start was initiated, this might be called by elements like + * queues downstream purging buffers from their internal queues. This can + * deadlock if queues use min-threshold-buffers/bytes/time with src_create + * trying to take the loop lock and buffer_recycle trying to take the loop + * lock down below. We return from here, to prevent deadlock with streaming + * thread in a queue thread. + * + * We will take care of queueing the buffer in on_remove_buffer. + */ + GstBuffer *buffer = GST_BUFFER_CAST(obj); + GST_DEBUG_OBJECT (src, + "flush-start initiated, skipping buffer recycle %p", buffer); + return TRUE; + } GST_OBJECT_LOCK (data->pool); if (!obj->dispose) { @@ -482,7 +569,6 @@ buffer_recycle (GstMiniObject *obj) } GST_BUFFER_FLAGS (obj) = data->flags; - src = data->owner; pw_thread_loop_lock (src->stream->core->loop); if (!obj->dispose) { @@ -519,6 +605,8 @@ on_add_buffer (void *_data, struct pw_buffer *b) data->owner = pwsrc; data->queued = TRUE; GST_MINI_OBJECT_CAST (data->buf)->dispose = buffer_recycle; + + pwsrc->n_buffers++; } static void @@ -527,17 +615,76 @@ on_remove_buffer (void *_data, struct pw_buffer *b) GstPipeWireSrc *pwsrc = _data; GstPipeWirePoolData *data = b->user_data; GstBuffer *buf = data->buf; + gboolean flush_on_remove; int res; - GST_DEBUG_OBJECT (pwsrc, "remove buffer %p", buf); + GST_DEBUG_OBJECT (pwsrc, "remove buffer %p, queued: %d", + buf, data->queued); GST_MINI_OBJECT_CAST (buf)->dispose = NULL; + flush_on_remove = + pwsrc->on_disconnect == GST_PIPEWIRE_SRC_ON_DISCONNECT_ERROR || + pwsrc->on_disconnect == GST_PIPEWIRE_SRC_ON_DISCONNECT_EOS; + + if (flush_on_remove && !pwsrc->flushing_on_remove_buffer) { + pwsrc->flushing_on_remove_buffer = TRUE; + + GST_DEBUG_OBJECT (pwsrc, "flush-start on remove buffer"); + /* + * It is possible that when buffers are being removed, a downstream + * element can be holding on to a buffer or in the middle of rendering + * the same. Former is possible with queues min-threshold-buffers or + * similar. Latter can result in a crash during gst_video_frame_copy. + * + * We send a flush-start event downstream to make elements discard + * any buffers they may be holding on to as well as return from their + * chain function ASAP. + */ + gst_pad_push_event (GST_BASE_SRC_PAD (pwsrc), + gst_event_new_flush_start ()); + } + if (data->queued) { gst_buffer_unref (buf); } else { if ((res = pw_stream_queue_buffer (pwsrc->stream->pwstream, b)) < 0) - GST_WARNING_OBJECT (pwsrc, "can't queue removed buffer %p, %s", buf, spa_strerror(res)); + GST_WARNING_OBJECT (pwsrc, "can't queue removed buffer %p, %s", + buf, spa_strerror(res)); + else + GST_DEBUG_OBJECT (pwsrc, "queued buffer %p", buf); + } + + pwsrc->n_buffers--; + + if (pwsrc->n_buffers == 0) { + GST_DEBUG_OBJECT (pwsrc, "removed all buffers"); + + pwsrc->flushing_on_remove_buffer = FALSE; + + switch (pwsrc->on_disconnect) { + case GST_PIPEWIRE_SRC_ON_DISCONNECT_ERROR: + GST_DEBUG_OBJECT (pwsrc, "flush-stop on removing all buffers"); + gst_pad_push_event (GST_BASE_SRC_PAD (pwsrc), + gst_event_new_flush_stop (FALSE)); + + GST_ELEMENT_ERROR (pwsrc, RESOURCE, NOT_FOUND, + ("all buffers have been removed"), + ("PipeWire link to remote node was destroyed")); + break; + case GST_PIPEWIRE_SRC_ON_DISCONNECT_EOS: + GST_DEBUG_OBJECT (pwsrc, "flush-stop on removing all buffers"); + gst_pad_push_event (GST_BASE_SRC_PAD (pwsrc), + gst_event_new_flush_stop (FALSE)); + + GST_DEBUG_OBJECT (pwsrc, "sending eos downstream"); + gst_pad_push_event (GST_BASE_SRC_PAD (pwsrc), + gst_event_new_eos()); + break; + case GST_PIPEWIRE_SRC_ON_DISCONNECT_NONE: + GST_DEBUG_OBJECT (pwsrc, "stream closed or removed"); + break; + } } } @@ -568,6 +715,7 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc) struct spa_meta_header *h; struct spa_meta_region *crop; enum spa_meta_videotransform_value transform_value; + struct spa_meta_cursor *cursor; struct pw_time time; guint i; @@ -660,9 +808,19 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc) pwsrc->transform_value = transform_value; } - if (pwsrc->is_video) { - gsize video_size = 0; + cursor = data->cursor; + if (cursor && cursor->id != 0) { + /* TODO: at some point, maybe we can figure out width and height from the bitmap, + * and even add that to the meta itself */ + gst_buffer_add_video_region_of_interest_meta (buf, "cursor", cursor->position.x, cursor->position.y, 0, 0); + } + + if (pwsrc->is_rawvideo) { GstVideoInfo *info = &pwsrc->video_info; + uint32_t n_datas = b->buffer->n_datas; + uint32_t n_planes = GST_VIDEO_INFO_N_PLANES (info); + gsize video_size = 0; + GstVideoMeta *meta = gst_buffer_add_video_meta_full (buf, GST_VIDEO_FRAME_FLAG_NONE, GST_VIDEO_INFO_FORMAT (info), GST_VIDEO_INFO_WIDTH (info), @@ -671,8 +829,10 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc) info->offset, info->stride); - for (i = 0; i < MIN (b->buffer->n_datas, GST_VIDEO_MAX_PLANES); i++) { + for (i = 0; i < MIN (n_datas, n_planes); i++) { struct spa_data *d = &b->buffer->datas[i]; + /* don't add the chunk offset here, this is done below when we + * share/copy the memory in the target buffer below */ meta->offset[i] = video_size; meta->stride[i] = d->chunk->stride; @@ -680,6 +840,10 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc) } } + if (b->buffer->n_datas != gst_buffer_n_memory(data->buf)) { + GST_ERROR_OBJECT(pwsrc, "n_datas != n_memory, (%d != %d)", b->buffer->n_datas, gst_buffer_n_memory(data->buf)); + } + for (i = 0; i < b->buffer->n_datas; i++) { struct spa_data *d = &b->buffer->datas[i]; @@ -730,13 +894,29 @@ on_state_changed (void *data, enum pw_stream_state state, const char *error) { GstPipeWireSrc *pwsrc = data; + GstState current_state = GST_ELEMENT_CAST (pwsrc)->current_state; - GST_DEBUG ("got stream state %s", pw_stream_state_as_string (state)); + GST_DEBUG_OBJECT (pwsrc, "got stream state %s", pw_stream_state_as_string (state)); switch (state) { case PW_STREAM_STATE_UNCONNECTED: case PW_STREAM_STATE_CONNECTING: + break; case PW_STREAM_STATE_PAUSED: + /* + * We may see a driver/quantum/clock rate change on switching audio + * sources. The same is not applicable for video. + * + * We post the clock lost message here to take care of a possible + * jump or shift in base_time/clock for the pipeline. Application + * must handle the clock lost message in it's bus handler by pausing + * the pipeline and then setting it back to playing. + */ + if (current_state == GST_STATE_PLAYING && !pwsrc->is_video) + gst_element_post_message (GST_ELEMENT_CAST (pwsrc), + gst_message_new_clock_lost (GST_OBJECT_CAST (pwsrc), + GST_CLOCK_CAST (pwsrc->stream->clock))); + break; case PW_STREAM_STATE_STREAMING: break; case PW_STREAM_STATE_ERROR: @@ -982,7 +1162,7 @@ gst_pipewire_src_negotiate (GstBaseSrc * basesrc) GST_DEBUG_OBJECT (basesrc, "connect capture with path %s, target-object %s", pwsrc->stream->path, pwsrc->stream->target_object); - pwsrc->possible_caps = possible_caps; + gst_caps_replace (&pwsrc->possible_caps, possible_caps); pwsrc->negotiated = FALSE; enum pw_stream_flags flags; @@ -1019,7 +1199,6 @@ gst_pipewire_src_negotiate (GstBaseSrc * basesrc) } negotiated_caps = g_steal_pointer (&pwsrc->caps); - pwsrc->possible_caps = NULL; pw_thread_loop_unlock (pwsrc->stream->core->loop); if (negotiated_caps == NULL) @@ -1027,7 +1206,7 @@ gst_pipewire_src_negotiate (GstBaseSrc * basesrc) gst_pipewire_clock_reset (GST_PIPEWIRE_CLOCK (pwsrc->stream->clock), 0); - GST_DEBUG_OBJECT (pwsrc, "set format %" GST_PTR_FORMAT, negotiated_caps); + GST_INFO_OBJECT (pwsrc, "set format %" GST_PTR_FORMAT, negotiated_caps); result = gst_base_src_set_caps (GST_BASE_SRC (pwsrc), negotiated_caps); if (!result) goto no_caps; @@ -1091,14 +1270,34 @@ handle_format_change (GstPipeWireSrc *pwsrc, } pw_peer_caps = gst_caps_from_format (param); + if (pw_peer_caps && pwsrc->possible_caps) { + GST_DEBUG_OBJECT (pwsrc, "peer caps %" GST_PTR_FORMAT, pw_peer_caps); + GST_DEBUG_OBJECT (pwsrc, "possible caps %" GST_PTR_FORMAT, pwsrc->possible_caps); + pwsrc->caps = gst_caps_intersect_full (pw_peer_caps, pwsrc->possible_caps, GST_CAPS_INTERSECT_FIRST); + + /* + * We expect pw_peer_caps to be fixed caps as we receive that from + * PipeWire. See pw_context_find_format() and SPA_PARAM_Format. + * possible_caps can be non-fixated caps based on what is downstream + * in the pipeline. + * + * The intersection result above might give us non-fixated caps. A + * possible scenario for this is the below pipeline. + * pipewiresrc ! audioconvert ! audio/x-raw,rate=44100,channels=2 ! .. + * + * So we fixate the caps explicitly here. + */ + pwsrc->caps = gst_caps_fixate (pwsrc->caps); gst_caps_maybe_fixate_dma_format (pwsrc->caps); } - if (pwsrc->caps && gst_caps_is_fixed (pwsrc->caps)) { + if (pwsrc->caps) { + g_return_if_fail (gst_caps_is_fixed (pwsrc->caps)); + pwsrc->negotiated = TRUE; structure = gst_caps_get_structure (pwsrc->caps, 0); @@ -1120,29 +1319,42 @@ handle_format_change (GstPipeWireSrc *pwsrc, pw_stream_set_error (pwsrc->stream->pwstream, -EINVAL, "internal error"); return; } + pwsrc->is_rawvideo = TRUE; } else { gst_video_info_dma_drm_init (&pwsrc->drm_info); #endif gst_video_info_from_caps (&pwsrc->video_info, pwsrc->caps); + + if (GST_VIDEO_FORMAT_INFO_IS_VALID_RAW (pwsrc->video_info.finfo) +#ifdef HAVE_GSTREAMER_DMA_DRM + && GST_VIDEO_FORMAT_INFO_FORMAT (pwsrc->video_info.finfo) != GST_VIDEO_FORMAT_DMA_DRM +#endif + ) + pwsrc->is_rawvideo = TRUE; + else + pwsrc->is_rawvideo = FALSE; + #ifdef HAVE_GSTREAMER_DMA_DRM } #endif + } else { + /* Don't provide bufferpool for audio if not requested by the + * application/user */ + if (pwsrc->use_bufferpool != USE_BUFFERPOOL_YES) + pwsrc->use_bufferpool = USE_BUFFERPOOL_NO; } } else { pwsrc->negotiated = FALSE; pwsrc->is_video = FALSE; - - /* Don't provide bufferpool for audio if not requested by the application/user */ - if (pwsrc->use_bufferpool != USE_BUFFERPOOL_YES) - pwsrc->use_bufferpool = USE_BUFFERPOOL_NO; + pwsrc->is_rawvideo = FALSE; } if (pwsrc->caps) { - const struct spa_pod *params[4]; + const struct spa_pod *params[10]; struct spa_pod_builder b = { NULL }; - uint8_t buffer[512]; + uint8_t buffer[16384]; uint32_t buffers = CLAMP (16, pwsrc->min_buffers, pwsrc->max_buffers); - int buffertypes; + int buffertypes, n_params = 0; buffertypes = (1<caps); spa_pod_builder_init (&b, buffer, sizeof (buffer)); - params[0] = spa_pod_builder_add_object (&b, + params[n_params++] = spa_pod_builder_add_object (&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers, pwsrc->min_buffers, pwsrc->max_buffers), SPA_PARAM_BUFFERS_blocks, SPA_POD_CHOICE_RANGE_Int(0, 1, INT32_MAX), - SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(0, 0, INT32_MAX), + SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(0, 1, INT32_MAX), SPA_PARAM_BUFFERS_stride, SPA_POD_CHOICE_RANGE_Int(0, 0, INT32_MAX), SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(buffertypes)); - params[1] = spa_pod_builder_add_object (&b, + params[n_params++] = spa_pod_builder_add_object (&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_header))); - params[2] = spa_pod_builder_add_object (&b, + params[n_params++] = spa_pod_builder_add_object (&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop), SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_region))); - params[3] = spa_pod_builder_add_object (&b, + params[n_params++] = spa_pod_builder_add_object (&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform), SPA_PARAM_META_size, SPA_POD_Int(sizeof (struct spa_meta_videotransform))); +#define CURSOR_META_SIZE(width, height) \ + (sizeof (struct spa_meta_cursor) + \ + sizeof (struct spa_meta_bitmap) + width * height * 4) + params[n_params++] = spa_pod_builder_add_object (&b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor), + SPA_PARAM_META_size, + SPA_POD_CHOICE_RANGE_Int (CURSOR_META_SIZE(384, 384), + sizeof (struct spa_meta_cursor), + CURSOR_META_SIZE(384, 384))); GST_DEBUG_OBJECT (pwsrc, "doing finish format"); - pw_stream_update_params (pwsrc->stream->pwstream, params, SPA_N_ELEMENTS(params)); + pw_stream_update_params (pwsrc->stream->pwstream, params, n_params); } else { GST_WARNING_OBJECT (pwsrc, "finish format with error"); pw_stream_set_error (pwsrc->stream->pwstream, -EINVAL, "unhandled format"); @@ -1309,6 +1531,8 @@ gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer) GstBuffer *buf; gboolean update_time = FALSE, timeout = FALSE; GstCaps *caps = NULL; + struct timespec abstime = { 0, }; + bool have_abstime = false; pwsrc = GST_PIPEWIRE_SRC (psrc); @@ -1352,13 +1576,11 @@ gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer) update_time = TRUE; GST_LOG_OBJECT (pwsrc, "EOS, send last buffer"); break; - } else if (timeout) { - if (pwsrc->last_buffer != NULL) { - update_time = TRUE; - buf = gst_buffer_ref(pwsrc->last_buffer); - GST_LOG_OBJECT (pwsrc, "timeout, send keepalive buffer"); - break; - } + } else if (timeout && pwsrc->last_buffer != NULL) { + update_time = TRUE; + buf = gst_buffer_ref(pwsrc->last_buffer); + GST_LOG_OBJECT (pwsrc, "timeout, send keepalive buffer"); + break; } else { buf = dequeue_buffer (pwsrc); GST_LOG_OBJECT (pwsrc, "popped buffer %p", buf); @@ -1370,9 +1592,13 @@ gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer) } timeout = FALSE; if (pwsrc->keepalive_time > 0) { - struct timespec abstime; - pw_thread_loop_get_time(pwsrc->stream->core->loop, &abstime, - pwsrc->keepalive_time * SPA_NSEC_PER_MSEC); + if (!have_abstime) { + /* Record the time we want to timeout at once, for this loop -- the loop might get unrelated signal()s, + * and we don't want the keepalive time to get reset by that */ + pw_thread_loop_get_time(pwsrc->stream->core->loop, &abstime, + pwsrc->keepalive_time * SPA_NSEC_PER_MSEC); + have_abstime = TRUE; + } if (pw_thread_loop_timed_wait_full (pwsrc->stream->core->loop, &abstime) == -ETIMEDOUT) timeout = TRUE; } else { @@ -1443,6 +1669,7 @@ gst_pipewire_src_stop (GstBaseSrc * basesrc) pwsrc->eos = false; gst_buffer_replace (&pwsrc->last_buffer, NULL); gst_caps_replace(&pwsrc->caps, NULL); + gst_caps_replace(&pwsrc->possible_caps, NULL); pwsrc->transform_value = UINT32_MAX; pw_thread_loop_unlock (pwsrc->stream->core->loop); diff --git a/src/gst/gstpipewiresrc.h b/src/gst/gstpipewiresrc.h index d5728cdc9..3ac88b10b 100644 --- a/src/gst/gstpipewiresrc.h +++ b/src/gst/gstpipewiresrc.h @@ -24,6 +24,22 @@ G_BEGIN_DECLS #define GST_PIPEWIRE_SRC_CAST(obj) ((GstPipeWireSrc *) (obj)) G_DECLARE_FINAL_TYPE (GstPipeWireSrc, gst_pipewire_src, GST, PIPEWIRE_SRC, GstPushSrc) +/** + * GstPipeWireSrcOnDisconnect: + * @GST_PIPEWIRE_SRC_ON_DISCONNECT_EOS: send EoS downstream + * @GST_PIPEWIRE_SRC_ON_DISCONNECT_ERROR: raise pipeline error + * @GST_PIPEWIRE_SRC_ON_DISCONNECT_NONE: no action + * + * Different actions on disconnect. + */ +typedef enum +{ + GST_PIPEWIRE_SRC_ON_DISCONNECT_NONE, + GST_PIPEWIRE_SRC_ON_DISCONNECT_EOS, + GST_PIPEWIRE_SRC_ON_DISCONNECT_ERROR, +} GstPipeWireSrcOnDisconnect; + +#define GST_TYPE_PIPEWIRE_SRC_ON_DISCONNECT (gst_pipewire_src_on_disconnect_get_type ()) /** * GstPipeWireSrc: @@ -36,6 +52,7 @@ struct _GstPipeWireSrc { GstPipeWireStream *stream; /*< private >*/ + gint n_buffers; gint use_bufferpool; gint min_buffers; gint max_buffers; @@ -47,6 +64,7 @@ struct _GstPipeWireSrc { GstCaps *possible_caps; gboolean is_video; + gboolean is_rawvideo; GstVideoInfo video_info; #ifdef HAVE_GSTREAMER_DMA_DRM GstVideoInfoDmaDrm drm_info; @@ -56,6 +74,7 @@ struct _GstPipeWireSrc { gboolean flushing; gboolean started; gboolean eos; + gboolean flushing_on_remove_buffer; gboolean is_live; int64_t delay; @@ -65,8 +84,12 @@ struct _GstPipeWireSrc { GstBuffer *last_buffer; enum spa_meta_videotransform_value transform_value; + + GstPipeWireSrcOnDisconnect on_disconnect; }; +GType gst_pipewire_src_on_stream_disconnect_get_type (void); + G_END_DECLS #endif /* __GST_PIPEWIRE_SRC_H__ */ diff --git a/src/gst/gstpipewirestream.h b/src/gst/gstpipewirestream.h index a301375c7..23dc996a9 100644 --- a/src/gst/gstpipewirestream.h +++ b/src/gst/gstpipewirestream.h @@ -31,6 +31,7 @@ struct _GstPipeWireStream { GstClock *clock; guint64 position; + guint64 buf_duration; struct spa_dll dll; double err_avg, err_var, err_wdw; guint64 last_ts; @@ -41,6 +42,8 @@ struct _GstPipeWireStream { struct pw_stream *pwstream; struct spa_hook pwstream_listener; + struct spa_io_position *io_position; + /* common properties */ int fd; gchar *path; diff --git a/src/modules/flatpak-utils.h b/src/modules/flatpak-utils.h index 9b839e210..76b14491c 100644 --- a/src/modules/flatpak-utils.h +++ b/src/modules/flatpak-utils.h @@ -16,8 +16,6 @@ #include #endif -#include "config.h" - #ifdef HAVE_GLIB2 #include #endif @@ -26,7 +24,7 @@ #include #include -static int pw_check_flatpak_parse_metadata(const char *buf, size_t size, char **app_id, char **devices) +static int pw_check_flatpak_parse_metadata(const char *buf, size_t size, char **app_id, char **instance_id, char **devices) { #ifdef HAVE_GLIB2 /* @@ -53,13 +51,19 @@ static int pw_check_flatpak_parse_metadata(const char *buf, size_t size, char ** g_free(s); } + if (instance_id) { + s = g_key_file_get_value(metadata, "Instance", "instance-id", NULL); + *instance_id = s ? strdup(s) : NULL; + g_free(s); + } + return 0; #else return -ENOTSUP; #endif } -static int pw_check_flatpak(pid_t pid, char **app_id, char **devices) +static int pw_check_flatpak(pid_t pid, char **app_id, char **instance_id, char **devices) { #if defined(__linux__) char root_path[2048]; @@ -68,6 +72,8 @@ static int pw_check_flatpak(pid_t pid, char **app_id, char **devices) if (app_id) *app_id = NULL; + if (instance_id) + *instance_id = NULL; if (devices) *devices = NULL; @@ -107,14 +113,14 @@ static int pw_check_flatpak(pid_t pid, char **app_id, char **devices) if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) { /* Some weird fd => failure, assume sandboxed */ pw_log_error("error fstat .flatpak-info: %m"); - } else if (app_id || devices) { + } else if (app_id || instance_id || devices) { /* Parse the application ID if needed */ const size_t size = stat_buf.st_size; if (size > 0) { void *buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, info_fd, 0); if (buf != MAP_FAILED) { - res = pw_check_flatpak_parse_metadata(buf, size, app_id, devices); + res = pw_check_flatpak_parse_metadata(buf, size, app_id, instance_id, devices); munmap(buf, size); } else { res = -errno; diff --git a/src/modules/meson.build b/src/modules/meson.build index 51db77334..e0bf6178b 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -289,7 +289,6 @@ pipewire_module_protocol_native = shared_library('pipewire-module-protocol-nativ 'module-protocol-native/local-socket.c', 'module-protocol-native/portal-screencast.c', 'module-protocol-native/protocol-native.c', - 'module-protocol-native/v0/protocol-native.c', 'module-protocol-native/protocol-footer.c', 'module-protocol-native/security-context.c', 'module-protocol-native/connection.c' ], @@ -476,9 +475,6 @@ pipewire_module_client_node = shared_library('pipewire-module-client-node', 'module-client-node/remote-node.c', 'module-client-node/client-node.c', 'module-client-node/protocol-native.c', - 'module-client-node/v0/client-node.c', - 'module-client-node/v0/transport.c', - 'module-client-node/v0/protocol-native.c', 'spa/spa-node.c', ], include_directories : [configinc], link_with : pipewire_module_protocol_native, @@ -620,7 +616,7 @@ if build_module_raop endif summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules') -roc_dep = dependency('roc', version: '>= 0.3.0', required: get_option('roc')) +roc_dep = dependency('roc', version: '>= 0.4.0', required: get_option('roc')) summary({'ROC': roc_dep.found()}, bool_yn: true, section: 'Streaming between daemons') pipewire_module_rtp_source = shared_library('pipewire-module-rtp-source', diff --git a/src/modules/module-access.c b/src/modules/module-access.c index 2e246ae91..26e64f749 100644 --- a/src/modules/module-access.c +++ b/src/modules/module-access.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -11,8 +13,6 @@ #include #include -#include "config.h" - #ifdef HAVE_SYS_VFS_H #include #endif @@ -167,12 +167,13 @@ context_check_access(void *data, struct pw_impl_client *client) { struct impl *impl = data; struct pw_permission permissions[1]; - struct spa_dict_item items[3]; + struct spa_dict_item items[4]; const struct pw_properties *props; const char *str; const char *access; const char *socket; spa_autofree char *flatpak_app_id = NULL; + spa_autofree char *flatpak_instance_id = NULL; int nitems = 0; bool sandbox_flatpak; int pid, res; @@ -197,7 +198,7 @@ context_check_access(void *data, struct pw_impl_client *client) } else { pw_log_info("client %p has trusted pid %d", client, pid); - res = pw_check_flatpak(pid, &flatpak_app_id, NULL); + res = pw_check_flatpak(pid, &flatpak_app_id, &flatpak_instance_id, NULL); if (res != 0) { if (res < 0) pw_log_warn("%p: client %p flatpak check failed: %s", @@ -233,6 +234,8 @@ context_check_access(void *data, struct pw_impl_client *client) if (sandbox_flatpak) { items[nitems++] = SPA_DICT_ITEM_INIT("pipewire.access.portal.app_id", flatpak_app_id); + items[nitems++] = SPA_DICT_ITEM_INIT("pipewire.access.portal.instance_id", + flatpak_instance_id); items[nitems++] = SPA_DICT_ITEM_INIT("pipewire.sec.flatpak", "true"); } diff --git a/src/modules/module-adapter.c b/src/modules/module-adapter.c index ea913ebad..9f5474a08 100644 --- a/src/modules/module-adapter.c +++ b/src/modules/module-adapter.c @@ -2,13 +2,13 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include #include -#include "config.h" - #include #include #include diff --git a/src/modules/module-adapter/adapter.c b/src/modules/module-adapter/adapter.c index 93ba0d305..6d2614f08 100644 --- a/src/modules/module-adapter/adapter.c +++ b/src/modules/module-adapter/adapter.c @@ -2,14 +2,14 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include #include #include -#include "config.h" - #include #include #include diff --git a/src/modules/module-avb.c b/src/modules/module-avb.c index b730b17c1..47afb3759 100644 --- a/src/modules/module-avb.c +++ b/src/modules/module-avb.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,8 +12,6 @@ #include #include -#include "config.h" - #include #include #include diff --git a/src/modules/module-avb/avb.c b/src/modules/module-avb/avb.c index 7bfa85eb9..eeacb7b7c 100644 --- a/src/modules/module-avb/avb.c +++ b/src/modules/module-avb/avb.c @@ -41,6 +41,7 @@ struct pw_avb *pw_avb_new(struct pw_context *context, impl->context = context; impl->loop = pw_context_get_main_loop(context); + impl->timer_queue = pw_context_get_timer_queue(context); impl->props = props; impl->core = pw_context_get_object(context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { diff --git a/src/modules/module-avb/avdecc.c b/src/modules/module-avb/avdecc.c index fd80ec8ac..0e2b33c34 100644 --- a/src/modules/module-avb/avdecc.c +++ b/src/modules/module-avb/avdecc.c @@ -14,6 +14,7 @@ #include #include +#include #include @@ -39,12 +40,18 @@ #define server_emit_periodic(s,n) server_emit(s, periodic, 0, n) #define server_emit_command(s,n,c,a,f) server_emit(s, command, 0, n, c, a, f) -static void on_timer_event(void *data, uint64_t expirations) +static void on_timer_event(void *data) { struct server *server = data; + struct impl *impl = server->impl; struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); server_emit_periodic(server, SPA_TIMESPEC_TO_NSEC(&now)); + + pw_timer_queue_add(impl->timer_queue, &server->timer, + &server->timer.timeout, DEFAULT_INTERVAL * SPA_NSEC_PER_SEC, + on_timer_event, server); } static void on_socket_data(void *data, int fd, uint32_t mask) @@ -201,7 +208,6 @@ static int setup_socket(struct server *server) struct impl *impl = server->impl; int fd, res; static const uint8_t bmac[6] = AVB_BROADCAST_MAC; - struct timespec value, interval; fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac); if (fd < 0) @@ -215,18 +221,13 @@ static int setup_socket(struct server *server) pw_log_error("server %p: can't create server source: %m", impl); goto error_no_source; } - server->timer = pw_loop_add_timer(impl->loop, on_timer_event, server); - if (server->timer == NULL) { - res = -errno; - pw_log_error("server %p: can't create timer source: %m", impl); + + if ((res = pw_timer_queue_add(impl->timer_queue, &server->timer, + NULL, DEFAULT_INTERVAL * SPA_NSEC_PER_SEC, + on_timer_event, server)) < 0) { + pw_log_error("server %p: can't create timer: %s", impl, spa_strerror(res)); goto error_no_timer; } - value.tv_sec = 0; - value.tv_nsec = 1; - interval.tv_sec = DEFAULT_INTERVAL; - interval.tv_nsec = 0; - pw_loop_update_timer(impl->loop, server->timer, &value, &interval, false); - return 0; error_no_timer: @@ -310,8 +311,7 @@ void avdecc_server_free(struct server *server) spa_list_remove(&server->link); if (server->source) pw_loop_destroy_source(impl->loop, server->source); - if (server->timer) - pw_loop_destroy_source(impl->loop, server->timer); + pw_timer_queue_cancel(&server->timer); spa_hook_list_clean(&server->listener_list); free(server); } diff --git a/src/modules/module-avb/internal.h b/src/modules/module-avb/internal.h index d1e72c8cb..90222511a 100644 --- a/src/modules/module-avb/internal.h +++ b/src/modules/module-avb/internal.h @@ -5,12 +5,12 @@ #ifndef AVB_INTERNAL_H #define AVB_INTERNAL_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - struct server; struct avb_mrp; @@ -19,6 +19,7 @@ struct avb_mrp; struct impl { struct pw_loop *loop; + struct pw_timer_queue *timer_queue; struct pw_context *context; struct spa_hook context_listener; struct pw_core *core; @@ -61,7 +62,7 @@ struct server { int ifindex; struct spa_source *source; - struct spa_source *timer; + struct pw_timer timer; struct spa_hook_list listener_list; diff --git a/src/modules/module-client-device.c b/src/modules/module-client-device.c index 84b8456b2..c02a4058f 100644 --- a/src/modules/module-client-device.c +++ b/src/modules/module-client-device.c @@ -2,13 +2,13 @@ /* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include #include -#include "config.h" - #include #include diff --git a/src/modules/module-client-device/protocol-native.c b/src/modules/module-client-device/protocol-native.c index 59d16e31f..cbcca0c9a 100644 --- a/src/modules/module-client-device/protocol-native.c +++ b/src/modules/module-client-device/protocol-native.c @@ -33,6 +33,8 @@ static inline int parse_item(struct spa_pod_parser *prs, struct spa_dict_item *i SPA_POD_String(&item->value), NULL)) < 0) return res; + if (item->key == NULL || item->value == NULL) + return -EINVAL; if (spa_strstartswith(item->value, "pointer:")) item->value = ""; return 0; diff --git a/src/modules/module-client-node.c b/src/modules/module-client-node.c index dc978084d..9ef2561f0 100644 --- a/src/modules/module-client-node.c +++ b/src/modules/module-client-node.c @@ -2,19 +2,18 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include #include -#include "config.h" - #include #include #define PW_API_CLIENT_NODE_IMPL SPA_EXPORT -#include "module-client-node/v0/client-node.h" #include "module-client-node/client-node.h" /** \page page_module_client_node Client Node @@ -108,7 +107,6 @@ struct pw_proxy *pw_core_spa_node_export(struct pw_core *core, const char *type, const struct spa_dict *props, void *object, size_t user_data_size); struct pw_protocol *pw_protocol_native_ext_client_node_init(struct pw_context *context); -struct pw_protocol *pw_protocol_native_ext_client_node0_init(struct pw_context *context); struct factory_data { struct pw_impl_factory *factory; @@ -146,7 +144,8 @@ static void *create_object(void *_data, } if (version == 0) { - result = pw_impl_client_node0_new(node_resource, properties); + result = NULL; + errno = ENOTSUP; } else { result = pw_impl_client_node_new(node_resource, properties, true); } @@ -265,7 +264,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) goto error_remove; pw_protocol_native_ext_client_node_init(context); - pw_protocol_native_ext_client_node0_init(context); pw_impl_factory_add_listener(factory, &data->factory_listener, &factory_events, data); pw_impl_module_add_listener(module, &data->module_listener, &module_events, data); diff --git a/src/modules/module-client-node/client-node.c b/src/modules/module-client-node/client-node.c index 3c6d6e4d8..a17dc87ce 100644 --- a/src/modules/module-client-node/client-node.c +++ b/src/modules/module-client-node/client-node.c @@ -30,7 +30,7 @@ PW_LOG_TOPIC_EXTERN(mod_topic); #define MAX_BUFFERS 64 #define MAX_METAS 16u -#define MAX_DATAS 64u +#define MAX_DATAS 256u #define AREA_SLOT (sizeof(struct spa_io_async_buffers)) #define AREA_SIZE (4096u / AREA_SLOT) #define MAX_AREAS 32 @@ -242,7 +242,11 @@ fail: static void clear_data(struct impl *impl, struct spa_data *d) { - switch (d->type) { + switch ((enum spa_data_type)d->type) { + case SPA_DATA_Invalid: + case SPA_DATA_MemPtr: + case _SPA_DATA_LAST: + break; case SPA_DATA_MemId: { uint32_t id; @@ -258,11 +262,10 @@ static void clear_data(struct impl *impl, struct spa_data *d) } case SPA_DATA_MemFd: case SPA_DATA_DmaBuf: + case SPA_DATA_SyncObj: pw_log_debug("%p: close fd:%d", impl, (int)d->fd); close(d->fd); break; - default: - break; } } @@ -1239,12 +1242,11 @@ static void client_node_resource_destroy(void *data) spa_hook_remove(&impl->object_listener); if (impl->data_source.fd != -1) { - spa_loop_invoke(impl->data_loop, + spa_loop_locked(impl->data_loop, do_remove_source, SPA_ID_INVALID, NULL, 0, - true, &impl->data_source); } if (this->node) diff --git a/src/modules/module-client-node/protocol-native.c b/src/modules/module-client-node/protocol-native.c index 3051703af..17f8e2da1 100644 --- a/src/modules/module-client-node/protocol-native.c +++ b/src/modules/module-client-node/protocol-native.c @@ -18,7 +18,7 @@ #define MAX_PARAM_INFO 128 #define MAX_BUFFERS 64 #define MAX_METAS 16u -#define MAX_DATAS 64u +#define MAX_DATAS 256u PW_LOG_TOPIC_EXTERN(mod_topic); #define PW_LOG_TOPIC_DEFAULT mod_topic diff --git a/src/modules/module-client-node/remote-node.c b/src/modules/module-client-node/remote-node.c index 8b5c7daf3..52134eaa4 100644 --- a/src/modules/module-client-node/remote-node.c +++ b/src/modules/module-client-node/remote-node.c @@ -481,15 +481,16 @@ static int client_node_command(void *_data, const struct spa_command *command) pw_proxy_error(proxy, res, "suspend failed"); } break; - case SPA_NODE_COMMAND_RequestProcess: - res = pw_impl_node_send_command(node, command); - break; default: - pw_log_warn("unhandled node command %d (%s)", id, - spa_debug_type_find_name(spa_type_node_command_id, id)); - res = -ENOTSUP; - pw_proxy_errorf(proxy, res, "command %d (%s) not supported", id, - spa_debug_type_find_name(spa_type_node_command_id, id)); + res = pw_impl_node_send_command(node, command); + if (res < 0) { + pw_log_warn("node command %d (%s) error: %s (%d)", id, + spa_debug_type_find_name(spa_type_node_command_id, id), + spa_strerror(res), res); + pw_proxy_errorf(proxy, res, "command %d (%s) error: %s (%d)", id, + spa_debug_type_find_name(spa_type_node_command_id, id), + spa_strerror(res), res); + } } return res; } diff --git a/src/modules/module-client-node/v0/client-node.c b/src/modules/module-client-node/v0/client-node.c deleted file mode 100644 index 771e16684..000000000 --- a/src/modules/module-client-node/v0/client-node.c +++ /dev/null @@ -1,1429 +0,0 @@ -/* PipeWire */ -/* SPDX-FileCopyrightText: Copyright © 2015 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#define PW_ENABLE_DEPRECATED - -#include "pipewire/pipewire.h" - -#include "pipewire/context.h" -#include "modules/spa/spa-node.h" -#include "client-node.h" -#include "transport.h" - -PW_LOG_TOPIC_EXTERN(mod_topic); -#define PW_LOG_TOPIC_DEFAULT mod_topic - -/** \cond */ - -#define MAX_INPUTS 64 -#define MAX_OUTPUTS 64 - -#define MAX_BUFFERS 64 - -#define CHECK_IN_PORT_ID(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_INPUTS) -#define CHECK_OUT_PORT_ID(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_OUTPUTS) -#define CHECK_PORT_ID(this,d,p) (CHECK_IN_PORT_ID(this,d,p) || CHECK_OUT_PORT_ID(this,d,p)) -#define CHECK_FREE_IN_PORT(this,d,p) (CHECK_IN_PORT_ID(this,d,p) && !(this)->in_ports[p].valid) -#define CHECK_FREE_OUT_PORT(this,d,p) (CHECK_OUT_PORT_ID(this,d,p) && !(this)->out_ports[p].valid) -#define CHECK_FREE_PORT(this,d,p) (CHECK_FREE_IN_PORT (this,d,p) || CHECK_FREE_OUT_PORT (this,d,p)) -#define CHECK_IN_PORT(this,d,p) (CHECK_IN_PORT_ID(this,d,p) && (this)->in_ports[p].valid) -#define CHECK_OUT_PORT(this,d,p) (CHECK_OUT_PORT_ID(this,d,p) && (this)->out_ports[p].valid) -#define CHECK_PORT(this,d,p) (CHECK_IN_PORT (this,d,p) || CHECK_OUT_PORT (this,d,p)) - -#define GET_IN_PORT(this,p) (&this->in_ports[p]) -#define GET_OUT_PORT(this,p) (&this->out_ports[p]) -#define GET_PORT(this,d,p) (d == SPA_DIRECTION_INPUT ? GET_IN_PORT(this,p) : GET_OUT_PORT(this,p)) - -#define CHECK_PORT_BUFFER(this,b,p) (b < p->n_buffers) - -extern uint32_t pw_protocol_native0_type_from_v2(struct pw_impl_client *client, uint32_t type); -extern uint32_t pw_protocol_native0_name_to_v2(struct pw_impl_client *client, const char *name); - -struct mem { - uint32_t id; - int ref; - int fd; - uint32_t type; - uint32_t flags; -}; - -struct buffer { - struct spa_buffer *outbuf; - struct spa_buffer buffer; - struct spa_meta metas[4]; - struct spa_data datas[4]; - bool outstanding; - uint32_t memid; -}; - -struct port { - uint32_t id; - enum spa_direction direction; - - bool valid; - struct spa_port_info info; - struct pw_properties *properties; - - bool have_format; - uint32_t n_params; - struct spa_pod **params; - struct spa_io_buffers *io; - - uint32_t n_buffers; - struct buffer buffers[MAX_BUFFERS]; -}; - -struct node { - struct spa_node node; - - struct impl *impl; - - struct spa_log *log; - struct spa_loop *data_loop; - struct spa_system *data_system; - - struct spa_hook_list hooks; - struct spa_callbacks callbacks; - - struct spa_io_position *position; - - struct pw_resource *resource; - - struct spa_source data_source; - int writefd; - - struct spa_node_info info; - - uint32_t n_inputs; - uint32_t n_outputs; - struct port in_ports[MAX_INPUTS]; - struct port out_ports[MAX_OUTPUTS]; - - uint32_t n_params; - struct spa_pod **params; - - uint32_t seq; - uint32_t init_pending; -}; - -struct impl { - struct pw_impl_client_node0 this; - - bool client_reuse; - - struct pw_context *context; - struct pw_mempool *context_pool; - - struct node node; - - struct pw_client_node0_transport *transport; - - struct spa_hook node_listener; - struct spa_hook resource_listener; - struct spa_hook object_listener; - - struct pw_array mems; - - int fds[2]; - int other_fds[2]; - - uint32_t input_ready; - bool out_pending; -}; - -/** \endcond */ - -static struct mem *ensure_mem(struct impl *impl, int fd, uint32_t type, uint32_t flags) -{ - struct mem *m, *f = NULL; - - pw_array_for_each(m, &impl->mems) { - if (m->ref <= 0) - f = m; - else if (m->fd == fd) - goto found; - } - - if (f == NULL) { - m = pw_array_add(&impl->mems, sizeof(struct mem)); - m->id = pw_array_get_len(&impl->mems, struct mem) - 1; - m->ref = 0; - } - else { - m = f; - } - m->fd = fd; - m->type = type; - m->flags = flags; - - pw_client_node0_resource_add_mem(impl->node.resource, - m->id, - type, - m->fd, - m->flags); - found: - m->ref++; - return m; -} - - -static int clear_buffers(struct node *this, struct port *port) -{ - uint32_t i, j; - struct impl *impl = this->impl; - - for (i = 0; i < port->n_buffers; i++) { - struct buffer *b = &port->buffers[i]; - struct mem *m; - - spa_log_debug(this->log, "node %p: clear buffer %d", this, i); - - for (j = 0; j < b->buffer.n_datas; j++) { - struct spa_data *d = &b->datas[j]; - - if (d->type == SPA_DATA_DmaBuf || - d->type == SPA_DATA_MemFd) { - uint32_t id; - - id = SPA_PTR_TO_UINT32(b->buffer.datas[j].data); - m = pw_array_get_unchecked(&impl->mems, id, struct mem); - m->ref--; - } - } - m = pw_array_get_unchecked(&impl->mems, b->memid, struct mem); - m->ref--; - } - port->n_buffers = 0; - return 0; -} - -static void emit_port_info(struct node *this, struct port *port) -{ - spa_node_emit_port_info(&this->hooks, - port->direction, port->id, &port->info); -} - -static int impl_node_add_listener(void *object, - struct spa_hook *listener, - const struct spa_node_events *events, - void *data) -{ - struct node *this = object; - struct spa_hook_list save; - uint32_t i; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - spa_hook_list_isolate(&this->hooks, &save, listener, events, data); - - for (i = 0; i < MAX_INPUTS; i++) { - if (this->in_ports[i].valid) - emit_port_info(this, &this->in_ports[i]); - } - for (i = 0; i < MAX_OUTPUTS; i++) { - if (this->out_ports[i].valid) - emit_port_info(this, &this->out_ports[i]); - } - spa_hook_list_join(&this->hooks, &save); - - return 0; -} - -static int impl_node_enum_params(void *object, int seq, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) -{ - struct node *this = object; - uint8_t buffer[1024]; - struct spa_pod_builder b = { 0 }; - struct spa_result_node_params result; - uint32_t count = 0; - bool found = false; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(num != 0, -EINVAL); - - result.id = id; - result.next = 0; - - while (true) { - struct spa_pod *param; - - result.index = result.next++; - if (result.index >= this->n_params) - break; - - param = this->params[result.index]; - - if (param == NULL || !spa_pod_is_object_id(param, id)) - continue; - - found = true; - - if (result.index < start) - continue; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - if (spa_pod_filter(&b, &result.param, param, filter) != 0) - continue; - - pw_log_debug("%p: %d param %u", this, seq, result.index); - spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); - - if (++count == num) - break; - } - return found ? 0 : -ENOENT; -} - -static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, - const struct spa_pod *param) -{ - struct node *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - if (this->resource == NULL) - return -EIO; - - pw_client_node0_resource_set_param(this->resource, this->seq, id, flags, param); - - return SPA_RESULT_RETURN_ASYNC(this->seq++); -} - -static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) -{ - struct node *this = object; - int res = 0; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - switch(id) { - case SPA_IO_Position: - this->position = data; - break; - default: - res = -ENOTSUP; - break; - } - return res; -} - -static inline void do_flush(struct node *this) -{ - if (spa_system_eventfd_write(this->data_system, this->writefd, 1) < 0) - spa_log_warn(this->log, "node %p: error flushing : %s", this, strerror(errno)); - -} - -static int send_clock_update(struct node *this) -{ - struct pw_impl_client *client = pw_resource_get_client(this->resource); - uint32_t type = pw_protocol_native0_name_to_v2(client, SPA_TYPE_INFO_NODE_COMMAND_BASE "ClockUpdate"); - struct timespec ts; - int64_t now; - - clock_gettime(CLOCK_MONOTONIC, &ts); - now = SPA_TIMESPEC_TO_NSEC(&ts); - pw_log_trace("%p: now %"PRIi64, this, now); - - struct spa_command_node0_clock_update cu = - SPA_COMMAND_NODE0_CLOCK_UPDATE_INIT(type, - SPA_COMMAND_NODE0_CLOCK_UPDATE_TIME | - SPA_COMMAND_NODE0_CLOCK_UPDATE_SCALE | - SPA_COMMAND_NODE0_CLOCK_UPDATE_STATE | - SPA_COMMAND_NODE0_CLOCK_UPDATE_LATENCY, /* change_mask */ - SPA_USEC_PER_SEC, /* rate */ - now / SPA_NSEC_PER_USEC, /* ticks */ - now, /* monotonic_time */ - 0, /* offset */ - (1 << 16) | 1, /* scale */ - SPA_CLOCK0_STATE_RUNNING, /* state */ - SPA_COMMAND_NODE0_CLOCK_UPDATE_FLAG_LIVE, /* flags */ - 0); /* latency */ - - pw_client_node0_resource_command(this->resource, this->seq, (const struct spa_command*)&cu); - return SPA_RESULT_RETURN_ASYNC(this->seq++); -} - -static int impl_node_send_command(void *object, const struct spa_command *command) -{ - struct node *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(command != NULL, -EINVAL); - - if (this->resource == NULL) - return -EIO; - - if (SPA_NODE_COMMAND_ID(command) == SPA_NODE_COMMAND_Start) { - send_clock_update(this); - } - - pw_client_node0_resource_command(this->resource, this->seq, command); - return SPA_RESULT_RETURN_ASYNC(this->seq++); -} - -static int -impl_node_set_callbacks(void *object, - const struct spa_node_callbacks *callbacks, - void *data) -{ - struct node *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); - - return 0; -} - -static int -impl_node_sync(void *object, int seq) -{ - struct node *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - pw_log_debug("%p: sync %p", this, this->resource); - - if (this->resource == NULL) - return -EIO; - - this->init_pending = SPA_RESULT_RETURN_ASYNC(this->seq++); - - return this->init_pending; -} - - -extern struct spa_pod *pw_protocol_native0_pod_from_v2(struct pw_impl_client *client, const struct spa_pod *pod); -extern int pw_protocol_native0_pod_to_v2(struct pw_impl_client *client, const struct spa_pod *pod, - struct spa_pod_builder *b); - -static void -do_update_port(struct node *this, - enum spa_direction direction, - uint32_t port_id, - uint32_t change_mask, - uint32_t n_params, - const struct spa_pod **params, - const struct spa_port_info *info) -{ - struct port *port; - - port = GET_PORT(this, direction, port_id); - - if (!port->valid) { - spa_log_debug(this->log, "node %p: adding port %d, direction %d", - this, port_id, direction); - port->id = port_id; - port->direction = direction; - port->have_format = false; - port->valid = true; - - if (direction == SPA_DIRECTION_INPUT) - this->n_inputs++; - else - this->n_outputs++; - } - - if (change_mask & PW_CLIENT_NODE0_PORT_UPDATE_PARAMS) { - uint32_t i; - - port->have_format = false; - - spa_log_debug(this->log, "node %p: port %u update %d params", this, port_id, n_params); - for (i = 0; i < port->n_params; i++) - free(port->params[i]); - port->n_params = n_params; - if (port->n_params == 0) { - free(port->params); - port->params = NULL; - } else { - void *p; - p = pw_reallocarray(port->params, port->n_params, sizeof(struct spa_pod *)); - if (p == NULL) { - pw_log_error("%p: port %u can't realloc: %m", this, port_id); - free(port->params); - port->n_params = 0; - } - port->params = p; - } - for (i = 0; i < port->n_params; i++) { - port->params[i] = params[i] ? - pw_protocol_native0_pod_from_v2(pw_resource_get_client(this->resource), params[i]) : NULL; - - if (port->params[i] && spa_pod_is_object_id(port->params[i], SPA_PARAM_Format)) - port->have_format = true; - } - } - - if (change_mask & PW_CLIENT_NODE0_PORT_UPDATE_INFO) { - pw_properties_free(port->properties); - port->properties = NULL; - port->info.props = NULL; - port->info.n_params = 0; - port->info.params = NULL; - - if (info) { - port->info = *info; - if (info->props) { - port->properties = pw_properties_new_dict(info->props); - port->info.props = &port->properties->dict; - } - } - spa_node_emit_port_info(&this->hooks, direction, port_id, info); - } -} - -static void -clear_port(struct node *this, - struct port *port, enum spa_direction direction, uint32_t port_id) -{ - do_update_port(this, - direction, - port_id, - PW_CLIENT_NODE0_PORT_UPDATE_PARAMS | - PW_CLIENT_NODE0_PORT_UPDATE_INFO, 0, NULL, NULL); - clear_buffers(this, port); -} - -static void do_uninit_port(struct node *this, enum spa_direction direction, uint32_t port_id) -{ - struct port *port; - - spa_log_debug(this->log, "node %p: removing port %d", this, port_id); - - if (direction == SPA_DIRECTION_INPUT) { - port = GET_IN_PORT(this, port_id); - this->n_inputs--; - } else { - port = GET_OUT_PORT(this, port_id); - this->n_outputs--; - } - clear_port(this, port, direction, port_id); - port->valid = false; - spa_node_emit_port_info(&this->hooks, direction, port_id, NULL); -} - -static int -impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, - const struct spa_dict *props) -{ - struct node *this = object; - struct port *port; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_FREE_PORT(this, direction, port_id), -EINVAL); - - port = GET_PORT(this, direction, port_id); - clear_port(this, port, direction, port_id); - - return 0; -} - -static int -impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) -{ - struct node *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - - do_uninit_port(this, direction, port_id); - - return 0; -} - -static int -impl_node_port_enum_params(void *object, int seq, - enum spa_direction direction, uint32_t port_id, - uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter) -{ - struct node *this = object; - struct port *port; - uint8_t buffer[1024]; - struct spa_pod_builder b = { 0 }; - struct spa_result_node_params result; - uint32_t count = 0; - bool found = false; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(num != 0, -EINVAL); - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - - port = GET_PORT(this, direction, port_id); - - pw_log_debug("%p: %d port %d.%d %u %u %u", this, seq, - direction, port_id, id, start, num); - - result.id = id; - result.next = 0; - - while (true) { - struct spa_pod *param; - - result.index = result.next++; - if (result.index >= port->n_params) - break; - - param = port->params[result.index]; - - if (param == NULL || !spa_pod_is_object_id(param, id)) - continue; - - found = true; - - if (result.index < start) - continue; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - if (spa_pod_filter(&b, &result.param, param, filter) < 0) - continue; - - pw_log_debug("%p: %d param %u", this, seq, result.index); - spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); - - if (++count == num) - break; - } - return found ? 0 : -ENOENT; -} - -static int -impl_node_port_set_param(void *object, - enum spa_direction direction, uint32_t port_id, - uint32_t id, uint32_t flags, - const struct spa_pod *param) -{ - struct node *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - - if (this->resource == NULL) - return -EIO; - - pw_client_node0_resource_port_set_param(this->resource, - this->seq, - direction, port_id, - id, flags, - param); - return SPA_RESULT_RETURN_ASYNC(this->seq++); -} - -static int -impl_node_port_set_io(void *object, - enum spa_direction direction, - uint32_t port_id, - uint32_t id, - void *data, size_t size) -{ - struct node *this = object; - struct impl *impl; - struct pw_memblock *mem; - struct mem *m; - uint32_t memid, mem_offset, mem_size; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - - impl = this->impl; - - spa_log_debug(this->log, "node %p: port %d.%d set io %d %p", this, - direction, port_id, id, data); - - if (id == SPA_IO_Buffers) { - struct port *port = GET_PORT(this, direction, port_id); - port->io = data; - } - - if (this->resource == NULL) - return -EIO; - - - if (data) { - if ((mem = pw_mempool_find_ptr(impl->context_pool, data)) == NULL) - return -EINVAL; - - mem_offset = SPA_PTRDIFF(data, mem->map->ptr); - mem_size = mem->size; - if (mem_size - mem_offset < size) - return -EINVAL; - - mem_offset += mem->map->offset; - m = ensure_mem(impl, mem->fd, SPA_DATA_MemFd, mem->flags); - memid = m->id; - } - else { - memid = SPA_ID_INVALID; - mem_offset = mem_size = 0; - } - - pw_client_node0_resource_port_set_io(this->resource, - this->seq, - direction, port_id, - id, - memid, - mem_offset, mem_size); - return SPA_RESULT_RETURN_ASYNC(this->seq++); -} - -static int -impl_node_port_use_buffers(void *object, - enum spa_direction direction, - uint32_t port_id, - uint32_t flags, - struct spa_buffer **buffers, - uint32_t n_buffers) -{ - struct node *this = object; - struct impl *impl; - struct port *port; - uint32_t i, j; - struct pw_client_node0_buffer *mb; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - - impl = this->impl; - spa_log_debug(this->log, "node %p: use buffers %p %u", this, buffers, n_buffers); - - port = GET_PORT(this, direction, port_id); - - if (!port->have_format) - return -EIO; - - clear_buffers(this, port); - - if (n_buffers > 0) { - mb = alloca(n_buffers * sizeof(struct pw_client_node0_buffer)); - } else { - mb = NULL; - } - - port->n_buffers = n_buffers; - - if (this->resource == NULL) - return -EIO; - - for (i = 0; i < n_buffers; i++) { - struct buffer *b = &port->buffers[i]; - struct pw_memblock *mem; - struct mem *m; - size_t data_size; - void *baseptr; - - b->outbuf = buffers[i]; - memcpy(&b->buffer, buffers[i], sizeof(struct spa_buffer)); - b->buffer.datas = b->datas; - b->buffer.metas = b->metas; - - if (buffers[i]->n_metas > 0) - baseptr = buffers[i]->metas[0].data; - else if (buffers[i]->n_datas > 0) - baseptr = buffers[i]->datas[0].chunk; - else - return -EINVAL; - - if ((mem = pw_mempool_find_ptr(impl->context_pool, baseptr)) == NULL) - return -EINVAL; - - data_size = 0; - for (j = 0; j < buffers[i]->n_metas; j++) { - data_size += buffers[i]->metas[j].size; - } - for (j = 0; j < buffers[i]->n_datas; j++) { - struct spa_data *d = buffers[i]->datas; - data_size += sizeof(struct spa_chunk); - if (d->type == SPA_DATA_MemPtr) - data_size += d->maxsize; - } - - m = ensure_mem(impl, mem->fd, SPA_DATA_MemFd, mem->flags); - b->memid = m->id; - - mb[i].buffer = &b->buffer; - mb[i].mem_id = b->memid; - mb[i].offset = SPA_PTRDIFF(baseptr, SPA_PTROFF(mem->map->ptr, mem->map->offset, void)); - mb[i].size = data_size; - - for (j = 0; j < buffers[i]->n_metas; j++) - memcpy(&b->buffer.metas[j], &buffers[i]->metas[j], sizeof(struct spa_meta)); - b->buffer.n_metas = j; - - for (j = 0; j < buffers[i]->n_datas; j++) { - struct spa_data *d = &buffers[i]->datas[j]; - - memcpy(&b->buffer.datas[j], d, sizeof(struct spa_data)); - - if (d->type == SPA_DATA_DmaBuf || - d->type == SPA_DATA_MemFd) { - m = ensure_mem(impl, d->fd, d->type, d->flags); - b->buffer.datas[j].data = SPA_UINT32_TO_PTR(m->id); - } else if (d->type == SPA_DATA_MemPtr) { - b->buffer.datas[j].data = SPA_INT_TO_PTR(SPA_PTRDIFF(d->data, baseptr)); - } else { - b->buffer.datas[j].type = SPA_ID_INVALID; - b->buffer.datas[j].data = 0; - spa_log_error(this->log, "invalid memory type %d", d->type); - } - } - } - - pw_client_node0_resource_port_use_buffers(this->resource, - this->seq, - direction, port_id, - n_buffers, mb); - - return SPA_RESULT_RETURN_ASYNC(this->seq++); -} - -static int -impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) -{ - struct node *this = object; - struct impl *impl; - - spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_OUT_PORT(this, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); - - impl = this->impl; - - spa_log_trace(this->log, "reuse buffer %d", buffer_id); - - pw_client_node0_transport_add_message(impl->transport, (struct pw_client_node0_message *) - &PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER_INIT(port_id, buffer_id)); - do_flush(this); - - return 0; -} - -static int impl_node_process_input(struct spa_node *node) -{ - struct node *this = SPA_CONTAINER_OF(node, struct node, node); - struct impl *impl = this->impl; -// bool client_reuse = impl->client_reuse; - uint32_t i; - int res; - - if (impl->input_ready == 0) { - /* the client is not ready to receive our buffers, recycle them */ - pw_log_trace("node not ready, recycle buffers"); - for (i = 0; i < MAX_INPUTS; i++) { - struct port *p = &this->in_ports[i]; - struct spa_io_buffers *io = p->io; - - if (!p->valid || io == NULL) - continue; - - io->status = SPA_STATUS_NEED_DATA; - } - res = SPA_STATUS_NEED_DATA; - } - else { - for (i = 0; i < MAX_INPUTS; i++) { - struct port *p = &this->in_ports[i]; - struct spa_io_buffers *io = p->io; - - if (!p->valid || io == NULL) - continue; - - pw_log_trace("set io status to %d %d", io->status, io->buffer_id); - impl->transport->inputs[p->id] = *io; - - /* explicitly recycle buffers when the client is not going to do it */ -// if (!client_reuse && (pp = p->peer)) -// spa_node_port_reuse_buffer(pp->node->implementation, -// pp->port_id, io->buffer_id); - } - pw_client_node0_transport_add_message(impl->transport, - &PW_CLIENT_NODE0_MESSAGE_INIT(PW_CLIENT_NODE0_MESSAGE_PROCESS_INPUT)); - do_flush(this); - - impl->input_ready--; - res = SPA_STATUS_OK; - } - return res; -} - -#if 0 -/** this is used for clients providing data to pipewire and currently - * not supported in the compat layer */ -static int impl_node_process_output(struct spa_node *node) -{ - struct node *this; - struct impl *impl; - uint32_t i; - - this = SPA_CONTAINER_OF(node, struct node, node); - impl = this->impl; - - if (impl->out_pending) - goto done; - - impl->out_pending = true; - - for (i = 0; i < MAX_OUTPUTS; i++) { - struct port *p = &this->out_ports[i]; - struct spa_io_buffers *io = p->io; - - if (!p->valid || io == NULL) - continue; - - impl->transport->outputs[p->id] = *io; - - pw_log_trace("%d %d -> %d %d", io->status, io->buffer_id, - impl->transport->outputs[p->id].status, - impl->transport->outputs[p->id].buffer_id); - } - - done: - pw_client_node0_transport_add_message(impl->transport, - &PW_CLIENT_NODE0_MESSAGE_INIT(PW_CLIENT_NODE0_MESSAGE_PROCESS_OUTPUT)); - do_flush(this); - - return SPA_STATUS_OK; -} -#endif - -static int impl_node_process(void *object) -{ - struct node *this = object; - struct impl *impl = this->impl; - struct pw_impl_node *n = impl->this.node; - return impl_node_process_input(pw_impl_node_get_implementation(n)); -} - -static int handle_node_message(struct node *this, struct pw_client_node0_message *message) -{ - struct impl *impl = SPA_CONTAINER_OF(this, struct impl, node); - uint32_t i; - - switch (PW_CLIENT_NODE0_MESSAGE_TYPE(message)) { - case PW_CLIENT_NODE0_MESSAGE_HAVE_OUTPUT: - for (i = 0; i < MAX_OUTPUTS; i++) { - struct port *p = &this->out_ports[i]; - struct spa_io_buffers *io = p->io; - if (!p->valid || io == NULL) - continue; - *io = impl->transport->outputs[p->id]; - pw_log_trace("have output %d %d", io->status, io->buffer_id); - } - impl->out_pending = false; - spa_node_call_ready(&this->callbacks, SPA_STATUS_HAVE_DATA); - break; - - case PW_CLIENT_NODE0_MESSAGE_NEED_INPUT: - for (i = 0; i < MAX_INPUTS; i++) { - struct port *p = &this->in_ports[i]; - struct spa_io_buffers *io = p->io; - if (!p->valid || io == NULL) - continue; - pw_log_trace("need input %d %d", i, p->id); - *io = impl->transport->inputs[p->id]; - pw_log_trace("need input %d %d", io->status, io->buffer_id); - } - impl->input_ready++; - spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA); - break; - - case PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER: - if (impl->client_reuse) { - struct pw_client_node0_message_port_reuse_buffer *p = - (struct pw_client_node0_message_port_reuse_buffer *) message; - spa_node_call_reuse_buffer(&this->callbacks, p->body.port_id.value, - p->body.buffer_id.value); - } - break; - - default: - pw_log_warn("unhandled message %d", PW_CLIENT_NODE0_MESSAGE_TYPE(message)); - return -ENOTSUP; - } - return 0; -} - -static void setup_transport(struct impl *impl) -{ - struct node *this = &impl->node; - uint32_t max_inputs = 0, max_outputs = 0, n_inputs = 0, n_outputs = 0; - struct spa_dict_item items[1]; - - n_inputs = this->n_inputs; - max_inputs = this->info.max_input_ports == 0 ? this->n_inputs : this->info.max_input_ports; - n_outputs = this->n_outputs; - max_outputs = this->info.max_output_ports == 0 ? this->n_outputs : this->info.max_output_ports; - - impl->transport = pw_client_node0_transport_new(impl->context, max_inputs, max_outputs); - impl->transport->area->n_input_ports = n_inputs; - impl->transport->area->n_output_ports = n_outputs; - - if (n_inputs > 0) { - items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Stream/Input/Video"); - } else { - items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_MEDIA_CLASS, "Stream/Output/Video"); - } - pw_impl_node_update_properties(impl->this.node, &SPA_DICT_INIT(items, 1)); -} - -static void -client_node0_done(void *data, int seq, int res) -{ - struct impl *impl = data; - struct node *this = &impl->node; - - if (seq == 0 && res == 0 && impl->transport == NULL) - setup_transport(impl); - - pw_log_debug("seq:%d res:%d pending:%d", seq, res, this->init_pending); - spa_node_emit_result(&this->hooks, seq, res, 0, NULL); - - if (this->init_pending != SPA_ID_INVALID) { - spa_node_emit_result(&this->hooks, this->init_pending, res, 0, NULL); - this->init_pending = SPA_ID_INVALID; - } -} - -static void -client_node0_update(void *data, - uint32_t change_mask, - uint32_t max_input_ports, - uint32_t max_output_ports, - uint32_t n_params, - const struct spa_pod **params) -{ - struct impl *impl = data; - struct node *this = &impl->node; - - if (change_mask & PW_CLIENT_NODE0_UPDATE_MAX_INPUTS) - this->info.max_input_ports = max_input_ports; - if (change_mask & PW_CLIENT_NODE0_UPDATE_MAX_OUTPUTS) - this->info.max_output_ports = max_output_ports; - if (change_mask & PW_CLIENT_NODE0_UPDATE_PARAMS) { - uint32_t i; - spa_log_debug(this->log, "node %p: update %d params", this, n_params); - - for (i = 0; i < this->n_params; i++) - free(this->params[i]); - this->n_params = n_params; - if (this->n_params == 0) { - free(this->params); - this->params = NULL; - } else { - void *p; - p = pw_reallocarray(this->params, this->n_params, sizeof(struct spa_pod *)); - if (p == NULL) { - pw_log_error("%p: can't realloc: %m", this); - free(this->params); - this->n_params = 0; - } - this->params = p; - } - for (i = 0; i < this->n_params; i++) - this->params[i] = params[i] ? spa_pod_copy(params[i]) : NULL; - } - if (change_mask & (PW_CLIENT_NODE0_UPDATE_MAX_INPUTS | PW_CLIENT_NODE0_UPDATE_MAX_OUTPUTS)) { - spa_node_emit_info(&this->hooks, &this->info); - } - - spa_log_debug(this->log, "node %p: got node update max_in %u, max_out %u", this, - this->info.max_input_ports, this->info.max_output_ports); -} - -static void -client_node0_port_update(void *data, - enum spa_direction direction, - uint32_t port_id, - uint32_t change_mask, - uint32_t n_params, - const struct spa_pod **params, - const struct spa_port_info *info) -{ - struct impl *impl = data; - struct node *this = &impl->node; - bool remove; - - spa_log_debug(this->log, "node %p: got port update", this); - if (!CHECK_PORT_ID(this, direction, port_id)) - return; - - remove = (change_mask == 0); - - if (remove) { - do_uninit_port(this, direction, port_id); - } else { - do_update_port(this, - direction, - port_id, - change_mask, - n_params, params, info); - } -} - -static void client_node0_set_active(void *data, bool active) -{ - struct impl *impl = data; - pw_impl_node_set_active(impl->this.node, active); -} - -static void client_node0_event(void *data, struct spa_event *event) -{ - struct impl *impl = data; - struct node *this = &impl->node; - - switch (SPA_EVENT_TYPE(event)) { - case SPA_NODE0_EVENT_RequestClockUpdate: - send_clock_update(this); - break; - default: - spa_node_emit_event(&this->hooks, event); - } -} - -static const struct pw_client_node0_methods client_node0_methods = { - PW_VERSION_CLIENT_NODE0_METHODS, - .done = client_node0_done, - .update = client_node0_update, - .port_update = client_node0_port_update, - .set_active = client_node0_set_active, - .event = client_node0_event, -}; - -static void node_on_data_fd_events(struct spa_source *source) -{ - struct node *this = source->data; - struct impl *impl = this->impl; - - if (source->rmask & (SPA_IO_ERR | SPA_IO_HUP)) { - spa_log_warn(this->log, "node %p: got error", this); - return; - } - - if (source->rmask & SPA_IO_IN) { - struct pw_client_node0_message message; - uint64_t cmd; - - if (spa_system_eventfd_read(this->data_system, this->data_source.fd, &cmd) < 0) - spa_log_warn(this->log, "node %p: error reading message: %s", - this, strerror(errno)); - - while (pw_client_node0_transport_next_message(impl->transport, &message) == 1) { - struct pw_client_node0_message *msg = alloca(SPA_POD_SIZE(&message)); - pw_client_node0_transport_parse_message(impl->transport, msg); - handle_node_message(this, msg); - } - } -} - -static const struct spa_node_methods impl_node = { - SPA_VERSION_NODE_METHODS, - .add_listener = impl_node_add_listener, - .set_callbacks = impl_node_set_callbacks, - .sync = impl_node_sync, - .enum_params = impl_node_enum_params, - .set_param = impl_node_set_param, - .set_io = impl_node_set_io, - .send_command = impl_node_send_command, - .add_port = impl_node_add_port, - .remove_port = impl_node_remove_port, - .port_enum_params = impl_node_port_enum_params, - .port_set_param = impl_node_port_set_param, - .port_use_buffers = impl_node_port_use_buffers, - .port_set_io = impl_node_port_set_io, - .port_reuse_buffer = impl_node_port_reuse_buffer, - .process = impl_node_process, -}; - -static int -node_init(struct node *this, - struct spa_dict *info, - const struct spa_support *support, - uint32_t n_support) -{ - this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log); - this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); - this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem); - - if (this->data_loop == NULL) { - spa_log_error(this->log, "a data-loop is needed"); - return -EINVAL; - } - - this->node.iface = SPA_INTERFACE_INIT( - SPA_TYPE_INTERFACE_Node, - SPA_VERSION_NODE, - &impl_node, this); - spa_hook_list_init(&this->hooks); - - this->data_source.func = node_on_data_fd_events; - this->data_source.data = this; - this->data_source.fd = -1; - this->data_source.mask = SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP; - this->data_source.rmask = 0; - - this->seq = 1; - this->init_pending = SPA_ID_INVALID; - - return SPA_RESULT_RETURN_ASYNC(this->seq++); -} - -static int node_clear(struct node *this) -{ - uint32_t i; - - for (i = 0; i < MAX_INPUTS; i++) { - if (this->in_ports[i].valid) - clear_port(this, &this->in_ports[i], SPA_DIRECTION_INPUT, i); - } - for (i = 0; i < MAX_OUTPUTS; i++) { - if (this->out_ports[i].valid) - clear_port(this, &this->out_ports[i], SPA_DIRECTION_OUTPUT, i); - } - - return 0; -} - -static int do_remove_source(struct spa_loop *loop, - bool async, - uint32_t seq, - const void *data, - size_t size, - void *user_data) -{ - struct spa_source *source = user_data; - spa_loop_remove_source(loop, source); - return 0; -} - -static void client_node0_resource_destroy(void *data) -{ - struct impl *impl = data; - struct pw_impl_client_node0 *this = &impl->this; - struct node *node = &impl->node; - - pw_log_debug("client-node %p: destroy", impl); - - impl->node.resource = this->resource = NULL; - spa_hook_remove(&impl->resource_listener); - spa_hook_remove(&impl->object_listener); - - if (node->data_source.fd != -1) { - spa_loop_invoke(node->data_loop, - do_remove_source, - SPA_ID_INVALID, - NULL, - 0, - true, - &node->data_source); - } - if (this->node) - pw_impl_node_destroy(this->node); -} - -static void node_initialized(void *data) -{ - struct impl *impl = data; - struct pw_impl_client_node0 *this = &impl->this; - struct pw_impl_node *node = this->node; - struct spa_system *data_system = impl->node.data_system; - - if (this->resource == NULL) - return; - - impl->fds[0] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); - impl->fds[1] = spa_system_eventfd_create(data_system, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); - impl->node.data_source.fd = impl->fds[0]; - impl->node.writefd = impl->fds[1]; - impl->other_fds[0] = impl->fds[1]; - impl->other_fds[1] = impl->fds[0]; - - spa_loop_add_source(impl->node.data_loop, &impl->node.data_source); - pw_log_debug("client-node %p: transport fd %d %d", node, impl->fds[0], impl->fds[1]); - - pw_client_node0_resource_transport(this->resource, - pw_global_get_id(pw_impl_node_get_global(node)), - impl->other_fds[0], - impl->other_fds[1], - impl->transport); -} - -static void node_free(void *data) -{ - struct impl *impl = data; - struct pw_impl_client_node0 *this = &impl->this; - struct spa_system *data_system = impl->node.data_system; - - this->node = NULL; - - pw_log_debug("client-node %p: free", &impl->this); - node_clear(&impl->node); - - if (impl->transport) - pw_client_node0_transport_destroy(impl->transport); - - spa_hook_remove(&impl->node_listener); - - if (this->resource) - pw_resource_destroy(this->resource); - - pw_array_clear(&impl->mems); - - if (impl->fds[0] != -1) - spa_system_close(data_system, impl->fds[0]); - if (impl->fds[1] != -1) - spa_system_close(data_system, impl->fds[1]); - free(impl); -} - -static const struct pw_impl_node_events node_events = { - PW_VERSION_IMPL_NODE_EVENTS, - .free = node_free, - .initialized = node_initialized, -}; - -static const struct pw_resource_events resource_events = { - PW_VERSION_RESOURCE_EVENTS, - .destroy = client_node0_resource_destroy, -}; - -static void convert_properties(struct pw_properties *properties) -{ - static const struct { - const char *from, *to; - } props[] = { - { "pipewire.autoconnect", PW_KEY_NODE_AUTOCONNECT, }, - /* XXX deprecated */ - { "pipewire.target.node", PW_KEY_NODE_TARGET, } - }; - - const char *str; - - SPA_FOR_EACH_ELEMENT_VAR(props, p) { - if ((str = pw_properties_get(properties, p->from)) != NULL) { - pw_properties_set(properties, p->to, str); - pw_properties_set(properties, p->from, NULL); - } - } -} - -/** Create a new client node - * \param client an owner \ref pw_client - * \param id an id - * \param name a name - * \param properties extra properties - * \return a newly allocated client node - * - * Create a new \ref pw_impl_node. - * - * \memberof pw_impl_client_node - */ -struct pw_impl_client_node0 *pw_impl_client_node0_new(struct pw_resource *resource, - struct pw_properties *properties) -{ - struct impl *impl; - struct pw_impl_client_node0 *this; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct pw_context *context = pw_impl_client_get_context(client); - const struct spa_support *support; - uint32_t n_support; - const char *name; - int res; - - impl = calloc(1, sizeof(struct impl)); - if (impl == NULL) - return NULL; - - this = &impl->this; - - if (properties == NULL) - properties = pw_properties_new(NULL, NULL); - if (properties == NULL) { - res = -errno; - goto error_exit_free; - } - convert_properties(properties); - - pw_properties_setf(properties, PW_KEY_CLIENT_ID, "%d", pw_global_get_id(pw_impl_client_get_global(client))); - - impl->context = context; - impl->context_pool = pw_context_get_mempool(context); - impl->fds[0] = impl->fds[1] = -1; - pw_log_debug("client-node %p: new", impl); - - support = pw_context_get_support(impl->context, &n_support); - - node_init(&impl->node, NULL, support, n_support); - impl->node.impl = impl; - - pw_array_init(&impl->mems, 64); - - if ((name = pw_properties_get(properties, "node.name")) == NULL) - name = "client-node"; - pw_properties_set(properties, PW_KEY_MEDIA_TYPE, "Video"); - - impl->node.resource = resource; - this->resource = resource; - this->node = pw_spa_node_new(context, - PW_SPA_NODE_FLAG_ASYNC, - &impl->node.node, - NULL, - properties, 0); - if (this->node == NULL) { - res = -errno; - goto error_no_node; - } - - impl->client_reuse = pw_properties_get_bool(properties, "pipewire.client.reuse", false); - - pw_resource_add_listener(this->resource, - &impl->resource_listener, - &resource_events, - impl); - pw_resource_add_object_listener(this->resource, - &impl->object_listener, - &client_node0_methods, - impl); - - - pw_impl_node_add_listener(this->node, &impl->node_listener, &node_events, impl); - - return this; - -error_no_node: - pw_resource_destroy(this->resource); - node_clear(&impl->node); -error_exit_free: - free(impl); - errno = -res; - return NULL; -} - -/** Destroy a client node - * \param node the client node to destroy - * \memberof pw_impl_client_node - */ -void pw_impl_client_node0_destroy(struct pw_impl_client_node0 *node) -{ - pw_resource_destroy(node->resource); -} diff --git a/src/modules/module-client-node/v0/client-node.h b/src/modules/module-client-node/v0/client-node.h deleted file mode 100644 index 5c274060b..000000000 --- a/src/modules/module-client-node/v0/client-node.h +++ /dev/null @@ -1,91 +0,0 @@ -/* PipeWire */ -/* SPDX-FileCopyrightText: Copyright © 2015 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -#ifndef PIPEWIRE_CLIENT_NODE0_H -#define PIPEWIRE_CLIENT_NODE0_H - -#include - -#include "ext-client-node.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** The state of the clock */ -enum spa_clock0_state { - SPA_CLOCK0_STATE_STOPPED, /*< the clock is stopped */ - SPA_CLOCK0_STATE_PAUSED, /*< the clock is paused */ - SPA_CLOCK0_STATE_RUNNING, /*< the clock is running */ -}; - -struct spa_command_node0_clock_update_body { - struct spa_pod_object_body body; -#define SPA_COMMAND_NODE0_CLOCK_UPDATE_TIME (1 << 0) -#define SPA_COMMAND_NODE0_CLOCK_UPDATE_SCALE (1 << 1) -#define SPA_COMMAND_NODE0_CLOCK_UPDATE_STATE (1 << 2) -#define SPA_COMMAND_NODE0_CLOCK_UPDATE_LATENCY (1 << 3) - struct spa_pod_int change_mask SPA_ALIGNED(8); - struct spa_pod_int rate SPA_ALIGNED(8); - struct spa_pod_long ticks SPA_ALIGNED(8); - struct spa_pod_long monotonic_time SPA_ALIGNED(8); - struct spa_pod_long offset SPA_ALIGNED(8); - struct spa_pod_int scale SPA_ALIGNED(8); - struct spa_pod_int state SPA_ALIGNED(8); -#define SPA_COMMAND_NODE0_CLOCK_UPDATE_FLAG_LIVE (1 << 0) - struct spa_pod_int flags SPA_ALIGNED(8); - struct spa_pod_long latency SPA_ALIGNED(8); -}; - -struct spa_command_node0_clock_update { - struct spa_pod pod; - struct spa_command_node0_clock_update_body body; -}; - -enum spa_node0_event { - SPA_NODE0_EVENT_START = SPA_TYPE_VENDOR_PipeWire, - SPA_NODE0_EVENT_RequestClockUpdate, -}; - -enum spa_node0_command { - SPA_NODE0_COMMAND_START = SPA_TYPE_VENDOR_PipeWire, - SPA_NODE0_COMMAND_ClockUpdate, -}; - -#define SPA_COMMAND_NODE0_CLOCK_UPDATE_INIT(type,change_mask,rate,ticks,monotonic_time,offset,scale,state,flags,latency) \ - SPA_COMMAND_INIT_FULL(struct spa_command_node0_clock_update, \ - sizeof(struct spa_command_node0_clock_update_body), 0, type, \ - SPA_POD_INIT_Int(change_mask), \ - SPA_POD_INIT_Int(rate), \ - SPA_POD_INIT_Long(ticks), \ - SPA_POD_INIT_Long(monotonic_time), \ - SPA_POD_INIT_Long(offset), \ - SPA_POD_INIT_Int(scale), \ - SPA_POD_INIT_Int(state), \ - SPA_POD_INIT_Int(flags), \ - SPA_POD_INIT_Long(latency)) - - -/** \class pw_impl_client_node0 - * - * PipeWire client node interface - */ -struct pw_impl_client_node0 { - struct pw_impl_node *node; - - struct pw_resource *resource; -}; - -struct pw_impl_client_node0 * -pw_impl_client_node0_new(struct pw_resource *resource, - struct pw_properties *properties); - -void -pw_impl_client_node0_destroy(struct pw_impl_client_node0 *node); - -#ifdef __cplusplus -} -#endif - -#endif /* PIPEWIRE_CLIENT_NODE0_H */ diff --git a/src/modules/module-client-node/v0/ext-client-node.h b/src/modules/module-client-node/v0/ext-client-node.h deleted file mode 100644 index bd2f69bd6..000000000 --- a/src/modules/module-client-node/v0/ext-client-node.h +++ /dev/null @@ -1,394 +0,0 @@ -/* PipeWire */ -/* SPDX-FileCopyrightText: Copyright © 2016 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -#ifndef __PIPEWIRE_EXT_CLIENT_NODE0_H__ -#define __PIPEWIRE_EXT_CLIENT_NODE0_H__ - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include - -#include - -#define PW_TYPE_INTERFACE_ClientNode PW_TYPE_INFO_INTERFACE_BASE "ClientNode" - -#define PW_VERSION_CLIENT_NODE0 0 - -struct pw_client_node0_message; - -/** Shared structure between client and server \memberof pw_client_node */ -struct pw_client_node0_area { - uint32_t max_input_ports; /**< max input ports of the node */ - uint32_t n_input_ports; /**< number of input ports of the node */ - uint32_t max_output_ports; /**< max output ports of the node */ - uint32_t n_output_ports; /**< number of output ports of the node */ -}; - -/** \class pw_client_node0_transport - * - * \brief Transport object - * - * The transport object contains shared data and ringbuffers to exchange - * events and data between the server and the client in a low-latency and - * lockfree way. - */ -struct pw_client_node0_transport { - struct pw_client_node0_area *area; /**< the transport area */ - struct spa_io_buffers *inputs; /**< array of buffer input io */ - struct spa_io_buffers *outputs; /**< array of buffer output io */ - void *input_data; /**< input memory for ringbuffer */ - struct spa_ringbuffer *input_buffer; /**< ringbuffer for input memory */ - void *output_data; /**< output memory for ringbuffer */ - struct spa_ringbuffer *output_buffer; /**< ringbuffer for output memory */ - - /** Destroy a transport - * \param trans a transport to destroy - * \memberof pw_client_node0_transport - */ - void (*destroy) (struct pw_client_node0_transport *trans); - - /** Add a message to the transport - * \param trans the transport to send the message on - * \param message the message to add - * \return 0 on success, < 0 on error - * - * Write \a message to the shared ringbuffer. - */ - int (*add_message) (struct pw_client_node0_transport *trans, struct pw_client_node0_message *message); - - /** Get next message from a transport - * \param trans the transport to get the message of - * \param[out] message the message to read - * \return < 0 on error, 1 when a message is available, - * 0 when no more messages are available. - * - * Get the skeleton next message from \a trans into \a message. This function will - * only read the head and object body of the message. - * - * After the complete size of the message has been calculated, you should call - * \ref parse_message() to read the complete message contents. - */ - int (*next_message) (struct pw_client_node0_transport *trans, struct pw_client_node0_message *message); - - /** Parse the complete message on transport - * \param trans the transport to read from - * \param[out] message memory that can hold the complete message - * \return 0 on success, < 0 on error - * - * Use this function after \ref next_message(). - */ - int (*parse_message) (struct pw_client_node0_transport *trans, void *message); -}; - -#define pw_client_node0_transport_destroy(t) ((t)->destroy((t))) -#define pw_client_node0_transport_add_message(t,m) ((t)->add_message((t), (m))) -#define pw_client_node0_transport_next_message(t,m) ((t)->next_message((t), (m))) -#define pw_client_node0_transport_parse_message(t,m) ((t)->parse_message((t), (m))) - -enum pw_client_node0_message_type { - PW_CLIENT_NODE0_MESSAGE_HAVE_OUTPUT, /*< signal that the node has output */ - PW_CLIENT_NODE0_MESSAGE_NEED_INPUT, /*< signal that the node needs input */ - PW_CLIENT_NODE0_MESSAGE_PROCESS_INPUT, /*< instruct the node to process input */ - PW_CLIENT_NODE0_MESSAGE_PROCESS_OUTPUT, /*< instruct the node output is processed */ - PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER, /*< reuse a buffer */ -}; - -struct pw_client_node0_message_body { - struct spa_pod_int type SPA_ALIGNED(8); /*< one of enum pw_client_node0_message_type */ -}; - -struct pw_client_node0_message { - struct spa_pod_struct pod; - struct pw_client_node0_message_body body; -}; - -struct pw_client_node0_message_port_reuse_buffer_body { - struct spa_pod_int type SPA_ALIGNED(8); /*< PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER */ - struct spa_pod_int port_id SPA_ALIGNED(8); /*< port id */ - struct spa_pod_int buffer_id SPA_ALIGNED(8); /*< buffer id to reuse */ -}; - -struct pw_client_node0_message_port_reuse_buffer { - struct spa_pod_struct pod; - struct pw_client_node0_message_port_reuse_buffer_body body; -}; - -#define PW_CLIENT_NODE0_MESSAGE_TYPE(message) (((struct pw_client_node0_message*)(message))->body.type.value) - -#define PW_CLIENT_NODE0_MESSAGE_INIT(message) ((struct pw_client_node0_message) \ - { { { sizeof(struct pw_client_node0_message_body), SPA_TYPE_Struct } }, \ - { SPA_POD_INIT_Int(message) } }) - -#define PW_CLIENT_NODE0_MESSAGE_INIT_FULL(type,size,message,...) (type) \ - { { { size, SPA_TYPE_Struct } }, \ - { SPA_POD_INIT_Int(message), ##__VA_ARGS__ } } \ - -#define PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER_INIT(port_id,buffer_id) \ - PW_CLIENT_NODE0_MESSAGE_INIT_FULL(struct pw_client_node0_message_port_reuse_buffer, \ - sizeof(struct pw_client_node0_message_port_reuse_buffer_body), \ - PW_CLIENT_NODE0_MESSAGE_PORT_REUSE_BUFFER, \ - SPA_POD_INIT_Int(port_id), \ - SPA_POD_INIT_Int(buffer_id)) - -/** information about a buffer */ -struct pw_client_node0_buffer { - uint32_t mem_id; /**< the memory id for the metadata */ - uint32_t offset; /**< offset in memory */ - uint32_t size; /**< size in memory */ - struct spa_buffer *buffer; /**< buffer describing metadata and buffer memory */ -}; - -#define PW_CLIENT_NODE0_METHOD_DONE 0 -#define PW_CLIENT_NODE0_METHOD_UPDATE 1 -#define PW_CLIENT_NODE0_METHOD_PORT_UPDATE 2 -#define PW_CLIENT_NODE0_METHOD_SET_ACTIVE 3 -#define PW_CLIENT_NODE0_METHOD_EVENT 4 -#define PW_CLIENT_NODE0_METHOD_DESTROY 5 -#define PW_CLIENT_NODE0_METHOD_NUM 6 - -/** \ref pw_client_node methods */ -struct pw_client_node0_methods { -#define PW_VERSION_CLIENT_NODE0_METHODS 0 - uint32_t version; - - /** Complete an async operation */ - void (*done) (void *object, int seq, int res); - - /** - * Update the node ports and properties - * - * Update the maximum number of ports and the params of the - * client node. - * \param change_mask bitfield with changed parameters - * \param max_input_ports new max input ports - * \param max_output_ports new max output ports - * \param params new params - */ - void (*update) (void *object, -#define PW_CLIENT_NODE0_UPDATE_MAX_INPUTS (1 << 0) -#define PW_CLIENT_NODE0_UPDATE_MAX_OUTPUTS (1 << 1) -#define PW_CLIENT_NODE0_UPDATE_PARAMS (1 << 2) - uint32_t change_mask, - uint32_t max_input_ports, - uint32_t max_output_ports, - uint32_t n_params, - const struct spa_pod **params); - - /** - * Update a node port - * - * Update the information of one port of a node. - * \param direction the direction of the port - * \param port_id the port id to update - * \param change_mask a bitfield of changed items - * \param n_params number of port parameters - * \param params array of port parameters - * \param info port information - */ - void (*port_update) (void *object, - enum spa_direction direction, - uint32_t port_id, -#define PW_CLIENT_NODE0_PORT_UPDATE_PARAMS (1 << 0) -#define PW_CLIENT_NODE0_PORT_UPDATE_INFO (1 << 1) - uint32_t change_mask, - uint32_t n_params, - const struct spa_pod **params, - const struct spa_port_info *info); - /** - * Activate or deactivate the node - */ - void (*set_active) (void *object, bool active); - /** - * Send an event to the node - * \param event the event to send - */ - void (*event) (void *object, struct spa_event *event); - /** - * Destroy the client_node - */ - void (*destroy) (void *object); -}; - -#define PW_CLIENT_NODE0_EVENT_ADD_MEM 0 -#define PW_CLIENT_NODE0_EVENT_TRANSPORT 1 -#define PW_CLIENT_NODE0_EVENT_SET_PARAM 2 -#define PW_CLIENT_NODE0_EVENT_EVENT 3 -#define PW_CLIENT_NODE0_EVENT_COMMAND 4 -#define PW_CLIENT_NODE0_EVENT_ADD_PORT 5 -#define PW_CLIENT_NODE0_EVENT_REMOVE_PORT 6 -#define PW_CLIENT_NODE0_EVENT_PORT_SET_PARAM 7 -#define PW_CLIENT_NODE0_EVENT_PORT_USE_BUFFERS 8 -#define PW_CLIENT_NODE0_EVENT_PORT_COMMAND 9 -#define PW_CLIENT_NODE0_EVENT_PORT_SET_IO 10 -#define PW_CLIENT_NODE0_EVENT_NUM 11 - -/** \ref pw_client_node events */ -struct pw_client_node0_events { -#define PW_VERSION_CLIENT_NODE0_EVENTS 0 - uint32_t version; - /** - * Memory was added to a node - * - * \param mem_id the id of the memory - * \param type the memory type - * \param memfd the fd of the memory - * \param flags flags for the \a memfd - */ - void (*add_mem) (void *data, - uint32_t mem_id, - uint32_t type, - int memfd, - uint32_t flags); - /** - * Notify of a new transport area - * - * The transport area is used to exchange real-time commands between - * the client and the server. - * - * \param node_id the node id created for this client node - * \param readfd fd for signal data can be read - * \param writefd fd for signal data can be written - * \param transport the shared transport area - */ - void (*transport) (void *data, - uint32_t node_id, - int readfd, - int writefd, - struct pw_client_node0_transport *transport); - /** - * Notify of a property change - * - * When the server configures the properties on the node - * this event is sent - * - * \param seq a sequence number - * \param id the id of the parameter - * \param flags parameter flags - * \param param the param to set - */ - void (*set_param) (void *data, uint32_t seq, - uint32_t id, uint32_t flags, - const struct spa_pod *param); - /** - * Receive an event from the client node - * \param event the received event */ - void (*event) (void *data, const struct spa_event *event); - /** - * Notify of a new node command - * - * \param seq a sequence number - * \param command the command - */ - void (*command) (void *data, uint32_t seq, const struct spa_command *command); - /** - * A new port was added to the node - * - * The server can at any time add a port to the node when there - * are free ports available. - * - * \param seq a sequence number - * \param direction the direction of the port - * \param port_id the new port id - */ - void (*add_port) (void *data, - uint32_t seq, - enum spa_direction direction, - uint32_t port_id); - /** - * A port was removed from the node - * - * \param seq a sequence number - * \param direction a port direction - * \param port_id the remove port id - */ - void (*remove_port) (void *data, - uint32_t seq, - enum spa_direction direction, - uint32_t port_id); - /** - * A parameter was configured on the port - * - * \param seq a sequence number - * \param direction a port direction - * \param port_id the port id - * \param id the id of the parameter - * \param flags flags used when setting the param - * \param param the new param - */ - void (*port_set_param) (void *data, - uint32_t seq, - enum spa_direction direction, - uint32_t port_id, - uint32_t id, uint32_t flags, - const struct spa_pod *param); - /** - * Notify the port of buffers - * - * \param seq a sequence number - * \param direction a port direction - * \param port_id the port id - * \param n_buffer the number of buffers - * \param buffers and array of buffer descriptions - */ - void (*port_use_buffers) (void *data, - uint32_t seq, - enum spa_direction direction, - uint32_t port_id, - uint32_t n_buffers, - struct pw_client_node0_buffer *buffers); - /** - * Notify of a new port command - * - * \param direction a port direction - * \param port_id the port id - * \param command the command - */ - void (*port_command) (void *data, - enum spa_direction direction, - uint32_t port_id, - const struct spa_command *command); - - /** - * Configure the io area with \a id of \a port_id. - * - * \param seq a sequence number - * \param direction the direction of the port - * \param port_id the port id - * \param id the id of the io area to set - * \param mem_id the id of the memory to use - * \param offset offset of io area in memory - * \param size size of the io area - */ - void (*port_set_io) (void *data, - uint32_t seq, - enum spa_direction direction, - uint32_t port_id, - uint32_t id, - uint32_t mem_id, - uint32_t offset, - uint32_t size); -}; -#define pw_client_node0_resource(r,m,v,...) pw_resource_call(r, struct pw_client_node0_events, m, v, ##__VA_ARGS__) - -#define pw_client_node0_resource_add_mem(r,...) pw_client_node0_resource(r,add_mem,0,__VA_ARGS__) -#define pw_client_node0_resource_transport(r,...) pw_client_node0_resource(r,transport,0,__VA_ARGS__) -#define pw_client_node0_resource_set_param(r,...) pw_client_node0_resource(r,set_param,0,__VA_ARGS__) -#define pw_client_node0_resource_event(r,...) pw_client_node0_resource(r,event,0,__VA_ARGS__) -#define pw_client_node0_resource_command(r,...) pw_client_node0_resource(r,command,0,__VA_ARGS__) -#define pw_client_node0_resource_add_port(r,...) pw_client_node0_resource(r,add_port,0,__VA_ARGS__) -#define pw_client_node0_resource_remove_port(r,...) pw_client_node0_resource(r,remove_port,0,__VA_ARGS__) -#define pw_client_node0_resource_port_set_param(r,...) pw_client_node0_resource(r,port_set_param,0,__VA_ARGS__) -#define pw_client_node0_resource_port_use_buffers(r,...) pw_client_node0_resource(r,port_use_buffers,0,__VA_ARGS__) -#define pw_client_node0_resource_port_command(r,...) pw_client_node0_resource(r,port_command,0,__VA_ARGS__) -#define pw_client_node0_resource_port_set_io(r,...) pw_client_node0_resource(r,port_set_io,0,__VA_ARGS__) - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif /* __PIPEWIRE_EXT_CLIENT_NODE0_H__ */ diff --git a/src/modules/module-client-node/v0/protocol-native.c b/src/modules/module-client-node/v0/protocol-native.c deleted file mode 100644 index a577c6f4a..000000000 --- a/src/modules/module-client-node/v0/protocol-native.c +++ /dev/null @@ -1,514 +0,0 @@ -/* PipeWire */ -/* SPDX-FileCopyrightText: Copyright © 2017 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -#include - -#include -#include -#include - -#include "pipewire/impl.h" - -#include "pipewire/extensions/protocol-native.h" - -#include "ext-client-node.h" - -#include "transport.h" - -#define PW_PROTOCOL_NATIVE_FLAG_REMAP (1<<0) - -extern uint32_t pw_protocol_native0_find_type(struct pw_impl_client *client, const char *type); -extern int pw_protocol_native0_pod_to_v2(struct pw_impl_client *client, const struct spa_pod *pod, - struct spa_pod_builder *b); -extern struct spa_pod * pw_protocol_native0_pod_from_v2(struct pw_impl_client *client, - const struct spa_pod *pod); -extern uint32_t pw_protocol_native0_type_to_v2(struct pw_impl_client *client, - const struct spa_type_info *info, uint32_t type); - -static void -client_node_marshal_add_mem(void *data, - uint32_t mem_id, - uint32_t type, - int memfd, uint32_t flags) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_builder *b; - const char *typename; - - switch (type) { - case SPA_DATA_MemFd: - typename = "Spa:Enum:DataType:Fd:MemFd"; - break; - case SPA_DATA_DmaBuf: - typename = "Spa:Enum:DataType:Fd:DmaBuf"; - break; - default: - case SPA_DATA_MemPtr: - return; - - } - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_ADD_MEM, NULL); - - spa_pod_builder_add_struct(b, - "i", mem_id, - "I", pw_protocol_native0_find_type(client, typename), - "i", pw_protocol_native_add_resource_fd(resource, memfd), - "i", flags); - - pw_protocol_native_end_resource(resource, b); -} - -static void client_node_marshal_transport(void *data, uint32_t node_id, int readfd, int writefd, - struct pw_client_node0_transport *transport) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - struct pw_client_node0_transport_info info; - - pw_client_node0_transport_get_info(transport, &info); - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_TRANSPORT, NULL); - - spa_pod_builder_add_struct(b, - "i", node_id, - "i", pw_protocol_native_add_resource_fd(resource, readfd), - "i", pw_protocol_native_add_resource_fd(resource, writefd), - "i", pw_protocol_native_add_resource_fd(resource, info.memfd), - "i", info.offset, - "i", info.size); - - pw_protocol_native_end_resource(resource, b); -} - -static void -client_node_marshal_set_param(void *data, uint32_t seq, uint32_t id, uint32_t flags, - const struct spa_pod *param) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_SET_PARAM, NULL); - - spa_pod_builder_add_struct(b, - "i", seq, - "I", id, - "i", flags, - "P", param); - - pw_protocol_native_end_resource(resource, b); -} - -static void client_node_marshal_event_event(void *data, const struct spa_event *event) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_EVENT, NULL); - - spa_pod_builder_add_struct(b, "P", event); - - pw_protocol_native_end_resource(resource, b); -} - -static void -client_node_marshal_command(void *data, uint32_t seq, const struct spa_command *command) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_builder *b; - struct spa_pod_frame f; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_COMMAND, NULL); - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, "i", seq, NULL); - if (SPA_COMMAND_TYPE(command) == 0) - spa_pod_builder_add(b, "P", command, NULL); - else - pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)command, b); - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void -client_node_marshal_add_port(void *data, - uint32_t seq, enum spa_direction direction, uint32_t port_id) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_ADD_PORT, NULL); - - spa_pod_builder_add_struct(b, - "i", seq, - "i", direction, - "i", port_id); - - pw_protocol_native_end_resource(resource, b); -} - -static void -client_node_marshal_remove_port(void *data, - uint32_t seq, enum spa_direction direction, uint32_t port_id) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_REMOVE_PORT, NULL); - - spa_pod_builder_add_struct(b, - "i", seq, - "i", direction, - "i", port_id); - - pw_protocol_native_end_resource(resource, b); -} - -static void -client_node_marshal_port_set_param(void *data, - uint32_t seq, - enum spa_direction direction, - uint32_t port_id, - uint32_t id, - uint32_t flags, - const struct spa_pod *param) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_builder *b; - struct spa_pod_frame f; - const char *typename; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_SET_PARAM, NULL); - - switch (id) { - case SPA_PARAM_Props: - typename = "Spa:Enum:ParamId:Props"; - break; - case SPA_PARAM_Format: - typename = "Spa:Enum:ParamId:Format"; - break; - default: - return; - } - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", seq, - "i", direction, - "i", port_id, - "I", pw_protocol_native0_find_type(client, typename), - "i", flags, NULL); - pw_protocol_native0_pod_to_v2(client, param, b); - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void -client_node_marshal_port_use_buffers(void *data, - uint32_t seq, - enum spa_direction direction, - uint32_t port_id, - uint32_t n_buffers, struct pw_client_node0_buffer *buffers) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_builder *b; - struct spa_pod_frame f; - uint32_t i, j; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_USE_BUFFERS, NULL); - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", seq, - "i", direction, - "i", port_id, - "i", n_buffers, NULL); - - for (i = 0; i < n_buffers; i++) { - struct spa_buffer *buf = buffers[i].buffer; - - spa_pod_builder_add(b, - "i", buffers[i].mem_id, - "i", buffers[i].offset, - "i", buffers[i].size, - "i", i, - "i", buf->n_metas, NULL); - - for (j = 0; j < buf->n_metas; j++) { - struct spa_meta *m = &buf->metas[j]; - spa_pod_builder_add(b, - "I", pw_protocol_native0_type_to_v2(client, spa_type_meta_type, m->type), - "i", m->size, NULL); - } - spa_pod_builder_add(b, "i", buf->n_datas, NULL); - for (j = 0; j < buf->n_datas; j++) { - struct spa_data *d = &buf->datas[j]; - spa_pod_builder_add(b, - "I", pw_protocol_native0_type_to_v2(client, spa_type_data_type, d->type), - "i", SPA_PTR_TO_UINT32(d->data), - "i", d->flags, - "i", d->mapoffset, - "i", d->maxsize, NULL); - } - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void -client_node_marshal_port_command(void *data, - uint32_t direction, - uint32_t port_id, - const struct spa_command *command) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_builder *b; - struct spa_pod_frame f; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_COMMAND, NULL); - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", direction, - "i", port_id, NULL); - pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)command, b); - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void -client_node_marshal_port_set_io(void *data, - uint32_t seq, - uint32_t direction, - uint32_t port_id, - uint32_t id, - uint32_t memid, - uint32_t offset, - uint32_t size) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_NODE0_EVENT_PORT_SET_IO, NULL); - - spa_pod_builder_add_struct(b, - "i", seq, - "i", direction, - "i", port_id, - "I", id, - "i", memid, - "i", offset, - "i", size); - - pw_protocol_native_end_resource(resource, b); -} - - -static int client_node_demarshal_done(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct spa_pod_parser prs; - uint32_t seq, res; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, - "i", &seq, - "i", &res) < 0) - return -EINVAL; - - return pw_resource_notify(resource, struct pw_client_node0_methods, done, 0, seq, res); -} - -static int client_node_demarshal_update(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct spa_pod_parser prs; - struct spa_pod_frame f; - uint32_t change_mask, max_input_ports, max_output_ports, n_params; - const struct spa_pod **params; - uint32_t i; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_push_struct(&prs, &f) < 0 || - spa_pod_parser_get(&prs, - "i", &change_mask, - "i", &max_input_ports, - "i", &max_output_ports, - "i", &n_params, NULL) < 0) - return -EINVAL; - - params = alloca(n_params * sizeof(struct spa_pod *)); - for (i = 0; i < n_params; i++) - if (spa_pod_parser_get(&prs, "O", ¶ms[i], NULL) < 0) - return -EINVAL; - - return pw_resource_notify(resource, struct pw_client_node0_methods, update, 0, change_mask, - max_input_ports, - max_output_ports, - n_params, - params); -} - -static int client_node_demarshal_port_update(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct spa_pod_parser prs; - struct spa_pod_frame f[2]; - uint32_t i, direction, port_id, change_mask, n_params; - const struct spa_pod **params = NULL; - struct spa_port_info info = { 0 }, *infop = NULL; - struct spa_dict props; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_push_struct(&prs, &f[0]) < 0 || - spa_pod_parser_get(&prs, - "i", &direction, - "i", &port_id, - "i", &change_mask, - "i", &n_params, NULL) < 0) - return -EINVAL; - - params = alloca(n_params * sizeof(struct spa_pod *)); - for (i = 0; i < n_params; i++) - if (spa_pod_parser_get(&prs, "O", ¶ms[i], NULL) < 0) - return -EINVAL; - - - if (spa_pod_parser_push_struct(&prs, &f[1]) >= 0) { - infop = &info; - - if (spa_pod_parser_get(&prs, - "i", &info.flags, - "i", &info.rate, - "i", &props.n_items, NULL) < 0) - return -EINVAL; - - if (props.n_items > 0) { - info.props = &props; - - props.items = alloca(props.n_items * sizeof(struct spa_dict_item)); - for (i = 0; i < props.n_items; i++) { - if (spa_pod_parser_get(&prs, - "s", &props.items[i].key, - "s", &props.items[i].value, - NULL) < 0) - return -EINVAL; - } - } - } - - return pw_resource_notify(resource, struct pw_client_node0_methods, port_update, 0, direction, - port_id, - change_mask, - n_params, - params, infop); -} - -static int client_node_demarshal_set_active(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct spa_pod_parser prs; - int active; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, - "b", &active) < 0) - return -EINVAL; - - return pw_resource_notify(resource, struct pw_client_node0_methods, set_active, 0, active); -} - -static int client_node_demarshal_event_method(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_parser prs; - struct spa_event *event; - int res; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, - "O", &event) < 0) - return -EINVAL; - - event = (struct spa_event*)pw_protocol_native0_pod_from_v2(client, (struct spa_pod *)event); - - res = pw_resource_notify(resource, struct pw_client_node0_methods, event, 0, event); - free(event); - - return res; -} - -static int client_node_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct spa_pod_parser prs; - int res; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, NULL) < 0) - return -EINVAL; - - res = pw_resource_notify(resource, struct pw_client_node0_methods, destroy, 0); - pw_resource_destroy(resource); - return res; -} - -static const struct pw_protocol_native_demarshal pw_protocol_native_client_node_method_demarshal[] = { - { &client_node_demarshal_done, 0, 0 }, - { &client_node_demarshal_update, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP }, - { &client_node_demarshal_port_update, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP }, - { &client_node_demarshal_set_active, 0, 0 }, - { &client_node_demarshal_event_method, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP }, - { &client_node_demarshal_destroy, 0, 0 }, -}; - -static const struct pw_client_node0_events pw_protocol_native_client_node_event_marshal = { - PW_VERSION_CLIENT_NODE0_EVENTS, - &client_node_marshal_add_mem, - &client_node_marshal_transport, - &client_node_marshal_set_param, - &client_node_marshal_event_event, - &client_node_marshal_command, - &client_node_marshal_add_port, - &client_node_marshal_remove_port, - &client_node_marshal_port_set_param, - &client_node_marshal_port_use_buffers, - &client_node_marshal_port_command, - &client_node_marshal_port_set_io, -}; - -static const struct pw_protocol_marshal pw_protocol_native_client_node_marshal = { - PW_TYPE_INTERFACE_ClientNode, - PW_VERSION_CLIENT_NODE0, - PW_CLIENT_NODE0_METHOD_NUM, - PW_CLIENT_NODE0_EVENT_NUM, - 0, - NULL, - .server_demarshal = &pw_protocol_native_client_node_method_demarshal, - .server_marshal = &pw_protocol_native_client_node_event_marshal, - NULL, -}; - -struct pw_protocol *pw_protocol_native_ext_client_node0_init(struct pw_context *context) -{ - struct pw_protocol *protocol; - - protocol = pw_context_find_protocol(context, PW_TYPE_INFO_PROTOCOL_Native); - - if (protocol == NULL) - return NULL; - - pw_protocol_add_marshal(protocol, &pw_protocol_native_client_node_marshal); - - return protocol; -} diff --git a/src/modules/module-client-node/v0/transport.c b/src/modules/module-client-node/v0/transport.c deleted file mode 100644 index d62f23ca2..000000000 --- a/src/modules/module-client-node/v0/transport.c +++ /dev/null @@ -1,241 +0,0 @@ -/* PipeWire */ -/* SPDX-FileCopyrightText: Copyright © 2016 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -#include -#include -#include - -#include -#include - -#include - -#include "ext-client-node.h" - -#include "transport.h" - -/** \cond */ - -#define INPUT_BUFFER_SIZE (1<<12) -#define OUTPUT_BUFFER_SIZE (1<<12) - -struct transport { - struct pw_client_node0_transport trans; - - struct pw_memblock *mem; - size_t offset; - - struct pw_client_node0_message current; - uint32_t current_index; -}; -/** \endcond */ - -static size_t area_get_size(struct pw_client_node0_area *area) -{ - size_t size; - size = sizeof(struct pw_client_node0_area); - size += area->max_input_ports * sizeof(struct spa_io_buffers); - size += area->max_output_ports * sizeof(struct spa_io_buffers); - size += sizeof(struct spa_ringbuffer); - size += INPUT_BUFFER_SIZE; - size += sizeof(struct spa_ringbuffer); - size += OUTPUT_BUFFER_SIZE; - return size; -} - -static void transport_setup_area(void *p, struct pw_client_node0_transport *trans) -{ - struct pw_client_node0_area *a; - - trans->area = a = p; - p = SPA_PTROFF(p, sizeof(struct pw_client_node0_area), struct spa_io_buffers); - - trans->inputs = p; - p = SPA_PTROFF(p, a->max_input_ports * sizeof(struct spa_io_buffers), void); - - trans->outputs = p; - p = SPA_PTROFF(p, a->max_output_ports * sizeof(struct spa_io_buffers), void); - - trans->input_buffer = p; - p = SPA_PTROFF(p, sizeof(struct spa_ringbuffer), void); - - trans->input_data = p; - p = SPA_PTROFF(p, INPUT_BUFFER_SIZE, void); - - trans->output_buffer = p; - p = SPA_PTROFF(p, sizeof(struct spa_ringbuffer), void); - - trans->output_data = p; - p = SPA_PTROFF(p, OUTPUT_BUFFER_SIZE, void); -} - -static void transport_reset_area(struct pw_client_node0_transport *trans) -{ - uint32_t i; - struct pw_client_node0_area *a = trans->area; - - for (i = 0; i < a->max_input_ports; i++) { - trans->inputs[i] = SPA_IO_BUFFERS_INIT; - } - for (i = 0; i < a->max_output_ports; i++) { - trans->outputs[i] = SPA_IO_BUFFERS_INIT; - } - spa_ringbuffer_init(trans->input_buffer); - spa_ringbuffer_init(trans->output_buffer); -} - -static void destroy(struct pw_client_node0_transport *trans) -{ - struct transport *impl = (struct transport *) trans; - - pw_log_debug("transport %p: destroy", trans); - - pw_memblock_free(impl->mem); - free(impl); -} - -static int add_message(struct pw_client_node0_transport *trans, struct pw_client_node0_message *message) -{ - struct transport *impl = (struct transport *) trans; - int32_t filled, avail; - uint32_t size, index; - - if (impl == NULL || message == NULL) - return -EINVAL; - - filled = spa_ringbuffer_get_write_index(trans->output_buffer, &index); - avail = OUTPUT_BUFFER_SIZE - filled; - size = SPA_POD_SIZE(message); - if (avail < (int)size) - return -ENOSPC; - - spa_ringbuffer_write_data(trans->output_buffer, - trans->output_data, OUTPUT_BUFFER_SIZE, - index & (OUTPUT_BUFFER_SIZE - 1), message, size); - spa_ringbuffer_write_update(trans->output_buffer, index + size); - - return 0; -} - -static int next_message(struct pw_client_node0_transport *trans, struct pw_client_node0_message *message) -{ - struct transport *impl = (struct transport *) trans; - int32_t avail; - - if (impl == NULL || message == NULL) - return -EINVAL; - - avail = spa_ringbuffer_get_read_index(trans->input_buffer, &impl->current_index); - if (avail < (int) sizeof(struct pw_client_node0_message)) - return 0; - - spa_ringbuffer_read_data(trans->input_buffer, - trans->input_data, INPUT_BUFFER_SIZE, - impl->current_index & (INPUT_BUFFER_SIZE - 1), - &impl->current, sizeof(struct pw_client_node0_message)); - - if (avail < (int) SPA_POD_SIZE(&impl->current)) - return 0; - - *message = impl->current; - - return 1; -} - -static int parse_message(struct pw_client_node0_transport *trans, void *message) -{ - struct transport *impl = (struct transport *) trans; - uint32_t size; - - if (impl == NULL || message == NULL) - return -EINVAL; - - size = SPA_POD_SIZE(&impl->current); - - spa_ringbuffer_read_data(trans->input_buffer, - trans->input_data, INPUT_BUFFER_SIZE, - impl->current_index & (INPUT_BUFFER_SIZE - 1), message, size); - spa_ringbuffer_read_update(trans->input_buffer, impl->current_index + size); - - return 0; -} - -/** Create a new transport - * \param max_input_ports maximum number of input_ports - * \param max_output_ports maximum number of output_ports - * \return a newly allocated \ref pw_client_node0_transport - * \memberof pw_client_node0_transport - */ -struct pw_client_node0_transport * -pw_client_node0_transport_new(struct pw_context *context, - uint32_t max_input_ports, uint32_t max_output_ports) -{ - struct transport *impl; - struct pw_client_node0_transport *trans; - struct pw_client_node0_area area = { 0 }; - - area.max_input_ports = max_input_ports; - area.n_input_ports = 0; - area.max_output_ports = max_output_ports; - area.n_output_ports = 0; - - impl = calloc(1, sizeof(struct transport)); - if (impl == NULL) - return NULL; - - pw_log_debug("transport %p: new %d %d", impl, max_input_ports, max_output_ports); - - trans = &impl->trans; - impl->offset = 0; - - impl->mem = pw_mempool_alloc(pw_context_get_mempool(context), - PW_MEMBLOCK_FLAG_READWRITE | - PW_MEMBLOCK_FLAG_SEAL | - PW_MEMBLOCK_FLAG_MAP, - SPA_DATA_MemFd, area_get_size(&area)); - if (impl->mem == NULL) { - free(impl); - return NULL; - } - - memcpy(impl->mem->map->ptr, &area, sizeof(struct pw_client_node0_area)); - transport_setup_area(impl->mem->map->ptr, trans); - transport_reset_area(trans); - - trans->destroy = destroy; - trans->add_message = add_message; - trans->next_message = next_message; - trans->parse_message = parse_message; - - return trans; -} - -struct pw_client_node0_transport * -pw_client_node0_transport_new_from_info(struct pw_client_node0_transport_info *info) -{ - errno = ENOTSUP; - return NULL; -} - -/** Get transport info - * \param trans the transport to get info of - * \param[out] info transport info - * \return 0 on success - * - * Fill \a info with the transport info of \a trans. This information can be - * passed to the client to set up the shared transport. - * - * \memberof pw_client_node0_transport - */ -int pw_client_node0_transport_get_info(struct pw_client_node0_transport *trans, - struct pw_client_node0_transport_info *info) -{ - struct transport *impl = (struct transport *) trans; - - info->memfd = impl->mem->fd; - info->offset = impl->offset; - info->size = impl->mem->size; - - return 0; -} diff --git a/src/modules/module-client-node/v0/transport.h b/src/modules/module-client-node/v0/transport.h deleted file mode 100644 index 46072a2e8..000000000 --- a/src/modules/module-client-node/v0/transport.h +++ /dev/null @@ -1,39 +0,0 @@ -/* PipeWire */ -/* SPDX-FileCopyrightText: Copyright © 2016 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -#ifndef __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__ -#define __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__ - -#ifdef __cplusplus -extern "C" { -#endif - -#include - -#include - -#include - -/** information about the transport region \memberof pw_client_node */ -struct pw_client_node0_transport_info { - int memfd; /**< the memfd of the transport area */ - uint32_t offset; /**< offset to map \a memfd at */ - uint32_t size; /**< size of memfd mapping */ -}; - -struct pw_client_node0_transport * -pw_client_node0_transport_new(struct pw_context *context, uint32_t max_input_ports, uint32_t max_output_ports); - -struct pw_client_node0_transport * -pw_client_node0_transport_new_from_info(struct pw_client_node0_transport_info *info); - -int -pw_client_node0_transport_get_info(struct pw_client_node0_transport *trans, - struct pw_client_node0_transport_info *info); - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif /* __PIPEWIRE_CLIENT_NODE0_TRANSPORT_H__ */ diff --git a/src/modules/module-combine-stream.c b/src/modules/module-combine-stream.c index 4afcb8f0b..453203ebb 100644 --- a/src/modules/module-combine-stream.c +++ b/src/modules/module-combine-stream.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -14,8 +16,6 @@ #include #include -#include "config.h" - #include #include #include @@ -64,6 +64,7 @@ * * - \ref PW_KEY_REMOTE_NAME * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_NAME * - \ref PW_KEY_NODE_LATENCY @@ -76,9 +77,10 @@ * ## Stream options * * - `audio.position`: Set the stream channel map. By default this is the same channel - * map as the combine stream. + * map as the combine stream. You can also use audio.layout * - `combine.audio.position`: map the combine audio positions to the stream positions. * combine input channels are mapped one-by-one to stream output channels. + * You can also use combine.audio.layout. * * ## Example configuration * @@ -231,9 +233,9 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); "( stream.props= ) " \ "( stream.rules= ) " +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define DELAYBUF_MAX_SIZE (20 * sizeof(float) * 96000) - static const struct spa_dict_item module_props[] = { { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, { PW_KEY_MODULE_DESCRIPTION, "Combine multiple streams into a single stream" }, @@ -312,27 +314,29 @@ struct stream { struct spa_latency_info latency; struct spa_audio_info_raw info; - uint32_t remap[SPA_AUDIO_MAX_CHANNELS]; + uint32_t remap[MAX_CHANNELS]; void *delaybuf; - struct ringbuffer delay[SPA_AUDIO_MAX_CHANNELS]; + struct ringbuffer delay[MAX_CHANNELS]; int64_t delay_samples; /* for main loop */ int64_t data_delay_samples; /* for data loop */ + int64_t compensate_samples; /* for main loop */ unsigned int ready:1; unsigned int added:1; unsigned int have_latency:1; }; -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), &props->dict, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -508,7 +512,7 @@ static void update_latency(struct impl *impl) struct replace_delay_info { struct stream *stream; void *buf; - struct ringbuffer delay[SPA_AUDIO_MAX_CHANNELS]; + struct ringbuffer delay[MAX_CHANNELS]; }; static int do_replace_delay(struct spa_loop *loop, bool async, uint32_t seq, @@ -553,7 +557,7 @@ static void resize_delay(struct stream *stream, uint32_t size) for (i = 0; i < channels; ++i) ringbuffer_init(&info.delay[i], SPA_PTROFF(info.buf, i*size, void), size); - pw_loop_invoke(stream->impl->data_loop, do_replace_delay, 0, NULL, 0, true, &info); + pw_loop_locked(stream->impl->data_loop, do_replace_delay, 0, NULL, 0, &info); free(info.buf); } @@ -574,6 +578,7 @@ static void update_delay(struct impl *impl) max_delay = SPA_MAX(max_delay, delay); s->delay_samples = delay; + s->compensate_samples = 0; } spa_list_for_each(s, &impl->streams, link) { @@ -581,9 +586,9 @@ static void update_delay(struct impl *impl) if (s->delay_samples != INT64_MIN) { int64_t delay = max_delay - s->delay_samples; + s->compensate_samples = delay; size = delay * sizeof(float); } - resize_delay(s, size); } @@ -616,7 +621,7 @@ static int do_clear_delaybuf(struct spa_loop *loop, bool async, uint32_t seq, static void clear_delaybuf(struct impl *impl) { - pw_loop_invoke(impl->data_loop, do_clear_delaybuf, 0, NULL, 0, true, impl); + pw_loop_locked(impl->data_loop, do_clear_delaybuf, 0, NULL, 0, impl); } static int do_add_stream(struct spa_loop *loop, bool async, uint32_t seq, @@ -653,6 +658,40 @@ static void param_tag_changed(struct impl *impl, const struct spa_pod *param) } } +static void param_latency_changed(struct impl *impl, const struct spa_pod *param) +{ + if (param == NULL) + return; + + pw_log_debug("latency update"); + struct stream *s; + struct spa_latency_info info; + const struct spa_pod *params[1]; + + if (spa_latency_parse(param, &info) < 0) + return; + + spa_list_for_each(s, &impl->streams, link) { + uint8_t buffer[1024]; + struct spa_pod_builder b; + + if (s->stream == NULL) + continue; + + pw_log_debug("updating stream %d", s->id); + if (impl->latency_compensate) { + struct spa_latency_info other = info; + other.min_rate += s->compensate_samples; + other.max_rate += s->compensate_samples; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &other); + } else { + params[0] = param; + } + pw_stream_update_params(s->stream, params, 1); + } +} + static int do_remove_stream(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { @@ -669,7 +708,7 @@ static void remove_stream(struct stream *s, bool destroy) { pw_log_debug("destroy stream %d", s->id); - pw_loop_invoke(s->impl->data_loop, do_remove_stream, 0, NULL, 0, true, s); + pw_loop_locked(s->impl->data_loop, do_remove_stream, 0, NULL, 0, s); if (destroy && s->stream) { spa_hook_remove(&s->stream_listener); @@ -730,6 +769,9 @@ static void stream_state_changed(void *d, enum pw_stream_state old, case PW_STREAM_STATE_UNCONNECTED: stream_destroy(s); break; + case PW_STREAM_STATE_STREAMING: + update_latency(s->impl); + break; default: break; } @@ -827,13 +869,21 @@ static int create_stream(struct stream_info *info) s->info = impl->info; if ((str = pw_properties_get(info->stream_props, SPA_KEY_AUDIO_POSITION)) != NULL) - spa_audio_parse_position(str, strlen(str), s->info.position, &s->info.channels); + spa_audio_parse_position_n(str, strlen(str), s->info.position, + SPA_N_ELEMENTS(s->info.position), &s->info.channels); + if ((str = pw_properties_get(info->stream_props, SPA_KEY_AUDIO_LAYOUT)) != NULL) + spa_audio_parse_layout(str, s->info.position, + SPA_N_ELEMENTS(s->info.position), &s->info.channels); if (s->info.channels == 0) s->info = impl->info; spa_zero(remap_info); if ((str = pw_properties_get(info->stream_props, "combine.audio.position")) != NULL) - spa_audio_parse_position(str, strlen(str), remap_info.position, &remap_info.channels); + spa_audio_parse_position_n(str, strlen(str), remap_info.position, + SPA_N_ELEMENTS(remap_info.position), &remap_info.channels); + if ((str = pw_properties_get(info->stream_props, "combine.audio.layout")) != NULL) + spa_audio_parse_layout(str, remap_info.position, + SPA_N_ELEMENTS(remap_info.position), &remap_info.channels); if (remap_info.channels == 0) remap_info = s->info; @@ -841,7 +891,10 @@ static int create_stream(struct stream_info *info) for (i = 0; i < remap_info.channels; i++) { s->remap[i] = i; for (j = 0; j < tmp_info.channels; j++) { - if (tmp_info.position[j] == remap_info.position[i]) { + uint32_t pj, pi; + pj = tmp_info.position[j]; + pi = remap_info.position[i]; + if (pj == pi) { s->remap[i] = j; break; } @@ -896,7 +949,7 @@ static int create_stream(struct stream_info *info) direction, PW_ID_ANY, flags, params, n_params)) < 0) goto error; - pw_loop_invoke(impl->data_loop, do_add_stream, 0, NULL, 0, true, s); + pw_loop_locked(impl->data_loop, do_add_stream, 0, NULL, 0, s); update_delay(impl); return 0; @@ -1070,6 +1123,7 @@ static void combine_state_changed(void *d, enum pw_stream_state old, enum pw_stream_state state, const char *error) { struct impl *impl = d; + struct stream *s; switch (state) { case PW_STREAM_STATE_ERROR: case PW_STREAM_STATE_UNCONNECTED: @@ -1077,6 +1131,10 @@ static void combine_state_changed(void *d, enum pw_stream_state old, break; case PW_STREAM_STATE_PAUSED: clear_delaybuf(impl); + spa_list_for_each(s, &impl->streams, link) { + pw_stream_flush(s->stream, false); + } + pw_stream_flush(impl->combine, false); impl->combine_id = pw_stream_get_node_id(impl->combine); pw_log_info("got combine id %d", impl->combine_id); break; @@ -1184,7 +1242,7 @@ static void combine_output_process(void *d) struct pw_buffer *in, *out; struct stream *s; bool delay_changed = false; - bool mix[SPA_AUDIO_MAX_CHANNELS]; + bool mix[MAX_CHANNELS]; if ((out = pw_stream_dequeue_buffer(impl->combine)) == NULL) { pw_log_debug("%p: out of output buffers: %m", impl); @@ -1291,10 +1349,12 @@ static void combine_param_changed(void *d, uint32_t id, const struct spa_pod *pa update_latency(impl); break; } - case SPA_PARAM_Tag: { + case SPA_PARAM_Tag: param_tag_changed(impl, param); break; - } + case SPA_PARAM_Latency: + param_latency_changed(impl, param); + break; default: break; } @@ -1489,7 +1549,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) spa_list_init(&impl->streams); if (args == NULL) - args = ""; + args = "{}"; props = pw_properties_new_string_checked(args, strlen(args), &loc); if (props == NULL) { @@ -1576,6 +1636,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(props, impl->combine_props, PW_KEY_NODE_LOOP_NAME); copy_props(props, impl->combine_props, PW_KEY_AUDIO_CHANNELS); + copy_props(props, impl->combine_props, SPA_KEY_AUDIO_LAYOUT); copy_props(props, impl->combine_props, SPA_KEY_AUDIO_POSITION); copy_props(props, impl->combine_props, PW_KEY_NODE_NAME); copy_props(props, impl->combine_props, PW_KEY_NODE_DESCRIPTION); @@ -1587,7 +1648,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(props, impl->combine_props, "resample.prefill"); copy_props(props, impl->combine_props, "resample.disable"); - parse_audio_info(impl->combine_props, &impl->info); + if ((res = parse_audio_info(impl->combine_props, &impl->info)) < 0) { + pw_log_error( "can't create format: %s", spa_strerror(res)); + goto error; + } copy_props(props, impl->stream_props, PW_KEY_NODE_LOOP_NAME); copy_props(props, impl->stream_props, PW_KEY_NODE_GROUP); diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c index 4f08927ac..390563db2 100644 --- a/src/modules/module-echo-cancel.c +++ b/src/modules/module-echo-cancel.c @@ -105,6 +105,7 @@ * * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_CLASS * - \ref PW_KEY_NODE_LATENCY @@ -154,6 +155,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define DEFAULT_RATE 48000 #define DEFAULT_POSITION "[ FL FR ]" +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS /* Hopefully this is enough for any combination of AEC engine and resampler * input requirement for rate matching */ @@ -203,7 +205,7 @@ struct impl { struct spa_hook source_listener; struct spa_audio_info_raw source_info; - void *rec_buffer[SPA_AUDIO_MAX_CHANNELS]; + void *rec_buffer[MAX_CHANNELS]; uint32_t rec_ringsize; struct spa_ringbuffer rec_ring; @@ -215,21 +217,23 @@ struct impl { struct pw_properties *sink_props; struct pw_stream *sink; struct spa_hook sink_listener; - void *play_buffer[SPA_AUDIO_MAX_CHANNELS]; + void *play_buffer[MAX_CHANNELS]; uint32_t play_ringsize; struct spa_ringbuffer play_ring; struct spa_ringbuffer play_delayed_ring; struct spa_audio_info_raw sink_info; - void *out_buffer[SPA_AUDIO_MAX_CHANNELS]; + void *out_buffer[MAX_CHANNELS]; uint32_t out_ringsize; struct spa_ringbuffer out_ring; struct spa_audio_aec *aec; uint32_t aec_blocksize; - unsigned int capture_ready:1; - unsigned int sink_ready:1; + struct spa_io_position *capture_position; + struct spa_io_position *sink_position; + uint32_t capture_cycle; + uint32_t sink_cycle; unsigned int do_disconnect:1; @@ -306,18 +310,24 @@ static void process(struct impl *impl) const float *play_delayed[impl->play_info.channels]; float *out[impl->out_info.channels]; struct spa_data *dd; - uint32_t i, size; - uint32_t rindex, pindex, oindex, pdindex, avail; - - if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) { - pw_log_debug("out of playback buffers: %m"); - goto done; - } + uint32_t i; + uint32_t rindex, pindex, oindex, pdindex, size; + int32_t avail, pavail, pdavail; size = impl->aec_blocksize; - /* First read a block from the playback and capture ring buffers */ - spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); + /* First read a block from the capture ring buffer */ + avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); + while (avail >= (int32_t)size * 2) { + /* drop samples that are not needed this or next cycle. Note + * that samples are kept in the ringbuffer until next cycle if + * size is not equal to or divisible by quantum, to avoid + * discontinuity */ + pw_log_debug("avail %d", avail); + spa_ringbuffer_read_update(&impl->rec_ring, rindex + size); + avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); + pw_log_debug("new avail %d, size %u", avail, size); + } for (i = 0; i < impl->rec_info.channels; i++) { /* captured samples, with echo from sink */ @@ -335,8 +345,33 @@ static void process(struct impl *impl) out[i] = &out_buf[i][0]; } - spa_ringbuffer_get_read_index(&impl->play_ring, &pindex); - spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex); + pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex); + pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex); + + if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) { + pw_log_debug("out of playback buffers: %m"); + + /* playback stream may not yet be in streaming state, drop play + * data to avoid introducing additional playback latency */ + spa_ringbuffer_read_update(&impl->play_ring, pindex + pavail); + spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + pdavail); + goto done; + } + + if (pavail > avail) { + /* drop too old samples from previous graph cycles */ + pw_log_debug("pavail %d, dropping %d", pavail, pavail - avail); + spa_ringbuffer_read_update(&impl->play_ring, pindex + pavail - avail); + pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex); + pw_log_debug("new pavail %d, avail %d", pavail, avail); + } + if (pdavail > avail) { + /* drop too old samples from previous graph cycles */ + pw_log_debug("pdavail %d, dropping %d", pdavail, pdavail - avail); + spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + pdavail - avail); + pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex); + pw_log_debug("new pdavail %d, avail %d", pdavail, avail); + } for (i = 0; i < impl->play_info.channels; i++) { /* echo from sink */ @@ -425,32 +460,56 @@ static void process(struct impl *impl) * available on the source */ avail = spa_ringbuffer_get_read_index(&impl->out_ring, &oindex); - while (avail >= size) { - if ((cout = pw_stream_dequeue_buffer(impl->source)) == NULL) { + while (avail >= (int32_t)size) { + if ((cout = pw_stream_dequeue_buffer(impl->source)) != NULL) { + for (i = 0; i < impl->out_info.channels; i++) { + dd = &cout->buffer->datas[i]; + spa_ringbuffer_read_data(&impl->out_ring, impl->out_buffer[i], + impl->out_ringsize, oindex % impl->out_ringsize, + (void *)dd->data, size); + dd->chunk->offset = 0; + dd->chunk->size = size; + dd->chunk->stride = sizeof(float); + } + pw_stream_queue_buffer(impl->source, cout); + } else { + /* drop data as to not cause delay */ pw_log_debug("out of source buffers: %m"); - break; } - for (i = 0; i < impl->out_info.channels; i++) { - dd = &cout->buffer->datas[i]; - spa_ringbuffer_read_data(&impl->out_ring, impl->out_buffer[i], - impl->out_ringsize, oindex % impl->out_ringsize, - (void *)dd->data, size); - dd->chunk->offset = 0; - dd->chunk->size = size; - dd->chunk->stride = sizeof(float); - } - - pw_stream_queue_buffer(impl->source, cout); - oindex += size; spa_ringbuffer_read_update(&impl->out_ring, oindex); avail -= size; } done: - impl->sink_ready = false; - impl->capture_ready = false; + impl->capture_cycle = 0; + impl->sink_cycle = 0; +} + +static void reset_buffers(struct impl *impl) +{ + uint32_t index, i; + + spa_ringbuffer_init(&impl->rec_ring); + spa_ringbuffer_init(&impl->play_ring); + spa_ringbuffer_init(&impl->play_delayed_ring); + spa_ringbuffer_init(&impl->out_ring); + + for (i = 0; i < impl->rec_info.channels; i++) + memset(impl->rec_buffer[i], 0, impl->rec_ringsize); + for (i = 0; i < impl->play_info.channels; i++) + memset(impl->play_buffer[i], 0, impl->play_ringsize); + for (i = 0; i < impl->out_info.channels; i++) + memset(impl->out_buffer[i], 0, impl->out_ringsize); + + spa_ringbuffer_get_write_index(&impl->play_ring, &index); + spa_ringbuffer_write_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay))); + spa_ringbuffer_get_read_index(&impl->play_ring, &index); + spa_ringbuffer_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay))); + + impl->capture_cycle = 0; + impl->sink_cycle = 0; } static void capture_destroy(void *d) @@ -516,8 +575,11 @@ static void capture_process(void *data) spa_ringbuffer_write_update(&impl->rec_ring, index + size); if (avail + size >= impl->aec_blocksize) { - impl->capture_ready = true; - if (impl->sink_ready) + if (impl->capture_position) + impl->capture_cycle = impl->capture_position->clock.cycle; + else + pw_log_warn("no capture position"); + if (impl->capture_cycle == impl->sink_cycle) process(impl); } @@ -537,6 +599,8 @@ static void capture_state_changed(void *data, enum pw_stream_state old, if (old == PW_STREAM_STATE_STREAMING) { if (pw_stream_get_state(impl->sink, NULL) != PW_STREAM_STATE_STREAMING) { + reset_buffers(impl); + pw_log_debug("%p: deactivate %s", impl, impl->aec->name); res = spa_audio_aec_deactivate(impl->aec); if (res < 0 && res != -EOPNOTSUPP) { @@ -588,28 +652,6 @@ static void source_state_changed(void *data, enum pw_stream_state old, } } -static void reset_buffers(struct impl *impl) -{ - uint32_t index, i; - - spa_ringbuffer_init(&impl->rec_ring); - spa_ringbuffer_init(&impl->play_ring); - spa_ringbuffer_init(&impl->play_delayed_ring); - spa_ringbuffer_init(&impl->out_ring); - - for (i = 0; i < impl->rec_info.channels; i++) - memset(impl->rec_buffer[i], 0, impl->rec_ringsize); - for (i = 0; i < impl->play_info.channels; i++) - memset(impl->play_buffer[i], 0, impl->play_ringsize); - for (i = 0; i < impl->out_info.channels; i++) - memset(impl->out_buffer[i], 0, impl->out_ringsize); - - spa_ringbuffer_get_write_index(&impl->play_ring, &index); - spa_ringbuffer_write_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay))); - spa_ringbuffer_get_read_index(&impl->play_ring, &index); - spa_ringbuffer_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay))); -} - static void input_param_latency_changed(struct impl *impl, const struct spa_pod *param) { struct spa_latency_info latency; @@ -624,9 +666,9 @@ static void input_param_latency_changed(struct impl *impl, const struct spa_pod params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency); if (latency.direction == SPA_DIRECTION_INPUT) - pw_stream_update_params(impl->source, params, 1); - else pw_stream_update_params(impl->capture, params, 1); + else + pw_stream_update_params(impl->source, params, 1); } static struct spa_pod* get_props_param(struct impl* impl, struct spa_pod_builder* b) @@ -730,12 +772,26 @@ static void input_param_changed(void *data, uint32_t id, const struct spa_pod* p } } +static void capture_io_changed(void *data, uint32_t id, void *area, uint32_t size) +{ + struct impl *impl = data; + + switch (id) { + case SPA_IO_Position: + impl->capture_position = area; + break; + default: + break; + } +} + static const struct pw_stream_events capture_events = { PW_VERSION_STREAM_EVENTS, .destroy = capture_destroy, .state_changed = capture_state_changed, .process = capture_process, - .param_changed = input_param_changed + .param_changed = input_param_changed, + .io_changed = capture_io_changed }; static void source_destroy(void *d) @@ -792,6 +848,8 @@ static void sink_state_changed(void *data, enum pw_stream_state old, impl->current_delay = 0; if (pw_stream_get_state(impl->capture, NULL) != PW_STREAM_STATE_STREAMING) { + reset_buffers(impl); + pw_log_debug("%p: deactivate %s", impl, impl->aec->name); res = spa_audio_aec_deactivate(impl->aec); if (res < 0 && res != -EOPNOTSUPP) { @@ -918,10 +976,15 @@ static void sink_process(void *data) SPA_PTROFF(d->data, offs, void), size); } spa_ringbuffer_write_update(&impl->play_ring, index + size); + spa_ringbuffer_get_write_index(&impl->play_delayed_ring, &index); + spa_ringbuffer_write_update(&impl->play_delayed_ring, index + size); if (avail + size >= impl->aec_blocksize) { - impl->sink_ready = true; - if (impl->capture_ready) + if (impl->sink_position) + impl->sink_cycle = impl->sink_position->clock.cycle; + else + pw_log_warn("no sink position"); + if (impl->capture_cycle == impl->sink_cycle) process(impl); } @@ -943,12 +1006,27 @@ static const struct pw_stream_events playback_events = { .state_changed = playback_state_changed, .param_changed = output_param_changed }; + +static void sink_io_changed(void *data, uint32_t id, void *area, uint32_t size) +{ + struct impl *impl = data; + + switch (id) { + case SPA_IO_Position: + impl->sink_position = area; + break; + default: + break; + } +} + static const struct pw_stream_events sink_events = { PW_VERSION_STREAM_EVENTS, .destroy = sink_destroy, .process = sink_process, .state_changed = sink_state_changed, - .param_changed = output_param_changed + .param_changed = output_param_changed, + .io_changed = sink_io_changed }; #define MAX_PARAMS 512u @@ -1191,9 +1269,9 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), @@ -1201,6 +1279,7 @@ static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_ &props->dict, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -1279,7 +1358,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) if (pw_properties_get(props, "resample.prefill") == NULL) pw_properties_set(props, "resample.prefill", "true"); - parse_audio_info(props, &info); + if ((res = parse_audio_info(props, &info)) < 0) { + pw_log_error( "can't parse format: %s", spa_strerror(res)); + goto error; + } impl->capture_info = info; impl->source_info = info; @@ -1336,6 +1418,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_LINK_GROUP); copy_props(impl, props, PW_KEY_NODE_VIRTUAL); copy_props(impl, props, SPA_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, "resample.prefill"); @@ -1359,24 +1442,41 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) } if ((str = pw_properties_get(impl->capture_props, SPA_KEY_AUDIO_POSITION)) != NULL) { - spa_audio_parse_position(str, strlen(str), - impl->capture_info.position, &impl->capture_info.channels); + spa_audio_parse_position_n(str, strlen(str), impl->capture_info.position, + SPA_N_ELEMENTS(impl->capture_info.position), &impl->capture_info.channels); + } + if ((str = pw_properties_get(impl->capture_props, SPA_KEY_AUDIO_LAYOUT)) != NULL) { + spa_audio_parse_layout(str, impl->capture_info.position, + SPA_N_ELEMENTS(impl->capture_info.position), &impl->capture_info.channels); } if ((str = pw_properties_get(impl->source_props, SPA_KEY_AUDIO_POSITION)) != NULL) { - spa_audio_parse_position(str, strlen(str), - impl->source_info.position, &impl->source_info.channels); + spa_audio_parse_position_n(str, strlen(str), impl->source_info.position, + SPA_N_ELEMENTS(impl->source_info.position), &impl->source_info.channels); + } + if ((str = pw_properties_get(impl->source_props, SPA_KEY_AUDIO_LAYOUT)) != NULL) { + spa_audio_parse_layout(str, impl->source_info.position, + SPA_N_ELEMENTS(impl->source_info.position), &impl->source_info.channels); } if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_POSITION)) != NULL) { - spa_audio_parse_position(str, strlen(str), - impl->sink_info.position, &impl->sink_info.channels); + spa_audio_parse_position_n(str, strlen(str), impl->sink_info.position, + SPA_N_ELEMENTS(impl->sink_info.position), &impl->sink_info.channels); + impl->playback_info = impl->sink_info; + } + if ((str = pw_properties_get(impl->sink_props, SPA_KEY_AUDIO_LAYOUT)) != NULL) { + spa_audio_parse_layout(str, impl->sink_info.position, + SPA_N_ELEMENTS(impl->sink_info.position), &impl->sink_info.channels); impl->playback_info = impl->sink_info; } if ((str = pw_properties_get(impl->playback_props, SPA_KEY_AUDIO_POSITION)) != NULL) { - spa_audio_parse_position(str, strlen(str), - impl->playback_info.position, &impl->playback_info.channels); - if (impl->playback_info.channels != impl->sink_info.channels) - impl->playback_info = impl->sink_info; + spa_audio_parse_position_n(str, strlen(str), impl->playback_info.position, + SPA_N_ELEMENTS(impl->playback_info.position), &impl->playback_info.channels); } + if ((str = pw_properties_get(impl->playback_props, SPA_KEY_AUDIO_LAYOUT)) != NULL) { + spa_audio_parse_layout(str, impl->playback_info.position, + SPA_N_ELEMENTS(impl->playback_info.position), &impl->playback_info.channels); + } + if (impl->playback_info.channels != impl->sink_info.channels) + impl->playback_info = impl->sink_info; if ((str = pw_properties_get(props, "aec.method")) != NULL) pw_log_warn("aec.method is not supported anymore use library.name"); diff --git a/src/modules/module-example-filter.c b/src/modules/module-example-filter.c index 5f6268dd8..209a23eac 100644 --- a/src/modules/module-example-filter.c +++ b/src/modules/module-example-filter.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,8 +12,6 @@ #include #include -#include "config.h" - #include #include #include @@ -46,6 +46,7 @@ * - \ref PW_KEY_REMOTE_NAME * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_NAME * - \ref PW_KEY_NODE_LATENCY @@ -140,13 +141,14 @@ struct impl { struct pw_stream *capture; struct spa_hook capture_listener; struct spa_audio_info_raw capture_info; - struct spa_latency_info capture_latency; struct pw_properties *playback_props; struct pw_stream *playback; struct spa_hook playback_listener; struct spa_audio_info_raw playback_info; - struct spa_latency_info playback_latency; + + struct spa_latency_info latency[2]; + struct spa_process_latency_info process_latency; unsigned int do_disconnect:1; }; @@ -236,22 +238,31 @@ static void playback_process(void *d) pw_stream_queue_buffer(impl->playback, out); } -static void param_latency_changed(struct impl *impl, const struct spa_pod *param, - struct spa_latency_info *info, struct pw_stream *other) +static void update_latency(struct impl *impl, enum spa_direction direction) { struct spa_latency_info latency; uint8_t buffer[1024]; struct spa_pod_builder b; const struct spa_pod *params[1]; + struct pw_stream *s = direction == SPA_DIRECTION_OUTPUT ? + impl->playback : impl->capture; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + latency = impl->latency[direction]; + spa_process_latency_info_add(&impl->process_latency, &latency); + params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency); + pw_stream_update_params(s, params, 1); +} + +static void param_latency_changed(struct impl *impl, const struct spa_pod *param) +{ + struct spa_latency_info latency; if (param == NULL || spa_latency_parse(param, &latency) < 0) return; - *info = latency; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency); - pw_stream_update_params(other, params, 1); + impl->latency[latency.direction] = latency; + update_latency(impl, latency.direction); } static void stream_state_changed(void *data, enum pw_stream_state old, @@ -288,13 +299,12 @@ static void capture_param_changed(void *data, uint32_t id, const struct spa_pod if (spa_format_audio_raw_parse(param, &info) < 0) return; if (info.rate == 0 || - info.channels == 0 || - info.channels > SPA_AUDIO_MAX_CHANNELS) + info.channels == 0) return; break; } case SPA_PARAM_Latency: - param_latency_changed(impl, param, &impl->capture_latency, impl->playback); + param_latency_changed(impl, param); break; } } @@ -320,7 +330,7 @@ static void playback_param_changed(void *data, uint32_t id, const struct spa_pod switch (id) { case SPA_PARAM_Latency: - param_latency_changed(impl, param, &impl->playback_latency, impl->capture); + param_latency_changed(impl, param); break; } } @@ -458,15 +468,16 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), &props->dict, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -520,6 +531,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->module = module; impl->context = context; + impl->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + impl->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL) pw_properties_setf(props, PW_KEY_NODE_GROUP, "filter-%u-%u", pid, id); @@ -537,6 +550,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); copy_props(impl, props, PW_KEY_NODE_GROUP); @@ -562,8 +576,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) if (pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION) == NULL) pw_properties_set(impl->playback_props, PW_KEY_NODE_DESCRIPTION, str); - parse_audio_info(impl->capture_props, &impl->capture_info); - parse_audio_info(impl->playback_props, &impl->playback_info); + if ((res = parse_audio_info(impl->capture_props, &impl->capture_info)) < 0 || + (res = parse_audio_info(impl->playback_props, &impl->playback_info)) < 0) { + pw_log_error( "can't parse formats: %s", spa_strerror(res)); + goto error; + } if (!impl->capture_info.rate && !impl->playback_info.rate) { if (pw_properties_get(impl->playback_props, "resample.disable") == NULL) diff --git a/src/modules/module-example-sink.c b/src/modules/module-example-sink.c index 8014f61f9..fecee854d 100644 --- a/src/modules/module-example-sink.c +++ b/src/modules/module-example-sink.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -14,8 +16,6 @@ #include #include -#include "config.h" - #include #include #include @@ -52,6 +52,7 @@ * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_NAME * - \ref PW_KEY_NODE_LATENCY @@ -273,9 +274,9 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), @@ -284,6 +285,7 @@ static void parse_audio_info(const struct pw_properties *props, struct spa_audio SPA_KEY_AUDIO_FORMAT, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -387,6 +389,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); @@ -395,7 +398,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_VIRTUAL); copy_props(impl, props, PW_KEY_MEDIA_CLASS); - parse_audio_info(impl->stream_props, &impl->info); + if ((res = parse_audio_info(impl->stream_props, &impl->info)) < 0) { + pw_log_error( "can't parse format: %s", spa_strerror(res)); + goto error; + } impl->frame_size = calc_frame_size(&impl->info); if (impl->frame_size == 0) { diff --git a/src/modules/module-example-source.c b/src/modules/module-example-source.c index 47a061891..21d3e34cc 100644 --- a/src/modules/module-example-source.c +++ b/src/modules/module-example-source.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -14,8 +16,6 @@ #include #include -#include "config.h" - #include #include #include @@ -52,6 +52,7 @@ * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_NAME * - \ref PW_KEY_NODE_LATENCY @@ -279,9 +280,9 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), @@ -290,6 +291,7 @@ static void parse_audio_info(const struct pw_properties *props, struct spa_audio SPA_KEY_AUDIO_FORMAT, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -393,6 +395,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); @@ -401,7 +404,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_VIRTUAL); copy_props(impl, props, PW_KEY_MEDIA_CLASS); - parse_audio_info(impl->stream_props, &impl->info); + if ((res = parse_audio_info(impl->stream_props, &impl->info)) < 0) { + pw_log_error( "can't parse format: %s", spa_strerror(res)); + goto error; + } impl->frame_size = calc_frame_size(&impl->info); if (impl->frame_size == 0) { diff --git a/src/modules/module-fallback-sink.c b/src/modules/module-fallback-sink.c index 86982e624..2e949cdb2 100644 --- a/src/modules/module-fallback-sink.c +++ b/src/modules/module-fallback-sink.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,8 +12,6 @@ #include #include -#include "config.h" - #include #include #include diff --git a/src/modules/module-ffado-driver.c b/src/modules/module-ffado-driver.c index 4345e5a19..f6a76bed0 100644 --- a/src/modules/module-ffado-driver.c +++ b/src/modules/module-ffado-driver.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -14,8 +16,6 @@ #include #include -#include "config.h" - #include #include #include @@ -66,6 +66,7 @@ * Options with well-known behavior. * * - \ref PW_KEY_REMOTE_NAME + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_NAME * - \ref PW_KEY_NODE_DESCRIPTION @@ -112,6 +113,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define MAX_PORTS 128 #define FFADO_RT_PRIORITY_PACKETIZER_RELATIVE 5 @@ -179,7 +181,7 @@ struct port { struct volume { bool mute; uint32_t n_volumes; - float volumes[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; }; struct stream { @@ -317,21 +319,21 @@ static inline void fix_midi_event(uint8_t *data, size_t size) static void midi_to_ffado(struct port *p, float *src, uint32_t n_samples) { - struct spa_pod *pod; - struct spa_pod_sequence *seq; - struct spa_pod_control *c; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + struct spa_pod_control c; + const void *seq_body, *c_body; uint32_t i, index = 0, unhandled = 0; uint32_t *dst = p->buffer; if (src == NULL) return; - if ((pod = spa_pod_from_data(src, n_samples * sizeof(float), 0, n_samples * sizeof(float))) == NULL) + spa_pod_parser_init_from_data(&parser, src, n_samples * sizeof(float), + 0, n_samples * sizeof(float)); + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) return; - if (!spa_pod_is_sequence(pod)) - return; - - seq = (struct spa_pod_sequence*)pod; clear_port_buffer(p, n_samples); @@ -342,31 +344,35 @@ static void midi_to_ffado(struct port *p, float *src, uint32_t n_samples) } p->event_pos = 0; - SPA_POD_SEQUENCE_FOREACH(seq, c) { + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { uint8_t data[16]; int j, size; + size_t c_size = c.value.size; + uint64_t state = 0; - if (c->type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_UMP) continue; - size = spa_ump_to_midi(SPA_POD_BODY(&c->value), - SPA_POD_BODY_SIZE(&c->value), data, sizeof(data)); - if (size <= 0) - continue; + if (index < c.offset) + index = SPA_ROUND_UP_N(c.offset, 8); - if (index < c->offset) - index = SPA_ROUND_UP_N(c->offset, 8); - for (j = 0; j < size; j++) { - if (index >= n_samples) { - /* keep events that don't fit for the next cycle */ - if (p->event_pos < sizeof(p->event_buffer)) - p->event_buffer[p->event_pos++] = data[j]; + while (c_size > 0) { + size = spa_ump_to_midi((const uint32_t**)&c_body, &c_size, data, sizeof(data), &state); + if (size <= 0) + break; + + for (j = 0; j < size; j++) { + if (index >= n_samples) { + /* keep events that don't fit for the next cycle */ + if (p->event_pos < sizeof(p->event_buffer)) + p->event_buffer[p->event_pos++] = data[j]; + else + unhandled++; + } else - unhandled++; + dst[index] = 0x01000000 | (uint32_t) data[j]; + index += 8; } - else - dst[index] = 0x01000000 | (uint32_t) data[j]; - index += 8; } } if (unhandled > 0) @@ -756,7 +762,7 @@ static int make_stream_ports(struct stream *s) struct port *port = s->ports[i]; char channel[32]; - snprintf(channel, sizeof(channel), "AUX%u", n_channels % SPA_AUDIO_MAX_CHANNELS); + snprintf(channel, sizeof(channel), "AUX%u", n_channels); switch (port->stream_type) { case ffado_stream_type_audio: @@ -772,7 +778,7 @@ static int make_stream_ports(struct stream *s) break; case ffado_stream_type_midi: props = pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, "8 bit raw midi", PW_KEY_PORT_NAME, port->name, PW_KEY_PORT_PHYSICAL, "true", PW_KEY_PORT_TERMINAL, "true", @@ -869,9 +875,9 @@ static void parse_props(struct stream *s, const struct spa_pod *param) case SPA_PROP_channelVolumes: { uint32_t n; - float vols[SPA_AUDIO_MAX_CHANNELS]; + float vols[MAX_CHANNELS]; if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - vols, SPA_AUDIO_MAX_CHANNELS)) > 0) { + vols, SPA_N_ELEMENTS(vols))) > 0) { s->volume.n_volumes = n; for (n = 0; n < s->volume.n_volumes; n++) s->volume.volumes[n] = vols[n]; @@ -1223,8 +1229,9 @@ static int probe_ffado_device(struct impl *impl) impl->source.ports[i] = port; } if (impl->source.info.channels != n_channels) { - impl->source.info.channels = n_channels; - for (i = 0; i < SPA_MIN(impl->source.info.channels, SPA_AUDIO_MAX_CHANNELS); i++) + uint32_t n_pos = SPA_MIN(n_channels, SPA_N_ELEMENTS(impl->source.info.position)); + impl->source.info.channels = n_pos; + for (i = 0; i < n_pos; i++) impl->source.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; } @@ -1249,8 +1256,9 @@ static int probe_ffado_device(struct impl *impl) impl->sink.ports[i] = port; } if (impl->sink.info.channels != n_channels) { - impl->sink.info.channels = n_channels; - for (i = 0; i < SPA_MIN(impl->sink.info.channels, SPA_AUDIO_MAX_CHANNELS); i++) + uint32_t n_pos = SPA_MIN(n_channels, SPA_N_ELEMENTS(impl->sink.info.position)); + impl->sink.info.channels = n_pos; + for (i = 0; i < n_pos; i++) impl->sink.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; } @@ -1422,14 +1430,15 @@ static void parse_devices(struct impl *impl, const char *val, size_t len) } } -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), &props->dict, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -1575,8 +1584,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_VIRTUAL); copy_props(impl, props, PW_KEY_NODE_PAUSE_ON_IDLE); - parse_audio_info(impl->source.props, &impl->source.info); - parse_audio_info(impl->sink.props, &impl->sink.info); + if ((res = parse_audio_info(impl->source.props, &impl->source.info)) < 0 || + (res = parse_audio_info(impl->sink.props, &impl->sink.info)) < 0) { + pw_log_error( "can't parse format: %s", spa_strerror(res)); + goto error; + } impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index 1446cc437..2cf053ff8 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -11,8 +13,6 @@ #include #include -#include "config.h" - #include #include #include @@ -32,8 +32,8 @@ extern struct spa_handle_factory spa_filter_graph_factory; * \page page_module_filter_chain Filter-Chain * * The filter-chain allows you to create an arbitrary processing graph - * from LADSPA, LV2 and builtin filters. This filter can be made into a - * virtual sink/source or between any 2 nodes in the graph. + * from LADSPA, LV2, sofa, ffmpeg and builtin filters. This filter can be + * made into a virtual sink/source or between any 2 nodes in the graph. * * The filter chain is built with 2 streams, a capture stream providing * the input to the filter chain and a playback stream sending out the @@ -95,7 +95,7 @@ extern struct spa_handle_factory spa_filter_graph_factory; * Nodes describe the processing filters in the graph. Use a tool like lv2ls * or listplugins to get a list of available plugins, labels and the port names. * - * - `type` is one of `ladspa`, `lv2`, `builtin`, `sofa` or `ebur128`. + * - `type` is one of `ladspa`, `lv2`, `builtin`, `sofa`, `ebur128` of `ffmpeg`. * - `name` is the name for this node, you might need this later to refer to this node * and its ports when setting controls or making links. * - `plugin` is the type specific plugin name. @@ -103,17 +103,68 @@ extern struct spa_handle_factory spa_filter_graph_factory; * name in the LADSPA plugin path. * - For LV2, this is the plugin URI obtained with lv2ls. * - For builtin, sofa and ebur128 this is ignored + * - For ffmpeg this should be filtergraph * - `label` is the type specific filter inside the plugin. * - For LADSPA this is the label * - For LV2 this is unused * - For builtin, sofa and ebur128 this is the name of the filter to use + * - For ffmpeg this is an FFMpeg filtergraph description * * - `config` contains a filter specific configuration section. Some plugins need * this. (convolver, sofa, delay, ...) + * - For lv2, the config can contain a set of state key/value pairs. If the lv2 + * plugin supports the LV2_STATE__interface, these values will be provided for + * the given keys. * - `control` contains the initial values for the control ports of the filter. * normally these are given with the port name but it is also possible * to give the control index as the key. * + * Some examples ladspa and lv2 plugins: + * + *\code{.unparsed} + * filter.graph = { + * nodes = [ + * { + * # an example ladspa plugin + * type = ladspa + * name = pitch + * plugin = "/usr/lib64/ladspa/ladspa-rubberband.so" + * label = "rubberband-r3-pitchshifter-mono" + * control = { + * # controls are using the ladspa port names as seen in analyseplugin + * "Semitones" = -3 + * } + * } + * { + * # an example lv2 plugin + * type = lv2 + * name = pitch + * plugin = "http://breakfastquay.com/rdf/lv2-rubberband#mono" + * control = { + * # controls are using the lv2 symbol as seen with lv2info + * "semitones" = -3 + * } + * } + * { + * # an example lv2 plugin with a state + * type = lv2 + * name = neural + * plugin = "http://aidadsp.cc/plugins/aidadsp-bundle/rt-neural-generic" + * control = { + * # use the port symbols as seen with lv2info + * PRESENCE = 1.0 + * } + * config = { + * # the config contains state keys and values + * "http://aidadsp.cc/plugins/aidadsp-bundle/rt-neural-generic#json" = + * "/usr/lib64/lv2/rt-neural-generic.lv2/models/deer ink studios/tw40_blues_solo_deerinkstudios.json" + * } + * } + * } + * ... + * } + *\endcode + * * ### Links * * Links can be made between ports of nodes. The `portname` is given as @@ -160,8 +211,8 @@ extern struct spa_handle_factory spa_filter_graph_factory; * * ## Builtin filters * - * There are some useful builtin filters available. You select them with the label - * of the filter node. + * There are some useful builtin filters available. The type should be `builtin` and + * you select the specific builtin filter with the `label` of the filter node. * * ### Mixer * @@ -330,6 +381,7 @@ extern struct spa_handle_factory spa_filter_graph_factory; * length = ... * channel = ... * resample_quality = ... + * latency = ... * } * ... * } @@ -361,6 +413,8 @@ extern struct spa_handle_factory spa_filter_graph_factory; * - `channel` The channel to use from the file as the IR. * - `resample_quality` The resample quality in case the IR does not match the graph * samplerate. + * - `latency` The extra latency in seconds to report. When left unspecified (or < 0.0) + * the convolver latency will be the length of the IR. * * ### Delay * @@ -379,6 +433,7 @@ extern struct spa_handle_factory spa_filter_graph_factory; * label = delay * config = { * "max-delay" = ... + * "latency" = ... * } * control = { * "Delay (s)" = ... @@ -392,6 +447,8 @@ extern struct spa_handle_factory spa_filter_graph_factory; * * - `max-delay` the maximum delay in seconds. The "Delay (s)" parameter will * be clamped to this value. + * - `latency` the latency in seconds. This is 0 by default but in some cases + * the delay can be used to introduce latency with this option. * * ### Invert * @@ -480,9 +537,12 @@ extern struct spa_handle_factory spa_filter_graph_factory; * * ### Max * - * Use the `max` plugin if you need to select the max value of two channels. + * Use the `max` plugin if you need to select the max value of a number of input ports. * - * It has two input ports "In 1" and "In 2" and one output port "Out". + * It has 8 input ports named "In 1" to "In 8" and one output port "Out". + * + * All input ports samples are checked to find the maximum value per sample. Unused + * input ports will be ignored and not cause overhead. * * ### dcblock * @@ -506,9 +566,87 @@ extern struct spa_handle_factory spa_filter_graph_factory; * a volume ramp up or down. For more a more coarse volume ramp, the "Current" value * can be used in the `linear` plugin. * - * ## SOFA filter + * ### Debug * - * There is an optional builtin SOFA filter available. + * The `debug` plugin can be used to debug the audio and control data of other plugins. + * + * It has an "In" input port and an "Out" output data ports. The data from "In" will + * be copied to "Out" and the data will be dumped into the INFO log. + * + * There is also a "Control" input port and an "Notify" output control ports. The + * control from "Control" will be copied to "Notify" and the control value will be + * dumped into the INFO log. + * + * ### Pipe + * + * The `pipe` plugin can be used to filter the audio with another application using pipes + * for sending and receiving the raw audio. + * + * The application needs to consume raw float32 samples from stdin and produce filtered + * float32 samples on stdout. + * + * It has an "In" input port and an "Out" output data ports. + * + * The node requires a `config` section with extra configuration: + * + *\code{.unparsed} + * filter.graph = { + * nodes = [ + * { + * type = builtin + * name = ... + * label = pipe + * config = { + * command = "ffmpeg -f f32le -ac 1 -ar 48000 -blocksize 1024 -fflags nobuffer -i \"pipe:\" \"-filter:a\" \"loudnorm=I=-18:TP=-3:LRA=4\" -f f32le -ac 1 -ar 48000 \"pipe:\"" + * } + * ... + * } + * } + * ... + * } + *\endcode + * + * - `command` the command to execute. It should consume samples from stdin and produce + * samples on stdout. + * + * ### Zeroramp + * + * The `zeroramp` plugin can be used to detect unnatural silence parts in the audio + * stream and ramp the volume down or up when entering or leaving the silent area + * respectively. + * This can be used to avoid loud pops and clicks that occur when the sample values + * suddenly drop to zero or jump from zero to a large value caused by a pause, + * resume or an error of the stream. It only detect areas where the sample values + * are absolute zero values, such as those inserted when pausing a stream. + * + * It has an "In" input port and an "Out" output data ports. + * + * There are also "Gap (s)" and an "Duration (s)" input control ports. "Gap (s)" + * determines how long the silence gap is in seconds (default 0.000666) and + * "Duration (s)" determines how long the fade-in and fade-out should last + * (default 0.000666). + * + * ### Noisegate + * + * The `noisegate` plugin can be used to remove low volume noise. + * + * It has an "In" input port and an "Out" output data ports. Normally the input + * data is passed directly to the output. + * + * The "Level" control port can be used to control the measured volume of the "In" + * port. When not connected, a simple volume algorithm on the "In" port will be + * used. + * + * If the volume drops below "Close threshold", the noisegate will ramp down the + * volume to zero for a duration of "Release (s)" seconds. When the volume is above + * "Open threshold", the noisegate will ramp up the volume to 1 for a duration + * of "Attack (s)" seconds. The noise gate stays open for at least "Hold (s)" + * seconds before it can close again. + * + * + * ## SOFA filters + * + * There is an optional `sofa` type available (when compiled with `libmysofa`). * * ### Spatializer * @@ -561,13 +699,15 @@ extern struct spa_handle_factory spa_filter_graph_factory; * - `Radius` controls how far away the signal is as a value between 0 and 100. * default is 1.0. * - * ## EBUR128 filter + * ## EBUR128 filters * - * There is an optional EBU R128 filter available. + * There is an optional EBU R128 plugin available (when compiled with + * `libebur128`) selected with the `ebur128` type. Filters in the plugin + * can be selected with the `label` field. * * ### ebur128 * - * The ebur128 plugin can be used to measure the loudness of a signal. + * The ebur128 filter can be used to measure the loudness of a signal. * * It has 7 input ports "In FL", "In FR", "In FC", "In UNUSED", "In SL", "In SR" * and "In DUAL MONO", corresponding to the different input channels for EBUR128. @@ -580,7 +720,7 @@ extern struct spa_handle_factory spa_filter_graph_factory; * and that can be used to control the processing of the audio. Some of these ports * contain values in LUFS, or "Loudness Units relative to Full Scale". These are * negative values, closer to 0 is louder. You can use the lufs2gain plugin to - * convert this value to again to adjust a volume (See below). + * convert this value to a gain to adjust a volume (See below). * * "Momentary LUFS" contains the momentary loudness measurement with a 400ms window * and 75% overlap. It works mostly like an R.M.S. meter. @@ -631,14 +771,222 @@ extern struct spa_handle_factory spa_filter_graph_factory; * * ### lufs2gain * - * The lufs2gain plugin can be used to convert LUFS control values to gain. It needs + * The lufs2gain filter can be used to convert LUFS control values to gain. It needs * a target LUFS control input to drive the conversion. * * It has 2 input control ports "LUFS" and "Target LUFS" and will produce 1 output * control value "Gain". This gain can be used as input for the builtin `linear` - * node, for example, to adust the gain. + * filter, for example, to adust the gain. * * + * ## FFmpeg + * + * There is an optional FFmpeg filter available (when compiled with `libavfilter`) + * that can be selected with the `ffmpeg` type. Use the `plugin` field to select + * the plugin to use. + * + * ### Filtergraph + * + * The filtergraph FFmpeg plugin is selected with the `filtergraph` plugin + * field in the node. + * + * The filtergraph filter allows you to specify an set of audio filters using + * the FFmpeg filtergraph syntax (https://ffmpeg.org/ffmpeg-filters.html). + * + * The `label` field should be used to describe the filtergraph in use. + * + * FFmpeg filtergraph input and output ports can have multiple channels. The + * filter-chain can split those into individual ports to use as input and output + * ports. For this, the ports in the filtergraph need to have a specific name + * convention, either `_` or `_`. + * + * When a single channel is specified, the port can be referenced in inputs and + * outputs sections with `:_`. When a channel-layout + * is specified, each port name gets a `_` appended, starting from 0 and + * counting up for each channel in the layout. + * + * The `filtergraph` plugin will automatically add format converters when the input + * port channel-layout, format or graph sample-rates don't match. + * + * Note that the FFmpeg filtergraph is not Real-time safe because it might do + * allocations from the processing thread. It is advised to run the filter-chain + * streams in async mode (`node.async = true`) to avoid interrupting the other + * RT threads. + * + * Some examples: + * + * The stereo ports are split into their channels with the `_0` and `_1` suffixes. + * + *\code{.unparsed} + * filter.graph = { + * nodes = [ + * { + * type = ffmpeg + * plugin = filtergraph + * name = filter + * label = "[in_stereo]loudnorm=I=-18:TP=-3:LRA=4[out_stereo]" + * } + * } + * inputs = [ "filter:in_stereo_0" "filter:in_stereo_1" ] + * outputs = [ "filter:out_stereo_0" "filter:out_stereo_1" ] + * ... + * } + *\endcode + * + * It is possible to have multiple input and output ports for the filtergraphs. + * In the next example, the ports have a single channel name and so don't have + * the `_0` suffix to identify them. This can be simplified by removing the `amerge` + * and `channelsplit` filters and using the `_stereo` suffix on port names to let + * PipeWire do the splitting and merging more efficiently. + * + *\code{.unparsed} + * filter.graph = { + * nodes = [ + * { + * type = ffmpeg + * plugin = filtergraph + * name = filter + * label = "[in_FL][in_FR]amerge,extrastereo,channelsplit[out_FL][out_FR]" + * } + * } + * inputs = [ "filter:in_FL" "filter:in_FR" ] + * outputs = [ "filter:out_FL" "filter:out_FR" ] + * ... + * } + *\endcode + * + * Here is a last example of a surround sound upmixer: + * + *\code{.unparsed} + * filter.graph = { + * nodes = [ + * { + * type = ffmpeg + * plugin = filtergraph + * name = filter + * label = "[in_stereo]surround[out_5.1]" + * } + * } + * inputs = [ "filter:in_FL" "filter:in_FR" ] + * outputs = [ "filter:out_5.1_0" "filter:out_5.1_1" "filter:out_5.1_2" + * "filter:out_5.1_3" "filter:out_5.1_4" "filter:out_5.1_5" ] + * ... + * } + *\endcode + + * ## ONNX filters + * + * There is an optional ONNX filter available (when compiled with `libonnxruntime`) + * that can be selected with the `onnx` type. Use the `label` field to select + * the model to use and how to map the tensors to ports. + * + *\code{.unparsed} + * filter.graph = { + * nodes = [ + * { + * type = onnx + * name = onnx + * label = { + * filename = "..." + * blocksize = 512 + * input-tensors = { + * "" = { + * dimensions = [ ... ] + * #retain = 64 + * data = "port:..."|"tensor:..."|"param:..."|"control:..." + * } + * ... + * } + * output-tensors = { + * "" = { + * dimensions = [ ... ] + * #retain = 64 + * data = "port:..."|"tensor:..."|"param:..."|"control:..." + * } + * ... + * } + * } + * } + * } + * ... + * } + *\endcode + * + * The label must contain an object with the configuration of the plugin. + * + * - `filename` the ONNX model to load. It must point to an existing onnx file. + * - `blocksize` the number of samples to give to the model. This depends on the model + * and the input/output tensor sizes. + * - `input-tensors` an object of input tensors of the model and how they should be + * used. Unlisted tensors will not be used. + * - `output-tensors` an object of output tensors of the model and how they should be + * used. Unlisted tensors will not be used. + * + * The `input-tensors` and `output-tensors` configuration must contain an object with + * keys named after the tensors in the model and the value must be an object with the + * the following keys: + * + * - `dimensions` and array of dimensions of the tensors. + * - `retain` an optional key for input tensors. This will prepend the last `retain` samples + * from the previous block to the input tensor. The size of the tensor should + * therefore at least be blocksize + retain samples large. + * - `data` where the data for the tensor is comming from. There are different options + * based on the value of this file, selected with a prefix: + * - `port:` a new input/output port is created on the plugin with the + * name and the data for the tensor will be obtained + * or copied from/to the port data. + * - `tensor:` the data of this tensor is copied from the given + * . You can use this to copy output state + * info to the input state, for example. + * - `param:` the data of this tensor is obtained from a parameter with + * . Currently only `rate` is a valid paramname, + * which has the value of the filter samplerate. + * - `control:` a new input/output control port is created and the tensor + * data will be obtained/copied from/to the control data. + * + * Here is an example of the silero VAD model: + * + *\code{.unparsed} + * filter.graph = { + * nodes = [ + * { + * type = onnx + * name = onnx + * label = { + * filename = "/home/wim/src/silero-vad/src/silero_vad/data/silero_vad.onnx" + * blocksize = 512 + * input-tensors = { + * "input" = { + * dimensions = [ 1, 576 ] + * retain = 64 + * data = "port:input" + * } + * "state" = { + * dimensions = [ 2, 1, 128 ] + * data = "tensor:stateN" + * } + * "sr" = { + * dimensions = [ 1 ] + * data = "param:rate" + * } + * } + * output-tensors = { + * "output" = { + * dimensions = [ 1, 1 ] + * data = "control:speech" + * } + * "stateN" = { + * dimensions = [ 2, 1, 128 ] + * } + * } + * } + * } + * ... + * ] + * .... + * } + *\endcode + * * ## General options * * Options with well-known behavior. Most options can be added to the global @@ -647,6 +995,7 @@ extern struct spa_handle_factory spa_filter_graph_factory; * - \ref PW_KEY_REMOTE_NAME * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_NAME * - \ref PW_KEY_NODE_LATENCY @@ -844,6 +1193,10 @@ struct impl { struct spa_hook graph_listener; uint32_t n_inputs; uint32_t n_outputs; + bool graph_active; + + struct spa_latency_info latency[2]; + struct spa_process_latency_info process_latency; }; static void capture_destroy(void *d) @@ -863,6 +1216,8 @@ static void capture_process(void *d) struct pw_buffer *t; if ((t = pw_stream_dequeue_buffer(impl->capture)) == NULL) break; + /* playback part is not ready, consume, discard and recycle + * the capture buffers */ pw_stream_queue_buffer(impl->capture, t); } } @@ -929,7 +1284,8 @@ static void playback_process(void *d) pw_log_trace_fp("%p: stride:%d size:%d requested:%"PRIu64" (%"PRIu64")", impl, stride, data_size, out->requested, out->requested * stride); - spa_filter_graph_process(impl->graph, cin, cout, data_size / sizeof(float)); + if (impl->graph_active) + spa_filter_graph_process(impl->graph, cin, cout, data_size / sizeof(float)); done: if (in != NULL) @@ -938,26 +1294,120 @@ done: pw_stream_queue_buffer(impl->playback, out); } -static void param_latency_changed(struct impl *impl, const struct spa_pod *param) +static int activate_graph(struct impl *impl) +{ + char rate[64]; + int res; + + if (impl->graph_active) + return 0; + + snprintf(rate, sizeof(rate), "%lu", impl->rate); + res = spa_filter_graph_activate(impl->graph, &SPA_DICT_ITEMS( + SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, rate))); + + if (res >= 0) { + struct pw_loop *data_loop = pw_stream_get_data_loop(impl->playback); + pw_loop_lock(data_loop); + impl->graph_active = true; + pw_loop_unlock(data_loop); + } + return res; +} + +static int deactivate_graph(struct impl *impl) +{ + struct pw_loop *data_loop; + + if (!impl->graph_active) + return 0; + + data_loop = pw_stream_get_data_loop(impl->playback); + + pw_loop_lock(data_loop); + impl->graph_active = false; + pw_loop_unlock(data_loop); + + return spa_filter_graph_deactivate(impl->graph); +} + +static int reset_graph(struct impl *impl) +{ + struct pw_loop *data_loop = pw_stream_get_data_loop(impl->playback); + int res; + bool old_active = impl->graph_active; + + pw_loop_lock(data_loop); + impl->graph_active = false; + pw_loop_unlock(data_loop); + + res = spa_filter_graph_reset(impl->graph); + + pw_loop_lock(data_loop); + impl->graph_active = old_active; + pw_loop_unlock(data_loop); + + return res; +} + +static void update_latency(struct impl *impl, enum spa_direction direction, bool process) { struct spa_latency_info latency; uint8_t buffer[1024]; struct spa_pod_builder b; - const struct spa_pod *params[1]; + const struct spa_pod *params[2]; + uint32_t n_params = 0; + struct pw_stream *s = direction == SPA_DIRECTION_OUTPUT ? + impl->playback : impl->capture; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + latency = impl->latency[direction]; + spa_process_latency_info_add(&impl->process_latency, &latency); + params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &latency); + + if (process) { + params[n_params++] = spa_process_latency_build(&b, + SPA_PARAM_ProcessLatency, &impl->process_latency); + } + pw_stream_update_params(s, params, n_params); +} + +static void update_latencies(struct impl *impl, bool process) +{ + update_latency(impl, SPA_DIRECTION_INPUT, process); + update_latency(impl, SPA_DIRECTION_OUTPUT, process); +} + +static void param_latency_changed(struct impl *impl, const struct spa_pod *param, + enum spa_direction direction) +{ + struct spa_latency_info latency; if (param == NULL || spa_latency_parse(param, &latency) < 0) return; - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - params[0] = spa_latency_build(&b, SPA_PARAM_Latency, &latency); - - if (latency.direction == SPA_DIRECTION_INPUT) - pw_stream_update_params(impl->capture, params, 1); - else - pw_stream_update_params(impl->playback, params, 1); + impl->latency[latency.direction] = latency; + update_latency(impl, latency.direction, false); } -static void param_tag_changed(struct impl *impl, const struct spa_pod *param) +static void param_process_latency_changed(struct impl *impl, const struct spa_pod *param, + enum spa_direction direction) +{ + struct spa_process_latency_info process_latency; + + if (param == NULL) + spa_zero(process_latency); + else if (spa_process_latency_parse(param, &process_latency) < 0) + return; + if (spa_process_latency_info_compare(&impl->process_latency, &process_latency) == 0) + return; + + impl->process_latency = process_latency; + update_latencies(impl, true); +} + +static void param_tag_changed(struct impl *impl, const struct spa_pod *param, + enum spa_direction direction) { struct spa_tag_info tag; const struct spa_pod *params[1] = { param }; @@ -972,18 +1422,109 @@ static void param_tag_changed(struct impl *impl, const struct spa_pod *param) pw_stream_update_params(impl->playback, params, 1); } -static void state_changed(void *data, enum pw_stream_state old, +static void capture_state_changed(void *data, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct impl *impl = data; + + switch (state) { + case PW_STREAM_STATE_PAUSED: + pw_stream_flush(impl->capture, false); + break; + case PW_STREAM_STATE_UNCONNECTED: + pw_log_info("module %p: unconnected", impl); + pw_impl_module_schedule_destroy(impl->module); + break; + case PW_STREAM_STATE_ERROR: + pw_log_info("module %p: error: %s", impl, error); + break; + case PW_STREAM_STATE_STREAMING: + default: + break; + } + return; +} + +static void io_changed(void *data, uint32_t id, void *area, uint32_t size) +{ + struct impl *impl = data; + switch (id) { + case SPA_IO_Position: + impl->position = area; + break; + default: + break; + } +} + +static void param_changed(struct impl *impl, uint32_t id, const struct spa_pod *param, + enum spa_direction direction, struct pw_stream *stream, struct pw_stream *other) +{ + int res; + + switch (id) { + case SPA_PARAM_Format: + { + struct spa_audio_info_raw info; + spa_zero(info); + if (param == NULL) { + pw_log_info("module %p: filter deactivate", impl); + if (direction == SPA_DIRECTION_OUTPUT) + deactivate_graph(impl); + impl->rate = 0; + } else { + if ((res = spa_format_audio_raw_parse(param, &info)) < 0) + goto error; + } + impl->info = info; + break; + } + case SPA_PARAM_Props: + if (param != NULL) + spa_filter_graph_set_props(impl->graph, direction, param); + break; + case SPA_PARAM_Latency: + param_latency_changed(impl, param, direction); + break; + case SPA_PARAM_ProcessLatency: + param_process_latency_changed(impl, param, direction); + break; + case SPA_PARAM_Tag: + param_tag_changed(impl, param, direction); + break; + } + return; + +error: + pw_stream_set_error(direction == SPA_DIRECTION_INPUT ? impl->capture : impl->playback, + res, "can't start graph: %s", spa_strerror(res)); +} + +static void capture_param_changed(void *data, uint32_t id, const struct spa_pod *param) +{ + struct impl *impl = data; + param_changed(impl, id, param, SPA_DIRECTION_INPUT, impl->capture, impl->playback); +} + +static const struct pw_stream_events in_stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = capture_destroy, + .process = capture_process, + .io_changed = io_changed, + .state_changed = capture_state_changed, + .param_changed = capture_param_changed +}; + +static void playback_state_changed(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error) { struct impl *impl = data; - struct spa_filter_graph *graph = impl->graph; int res; switch (state) { case PW_STREAM_STATE_PAUSED: pw_stream_flush(impl->playback, false); - pw_stream_flush(impl->capture, false); - spa_filter_graph_reset(graph); + reset_graph(impl); break; case PW_STREAM_STATE_UNCONNECTED: pw_log_info("module %p: unconnected", impl); @@ -1003,15 +1544,11 @@ static void state_changed(void *data, enum pw_stream_state old, goto error; } if (impl->rate != target) { - char rate[64]; impl->rate = target; - snprintf(rate, sizeof(rate), "%lu", impl->rate); - spa_filter_graph_deactivate(graph); - if ((res = spa_filter_graph_activate(graph, - &SPA_DICT_ITEMS( - SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, rate)))) < 0) - goto error; + deactivate_graph(impl); } + if ((res = activate_graph(impl)) < 0) + goto error; break; } default: @@ -1023,77 +1560,10 @@ error: spa_strerror(res)); } -static void io_changed(void *data, uint32_t id, void *area, uint32_t size) -{ - struct impl *impl = data; - switch (id) { - case SPA_IO_Position: - impl->position = area; - break; - default: - break; - } -} - -static void param_changed(void *data, uint32_t id, const struct spa_pod *param, - bool capture) -{ - struct impl *impl = data; - struct spa_filter_graph *graph = impl->graph; - int res; - - switch (id) { - case SPA_PARAM_Format: - { - struct spa_audio_info_raw info; - spa_zero(info); - if (param == NULL) { - spa_filter_graph_deactivate(graph); - impl->rate = 0; - } else { - if ((res = spa_format_audio_raw_parse(param, &info)) < 0) - goto error; - } - impl->info = info; - break; - } - case SPA_PARAM_Props: - if (param != NULL) - spa_filter_graph_set_props(impl->graph, - capture ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT, param); - - break; - case SPA_PARAM_Latency: - param_latency_changed(impl, param); - break; - case SPA_PARAM_Tag: - param_tag_changed(impl, param); - break; - } - return; - -error: - pw_stream_set_error(capture ? impl->capture : impl->playback, - res, "can't start graph: %s", spa_strerror(res)); -} - -static void capture_param_changed(void *data, uint32_t id, const struct spa_pod *param) -{ - param_changed(data, id, param, true); -} - -static const struct pw_stream_events in_stream_events = { - PW_VERSION_STREAM_EVENTS, - .destroy = capture_destroy, - .process = capture_process, - .io_changed = io_changed, - .state_changed = state_changed, - .param_changed = capture_param_changed -}; - static void playback_param_changed(void *data, uint32_t id, const struct spa_pod *param) { - param_changed(data, id, param, false); + struct impl *impl = data; + param_changed(impl, id, param, SPA_DIRECTION_OUTPUT, impl->playback, impl->capture); } static void playback_destroy(void *d) @@ -1108,7 +1578,7 @@ static const struct pw_stream_events out_stream_events = { .destroy = playback_destroy, .process = playback_process, .io_changed = io_changed, - .state_changed = state_changed, + .state_changed = playback_state_changed, .param_changed = playback_param_changed, }; @@ -1153,16 +1623,22 @@ static int setup_streams(struct impl *impl) SPA_PARAM_EnumFormat, &impl->capture_info); for (i = 0;; i++) { - if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL) - *offs = b.b.state.offset; + uint32_t save = b.b.state.offset; if (spa_filter_graph_enum_prop_info(graph, i, &b.b, NULL) != 1) break; + if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL) + *offs = save; } if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL) *offs = b.b.state.offset; spa_filter_graph_get_props(graph, &b.b, NULL); + if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL) + *offs = b.b.state.offset; + spa_process_latency_build(&b.b, + SPA_PARAM_ProcessLatency, &impl->process_latency); + n_params = pw_array_get_len(&offsets, uint32_t); if (n_params == 0) { res = -ENOMEM; @@ -1225,6 +1701,9 @@ static void copy_position(struct spa_audio_info_raw *dst, const struct spa_audio static void graph_info(void *object, const struct spa_filter_graph_info *info) { struct impl *impl = object; + struct spa_dict *props = info->props; + uint32_t i; + if (impl->capture_info.channels == 0) impl->capture_info.channels = info->n_inputs; if (impl->playback_info.channels == 0) @@ -1237,6 +1716,20 @@ static void graph_info(void *object, const struct spa_filter_graph_info *info) copy_position(&impl->capture_info, &impl->playback_info); copy_position(&impl->playback_info, &impl->capture_info); } + for (i = 0; props && i < props->n_items; i++) { + const char *k = props->items[i].key; + const char *s = props->items[i].value; + pw_log_info("%s %s", k, s); + if (spa_streq(k, "latency")) { + double latency; + if (spa_atod(s, &latency)) { + if (impl->process_latency.rate != (int32_t)latency) { + impl->process_latency.rate = (int32_t)latency; + update_latencies(impl, true); + } + } + } + } } static void graph_apply_props(void *object, enum spa_direction direction, const struct spa_pod *props) @@ -1340,13 +1833,15 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P")), &props->dict, + SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -1404,6 +1899,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->module = module; impl->context = context; + impl->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + impl->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); if (pw_properties_get(props, PW_KEY_NODE_GROUP) == NULL) pw_properties_setf(props, PW_KEY_NODE_GROUP, "filter-chain-%u-%u", pid, id); @@ -1423,6 +1920,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); copy_props(impl, props, PW_KEY_NODE_GROUP); @@ -1432,8 +1930,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_MEDIA_NAME); copy_props(impl, props, "resample.prefill"); - parse_audio_info(impl->capture_props, &impl->capture_info); - parse_audio_info(impl->playback_props, &impl->playback_info); + if ((res = parse_audio_info(impl->capture_props, &impl->capture_info)) < 0 || + (res = parse_audio_info(impl->playback_props, &impl->playback_info)) < 0) { + pw_log_error( "can't parse format: %s", spa_strerror(res)); + goto error; + } if (!impl->capture_info.rate && !impl->playback_info.rate) { if (pw_properties_get(impl->playback_props, "resample.disable") == NULL) diff --git a/src/modules/module-jack-tunnel.c b/src/modules/module-jack-tunnel.c index 48e8c6f8e..760da1669 100644 --- a/src/modules/module-jack-tunnel.c +++ b/src/modules/module-jack-tunnel.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -14,8 +16,6 @@ #include #include -#include "config.h" - #include #include #include @@ -49,12 +49,18 @@ * ## Module Options * * - `jack.library`: the libjack to load, by default libjack.so.0 is searched in - * JACK_PATH directories and then some standard library paths. + * LIBJACK_PATH directories and then some standard library paths. * Can be an absolute path. * - `jack.server`: the name of the JACK server to tunnel to. * - `jack.client-name`: the name of the JACK client. * - `jack.connect`: if jack ports should be connected automatically. Can also be * placed per stream. + * - `jack.connect-audio`: An array of audio ports to connect to. Can also be placed per + * stream. An empty array will not connect anything, even when + * jack.connect is true. + * - `jack.connect-midi`: An array of midi ports to connect to. Can also be placed per + * stream. An empty array will not connect anything, even when + * jack.connect is true. * - `tunnel.mode`: the tunnel mode, sink|source|duplex, default duplex * - `midi.ports`: the number of midi ports. Can also be added to the stream props. * - `source.props`: Extra properties for the source filter. @@ -66,6 +72,7 @@ * * - \ref PW_KEY_REMOTE_NAME * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_NAME * - \ref PW_KEY_NODE_DESCRIPTION @@ -86,6 +93,8 @@ * #jack.server = null * #jack.client-name = PipeWire * #jack.connect = true + * #jack.connect-audio = [ playback_1 playback_2 ] + * #jack.connect-midi = [ midi_playback_1 ] * #tunnel.mode = duplex * #midi.ports = 0 * #audio.channels = 2 @@ -107,6 +116,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define MAX_PORTS 128 #define DEFAULT_CLIENT_NAME "PipeWire" @@ -118,6 +128,8 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); "( jack.server= ) " \ "( jack.client-name= ] " \ "( jack.connect= ] " \ + "( jack.connect-audio= ] "\ + "( jack.connect-midi= ] " \ "( tunnel.mode= ] " \ "( midi.ports= ] " \ "( audio.channels= ] " \ @@ -147,7 +159,7 @@ struct port { struct volume { bool mute; uint32_t n_volumes; - float volumes[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; }; struct stream { @@ -164,7 +176,6 @@ struct stream { struct volume volume; unsigned int running:1; - unsigned int connect:1; }; struct impl { @@ -243,40 +254,56 @@ static inline void fix_midi_event(uint8_t *data, size_t size) static void midi_to_jack(struct impl *impl, float *dst, float *src, uint32_t n_samples) { - struct spa_pod *pod; - struct spa_pod_sequence *seq; - struct spa_pod_control *c; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + struct spa_pod_control c; + const void *seq_body, *c_body; int res; + bool in_sysex = false; + uint8_t tmp[n_samples * 4]; + size_t tmp_size = 0; jack.midi_clear_buffer(dst); if (src == NULL) return; - if ((pod = spa_pod_from_data(src, n_samples * sizeof(float), 0, n_samples * sizeof(float))) == NULL) - return; - if (!spa_pod_is_sequence(pod)) + spa_pod_parser_init_from_data(&parser, src, n_samples * sizeof(float), + 0, n_samples * sizeof(float)); + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) return; - seq = (struct spa_pod_sequence*)pod; - - SPA_POD_SEQUENCE_FOREACH(seq, c) { - uint8_t data[16]; + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { int size; + size_t c_size = c.value.size; + uint64_t state = 0; - if (c->type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_UMP) continue; - size = spa_ump_to_midi(SPA_POD_BODY(&c->value), - SPA_POD_BODY_SIZE(&c->value), data, sizeof(data)); - if (size <= 0) - continue; + while (c_size > 0) { + size = spa_ump_to_midi((const uint32_t**)&c_body, &c_size, + &tmp[tmp_size], sizeof(tmp) - tmp_size, &state); + if (size <= 0) + break; - if (impl->fix_midi) - fix_midi_event(data, size); + if (impl->fix_midi) + fix_midi_event(&tmp[tmp_size], size); - if ((res = jack.midi_event_write(dst, c->offset, data, size)) < 0) - pw_log_warn("midi %p: can't write event: %s", dst, - spa_strerror(res)); + if (!in_sysex && tmp[tmp_size] == 0xf0) + in_sysex = true; + + tmp_size += size; + if (in_sysex && tmp[tmp_size-1] == 0xf7) + in_sysex = false; + + if (!in_sysex) { + if ((res = jack.midi_event_write(dst, c.offset, tmp, tmp_size)) < 0) + pw_log_warn("midi %p: can't write event: %s", dst, + spa_strerror(res)); + tmp_size = 0; + } + } } } @@ -450,10 +477,10 @@ static void make_stream_ports(struct stream *s) struct pw_properties *props; const char *str, *prefix, *type; char name[256]; - const char **audio_ports = NULL, **link_ports = NULL; - const char **midi_ports = NULL; + char **audio_ports = NULL, **midi_ports = NULL; unsigned long jack_peer, jack_flags; - bool is_midi; + bool do_connect, is_midi, strv_audio = false, strv_midi = false; + int res, n_audio_ports = 0, n_midi_ports = 0; if (s->direction == PW_DIRECTION_INPUT) { /* sink */ @@ -467,14 +494,29 @@ static void make_stream_ports(struct stream *s) prefix = "capture"; } - if (s->connect) { - audio_ports = jack.get_ports(impl->client, NULL, JACK_DEFAULT_AUDIO_TYPE, + do_connect = pw_properties_get_bool(s->props, "jack.connect", true); + + str = pw_properties_get(s->props, "jack.connect-audio"); + if (str != NULL) { + audio_ports = pw_strv_parse(str, strlen(str), INT_MAX, NULL); + strv_audio = true; + } else if (do_connect) { + audio_ports = (char**)jack.get_ports(impl->client, NULL, JACK_DEFAULT_AUDIO_TYPE, JackPortIsPhysical|jack_peer); - midi_ports = jack.get_ports(impl->client, NULL, JACK_DEFAULT_MIDI_TYPE, + } + str = pw_properties_get(s->props, "jack.connect-midi"); + if (str != NULL) { + midi_ports = pw_strv_parse(str, strlen(str), INT_MAX, NULL); + strv_midi = true; + } else if (do_connect) { + midi_ports = (char**)jack.get_ports(impl->client, NULL, JACK_DEFAULT_MIDI_TYPE, JackPortIsPhysical|jack_peer); } for (i = 0; i < s->n_ports; i++) { struct port *port = s->ports[i]; + char *link_port = NULL; + char pos[8]; + if (port != NULL) { s->ports[i] = NULL; if (port->jack_port) @@ -483,12 +525,12 @@ static void make_stream_ports(struct stream *s) } if (i < s->info.channels) { - str = spa_debug_type_find_short_name(spa_type_audio_channel, - s->info.position[i]); + str = spa_type_audio_channel_make_short_name( + s->info.position[i], pos, sizeof(pos), NULL); if (str) snprintf(name, sizeof(name), "%s_%s", prefix, str); else - snprintf(name, sizeof(name), "%s_%d", prefix, i); + snprintf(name, sizeof(name), "%s_%d", prefix, i+1); props = pw_properties_new( PW_KEY_FORMAT_DSP, "32 bit float mono audio", @@ -498,18 +540,21 @@ static void make_stream_ports(struct stream *s) NULL); type = JACK_DEFAULT_AUDIO_TYPE; - link_ports = audio_ports; + if (audio_ports && audio_ports[n_audio_ports]) + link_port = audio_ports[n_audio_ports++]; + is_midi = false; } else { - snprintf(name, sizeof(name), "%s_%d", prefix, i - s->info.channels); + snprintf(name, sizeof(name), "midi_%s_%d", prefix, i - s->info.channels + 1); props = pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, "8 bit raw midi", PW_KEY_PORT_NAME, name, PW_KEY_PORT_PHYSICAL, "true", NULL); type = JACK_DEFAULT_MIDI_TYPE; - link_ports = midi_ports; + if (midi_ports && midi_ports[n_midi_ports]) + link_port = midi_ports[n_midi_ports++]; is_midi = true; } @@ -522,21 +567,35 @@ static void make_stream_ports(struct stream *s) port->is_midi = is_midi; port->jack_port = jack.port_register (impl->client, name, type, jack_flags, 0); - if (link_ports != NULL && link_ports[i] != NULL) { + if (link_port != NULL) { if (jack_flags & JackPortIsOutput) { - if (jack.connect(impl->client, jack.port_name(port->jack_port), link_ports[i])) - pw_log_warn("cannot connect ports"); + pw_log_info("connecting ports '%s' to '%s'", + jack.port_name(port->jack_port), link_port); + if ((res = jack.connect(impl->client, jack.port_name(port->jack_port), link_port))) + pw_log_warn("cannot connect ports '%s' to '%s': %s", + jack.port_name(port->jack_port), link_port, strerror(res)); } else { - if (jack.connect(impl->client, link_ports[i], jack.port_name(port->jack_port))) - pw_log_warn("cannot connect ports"); + pw_log_info("connecting ports '%s' to '%s'", + link_port, jack.port_name(port->jack_port)); + if ((res = jack.connect(impl->client, link_port, jack.port_name(port->jack_port)))) + pw_log_warn("cannot connect ports '%s' to '%s': %s", + link_port, jack.port_name(port->jack_port), strerror(res)); } } s->ports[i] = port; } - if (audio_ports) - jack.free(audio_ports); - if (midi_ports) - jack.free(midi_ports); + if (audio_ports) { + if (strv_audio) + pw_free_strv(audio_ports); + else + jack.free(audio_ports); + } + if (midi_ports) { + if (strv_midi) + pw_free_strv(midi_ports); + else + jack.free(midi_ports); + } } static struct spa_pod *make_props_param(struct spa_pod_builder *b, @@ -568,9 +627,9 @@ static void parse_props(struct stream *s, const struct spa_pod *param) case SPA_PROP_channelVolumes: { uint32_t n; - float vols[SPA_AUDIO_MAX_CHANNELS]; + float vols[MAX_CHANNELS]; if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - vols, SPA_AUDIO_MAX_CHANNELS)) > 0) { + vols, SPA_N_ELEMENTS(vols))) > 0) { s->volume.n_volumes = n; for (n = 0; n < s->volume.n_volumes; n++) s->volume.volumes[n] = vols[n]; @@ -640,8 +699,7 @@ static int make_stream(struct stream *s, const char *name) n_params = 0; spa_pod_builder_init(&b, buffer, sizeof(buffer)); - s->filter = pw_filter_new(impl->core, name, s->props); - s->props = NULL; + s->filter = pw_filter_new(impl->core, name, pw_properties_copy(s->props)); if (s->filter == NULL) return -errno; @@ -930,12 +988,8 @@ static int create_jack_client(struct impl *impl) impl->source.info.rate = impl->samplerate; impl->sink.info.rate = impl->samplerate; - return 0; -} - -static int start_jack_clients(struct impl *impl) -{ jack.activate(impl->client); + return 0; } @@ -999,14 +1053,15 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), &props->dict, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -1116,14 +1171,20 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_properties_update_string(impl->source.props, str, strlen(str)); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_ALWAYS_PROCESS); copy_props(impl, props, PW_KEY_NODE_GROUP); copy_props(impl, props, PW_KEY_NODE_VIRTUAL); copy_props(impl, props, "jack.connect"); + copy_props(impl, props, "jack.connect-audio"); + copy_props(impl, props, "jack.connect-midi"); - parse_audio_info(impl->source.props, &impl->source.info); - parse_audio_info(impl->sink.props, &impl->sink.info); + if ((res = parse_audio_info(impl->source.props, &impl->source.info)) < 0 || + (res = parse_audio_info(impl->sink.props, &impl->sink.info)) < 0) { + pw_log_error( "can't parse format: %s", spa_strerror(res)); + goto error; + } impl->source.n_midi = pw_properties_get_uint32(impl->source.props, "midi.ports", DEFAULT_MIDI_PORTS); @@ -1138,11 +1199,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) goto error; } - impl->source.connect = pw_properties_get_bool(impl->source.props, - "jack.connect", true); - impl->sink.connect = pw_properties_get_bool(impl->sink.props, - "jack.connect", true); - impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { str = pw_properties_get(props, PW_KEY_REMOTE_NAME); @@ -1172,9 +1228,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) if ((res = create_filters(impl)) < 0) goto error; - if ((res = start_jack_clients(impl)) < 0) - goto error; - pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); diff --git a/src/modules/module-jack-tunnel/weakjack.h b/src/modules/module-jack-tunnel/weakjack.h index 1b057b0ba..6d5b5503e 100644 --- a/src/modules/module-jack-tunnel/weakjack.h +++ b/src/modules/module-jack-tunnel/weakjack.h @@ -5,10 +5,6 @@ #ifndef PIPEWIRE_WEAK_JACK_H #define PIPEWIRE_WEAK_JACK_H -#ifdef __cplusplus -extern "C" { -#endif - #include "config.h" #include @@ -17,6 +13,10 @@ extern "C" { #include #include +#ifdef __cplusplus +extern "C" { +#endif + struct weakjack { jack_nframes_t (*cycle_wait) (jack_client_t* client); void (*cycle_signal) (jack_client_t* client, int status); diff --git a/src/modules/module-jackdbus-detect.c b/src/modules/module-jackdbus-detect.c index 23bce1b45..622ca6efe 100644 --- a/src/modules/module-jackdbus-detect.c +++ b/src/modules/module-jackdbus-detect.c @@ -3,6 +3,8 @@ /* SPDX-FileCopyrightText: Copyright © 2019 Red Hat Inc. */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -11,8 +13,6 @@ #include #include -#include "config.h" - #include #include diff --git a/src/modules/module-link-factory.c b/src/modules/module-link-factory.c index ca2b8a75c..d54df2758 100644 --- a/src/modules/module-link-factory.c +++ b/src/modules/module-link-factory.c @@ -2,13 +2,13 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include #include -#include "config.h" - #include #include @@ -193,8 +193,7 @@ static const struct pw_resource_events resource_events = { static void global_destroy(void *data) { struct link_data *ld = data; - struct factory_data *d = ld->data; - pw_work_queue_cancel(d->work, ld, SPA_ID_INVALID); + spa_hook_remove(&ld->global_listener); ld->global = NULL; } @@ -213,6 +212,7 @@ static void link_destroy(void *data) spa_hook_remove(&ld->global_listener); if (ld->resource) spa_hook_remove(&ld->resource_listener); + pw_work_queue_cancel(ld->data->work, ld, SPA_ID_INVALID); } static void link_initialized(void *data) @@ -279,28 +279,12 @@ static const struct pw_impl_link_events link_events = { static struct pw_impl_port *get_port(struct pw_impl_node *node, enum spa_direction direction) { struct pw_impl_port *p; - struct pw_context *context = pw_impl_node_get_context(node); - int res; p = pw_impl_node_find_port(node, direction, PW_ID_ANY); - if (p == NULL || pw_impl_port_is_linked(p)) { - uint32_t port_id; + if (p == NULL || pw_impl_port_is_linked(p)) + p = pw_impl_node_get_free_port(node, direction); - port_id = pw_impl_node_get_free_port_id(node, direction); - if (port_id == SPA_ID_INVALID) - return NULL; - - p = pw_context_create_port(context, direction, port_id, NULL, 0); - if (p == NULL) - return NULL; - - if ((res = pw_impl_port_add(p, node)) < 0) { - pw_log_warn("can't add port: %s", spa_strerror(res)); - errno = -res; - return NULL; - } - } return p; } diff --git a/src/modules/module-loopback.c b/src/modules/module-loopback.c index f4b068be7..5b34834ac 100644 --- a/src/modules/module-loopback.c +++ b/src/modules/module-loopback.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,8 +12,6 @@ #include #include -#include "config.h" - #include #include #include @@ -51,6 +51,7 @@ * - \ref PW_KEY_REMOTE_NAME * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_NAME * - \ref PW_KEY_NODE_LATENCY @@ -290,7 +291,7 @@ struct impl { struct spa_hook playback_listener; struct spa_audio_info_raw playback_info; - struct spa_process_latency_info process_latency[2]; + struct spa_process_latency_info process_latency; struct spa_latency_info latency[2]; unsigned int do_disconnect:1; @@ -441,70 +442,55 @@ static void playback_process(void *d) pw_stream_queue_buffer(impl->playback, out); } -static void build_latency_params(struct impl *impl, struct spa_pod_builder *b, - const struct spa_pod *params[], uint32_t max_params) -{ - struct spa_latency_info latency; - latency = impl->latency[0]; - spa_process_latency_info_add(&impl->process_latency[0], &latency); - params[0] = spa_latency_build(b, SPA_PARAM_Latency, &latency); - latency = impl->latency[1]; - spa_process_latency_info_add(&impl->process_latency[1], &latency); - params[1] = spa_latency_build(b, SPA_PARAM_Latency, &latency); -} - -static struct spa_pod *build_props(struct impl *impl, struct spa_pod_builder *b, - enum spa_direction direction) -{ - int64_t nsec = impl->process_latency[direction].ns; - - return spa_pod_builder_add_object(b, - SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, - SPA_PROP_latencyOffsetNsec, SPA_POD_Long(nsec)); -} - -static void param_latency_changed(struct impl *impl, const struct spa_pod *param, - struct pw_stream *stream, struct pw_stream *other) +static void update_latency(struct impl *impl, enum spa_direction direction, bool props, bool process) { struct spa_latency_info latency; uint8_t buffer[1024]; struct spa_pod_builder b; - const struct spa_pod *params[2]; + const struct spa_pod *params[3]; + uint32_t n_params = 0; + struct pw_stream *s = direction == SPA_DIRECTION_OUTPUT ? + impl->playback : impl->capture; + + if (s == NULL) + return; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + latency = impl->latency[direction]; + spa_process_latency_info_add(&impl->process_latency, &latency); + params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &latency); + + if (props) { + int64_t nsec = impl->process_latency.ns; + params[n_params++] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_latencyOffsetNsec, SPA_POD_Long(nsec)); + } + if (process) { + params[n_params++] = spa_process_latency_build(&b, + SPA_PARAM_ProcessLatency, &impl->process_latency); + } + pw_stream_update_params(s, params, n_params); +} + +static void update_latencies(struct impl *impl, bool props, bool process) +{ + update_latency(impl, SPA_DIRECTION_INPUT, props, process); + update_latency(impl, SPA_DIRECTION_OUTPUT, props, process); +} + +static void param_latency_changed(struct impl *impl, const struct spa_pod *param) +{ + struct spa_latency_info latency; if (param == NULL || spa_latency_parse(param, &latency) < 0) return; impl->latency[latency.direction] = latency; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - build_latency_params(impl, &b, params, 2); - - pw_stream_update_params(stream, params, 2); - pw_stream_update_params(other, params, 2); - - impl->recalc_delay = true; + update_latency(impl, latency.direction, false, false); } -static void emit_process_latency_changed(struct impl *impl, - enum spa_direction direction, struct pw_stream *stream) -{ - uint8_t buffer[4096]; - struct spa_pod_builder b; - const struct spa_pod *params[4]; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - params[0] = spa_process_latency_build(&b, SPA_PARAM_ProcessLatency, - &impl->process_latency[direction]); - if (stream == impl->capture) - params[1] = build_props(impl, &b, SPA_DIRECTION_INPUT); - else - params[1] = build_props(impl, &b, SPA_DIRECTION_OUTPUT); - build_latency_params(impl, &b, ¶ms[2], 2); - pw_stream_update_params(stream, params, 4); -} - -static void param_process_latency_changed(struct impl *impl, const struct spa_pod *param, - enum spa_direction direction, struct pw_stream *stream) +static void param_process_latency_changed(struct impl *impl, const struct spa_pod *param) { struct spa_process_latency_info info; @@ -512,14 +498,14 @@ static void param_process_latency_changed(struct impl *impl, const struct spa_po spa_zero(info); else if (spa_process_latency_parse(param, &info) < 0) return; + if (spa_process_latency_info_compare(&impl->process_latency, &info) == 0) + return; - impl->process_latency[direction] = info; - - emit_process_latency_changed(impl, direction, stream); + impl->process_latency = info; + update_latencies(impl, true, true); } -static void param_props_changed(struct impl *impl, const struct spa_pod *param, - enum spa_direction direction, struct pw_stream *stream) +static void param_props_changed(struct impl *impl, const struct spa_pod *param) { int64_t nsec; @@ -530,8 +516,10 @@ static void param_props_changed(struct impl *impl, const struct spa_pod *param, SPA_PROP_latencyOffsetNsec, SPA_POD_Long(&nsec)) < 0) return; - impl->process_latency[direction].ns = nsec; - emit_process_latency_changed(impl, direction, stream); + if (impl->process_latency.ns == nsec) + return; + impl->process_latency.ns = nsec; + update_latencies(impl, true, true); } static void param_tag_changed(struct impl *impl, const struct spa_pod *param, @@ -551,8 +539,7 @@ static void param_format_changed(struct impl *impl, const struct spa_pod *param, spa_zero(info); if (param != NULL) { if (spa_format_audio_raw_parse(param, &info) < 0 || - info.channels == 0 || - info.channels > SPA_AUDIO_MAX_CHANNELS) + info.channels == 0) return; if ((impl->info.format != 0 && impl->info.format != info.format) || @@ -652,13 +639,13 @@ static void capture_param_changed(void *data, uint32_t id, const struct spa_pod param_format_changed(impl, param, impl->capture, true); break; case SPA_PARAM_Latency: - param_latency_changed(impl, param, impl->capture, impl->playback); + param_latency_changed(impl, param); break; case SPA_PARAM_Props: - param_props_changed(impl, param, SPA_DIRECTION_INPUT, impl->capture); + param_props_changed(impl, param); break; case SPA_PARAM_ProcessLatency: - param_process_latency_changed(impl, param, SPA_DIRECTION_INPUT, impl->capture); + param_process_latency_changed(impl, param); break; case SPA_PARAM_Tag: param_tag_changed(impl, param, impl->playback); @@ -703,13 +690,13 @@ static void playback_param_changed(void *data, uint32_t id, const struct spa_pod param_format_changed(impl, param, impl->playback, false); break; case SPA_PARAM_Latency: - param_latency_changed(impl, param, impl->playback, impl->capture); + param_latency_changed(impl, param); break; case SPA_PARAM_Props: - param_props_changed(impl, param, SPA_DIRECTION_OUTPUT, impl->playback); + param_props_changed(impl, param); break; case SPA_PARAM_ProcessLatency: - param_process_latency_changed(impl, param, SPA_DIRECTION_OUTPUT, impl->playback); + param_process_latency_changed(impl, param); break; case SPA_PARAM_Tag: param_tag_changed(impl, param, impl->capture); @@ -852,14 +839,15 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P")), &props->dict, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -941,6 +929,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); copy_props(impl, props, PW_KEY_NODE_GROUP); @@ -966,9 +955,12 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) if (pw_properties_get(impl->playback_props, PW_KEY_NODE_DESCRIPTION) == NULL) pw_properties_set(impl->playback_props, PW_KEY_NODE_DESCRIPTION, str); - parse_audio_info(props, &impl->info); - parse_audio_info(impl->capture_props, &impl->capture_info); - parse_audio_info(impl->playback_props, &impl->playback_info); + if ((res = parse_audio_info(props, &impl->info)) < 0 || + (res = parse_audio_info(impl->capture_props, &impl->capture_info)) < 0 || + (res = parse_audio_info(impl->playback_props, &impl->playback_info)) < 0) { + pw_log_error( "can't parse formats: %s", spa_strerror(res)); + goto error; + } if (!impl->capture_info.rate && !impl->playback_info.rate) { if (pw_properties_get(impl->playback_props, "resample.disable") == NULL) diff --git a/src/modules/module-metadata.c b/src/modules/module-metadata.c index a1255da30..79d80d1b4 100644 --- a/src/modules/module-metadata.c +++ b/src/modules/module-metadata.c @@ -2,13 +2,13 @@ /* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include #include -#include "config.h" - #include #include diff --git a/src/modules/module-netjack2-driver.c b/src/modules/module-netjack2-driver.c index 3675a33f7..07a756266 100644 --- a/src/modules/module-netjack2-driver.c +++ b/src/modules/module-netjack2-driver.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -18,8 +20,6 @@ #include #include -#include "config.h" - #include #include #include @@ -45,7 +45,26 @@ /** \page page_module_netjack2_driver Netjack2 driver * * The netjack2-driver module provides a source or sink that is following a - * netjack2 manager. + * netjack2 manager. It is meant to be used over stable (ethernet) network + * connections with minimal latency and jitter. + * + * The driver normally decides how many ports it will send and receive from the + * manager. By default however, these values are set to -1 so that the manager + * decides on the number of ports. + * + * With the global or per stream audio.port and midi.ports properties this + * behaviour can be adjusted. + * + * The driver will send out UDP messages on a (typically) multicast address to + * inform the manager of the available driver. This will then instruct the manager + * to configure and start the driver. + * + * On the driver side, a sink and/or source with the specified numner of audio and + * midi ports will be created. On the manager side there will be a corresponding + * source and/or sink created respectively. + * + * The driver will be scheduled with exactly the same period as the manager but with + * a configurable number of periods of delay (see netjack2.latency, default 2). * * ## Module Name * @@ -53,7 +72,10 @@ * * ## Module Options * - * - `driver.mode`: the driver mode, sink|source|duplex, default duplex + * - `driver.mode`: the driver mode, sink|source|duplex, default duplex. This set the + * per stream audio.port and midi.ports default from -1 to 0. sink mode defaults to + * no source ports, source mode to no sink ports and duplex leaves the defaults as + * they are. * - `local.ifname = `: interface name to use * - `net.ip =`: multicast IP address, default "225.3.19.154" * - `net.port =`: control port, default 19000 @@ -63,11 +85,11 @@ * - `source.ip =`: IP address to bind to, default "0.0.0.0" * - `source.port =`: port to bind to, default 0 (allocate) * - `netjack2.client-name`: the name of the NETJACK2 client. - * - `netjack2.save`: if jack port connections should be save automatically. Can also be - * placed per stream. * - `netjack2.latency`: the latency in cycles, default 2 - * - `audio.channels`: the number of audio ports. Can also be added to the stream props. + * - `audio.ports`: the number of audio ports. Can also be added to the stream props. + * A value of -1 will configure to the number of audio ports on the manager. * - `midi.ports`: the number of midi ports. Can also be added to the stream props. + * A value of -1 will configure to the number of midi ports on the manager. * - `source.props`: Extra properties for the source filter. * - `sink.props`: Extra properties for the sink filter. * @@ -77,6 +99,7 @@ * * - \ref PW_KEY_REMOTE_NAME * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_NAME * - \ref PW_KEY_NODE_DESCRIPTION @@ -93,15 +116,14 @@ * context.modules = [ * { name = libpipewire-module-netjack2-driver * args = { - * #driver.mode = duplex * #netjack2.client-name = PipeWire - * #netjack2.save = false * #netjack2.latency = 2 * #midi.ports = 0 + * #audio.ports = -1 * #audio.channels = 2 * #audio.position = [ FL FR ] * source.props = { - * # extra sink properties + * # extra source properties * } * sink.props = { * # extra sink properties @@ -133,15 +155,14 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define NETWORK_MAX_LATENCY 30 #define DEFAULT_CLIENT_NAME "PipeWire" -#define DEFAULT_CHANNELS 2 -#define DEFAULT_POSITION "[ FL FR ]" -#define DEFAULT_MIDI_PORTS 1 +#define DEFAULT_MIDI_PORTS -1 +#define DEFAULT_AUDIO_PORTS -1 #define FOLLOWER_INIT_TIMEOUT 1 #define FOLLOWER_INIT_RETRY -1 #define MODULE_USAGE "( remote.name= ) " \ - "( driver.mode= ) " \ + "( driver.mode= ) " \ "( local.ifname= ) " \ "( net.ip= ) " \ "( net.port= ) " \ @@ -151,11 +172,11 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); "( source.ip= ) " \ "( source.port= ) " \ "( netjack2.client-name= ) " \ - "( netjack2.save= ) " \ "( netjack2.latency= ) " \ - "( midi.ports= ) " \ - "( audio.channels= ) " \ - "( audio.position= ) " \ + "( audio.ports= ) " \ + "( midi.ports= ) " \ + "( audio.channels= ) " \ + "( audio.position= ) " \ "( source.props= ) " \ "( sink.props= ) " @@ -182,9 +203,13 @@ struct stream { struct pw_filter *filter; struct spa_hook listener; + int32_t wanted_n_midi; + int32_t wanted_n_audio; + + struct spa_io_position *position; + struct spa_audio_info_raw info; - uint32_t n_midi; uint32_t n_ports; struct port *ports[MAX_PORTS]; @@ -201,6 +226,7 @@ struct impl { struct pw_loop *main_loop; struct pw_loop *data_loop; struct spa_system *system; + struct pw_timer_queue *timer_queue; #define MODE_SINK (1<<0) #define MODE_SOURCE (1<<1) @@ -222,8 +248,6 @@ struct impl { struct spa_hook core_proxy_listener; struct spa_hook core_listener; - struct spa_io_position *position; - struct stream source; struct stream sink; @@ -241,7 +265,7 @@ struct impl { struct spa_source *setup_socket; struct spa_source *socket; - struct spa_source *timer; + struct pw_timer timer; int32_t init_retry; struct netjack2_peer peer; @@ -369,11 +393,10 @@ static void source_process(void *d, struct spa_io_position *position) static void stream_io_changed(void *data, void *port_data, uint32_t id, void *area, uint32_t size) { struct stream *s = data; - struct impl *impl = s->impl; if (port_data == NULL) { switch (id) { case SPA_IO_Position: - impl->position = area; + s->position = area; break; default: break; @@ -419,11 +442,11 @@ static void make_stream_ports(struct stream *s) } if (i < s->info.channels) { - str = spa_debug_type_find_short_name(spa_type_audio_channel, - s->info.position[i % SPA_AUDIO_MAX_CHANNELS]); + str = spa_type_audio_channel_make_short_name( + s->info.position[i], name, sizeof(name), "UNK"); props = pw_properties_new( PW_KEY_FORMAT_DSP, "32 bit float mono audio", - PW_KEY_AUDIO_CHANNEL, str ? str : "UNK", + PW_KEY_AUDIO_CHANNEL, str, PW_KEY_PORT_PHYSICAL, "true", NULL); @@ -431,7 +454,7 @@ static void make_stream_ports(struct stream *s) } else { snprintf(name, sizeof(name), "midi%d", i - s->info.channels); props = pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, "8 bit raw midi", PW_KEY_AUDIO_CHANNEL, name, PW_KEY_PORT_PHYSICAL, "true", NULL); @@ -458,6 +481,7 @@ static void make_stream_ports(struct stream *s) s->ports[i] = port; } + pw_filter_set_active(s->filter, true); } static struct spa_pod *make_props_param(struct spa_pod_builder *b, @@ -489,9 +513,9 @@ static void parse_props(struct stream *s, const struct spa_pod *param) case SPA_PROP_channelVolumes: { uint32_t n; - float vols[SPA_AUDIO_MAX_CHANNELS]; + float vols[MAX_CHANNELS]; if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - vols, SPA_AUDIO_MAX_CHANNELS)) > 0) { + vols, SPA_N_ELEMENTS(vols))) > 0) { s->volume.n_volumes = n; for (n = 0; n < s->volume.n_volumes; n++) s->volume.volumes[n] = vols[n]; @@ -523,7 +547,6 @@ static void stream_param_changed(void *data, void *port_data, uint32_t id, case SPA_PARAM_PortConfig: pw_log_debug("PortConfig"); make_stream_ports(s); - pw_filter_set_active(s->filter, true); break; case SPA_PARAM_Props: pw_log_debug("Props"); @@ -558,6 +581,7 @@ static int make_stream(struct stream *s, const char *name) const struct spa_pod *params[4]; uint8_t buffer[1024]; struct spa_pod_builder b; + int res; n_params = 0; spa_pod_builder_init(&b, buffer, sizeof(buffer)); @@ -583,12 +607,18 @@ static int make_stream(struct stream *s, const char *name) SPA_PARAM_Format, &s->info); params[n_params++] = make_props_param(&b, &s->volume); - return pw_filter_connect(s->filter, + if ((res = pw_filter_connect(s->filter, PW_FILTER_FLAG_INACTIVE | PW_FILTER_FLAG_DRIVER | PW_FILTER_FLAG_RT_PROCESS | PW_FILTER_FLAG_CUSTOM_LATENCY, - params, n_params); + params, n_params)) < 0) + return res; + + if (s->info.channels == 0) + make_stream_ports(s); + + return res; } static int create_filters(struct impl *impl) @@ -616,6 +646,24 @@ static inline uint64_t get_time_nsec(struct impl *impl) return nsec; } +static void update_clock(struct impl *impl, struct stream *s, uint64_t nsec, uint32_t nframes) +{ + if (s->position) { + struct spa_io_clock *c = &s->position->clock; + + c->nsec = nsec; + c->rate = SPA_FRACTION(1, impl->samplerate); + c->position = impl->frame_time; + c->duration = nframes; + c->delay = 0; + c->rate_diff = 1.0; + c->next_nsec = nsec; + + c->target_rate = c->rate; + c->target_duration = c->duration; + } +} + static void on_data_io(void *data, int fd, uint32_t mask) { @@ -648,27 +696,13 @@ on_data_io(void *data, int fd, uint32_t mask) impl->frame_time += nframes; - pw_log_trace_fp("process %d %u %u %p %"PRIu64, nframes, source_running, - sink_running, impl->position, impl->frame_time); + pw_log_trace_fp("process %d %u %u %"PRIu64, nframes, source_running, + sink_running, impl->frame_time); if (impl->new_xrun) { pw_log_warn("Xrun netjack2:%u PipeWire:%u", impl->nj2_xrun, impl->pw_xrun); impl->new_xrun = false; } - if (impl->position) { - struct spa_io_clock *c = &impl->position->clock; - - c->nsec = nsec; - c->rate = SPA_FRACTION(1, impl->samplerate); - c->position = impl->frame_time; - c->duration = nframes; - c->delay = 0; - c->rate_diff = 1.0; - c->next_nsec = nsec; - - c->target_rate = c->rate; - c->target_duration = c->duration; - } if (!source_running) netjack2_recv_data(&impl->peer, NULL, 0, NULL, 0); @@ -676,12 +710,16 @@ on_data_io(void *data, int fd, uint32_t mask) impl->done = false; impl->triggered = true; impl->driving = MODE_SOURCE; - pw_filter_trigger_process(impl->source.filter); + update_clock(impl, &impl->source, nsec, nframes); + if (pw_filter_trigger_process(impl->source.filter) < 0) + pw_log_warn("source not ready"); } else if (impl->mode == MODE_SINK && sink_running) { impl->done = false; impl->triggered = true; impl->driving = MODE_SINK; - pw_filter_trigger_process(impl->sink.filter); + update_clock(impl, &impl->sink, nsec, nframes); + if (pw_filter_trigger_process(impl->sink.filter) < 0) + pw_log_warn("sink not ready"); } else { sink_running = false; impl->done = true; @@ -706,7 +744,7 @@ static bool is_multicast(struct sockaddr *sa, socklen_t salen) static int make_socket(struct sockaddr_storage *src, socklen_t src_len, struct sockaddr_storage *dst, socklen_t dst_len, - bool loop, int ttl, int dscp) + bool loop, int ttl, int dscp, const char *ifname) { int af, fd, val, res; struct timeval timeout; @@ -722,7 +760,13 @@ static int make_socket(struct sockaddr_storage *src, socklen_t src_len, pw_log_error("setsockopt failed: %m"); goto error; } - +#ifdef SO_BINDTODEVICE + if (ifname && setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname)) < 0) { + res = -errno; + pw_log_error("setsockopt(SO_BINDTODEVICE) failed: %m"); + goto error; + } +#endif #ifdef SO_PRIORITY val = 6; if (setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &val, sizeof(val)) < 0) @@ -759,14 +803,14 @@ error: return res; } +static void on_timer_event(void *data); + static void update_timer(struct impl *impl, uint64_t timeout) { - struct timespec value, interval; - value.tv_sec = 0; - value.tv_nsec = timeout ? 1 : 0; - interval.tv_sec = timeout; - interval.tv_nsec = 0; - pw_loop_update_timer(impl->main_loop, impl->timer, &value, &interval, false); + pw_timer_queue_cancel(&impl->timer); + pw_timer_queue_add(impl->timer_queue, &impl->timer, + NULL, timeout * SPA_NSEC_PER_SEC, + on_timer_event, impl); } static bool encoding_supported(uint32_t encoder) @@ -789,13 +833,12 @@ static int handle_follower_setup(struct impl *impl, struct nj2_session_params *p int res; struct netjack2_peer *peer = &impl->peer; uint32_t i; + const char *media; pw_log_info("got follower setup"); nj2_dump_session_params(params); nj2_session_params_ntoh(&peer->params, params); - SPA_SWAP(peer->params.send_audio_channels, peer->params.recv_audio_channels); - SPA_SWAP(peer->params.send_midi_channels, peer->params.recv_midi_channels); if (peer->params.send_audio_channels < 0 || peer->params.recv_audio_channels < 0 || @@ -807,23 +850,35 @@ static int handle_follower_setup(struct impl *impl, struct nj2_session_params *p pw_log_warn("invalid follower setup"); return -EINVAL; } + /* the params are from the perspective of the manager, so send is our + * receive (source) and recv is our send (sink) */ + SPA_SWAP(peer->params.send_audio_channels, peer->params.recv_audio_channels); + SPA_SWAP(peer->params.send_midi_channels, peer->params.recv_midi_channels); pw_loop_update_io(impl->main_loop, impl->setup_socket, 0); - impl->source.n_ports = peer->params.send_audio_channels + peer->params.send_midi_channels; - impl->source.info.rate = peer->params.sample_rate; - if ((uint32_t)peer->params.send_audio_channels != impl->source.info.channels) { - impl->source.info.channels = peer->params.send_audio_channels; - for (i = 0; i < SPA_MIN(impl->source.info.channels, SPA_AUDIO_MAX_CHANNELS); i++) - impl->source.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + impl->sink.n_ports = peer->params.send_audio_channels + peer->params.send_midi_channels; + if (impl->sink.n_ports > MAX_PORTS) { + pw_log_warn("Too many follower sink ports %d > %d", impl->sink.n_ports, MAX_PORTS); + return -EINVAL; } - impl->sink.n_ports = peer->params.recv_audio_channels + peer->params.recv_midi_channels; impl->sink.info.rate = peer->params.sample_rate; - if ((uint32_t)peer->params.recv_audio_channels != impl->sink.info.channels) { - impl->sink.info.channels = peer->params.recv_audio_channels; - for (i = 0; i < SPA_MIN(impl->sink.info.channels, SPA_AUDIO_MAX_CHANNELS); i++) + if ((uint32_t)peer->params.send_audio_channels != impl->sink.info.channels) { + impl->sink.info.channels = peer->params.send_audio_channels; + for (i = 0; i < impl->sink.info.channels; i++) impl->sink.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; } + impl->source.n_ports = peer->params.recv_audio_channels + peer->params.recv_midi_channels; + if (impl->source.n_ports > MAX_PORTS) { + pw_log_warn("Too many follower source ports %d > %d", impl->source.n_ports, MAX_PORTS); + return -EINVAL; + } + impl->source.info.rate = peer->params.sample_rate; + if ((uint32_t)peer->params.recv_audio_channels != impl->source.info.channels) { + impl->source.info.channels = peer->params.recv_audio_channels; + for (i = 0; i < impl->source.info.channels; i++) + impl->source.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } impl->samplerate = peer->params.sample_rate; impl->period_size = peer->params.period_size; @@ -843,6 +898,20 @@ static int handle_follower_setup(struct impl *impl, struct nj2_session_params *p pw_properties_setf(impl->source.props, PW_KEY_NODE_FORCE_QUANTUM, "%u", impl->period_size); + media = impl->sink.info.channels > 0 ? "Audio" : "Midi"; + if (pw_properties_get(impl->sink.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(impl->sink.props, PW_KEY_MEDIA_CLASS, "%s/Sink", media); + + media = impl->source.info.channels > 0 ? "Audio" : "Midi"; + if (pw_properties_get(impl->source.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(impl->source.props, PW_KEY_MEDIA_CLASS, "%s/Source", media); + + impl->mode = 0; + if (impl->source.n_ports > 0) + impl->mode |= MODE_SOURCE; + if (impl->sink.n_ports > 0) + impl->mode |= MODE_SINK; + if ((res = create_filters(impl)) < 0) return res; @@ -942,10 +1011,12 @@ static int send_follower_available(struct impl *impl) snprintf(params.follower_name, sizeof(params.follower_name), "%s", pw_get_host_name()); params.mtu = htonl(impl->mtu); params.transport_sync = htonl(0); - params.send_audio_channels = htonl(-1); - params.recv_audio_channels = htonl(-1); - params.send_midi_channels = htonl(-1); - params.recv_midi_channels = htonl(-1); + /* send/recv is from the perspective of the manager, so what we send (sink) + * is recv on the manager and vice versa. */ + params.recv_audio_channels = htonl(impl->sink.wanted_n_audio); + params.send_audio_channels = htonl(impl->source.wanted_n_audio); + params.recv_midi_channels = htonl(impl->sink.wanted_n_midi); + params.send_midi_channels = htonl(impl->source.wanted_n_midi); params.sample_encoder = htonl(NJ2_ENCODER_FLOAT); params.follower_sync_mode = htonl(1); params.network_latency = htonl(impl->latency); @@ -982,9 +1053,11 @@ static int create_netjack2_socket(struct impl *impl) impl->ttl = pw_properties_get_uint32(impl->props, "net.ttl", DEFAULT_NET_TTL); impl->loop = pw_properties_get_bool(impl->props, "net.loop", DEFAULT_NET_LOOP); impl->dscp = pw_properties_get_uint32(impl->props, "net.dscp", DEFAULT_NET_DSCP); + str = pw_properties_get(impl->props, "local.ifname"); fd = make_socket(&impl->src_addr, impl->src_len, - &impl->dst_addr, impl->dst_len, impl->loop, impl->ttl, impl->dscp); + &impl->dst_addr, impl->dst_len, impl->loop, impl->ttl, impl->dscp, + str); if (fd < 0) { res = -errno; pw_log_error("can't create socket: %s", spa_strerror(res)); @@ -1061,7 +1134,7 @@ static void restart_netjack2_socket(struct impl *impl) create_netjack2_socket(impl); } -static void on_timer_event(void *data, uint64_t expirations) +static void on_timer_event(void *data) { struct impl *impl = data; @@ -1122,8 +1195,7 @@ static void impl_destroy(struct impl *impl) if (impl->core && impl->do_disconnect) pw_core_disconnect(impl->core); - if (impl->timer) - pw_loop_destroy_source(impl->main_loop, impl->timer); + pw_timer_queue_cancel(&impl->timer); if (impl->data_loop) pw_context_release_loop(impl->context, impl->data_loop); @@ -1147,14 +1219,14 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( - SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), - SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P")), &props->dict, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -1213,6 +1285,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) } impl->main_loop = pw_context_get_main_loop(context); + impl->timer_queue = pw_context_get_timer_queue(context); impl->system = impl->main_loop->system; impl->source.impl = impl; @@ -1220,20 +1293,20 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->sink.impl = impl; impl->sink.direction = PW_DIRECTION_INPUT; - impl->mode = MODE_DUPLEX; if ((str = pw_properties_get(props, "driver.mode")) != NULL) { if (spa_streq(str, "source")) { - impl->mode = MODE_SOURCE; + pw_properties_set(impl->sink.props, "audio.ports", "0"); + pw_properties_set(impl->sink.props, "midi.ports", "0"); } else if (spa_streq(str, "sink")) { - impl->mode = MODE_SINK; - } else if (spa_streq(str, "duplex")) { - impl->mode = MODE_DUPLEX; - } else { + pw_properties_set(impl->source.props, "audio.ports", "0"); + pw_properties_set(impl->source.props, "midi.ports", "0"); + } else if (!spa_streq(str, "duplex")) { pw_log_error("invalid driver.mode '%s'", str); res = -EINVAL; goto error; } } + impl->latency = pw_properties_get_uint32(impl->props, "netjack2.latency", DEFAULT_NETWORK_LATENCY); @@ -1245,11 +1318,9 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) if (pw_properties_get(props, PW_KEY_NODE_ALWAYS_PROCESS) == NULL) pw_properties_set(props, PW_KEY_NODE_ALWAYS_PROCESS, "true"); - pw_properties_set(impl->sink.props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); pw_properties_set(impl->sink.props, PW_KEY_PRIORITY_DRIVER, "40000"); pw_properties_set(impl->sink.props, PW_KEY_NODE_NAME, "netjack2_driver_send"); - pw_properties_set(impl->source.props, PW_KEY_MEDIA_CLASS, "Audio/Source"); pw_properties_set(impl->source.props, PW_KEY_PRIORITY_DRIVER, "40001"); pw_properties_set(impl->source.props, PW_KEY_NODE_NAME, "netjack2_driver_receive"); @@ -1260,27 +1331,29 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_LOOP_NAME); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_ALWAYS_PROCESS); copy_props(impl, props, PW_KEY_NODE_GROUP); copy_props(impl, props, PW_KEY_NODE_VIRTUAL); + copy_props(impl, props, "midi.ports"); + copy_props(impl, props, "audio.ports"); - parse_audio_info(impl->source.props, &impl->source.info); - parse_audio_info(impl->sink.props, &impl->sink.info); - - impl->source.n_midi = pw_properties_get_uint32(impl->source.props, - "midi.ports", DEFAULT_MIDI_PORTS); - impl->sink.n_midi = pw_properties_get_uint32(impl->sink.props, - "midi.ports", DEFAULT_MIDI_PORTS); - - impl->source.n_ports = impl->source.n_midi + impl->source.info.channels; - impl->sink.n_ports = impl->sink.n_midi + impl->sink.info.channels; - if (impl->source.n_ports > MAX_PORTS || impl->sink.n_ports > MAX_PORTS) { - pw_log_error("too many ports"); - res = -EINVAL; + if ((res = parse_audio_info(impl->source.props, &impl->source.info)) < 0 || + (res = parse_audio_info(impl->sink.props, &impl->sink.info)) < 0) { + pw_log_error( "can't parse format: %s", spa_strerror(res)); goto error; } + impl->source.wanted_n_midi = pw_properties_get_int32(impl->source.props, + "midi.ports", DEFAULT_MIDI_PORTS); + impl->sink.wanted_n_midi = pw_properties_get_int32(impl->sink.props, + "midi.ports", DEFAULT_MIDI_PORTS); + impl->source.wanted_n_audio = pw_properties_get_int32(impl->source.props, + "audio.ports", DEFAULT_AUDIO_PORTS); + impl->sink.wanted_n_audio = pw_properties_get_int32(impl->sink.props, + "audio.ports", DEFAULT_AUDIO_PORTS); + impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { str = pw_properties_get(props, PW_KEY_REMOTE_NAME); @@ -1304,13 +1377,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) &impl->core_listener, &core_events, impl); - impl->timer = pw_loop_add_timer(impl->main_loop, on_timer_event, impl); - if (impl->timer == NULL) { - res = -errno; - pw_log_error("can't create timer source: %m"); - goto error; - } - if ((res = create_netjack2_socket(impl)) < 0) goto error; diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index 8229b5d5f..0ff62d93b 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -19,8 +21,6 @@ #include #include -#include "config.h" - #include #include #include @@ -49,6 +49,19 @@ * The netjack2 manager module listens for new netjack2 driver messages and will * start a communication channel with them. * + * Messages are received on a (typically) multicast address. + * + * Normally, the driver will specify the number of send and receive channels it + * wants to set up with the manager. If the driver however specifies a don't-care + * value of -1, the audio.ports and midi.ports configuration values of the manager + * are used. + * + * The manager will create the corresponding streams to send and receive data + * to/from the drivers. These are usually sink and sources but with the + * netjack2.connect property, these will be streams that will be autoconnected to + * the default source and sink by the session manager. + * + * * ## Module Name * * `libpipewire-module-netjack2-manager` @@ -67,8 +80,11 @@ * - `netjack2.period-size`: the buffer size to use, default 1024 * - `netjack2.encoding`: the encoding, float|opus|int, default float * - `netjack2.kbps`: the number of kilobits per second when encoding, default 64 - * - `audio.channels`: the number of audio ports. Can also be added to the stream props. - * - `midi.ports`: the number of midi ports. Can also be added to the stream props. + * - `audio.ports`: the number of audio ports. Can also be added to the stream props. This + * is the default suggestion for drivers that don't specify any number of audio channels. + * - `midi.ports`: the number of midi ports. Can also be added to the stream props. This + * is the default suggestion for drivers that don't specify any number of midi channels. + * - `audio.position`: default channel position for the number of audio.ports. * - `source.props`: Extra properties for the source filter. * - `sink.props`: Extra properties for the sink filter. * @@ -78,6 +94,7 @@ * * - \ref PW_KEY_REMOTE_NAME * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_NAME * - \ref PW_KEY_NODE_DESCRIPTION @@ -99,11 +116,12 @@ * #netjack2.period-size = 1024 * #netjack2.encoding = float # float|opus * #netjack2.kbps = 64 + * #audio.ports = 0 * #midi.ports = 0 * #audio.channels = 2 * #audio.position = [ FL FR ] * source.props = { - * # extra sink properties + * # extra source properties * } * sink.props = { * # extra sink properties @@ -137,8 +155,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define DEFAULT_PERIOD_SIZE 1024 #define DEFAULT_ENCODING "float" #define DEFAULT_KBPS 64 -#define DEFAULT_CHANNELS 2 -#define DEFAULT_POSITION "[ FL FR ]" +#define DEFAULT_AUDIO_PORTS 2 #define DEFAULT_MIDI_PORTS 1 #define MODULE_USAGE "( remote.name= ) " \ @@ -151,8 +168,8 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); "( netjack2.connect= ) " \ "( netjack2.sample-rate= ) "\ "( netjack2.period-size= ) " \ - "( midi.ports= ) " \ - "( audio.channels= ) " \ + "( midi.ports= ) " \ + "( audio.channels= ) " \ "( audio.position= ) " \ "( source.props= ) " \ "( sink.props= ) " @@ -165,7 +182,7 @@ static const struct spa_dict_item module_props[] = { { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info); +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info); struct port { enum spa_direction direction; @@ -183,9 +200,12 @@ struct stream { struct pw_filter *filter; struct spa_hook listener; - struct spa_audio_info_raw info; + struct spa_io_position *position; + struct spa_audio_info_raw info; + uint32_t n_audio; uint32_t n_midi; + uint32_t n_ports; struct port *ports[MAX_PORTS]; @@ -202,7 +222,10 @@ struct follower { struct spa_list link; struct impl *impl; - struct spa_io_position *position; +#define MODE_SINK (1<<0) +#define MODE_SOURCE (1<<1) +#define MODE_DUPLEX (MODE_SINK|MODE_SOURCE) + uint32_t mode; struct stream source; struct stream sink; @@ -227,6 +250,7 @@ struct follower { unsigned int done:1; unsigned int new_xrun:1; unsigned int started:1; + unsigned int freeing:1; }; struct impl { @@ -235,10 +259,6 @@ struct impl { struct pw_loop *data_loop; struct spa_system *system; -#define MODE_SINK (1<<0) -#define MODE_SOURCE (1<<1) -#define MODE_DUPLEX (MODE_SINK|MODE_SOURCE) - uint32_t mode; struct pw_properties *props; struct pw_properties *sink_props; struct pw_properties *source_props; @@ -284,6 +304,7 @@ static void stream_destroy(void *d) struct stream *s = d; uint32_t i; + s->running = false; spa_hook_remove(&s->listener); for (i = 0; i < s->n_ports; i++) s->ports[i] = NULL; @@ -354,9 +375,17 @@ static void sink_process(void *d, struct spa_io_position *position) pw_loop_update_io(s->impl->data_loop, follower->socket, SPA_IO_IN); } -static void source_process(void *d, struct spa_io_position *position) +static int stop_follower(struct follower *follower); + +static int do_stop_follower(struct spa_loop *loop, + bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + stop_follower(user_data); + return 0; +} + +static inline void handle_source_process(struct stream *s, struct spa_io_position *position) { - struct stream *s = d; struct follower *follower = s->follower; uint32_t nframes = position->clock.duration; struct data_info midi[s->n_ports]; @@ -365,28 +394,57 @@ static void source_process(void *d, struct spa_io_position *position) set_info(s, nframes, midi, &n_midi, audio, &n_audio); - netjack2_manager_sync_wait(&follower->peer); + if (netjack2_manager_sync_wait(&follower->peer) < 0) { + pw_loop_invoke(s->impl->main_loop, do_stop_follower, 0, NULL, 0, false, follower); + return; + } netjack2_recv_data(&follower->peer, midi, n_midi, audio, n_audio); } +static void source_process(void *d, struct spa_io_position *position) +{ + struct stream *s = d; + struct follower *follower = s->follower; + + if (!(follower->mode & MODE_SINK)) + sink_process(&follower->sink, position); + + handle_source_process(s, position); +} + static void follower_free(struct follower *follower) { struct impl *impl = follower->impl; + if (follower->freeing) + return; + + follower->freeing = true; + spa_list_remove(&follower->link); - if (follower->source.filter) + if (follower->socket) { + pw_loop_destroy_source(impl->data_loop, follower->socket); + follower->socket = NULL; + } + if (follower->setup_socket) { + pw_loop_destroy_source(impl->main_loop, follower->setup_socket); + follower->setup_socket = NULL; + } + + if (follower->source.filter) { pw_filter_destroy(follower->source.filter); - if (follower->sink.filter) + follower->source.filter = NULL; + } + if (follower->sink.filter) { pw_filter_destroy(follower->sink.filter); + follower->sink.filter = NULL; + } pw_properties_free(follower->source.props); + follower->source.props = NULL; pw_properties_free(follower->sink.props); - - if (follower->socket) - pw_loop_destroy_source(impl->data_loop, follower->socket); - if (follower->setup_socket) - pw_loop_destroy_source(impl->main_loop, follower->setup_socket); + follower->sink.props = NULL; netjack2_cleanup(&follower->peer); free(follower); @@ -420,10 +478,13 @@ static void on_setup_io(void *data, int fd, uint32_t mask) { struct follower *follower = data; + struct impl *impl = follower->impl; if (mask & (SPA_IO_ERR | SPA_IO_HUP)) { pw_log_warn("error:%08x", mask); - stop_follower(follower); + pw_loop_destroy_source(impl->main_loop, follower->setup_socket); + follower->setup_socket = NULL; + pw_loop_invoke(impl->main_loop, do_stop_follower, 0, NULL, 0, false, follower); return; } if (mask & SPA_IO_IN) { @@ -468,23 +529,32 @@ on_data_io(void *data, int fd, uint32_t mask) pw_log_warn("error:%08x", mask); pw_loop_destroy_source(impl->data_loop, follower->socket); follower->socket = NULL; + pw_loop_invoke(impl->main_loop, do_stop_follower, 0, NULL, 0, false, follower); return; } if (mask & SPA_IO_IN) { pw_loop_update_io(impl->data_loop, follower->socket, 0); - pw_filter_trigger_process(follower->source.filter); + if (follower->mode & MODE_SOURCE) { + if (pw_filter_trigger_process(follower->source.filter) < 0) { + pw_log_warn("source not ready"); + handle_source_process(&follower->source, follower->source.position); + } + } else { + /* There is no source, handle the source receive side (without ports) + * with the sink position io */ + handle_source_process(&follower->source, follower->sink.position); + } } } static void stream_io_changed(void *data, void *port_data, uint32_t id, void *area, uint32_t size) { struct stream *s = data; - struct follower *follower = s->follower; if (port_data == NULL) { switch (id) { case SPA_IO_Position: - follower->position = area; + s->position = area; break; default: break; @@ -521,6 +591,9 @@ static void make_stream_ports(struct stream *s) struct spa_latency_info latency; const struct spa_pod *params[1]; + if (s->ready) + return; + for (i = 0; i < s->n_ports; i++) { struct port *port = s->ports[i]; if (port != NULL) { @@ -529,12 +602,11 @@ static void make_stream_ports(struct stream *s) } if (i < s->info.channels) { - str = spa_debug_type_find_short_name(spa_type_audio_channel, - s->info.position[i]); - + str = spa_type_audio_channel_make_short_name( + s->info.position[i], name, sizeof(name), "UNK"); props = pw_properties_new( PW_KEY_FORMAT_DSP, "32 bit float mono audio", - PW_KEY_AUDIO_CHANNEL, str ? str : "UNK", + PW_KEY_AUDIO_CHANNEL, str, PW_KEY_PORT_PHYSICAL, "true", NULL); @@ -542,7 +614,7 @@ static void make_stream_ports(struct stream *s) } else { snprintf(name, sizeof(name), "midi%d", i - s->info.channels); props = pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, "8 bit raw midi", PW_KEY_PORT_PHYSICAL, "true", PW_KEY_AUDIO_CHANNEL, name, NULL); @@ -571,6 +643,9 @@ static void make_stream_ports(struct stream *s) s->ports[i] = port; } + s->ready = true; + if (s->follower->started) + pw_filter_set_active(s->filter, true); } static struct spa_pod *make_props_param(struct spa_pod_builder *b, @@ -602,9 +677,9 @@ static void parse_props(struct stream *s, const struct spa_pod *param) case SPA_PROP_channelVolumes: { uint32_t n; - float vols[SPA_AUDIO_MAX_CHANNELS]; + float vols[MAX_CHANNELS]; if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - vols, SPA_AUDIO_MAX_CHANNELS)) > 0) { + vols, SPA_N_ELEMENTS(vols))) > 0) { s->volume.n_volumes = n; for (n = 0; n < s->volume.n_volumes; n++) s->volume.volumes[n] = vols[n]; @@ -636,9 +711,6 @@ static void stream_param_changed(void *data, void *port_data, uint32_t id, case SPA_PARAM_PortConfig: pw_log_debug("PortConfig"); make_stream_ports(s); - s->ready = true; - if (s->follower->started) - pw_filter_set_active(s->filter, true); break; case SPA_PARAM_Props: pw_log_debug("Props"); @@ -674,6 +746,7 @@ static int make_stream(struct stream *s, const char *name) uint8_t buffer[1024]; struct spa_pod_builder b; uint32_t flags; + int res; n_params = 0; spa_pod_builder_init(&b, buffer, sizeof(buffer)); @@ -693,7 +766,8 @@ static int make_stream(struct stream *s, const char *name) } else { pw_filter_add_listener(s->filter, &s->listener, &source_events, s); - flags |= PW_FILTER_FLAG_TRIGGER; + if (s->follower->mode & MODE_SINK) + flags |= PW_FILTER_FLAG_TRIGGER; } reset_volume(&s->volume, s->info.channels); @@ -705,18 +779,23 @@ static int make_stream(struct stream *s, const char *name) SPA_PARAM_Format, &s->info); params[n_params++] = make_props_param(&b, &s->volume); - return pw_filter_connect(s->filter, flags, params, n_params); + if ((res = pw_filter_connect(s->filter, flags, params, n_params)) < 0) + return res; + + if (s->info.channels == 0) + make_stream_ports(s); + + return res; } static int create_filters(struct follower *follower) { - struct impl *impl = follower->impl; int res = 0; - if (impl->mode & MODE_SINK) + if (follower->mode & MODE_SINK) res = make_stream(&follower->sink, "NETJACK2 Send"); - if (impl->mode & MODE_SOURCE) + if (follower->mode & MODE_SOURCE) res = make_stream(&follower->source, "NETJACK2 Receive"); return res; @@ -860,6 +939,8 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param struct follower *follower; char buffer[256]; struct netjack2_peer *peer; + uint32_t i; + const char *media; pw_log_info("got follower available"); nj2_dump_session_params(params); @@ -888,9 +969,18 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param follower->sink.direction = PW_DIRECTION_INPUT; follower->sink.props = pw_properties_copy(impl->sink_props); - parse_audio_info(follower->source.props, &follower->source.info); - parse_audio_info(follower->sink.props, &follower->sink.info); + if ((res = parse_audio_info(follower->source.props, &follower->source.info)) < 0 || + (res = parse_audio_info(follower->sink.props, &follower->sink.info)) < 0) { + pw_log_error("can't parse format: %s", spa_strerror(res)); + return res; + } + follower->source.n_audio = pw_properties_get_uint32(follower->source.props, + "audio.ports", follower->source.info.channels ? + follower->source.info.channels : DEFAULT_AUDIO_PORTS); + follower->sink.n_audio = pw_properties_get_uint32(follower->sink.props, + "audio.ports", follower->sink.info.channels ? + follower->sink.info.channels : DEFAULT_AUDIO_PORTS); follower->source.n_midi = pw_properties_get_uint32(follower->source.props, "midi.ports", DEFAULT_MIDI_PORTS); follower->sink.n_midi = pw_properties_get_uint32(follower->sink.props, @@ -925,29 +1015,64 @@ static int handle_follower_available(struct impl *impl, struct nj2_session_param peer->params.sample_encoder = impl->encoding; peer->params.kbps = impl->kbps; + /* params send and recv are from the manager point of view and reversed for the + * driver. So, for us send = sink and recv = source */ if (peer->params.send_audio_channels < 0) - peer->params.send_audio_channels = follower->sink.info.channels; + peer->params.send_audio_channels = follower->sink.n_audio; if (peer->params.recv_audio_channels < 0) - peer->params.recv_audio_channels = follower->source.info.channels; + peer->params.recv_audio_channels = follower->source.n_audio; if (peer->params.send_midi_channels < 0) peer->params.send_midi_channels = follower->sink.n_midi; if (peer->params.recv_midi_channels < 0) peer->params.recv_midi_channels = follower->source.n_midi; - follower->source.n_ports = peer->params.send_audio_channels + peer->params.send_midi_channels; - follower->source.info.rate = peer->params.sample_rate; - follower->source.info.channels = peer->params.send_audio_channels; - follower->sink.n_ports = peer->params.recv_audio_channels + peer->params.recv_midi_channels; - follower->sink.info.rate = peer->params.sample_rate; - follower->sink.info.channels = peer->params.recv_audio_channels; + follower->source.n_ports = peer->params.recv_audio_channels + peer->params.recv_midi_channels; + follower->source.info.rate = peer->params.sample_rate; + if ((uint32_t)peer->params.recv_audio_channels != follower->source.info.channels) { + follower->source.info.channels = peer->params.recv_audio_channels; + for (i = 0; i < follower->source.info.channels; i++) + follower->source.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } + follower->sink.n_ports = peer->params.send_audio_channels + peer->params.send_midi_channels; + follower->sink.info.rate = peer->params.sample_rate; + if ((uint32_t)peer->params.send_audio_channels != follower->sink.info.channels) { + follower->sink.info.channels = peer->params.send_audio_channels; + for (i = 0; i < follower->sink.info.channels; i++) + follower->sink.info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } - follower->source.n_ports = follower->source.n_midi + follower->source.info.channels; - follower->sink.n_ports = follower->sink.n_midi + follower->sink.info.channels; if (follower->source.n_ports > MAX_PORTS || follower->sink.n_ports > MAX_PORTS) { - pw_log_error("too many ports"); + pw_log_error("too many ports source:%d sink:%d max:%d", follower->source.n_ports, + follower->sink.n_ports, MAX_PORTS); res = -EINVAL; goto cleanup; } + media = follower->sink.info.channels > 0 ? "Audio" : "Midi"; + if (pw_properties_get_bool(follower->sink.props, "netjack2.connect", DEFAULT_CONNECT)) { + if (pw_properties_get(follower->sink.props, PW_KEY_NODE_AUTOCONNECT) == NULL) + pw_properties_set(follower->sink.props, PW_KEY_NODE_AUTOCONNECT, "true"); + if (pw_properties_get(follower->sink.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(follower->sink.props, PW_KEY_MEDIA_CLASS, "Stream/Input/%s", media); + } else { + if (pw_properties_get(follower->sink.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(follower->sink.props, PW_KEY_MEDIA_CLASS, "%s/Sink", media); + } + media = follower->source.info.channels > 0 ? "Audio" : "Midi"; + if (pw_properties_get_bool(follower->source.props, "netjack2.connect", DEFAULT_CONNECT)) { + if (pw_properties_get(follower->source.props, PW_KEY_NODE_AUTOCONNECT) == NULL) + pw_properties_set(follower->source.props, PW_KEY_NODE_AUTOCONNECT, "true"); + if (pw_properties_get(follower->source.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(follower->source.props, PW_KEY_MEDIA_CLASS, "Stream/Output/%s", media); + } else { + if (pw_properties_get(follower->source.props, PW_KEY_MEDIA_CLASS) == NULL) + pw_properties_setf(follower->source.props, PW_KEY_MEDIA_CLASS, "%s/Source", media); + } + + follower->mode = 0; + if (follower->sink.n_ports > 0) + follower->mode |= MODE_SINK; + if (follower->source.n_ports > 0) + follower->mode |= MODE_SOURCE; if ((res = create_filters(follower)) < 0) goto create_failed; @@ -1082,8 +1207,7 @@ static int create_netjack2_socket(struct impl *impl) impl->dscp = pw_properties_get_uint32(impl->props, "net.dscp", DEFAULT_NET_DSCP); str = pw_properties_get(impl->props, "local.ifname"); - fd = make_announce_socket(&impl->src_addr, impl->src_len, - pw_properties_get(impl->props, "local.ifname")); + fd = make_announce_socket(&impl->src_addr, impl->src_len, str); if (fd < 0) { res = fd; pw_log_error("can't create socket: %s", spa_strerror(res)); @@ -1169,14 +1293,14 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( - SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P"), - SPA_DICT_ITEM(SPA_KEY_AUDIO_POSITION, DEFAULT_POSITION)), + SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, "F32P")), &props->dict, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -1238,20 +1362,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->main_loop = pw_context_get_main_loop(context); impl->system = impl->main_loop->system; - impl->mode = MODE_DUPLEX; - if ((str = pw_properties_get(props, "tunnel.mode")) != NULL) { - if (spa_streq(str, "source")) { - impl->mode = MODE_SOURCE; - } else if (spa_streq(str, "sink")) { - impl->mode = MODE_SINK; - } else if (spa_streq(str, "duplex")) { - impl->mode = MODE_DUPLEX; - } else { - pw_log_error("invalid tunnel.mode '%s'", str); - res = -EINVAL; - goto error; - } - } impl->samplerate = pw_properties_get_uint32(impl->props, "netjack2.sample-rate", DEFAULT_SAMPLE_RATE); impl->period_size = pw_properties_get_uint32(impl->props, "netjack2.period-size", @@ -1303,33 +1413,18 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_LOOP_NAME); copy_props(impl, props, PW_KEY_NODE_VIRTUAL); copy_props(impl, props, PW_KEY_NODE_NETWORK); + copy_props(impl, props, PW_KEY_NODE_GROUP); copy_props(impl, props, PW_KEY_NODE_LINK_GROUP); copy_props(impl, props, PW_KEY_NODE_ALWAYS_PROCESS); copy_props(impl, props, PW_KEY_NODE_LOCK_QUANTUM); copy_props(impl, props, PW_KEY_NODE_LOCK_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); + copy_props(impl, props, "audio.ports"); + copy_props(impl, props, "midi.ports"); copy_props(impl, props, "netjack2.connect"); - if (pw_properties_get_bool(impl->sink_props, "netjack2.connect", DEFAULT_CONNECT)) { - if (pw_properties_get(impl->sink_props, PW_KEY_NODE_AUTOCONNECT) == NULL) - pw_properties_set(impl->sink_props, PW_KEY_NODE_AUTOCONNECT, "true"); - if (pw_properties_get(impl->sink_props, PW_KEY_MEDIA_CLASS) == NULL) - pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS, "Stream/Input/Audio"); - } else { - if (pw_properties_get(impl->sink_props, PW_KEY_MEDIA_CLASS) == NULL) - pw_properties_set(impl->sink_props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); - } - if (pw_properties_get_bool(impl->source_props, "netjack2.connect", DEFAULT_CONNECT)) { - if (pw_properties_get(impl->source_props, PW_KEY_NODE_AUTOCONNECT) == NULL) - pw_properties_set(impl->source_props, PW_KEY_NODE_AUTOCONNECT, "true"); - if (pw_properties_get(impl->source_props, PW_KEY_MEDIA_CLASS) == NULL) - pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Stream/Output/Audio"); - } else { - if (pw_properties_get(impl->source_props, PW_KEY_MEDIA_CLASS) == NULL) - pw_properties_set(impl->source_props, PW_KEY_MEDIA_CLASS, "Audio/Source"); - } - impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { str = pw_properties_get(props, PW_KEY_REMOTE_NAME); diff --git a/src/modules/module-netjack2/packets.h b/src/modules/module-netjack2/packets.h index af60a6c78..69791cd11 100644 --- a/src/modules/module-netjack2/packets.h +++ b/src/modules/module-netjack2/packets.h @@ -120,7 +120,7 @@ struct nj2_packet_header { uint32_t cycle; /* process cycle counter */ uint32_t sub_cycle; /* midi/audio subcycle counter */ int32_t frames; /* process cycle size in frames (can be -1 to indicate entire buffer) */ - uint32_t is_last; /* is it the last packet of a given cycle ('y' or 'n') */ + uint32_t is_last; /* is it the last packet of a given cycle (1=yes or 0=no) */ } __attribute__ ((packed)); #define UDP_HEADER_SIZE 64 /* 40 bytes for IP header in IPV6, 20 in IPV4, 8 for UDP, so take 64 */ diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index bac6f1322..eacc1c95b 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -7,10 +7,12 @@ #include #endif +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS + struct volume { bool mute; uint32_t n_volumes; - float volumes[SPA_AUDIO_MAX_CHANNELS]; + float volumes[MAX_CHANNELS]; }; static inline float bswap_f32(float f) @@ -242,17 +244,80 @@ static inline void fix_midi_event(uint8_t *data, size_t size) } } +static inline void *n2j_midi_buffer_reserve(struct nj2_midi_buffer *buf, + uint32_t offset, uint32_t size) +{ + struct nj2_midi_event *ev; + void *ptr; + + if (size <= 0) + return NULL; + + size_t used_size = sizeof(*buf) + buf->write_pos + + ((buf->event_count + 1) * sizeof(struct nj2_midi_event)); + + ev = &buf->event[buf->event_count]; + ev->time = offset; + ev->size = size; + if (size <= MIDI_INLINE_MAX) { + ptr = ev->buffer; + } else { + if (used_size + size > buf->buffer_size) + return NULL; + buf->write_pos += size; + ev->offset = buf->buffer_size - buf->write_pos; + ptr = SPA_PTROFF(buf, ev->offset, void); + } + buf->event_count++; + return ptr; +} + +static inline void n2j_midi_buffer_write(struct nj2_midi_buffer *buf, + uint32_t offset, void *data, uint32_t size) +{ + void *ptr = n2j_midi_buffer_reserve(buf, offset, size); + if (ptr != NULL) + memcpy(ptr, data, size); + else + buf->lost_events++; +} + +static inline void n2j_midi_buffer_append(struct nj2_midi_buffer *buf, + void *data, uint32_t size) +{ + struct nj2_midi_event *ev; + uint32_t old_size; + uint8_t *old_ptr, *new_ptr; + + ev = &buf->event[--buf->event_count]; + old_size = ev->size; + if (old_size <= MIDI_INLINE_MAX) { + old_ptr = ev->buffer; + } else { + buf->write_pos -= old_size; + old_ptr = SPA_PTROFF(buf, ev->offset, void); + } + new_ptr = n2j_midi_buffer_reserve(buf, ev->time, old_size + size); + if (new_ptr == NULL) { + buf->lost_events++; + } else { + memmove(new_ptr, old_ptr, old_size); + memcpy(new_ptr+old_size, data, size); + } +} + static void midi_to_netjack2(struct netjack2_peer *peer, struct nj2_midi_buffer *buf, float *src, uint32_t n_samples) { - struct spa_pod *pod; - struct spa_pod_sequence *seq; - struct spa_pod_control *c; - struct nj2_midi_event *ev; - int free_size; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + struct spa_pod_control c; + const void *seq_body, *c_body; + bool in_sysex = false; buf->magic = MIDI_BUFFER_MAGIC; - buf->buffer_size = peer->quantum_limit * sizeof(float); + buf->buffer_size = peer->params.period_size * sizeof(float); buf->nframes = n_samples; buf->write_pos = 0; buf->event_count = 0; @@ -261,52 +326,45 @@ static void midi_to_netjack2(struct netjack2_peer *peer, if (src == NULL) return; - if ((pod = spa_pod_from_data(src, n_samples * sizeof(float), - 0, n_samples * sizeof(float))) == NULL) - return; - if (!spa_pod_is_sequence(pod)) + spa_pod_parser_init_from_data(&parser, src, n_samples * sizeof(float), + 0, n_samples * sizeof(float)); + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) return; - seq = (struct spa_pod_sequence*)pod; - - free_size = buf->buffer_size - sizeof(*buf); - - SPA_POD_SEQUENCE_FOREACH(seq, c) { + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { int size; uint8_t data[16]; - void *ptr; + bool was_sysex = in_sysex; + size_t c_size = c.value.size; + uint64_t state = 0; - if (c->type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_UMP) continue; - size = spa_ump_to_midi(SPA_POD_BODY(&c->value), - SPA_POD_BODY_SIZE(&c->value), data, sizeof(data)); - if (size <= 0) - continue; + while (c_size > 0) { + size = spa_ump_to_midi((const uint32_t**)&c_body, &c_size, data, sizeof(data), &state); + if (size <= 0) + break; - if (c->offset >= n_samples || - size >= free_size) { - buf->lost_events++; - continue; + if (c.offset >= n_samples) { + buf->lost_events++; + continue; + } + + if (!in_sysex && data[0] == 0xf0) + in_sysex = true; + + if (!in_sysex && peer->fix_midi) + fix_midi_event(data, size); + + if (in_sysex && data[size-1] == 0xf7) + in_sysex = false; + + if (was_sysex) + n2j_midi_buffer_append(buf, data, size); + else + n2j_midi_buffer_write(buf, c.offset, data, size); } - - if (peer->fix_midi) - fix_midi_event(data, size); - - ev = &buf->event[buf->event_count]; - ev->time = c->offset; - ev->size = size; - if (size <= MIDI_INLINE_MAX) { - ptr = ev->buffer; - } else { - buf->write_pos += size; - ev->offset = buf->buffer_size - 1 - buf->write_pos; - free_size -= size; - ptr = SPA_PTROFF(buf, ev->offset, void); - } - memcpy(ptr, data, size); - buf->event_count++; - free_size -= sizeof(*ev); } if (buf->write_pos > 0) memmove(SPA_PTROFF(buf, sizeof(*buf) + buf->event_count * sizeof(struct nj2_midi_event), void), @@ -314,15 +372,27 @@ static void midi_to_netjack2(struct netjack2_peer *peer, buf->write_pos); } +static inline void netjack2_clear_midi(float *dst, uint32_t size) +{ + struct spa_pod_builder b = { 0, }; + struct spa_pod_frame f; + spa_pod_builder_init(&b, dst, size); + spa_pod_builder_push_sequence(&b, &f, 0); + spa_pod_builder_pop(&b, &f); +} + static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_buffer *buf) { struct spa_pod_builder b = { 0, }; uint32_t i; struct spa_pod_frame f; + size_t offset = size - buf->write_pos - sizeof(*buf) - + (buf->event_count * sizeof(struct nj2_midi_event)); spa_pod_builder_init(&b, dst, size); spa_pod_builder_push_sequence(&b, &f, 0); - for (i = 0; buf != NULL && i < buf->event_count; i++) { + + for (i = 0; i < buf->event_count; i++) { struct nj2_midi_event *ev = &buf->event[i]; uint8_t *data; size_t s; @@ -330,8 +400,8 @@ static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_b if (ev->size <= MIDI_INLINE_MAX) data = ev->buffer; - else if (ev->offset > buf->write_pos) - data = SPA_PTROFF(buf, ev->offset - buf->write_pos, void); + else if (ev->offset > offset) + data = SPA_PTROFF(buf, ev->offset - offset, void); else continue; @@ -339,9 +409,10 @@ static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_b while (s > 0) { uint32_t ump[4]; int ump_size = spa_ump_from_midi(&data, &s, ump, sizeof(ump), 0, &state); - if (ump_size <= 0) + if (ump_size <= 0) { + pw_log_warn("invalid MIDI received: %s", spa_strerror(ump_size)); break; - + } spa_pod_builder_control(&b, ev->time, SPA_CONTROL_UMP); spa_pod_builder_bytes(&b, ump, ump_size); } @@ -362,7 +433,7 @@ static int netjack2_send_sync(struct netjack2_peer *peer, uint32_t nframes) is_last = peer->params.send_midi_channels == 0 && peer->params.send_audio_channels == 0 ? 1 : 0; - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('s'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); @@ -416,7 +487,7 @@ static int netjack2_send_midi(struct netjack2_peer *peer, uint32_t nframes, max_size = peer->params.mtu - sizeof(header); num_packets = (midi_size + max_size-1) / max_size; - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('m'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); @@ -468,7 +539,7 @@ static int netjack2_send_float(struct netjack2_peer *peer, uint32_t nframes, sub_period_bytes = sub_period_size * sizeof(float) + sizeof(int32_t); num_packets = nframes / sub_period_size; - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('a'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); @@ -543,7 +614,7 @@ static int netjack2_send_opus(struct netjack2_peer *peer, uint32_t nframes, } } - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('a'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); @@ -611,7 +682,7 @@ static int netjack2_send_int(struct netjack2_peer *peer, uint32_t nframes, memset(ap, 0, max_encoded); } - strcpy(header.type, "header"); + strncpy(header.type, "header", sizeof(header.type)); header.data_type = htonl('a'); header.data_stream = htonl(peer->our_stream); header.id = htonl(peer->params.id); @@ -691,7 +762,7 @@ static inline int32_t netjack2_driver_sync_wait(struct netjack2_peer *peer) receive_error: pw_log_warn("recv error: %m"); - return 0; + return -errno; } static inline int32_t netjack2_manager_sync_wait(struct netjack2_peer *peer) @@ -735,7 +806,7 @@ static inline int32_t netjack2_manager_sync_wait(struct netjack2_peer *peer) receive_error: pw_log_warn("recv error: %m"); - return 0; + return -errno; } static int netjack2_recv_midi(struct netjack2_peer *peer, struct nj2_packet_header *header, uint32_t *count, @@ -748,6 +819,8 @@ static int netjack2_recv_midi(struct netjack2_peer *peer, struct nj2_packet_head if ((len = recv(peer->fd, buffer, packet_size, 0)) < 0) return -errno; + if ((size_t)len < sizeof(*header)) + return -EINVAL; active_ports = peer->params.recv_midi_channels; if (active_ports == 0) @@ -1031,7 +1104,7 @@ static int netjack2_recv_data(struct netjack2_peer *peer, } for (i = 0; i < n_midi; i++) { if (!midi[i].filled && midi[i].data != NULL) - netjack2_to_midi(midi[i].data, peer->params.period_size * sizeof(float), NULL); + netjack2_clear_midi(midi[i].data, peer->params.period_size * sizeof(float)); } peer->sync.cycle = ntohl(header.cycle); return 0; diff --git a/src/modules/module-parametric-equalizer.c b/src/modules/module-parametric-equalizer.c index 315662f82..7eb89f7f5 100644 --- a/src/modules/module-parametric-equalizer.c +++ b/src/modules/module-parametric-equalizer.c @@ -3,11 +3,11 @@ /* SPDX-FileCopyrightText: Copyright © 2024 Asymptotic Inc. */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include -#include "config.h" - #include #include #include @@ -70,6 +70,7 @@ * Options with well-known behaviour: * * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_REMOTE_NAME * diff --git a/src/modules/module-pipe-tunnel.c b/src/modules/module-pipe-tunnel.c index afc8e0f14..62de8717c 100644 --- a/src/modules/module-pipe-tunnel.c +++ b/src/modules/module-pipe-tunnel.c @@ -3,6 +3,8 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -16,8 +18,6 @@ #include #include -#include "config.h" - #include #include #include @@ -79,6 +79,7 @@ * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_LATENCY * - \ref PW_KEY_NODE_NAME @@ -745,9 +746,9 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), @@ -756,6 +757,7 @@ static void parse_audio_info(const struct pw_properties *props, struct spa_audio SPA_KEY_AUDIO_FORMAT, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -886,6 +888,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_FORMAT); copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); @@ -896,7 +899,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_TARGET_OBJECT); copy_props(impl, props, "pipe.filename"); - parse_audio_info(impl->stream_props, &impl->info); + if ((res = parse_audio_info(impl->stream_props, &impl->info)) < 0) { + pw_log_error("can't parse format: %s", spa_strerror(res)); + goto error; + } impl->frame_size = calc_frame_size(&impl->info); if (impl->frame_size == 0) { diff --git a/src/modules/module-portal.c b/src/modules/module-portal.c index 566e67d4b..4491b0317 100644 --- a/src/modules/module-portal.c +++ b/src/modules/module-portal.c @@ -3,6 +3,8 @@ /* SPDX-FileCopyrightText: Copyright © 2019 Red Hat Inc. */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -11,8 +13,6 @@ #include #include -#include "config.h" - #include #include diff --git a/src/modules/module-profiler.c b/src/modules/module-profiler.c index ec8c5c7f0..be1da5724 100644 --- a/src/modules/module-profiler.c +++ b/src/modules/module-profiler.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -11,8 +13,6 @@ #include #include -#include "config.h" - #include #include #include @@ -266,53 +266,58 @@ static void context_do_profile(void *data) SPA_POD_Int(a->xrun_count)); spa_list_for_each(t, &node->rt.target_list, link) { - struct pw_impl_node *n = t->node; - struct pw_node_activation *na; + struct pw_impl_node *tn = t->node; + struct pw_node_activation *ta = t->activation; struct spa_fraction latency; - struct pw_node_activation *a = n->rt.target.activation; - struct spa_io_position *pos = &a->position; + bool async; + int64_t prev_signal_time; if (t->id == id) continue; - if (n != NULL) { - latency = n->latency; - if (n->force_quantum != 0) - latency.num = n->force_quantum; - if (n->force_rate != 0) - update_denom(&latency, n->force_rate); - else if (n->rate.denom != 0) - update_denom(&latency, n->rate.denom); + if (tn != NULL) { + latency = tn->latency; + if (tn->force_quantum != 0) + latency.num = tn->force_quantum; + if (tn->force_rate != 0) + update_denom(&latency, tn->force_rate); + else if (tn->rate.denom != 0) + update_denom(&latency, tn->rate.denom); + async = tn->async; + prev_signal_time = tn->rt.target.activation->prev_signal_time; } else { spa_zero(latency); + async = false; + prev_signal_time = ta->prev_signal_time; } - na = t->activation; spa_pod_builder_prop(&b, SPA_PROFILER_followerBlock, 0); spa_pod_builder_add_struct(&b, SPA_POD_Int(t->id), SPA_POD_String(t->name), - SPA_POD_Long(a->prev_signal_time), - SPA_POD_Long(n->async ? na->prev_signal_time : na->signal_time), - SPA_POD_Long(n->async ? na->prev_awake_time : na->awake_time), - SPA_POD_Long(n->async ? na->prev_finish_time : na->finish_time), - SPA_POD_Int(na->status), + SPA_POD_Long(prev_signal_time), + SPA_POD_Long(async ? ta->prev_signal_time : ta->signal_time), + SPA_POD_Long(async ? ta->prev_awake_time : ta->awake_time), + SPA_POD_Long(async ? ta->prev_finish_time : ta->finish_time), + SPA_POD_Int(ta->status), SPA_POD_Fraction(&latency), - SPA_POD_Int(na->xrun_count)); + SPA_POD_Int(ta->xrun_count), + SPA_POD_Bool(async)); - if (n->driver) { + if (tn && tn->driver) { + struct spa_io_position *tpos = &tn->rt.target.activation->position; spa_pod_builder_prop(&b, SPA_PROFILER_followerClock, 0); spa_pod_builder_add_struct(&b, - SPA_POD_Int(pos->clock.id), - SPA_POD_String(pos->clock.name), - SPA_POD_Long(pos->clock.nsec), - SPA_POD_Fraction(&pos->clock.rate), - SPA_POD_Long(pos->clock.position), - SPA_POD_Long(pos->clock.duration), - SPA_POD_Long(pos->clock.delay), - SPA_POD_Double(pos->clock.rate_diff), - SPA_POD_Long(pos->clock.next_nsec), - SPA_POD_Long(pos->clock.xrun)); + SPA_POD_Int(tpos->clock.id), + SPA_POD_String(tpos->clock.name), + SPA_POD_Long(tpos->clock.nsec), + SPA_POD_Fraction(&tpos->clock.rate), + SPA_POD_Long(tpos->clock.position), + SPA_POD_Long(tpos->clock.duration), + SPA_POD_Long(tpos->clock.delay), + SPA_POD_Double(tpos->clock.rate_diff), + SPA_POD_Long(tpos->clock.next_nsec), + SPA_POD_Long(tpos->clock.xrun)); } } spa_pod_builder_pop(&b, &f[0]); diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c index c7536bd0f..96e99f35e 100644 --- a/src/modules/module-protocol-native.c +++ b/src/modules/module-protocol-native.c @@ -26,7 +26,6 @@ #include #endif -#include #include #include #include @@ -190,7 +189,6 @@ static const struct spa_dict_item module_props[] = { #define LOCK_SUFFIXLEN 5 void pw_protocol_native_init(struct pw_protocol *protocol); -void pw_protocol_native0_init(struct pw_protocol *protocol); void *protocol_native_security_context_init(struct pw_impl_module *module, struct pw_protocol *protocol); void protocol_native_security_context_free(void *data); @@ -271,8 +269,6 @@ struct client_data { unsigned int busy:1; unsigned int need_flush:1; - - struct protocol_compat_v2 compat_v2; }; static void debug_msg(const char *prefix, const struct pw_protocol_native_message *msg, bool hex) @@ -536,8 +532,6 @@ static void client_free(void *data) pw_loop_destroy_source(client->context->main_loop, this->source); if (this->connection) pw_protocol_native_connection_destroy(this->connection); - - pw_map_clear(&this->compat_v2.types); } static const struct pw_impl_client_events client_events = { @@ -567,9 +561,6 @@ static void on_start(void *data, uint32_t version) PW_PERM_ALL, version, 0) < 0) return; - if (version == 0) - client->compat_v2 = &this->compat_v2; - return; } @@ -687,7 +678,6 @@ static struct client_data *client_new(struct server *s, int fd) this->server = s; this->client = client; - pw_map_init(&this->compat_v2.types, 0, 32); pw_impl_client_add_listener(client, &this->client_listener, &client_events, this); @@ -1836,7 +1826,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args_str) this->extension = &protocol_ext_impl; pw_protocol_native_init(this); - pw_protocol_native0_init(this); pw_log_debug("%p: new", this); diff --git a/src/modules/module-protocol-native/connection.h b/src/modules/module-protocol-native/connection.h index 56276fbac..bc9bb530d 100644 --- a/src/modules/module-protocol-native/connection.h +++ b/src/modules/module-protocol-native/connection.h @@ -5,15 +5,19 @@ #ifndef PIPEWIRE_PROTOCOL_NATIVE_CONNECTION_H #define PIPEWIRE_PROTOCOL_NATIVE_CONNECTION_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#define MAX_DICT 1024 +#define MAX_PARAM_INFO 128 +#define MAX_PERMISSIONS 4096 + +#ifdef __cplusplus +extern "C" { +#endif + struct pw_protocol_native_connection_events { #define PW_VERSION_PROTOCOL_NATIVE_CONNECTION_EVENTS 0 uint32_t version; diff --git a/src/modules/module-protocol-native/defs.h b/src/modules/module-protocol-native/defs.h index 429934e0a..3b5519097 100644 --- a/src/modules/module-protocol-native/defs.h +++ b/src/modules/module-protocol-native/defs.h @@ -27,9 +27,3 @@ static inline void *get_first_pod_from_data(void *data, uint32_t maxsize, uint64 return NULL; return pod; } - -struct protocol_compat_v2 { - /* v2 typemap */ - struct pw_map types; - unsigned int send_types:1; -}; diff --git a/src/modules/module-protocol-native/protocol-native.c b/src/modules/module-protocol-native/protocol-native.c index 03f4c8a7e..3a8580efb 100644 --- a/src/modules/module-protocol-native/protocol-native.c +++ b/src/modules/module-protocol-native/protocol-native.c @@ -15,10 +15,6 @@ #include "connection.h" -#define MAX_DICT 1024 -#define MAX_PARAM_INFO 128 -#define MAX_PERMISSIONS 4096 - PW_LOG_TOPIC_EXTERN(mod_topic); #define PW_LOG_TOPIC_DEFAULT mod_topic @@ -981,7 +977,7 @@ static int device_demarshal_subscribe_params(void *object, const struct pw_proto SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_resource_notify(resource, struct pw_device_methods, subscribe_params, 0, @@ -1242,7 +1238,7 @@ static int node_demarshal_subscribe_params(void *object, const struct pw_protoco SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_resource_notify(resource, struct pw_node_methods, subscribe_params, 0, @@ -1466,7 +1462,7 @@ static int port_demarshal_subscribe_params(void *object, const struct pw_protoco SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_resource_notify(resource, struct pw_port_methods, subscribe_params, 0, diff --git a/src/modules/module-protocol-native/v0/interfaces.h b/src/modules/module-protocol-native/v0/interfaces.h deleted file mode 100644 index 1951e69a9..000000000 --- a/src/modules/module-protocol-native/v0/interfaces.h +++ /dev/null @@ -1,514 +0,0 @@ -/* PipeWire */ -/* SPDX-FileCopyrightText: Copyright © 2016 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -#ifndef PIPEWIRE_INTERFACES_V0_H -#define PIPEWIRE_INTERFACES_V0_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include - -#include - -/** Core */ - -#define PW_VERSION_CORE_V0 0 - -#define PW_CORE_V0_METHOD_HELLO 0 -#define PW_CORE_V0_METHOD_UPDATE_TYPES 1 -#define PW_CORE_V0_METHOD_SYNC 2 -#define PW_CORE_V0_METHOD_GET_REGISTRY 3 -#define PW_CORE_V0_METHOD_CLIENT_UPDATE 4 -#define PW_CORE_V0_METHOD_PERMISSIONS 5 -#define PW_CORE_V0_METHOD_CREATE_OBJECT 6 -#define PW_CORE_V0_METHOD_DESTROY 7 -#define PW_CORE_V0_METHOD_NUM 8 - -/** - * Key to update default permissions of globals without specific - * permissions. value is "[r][w][x]" */ -#define PW_CORE_PERMISSIONS_DEFAULT "permissions.default" - -/** - * Key to update specific permissions of a global. If the global - * did not have specific permissions, it will first be assigned - * the default permissions before it is updated. - * Value is ":[r][w][x]"*/ -#define PW_CORE_PERMISSIONS_GLOBAL "permissions.global" - -/** - * Key to update specific permissions of all existing globals. - * This is equivalent to using \ref PW_CORE_PERMISSIONS_GLOBAL - * on each global id individually that did not have specific - * permissions. - * Value is "[r][w][x]" */ -#define PW_CORE_PERMISSIONS_EXISTING "permissions.existing" - -#define PW_LINK_OUTPUT_NODE_ID "link.output_node.id" -#define PW_LINK_OUTPUT_PORT_ID "link.output_port.id" -#define PW_LINK_INPUT_NODE_ID "link.input_node.id" -#define PW_LINK_INPUT_PORT_ID "link.input_port.id" - -/** - * \struct pw_core_v0_methods - * \brief Core methods - * - * The core global object. This is a singleton object used for - * creating new objects in the remote PipeWire instance. It is - * also used for internal features. - */ -struct pw_core_v0_methods { -#define PW_VERSION_CORE_V0_METHODS 0 - uint32_t version; - /** - * Start a conversation with the server. This will send - * the core info and server types. - * - * All the existing resources for the client (except the core - * resource) will be destroyed. - */ - void (*hello) (void *object); - /** - * Update the type map - * - * Send a type map update to the PipeWire server. The server uses this - * information to keep a mapping between client types and the server types. - * \param first_id the id of the first type - * \param types the types as a string - * \param n_types the number of types - */ - void (*update_types) (void *object, - uint32_t first_id, - const char **types, - uint32_t n_types); - /** - * Do server roundtrip - * - * Ask the server to emit the 'done' event with \a id. - * 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. - * \param seq the sequence number passed to the done event - */ - void (*sync) (void *object, uint32_t seq); - /** - * Get the registry object - * - * Create a registry object that allows the client to list and bind - * the global objects available from the PipeWire server - * \param version the client proxy id - * \param id the client proxy id - */ - void (*get_registry) (void *object, uint32_t version, uint32_t new_id); - /** - * Update the client properties - * \param props the new client properties - */ - void (*client_update) (void *object, const struct spa_dict *props); - /** - * Manage the permissions of the global objects - * - * Update the permissions of the global objects using the - * dictionary with properties. - * - * Globals can use the default permissions or can have specific - * permissions assigned to them. - * - * \param id the global id to change - * \param props dictionary with permission properties - */ - void (*permissions) (void *object, const struct spa_dict *props); - /** - * Create a new object on the PipeWire server from a factory. - * Use a \a factory_name of "client-node" to create a - * \ref pw_client_node. - * - * \param factory_name the factory name to use - * \param type the interface to bind to - * \param version the version of the interface - * \param props extra properties - * \param new_id the client proxy id - */ - void (*create_object) (void *object, - const char *factory_name, - uint32_t type, - uint32_t version, - const struct spa_dict *props, - uint32_t new_id); - - /** - * Destroy an object id - * - * \param id the object id to destroy - */ - void (*destroy) (void *object, uint32_t id); -}; - -#define PW_CORE_V0_EVENT_UPDATE_TYPES 0 -#define PW_CORE_V0_EVENT_DONE 1 -#define PW_CORE_V0_EVENT_ERROR 2 -#define PW_CORE_V0_EVENT_REMOVE_ID 3 -#define PW_CORE_V0_EVENT_INFO 4 -#define PW_CORE_V0_EVENT_NUM 5 - -/** \struct pw_core_v0_events - * \brief Core events - * \ingroup pw_core_interface The pw_core interface - */ -struct pw_core_v0_events { -#define PW_VERSION_CORE_V0_EVENTS 0 - uint32_t version; - /** - * Update the type map - * - * Send a type map update to the client. The client uses this - * information to keep a mapping between server types and the client types. - * \param first_id the id of the first type - * \param types the types as a string - * \param n_types the number of \a types - */ - void (*update_types) (void *data, - uint32_t first_id, - const char **types, - uint32_t n_types); - /** - * Emit a done event - * - * The done event is emitted as a result of a sync method with the - * same sequence number. - * \param seq the sequence number passed to the sync method call - */ - void (*done) (void *data, uint32_t seq); - /** - * Fatal error event - * - * The error event is sent out when a fatal (non-recoverable) - * error has occurred. The id argument is the object where - * the error occurred, most often in response to a request to that - * object. The message is a brief description of the error, - * for (debugging) convenience. - * \param id object where the error occurred - * \param res error code - * \param error error description - */ - void (*error) (void *data, uint32_t id, int res, const char *error, ...); - /** - * Remove an object ID - * - * This event is used internally by the object ID management - * logic. When a client deletes an object, the server will send - * this event to acknowledge that it has seen the delete request. - * When the client receives this event, it will know that it can - * safely reuse the object ID. - * \param id deleted object ID - */ - void (*remove_id) (void *data, uint32_t id); - /** - * Notify new core info - * - * \param info new core info - */ - void (*info) (void *data, struct pw_core_info *info); -}; - -#define pw_core_resource_v0_update_types(r,...) pw_resource_notify(r,struct pw_core_v0_events,update_types,__VA_ARGS__) -#define pw_core_resource_v0_done(r,...) pw_resource_notify(r,struct pw_core_v0_events,done,__VA_ARGS__) -#define pw_core_resource_v0_error(r,...) pw_resource_notify(r,struct pw_core_v0_events,error,__VA_ARGS__) -#define pw_core_resource_v0_remove_id(r,...) pw_resource_notify(r,struct pw_core_v0_events,remove_id,__VA_ARGS__) -#define pw_core_resource_v0_info(r,...) pw_resource_notify(r,struct pw_core_v0_events,info,__VA_ARGS__) - - -#define PW_VERSION_REGISTRY_V0 0 - -/** \page page_registry Registry - * - * \section page_registry_overview Overview - * - * The registry object is a singleton object that keeps track of - * global objects on the PipeWire instance. See also \ref page_global. - * - * Global objects typically represent an actual object in PipeWire - * (for example, a module or node) or they are singleton - * objects such as the core. - * - * When a client creates a registry object, the registry object - * will emit a global event for each global currently in the - * registry. Globals come and go as a result of device hotplugs or - * reconfiguration or other events, and the registry will send out - * global and global_remove events to keep the client up to date - * with the changes. To mark the end of the initial burst of - * events, the client can use the pw_core.sync methosd immediately - * after calling pw_core.get_registry. - * - * A client can bind to a global object by using the bind - * request. This creates a client-side proxy that lets the object - * emit events to the client and lets the client invoke methods on - * the object. See \ref page_proxy - * - * Clients can also change the permissions of the global objects that - * it can see. This is interesting when you want to configure a - * pipewire session before handing it to another application. You - * can, for example, hide certain existing or new objects or limit - * the access permissions on an object. - */ -#define PW_REGISTRY_V0_METHOD_BIND 0 -#define PW_REGISTRY_V0_METHOD_NUM 1 - -/** Registry methods */ -struct pw_registry_v0_methods { -#define PW_VERSION_REGISTRY_V0_METHODS 0 - uint32_t version; - /** - * Bind to a global object - * - * Bind to the global object with \a id and use the client proxy - * with new_id as the proxy. After this call, methods can be - * send to the remote global object and events can be received - * - * \param id the global id to bind to - * \param type the interface type to bind to - * \param version the interface version to use - * \param new_id the client proxy to use - */ - void (*bind) (void *object, uint32_t id, uint32_t type, uint32_t version, uint32_t new_id); -}; - -#define PW_REGISTRY_V0_EVENT_GLOBAL 0 -#define PW_REGISTRY_V0_EVENT_GLOBAL_REMOVE 1 -#define PW_REGISTRY_V0_EVENT_NUM 2 - -/** Registry events */ -struct pw_registry_v0_events { -#define PW_VERSION_REGISTRY_V0_EVENTS 0 - uint32_t version; - /** - * Notify of a new global object - * - * The registry emits this event when a new global object is - * available. - * - * \param id the global object id - * \param parent_id the parent global id - * \param permissions the permissions of the object - * \param type the type of the interface - * \param version the version of the interface - * \param props extra properties of the global - */ - void (*global) (void *data, uint32_t id, uint32_t parent_id, - uint32_t permissions, uint32_t type, uint32_t version, - const struct spa_dict *props); - /** - * Notify of a global object removal - * - * Emitted when a global object was removed from the registry. - * If the client has any bindings to the global, it should destroy - * those. - * - * \param id the id of the global that was removed - */ - void (*global_remove) (void *data, uint32_t id); -}; - -#define pw_registry_resource_v0_global(r,...) pw_resource_notify(r,struct pw_registry_v0_events,global,__VA_ARGS__) -#define pw_registry_resource_v0_global_remove(r,...) pw_resource_notify(r,struct pw_registry_v0_events,global_remove,__VA_ARGS__) - - -#define PW_VERSION_MODULE_V0 0 - -#define PW_MODULE_V0_EVENT_INFO 0 -#define PW_MODULE_V0_EVENT_NUM 1 - -/** Module events */ -struct pw_module_v0_events { -#define PW_VERSION_MODULE_V0_EVENTS 0 - uint32_t version; - /** - * Notify module info - * - * \param info info about the module - */ - void (*info) (void *data, struct pw_module_info *info); -}; - -#define pw_module_resource_v0_info(r,...) pw_resource_notify(r,struct pw_module_v0_events,info,__VA_ARGS__) - -#define PW_VERSION_NODE_V0 0 - -#define PW_NODE_V0_EVENT_INFO 0 -#define PW_NODE_V0_EVENT_PARAM 1 -#define PW_NODE_V0_EVENT_NUM 2 - -/** Node events */ -struct pw_node_v0_events { -#define PW_VERSION_NODE_V0_EVENTS 0 - uint32_t version; - /** - * Notify node info - * - * \param info info about the node - */ - void (*info) (void *data, struct pw_node_info *info); - /** - * Notify a node param - * - * Event emitted as a result of the enum_params method. - * - * \param id the param id - * \param index the param index - * \param next the param index of the next param - * \param param the parameter - */ - void (*param) (void *data, - uint32_t id, uint32_t index, uint32_t next, - const struct spa_pod *param); -}; - -#define pw_node_resource_v0_info(r,...) pw_resource_notify(r,struct pw_node_v0_events,info,__VA_ARGS__) -#define pw_node_resource_v0_param(r,...) pw_resource_notify(r,struct pw_node_v0_events,param,__VA_ARGS__) - -#define PW_NODE_V0_METHOD_ENUM_PARAMS 0 -#define PW_NODE_V0_METHOD_NUM 1 - -/** Node methods */ -struct pw_node_v0_methods { -#define PW_VERSION_NODE_V0_METHODS 0 - uint32_t version; - /** - * Enumerate node parameters - * - * Start enumeration of node parameters. For each param, a - * param event will be emitted. - * - * \param id the parameter id to enum or PW_ID_ANY for all - * \param start the start index or 0 for the first param - * \param num the maximum number of params to retrieve - * \param filter a param filter or NULL - */ - void (*enum_params) (void *object, uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter); -}; - -#define PW_VERSION_PORT_V0 0 - -#define PW_PORT_V0_EVENT_INFO 0 -#define PW_PORT_V0_EVENT_PARAM 1 -#define PW_PORT_V0_EVENT_NUM 2 - -/** Port events */ -struct pw_port_v0_events { -#define PW_VERSION_PORT_V0_EVENTS 0 - uint32_t version; - /** - * Notify port info - * - * \param info info about the port - */ - void (*info) (void *data, struct pw_port_info *info); - /** - * Notify a port param - * - * Event emitted as a result of the enum_params method. - * - * \param id the param id - * \param index the param index - * \param next the param index of the next param - * \param param the parameter - */ - void (*param) (void *data, - uint32_t id, uint32_t index, uint32_t next, - const struct spa_pod *param); -}; - -#define pw_port_resource_v0_info(r,...) pw_resource_notify(r,struct pw_port_v0_events,info,__VA_ARGS__) -#define pw_port_resource_v0_param(r,...) pw_resource_notify(r,struct pw_port_v0_events,param,__VA_ARGS__) - -#define PW_PORT_V0_METHOD_ENUM_PARAMS 0 -#define PW_PORT_V0_METHOD_NUM 1 - -/** Port methods */ -struct pw_port_v0_methods { -#define PW_VERSION_PORT_V0_METHODS 0 - uint32_t version; - /** - * Enumerate port parameters - * - * Start enumeration of port parameters. For each param, a - * param event will be emitted. - * - * \param id the parameter id to enumerate - * \param start the start index or 0 for the first param - * \param num the maximum number of params to retrieve - * \param filter a param filter or NULL - */ - void (*enum_params) (void *object, uint32_t id, uint32_t start, uint32_t num, - const struct spa_pod *filter); -}; - -#define PW_VERSION_FACTORY_V0 0 - -#define PW_FACTORY_V0_EVENT_INFO 0 -#define PW_FACTORY_V0_EVENT_NUM 1 - -/** Factory events */ -struct pw_factory_v0_events { -#define PW_VERSION_FACTORY_V0_EVENTS 0 - uint32_t version; - /** - * Notify factory info - * - * \param info info about the factory - */ - void (*info) (void *data, struct pw_factory_info *info); -}; - -#define pw_factory_resource_v0_info(r,...) pw_resource_notify(r,struct pw_factory_v0_events,info,__VA_ARGS__) - -#define PW_VERSION_CLIENT_V0 0 - -#define PW_CLIENT_V0_EVENT_INFO 0 -#define PW_CLIENT_V0_EVENT_NUM 1 - -/** Client events */ -struct pw_client_v0_events { -#define PW_VERSION_CLIENT_V0_EVENTS 0 - uint32_t version; - /** - * Notify client info - * - * \param info info about the client - */ - void (*info) (void *data, struct pw_client_info *info); -}; - -#define pw_client_resource_v0_info(r,...) pw_resource_notify(r,struct pw_client_v0_events,info,__VA_ARGS__) - - -#define PW_VERSION_LINK_V0 0 - -#define PW_LINK_V0_EVENT_INFO 0 -#define PW_LINK_V0_EVENT_NUM 1 - -/** Link events */ -struct pw_link_v0_events { -#define PW_VERSION_LINK_V0_EVENTS 0 - uint32_t version; - /** - * Notify link info - * - * \param info info about the link - */ - void (*info) (void *data, struct pw_link_info *info); -}; - -#define pw_link_resource_v0_info(r,...) pw_resource_notify(r,struct pw_link_v0_events,info,__VA_ARGS__) - -#ifdef __cplusplus -} /* extern "C" */ -#endif - -#endif /* PIPEWIRE_INTERFACES_V0_H */ diff --git a/src/modules/module-protocol-native/v0/protocol-native.c b/src/modules/module-protocol-native/v0/protocol-native.c deleted file mode 100644 index 862d9eda1..000000000 --- a/src/modules/module-protocol-native/v0/protocol-native.c +++ /dev/null @@ -1,1352 +0,0 @@ -/* PipeWire */ -/* SPDX-FileCopyrightText: Copyright © 2017 Wim Taymans */ -/* SPDX-License-Identifier: MIT */ - -#include -#include - -#include "spa/pod/parser.h" -#include "spa/pod/builder.h" -#include "spa/debug/types.h" -#include "spa/utils/string.h" - -#include "pipewire/pipewire.h" -#include "pipewire/private.h" -#include "pipewire/protocol.h" -#include "pipewire/resource.h" -#include "pipewire/extensions/protocol-native.h" -#include "pipewire/extensions/metadata.h" -#include "pipewire/extensions/session-manager.h" -#include "pipewire/extensions/client-node.h" - -#include "interfaces.h" -#include "typemap.h" - -#include "../defs.h" -#include "../connection.h" - -PW_LOG_TOPIC_EXTERN(mod_topic); -#define PW_LOG_TOPIC_DEFAULT mod_topic - -#define PW_PROTOCOL_NATIVE_FLAG_REMAP (1<<0) - -SPA_EXPORT -uint32_t pw_protocol_native0_find_type(struct pw_impl_client *client, const char *type) -{ - uint32_t i; - for (i = 0; i < SPA_N_ELEMENTS(type_map); i++) { - if (spa_streq(type_map[i].type, type)) - return i; - } - return SPA_ID_INVALID; -} - -static void -update_types_server(struct pw_resource *resource) -{ - struct spa_pod_builder *b; - struct spa_pod_frame f; - uint32_t i; - - b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_UPDATE_TYPES, NULL); - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", 0, - "i", SPA_N_ELEMENTS(type_map), NULL); - - for (i = 0; i < SPA_N_ELEMENTS(type_map); i++) { - spa_pod_builder_add(b, "s", type_map[i].type, NULL); - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - - -static void core_marshal_info(void *data, const struct pw_core_info *info) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct protocol_compat_v2 *compat_v2 = client->compat_v2; - struct spa_pod_builder *b; - uint32_t i, n_items; - uint64_t change_mask = 0; - struct spa_pod_frame f; - struct pw_protocol_native_message *msg; - -#define PW_CORE_V0_CHANGE_MASK_USER_NAME (1 << 0) -#define PW_CORE_V0_CHANGE_MASK_HOST_NAME (1 << 1) -#define PW_CORE_V0_CHANGE_MASK_VERSION (1 << 2) -#define PW_CORE_V0_CHANGE_MASK_NAME (1 << 3) -#define PW_CORE_V0_CHANGE_MASK_COOKIE (1 << 4) -#define PW_CORE_V0_CHANGE_MASK_PROPS (1 << 5) - - if (compat_v2->send_types) { - update_types_server(resource); - change_mask |= PW_CORE_V0_CHANGE_MASK_USER_NAME | - PW_CORE_V0_CHANGE_MASK_HOST_NAME | - PW_CORE_V0_CHANGE_MASK_VERSION | - PW_CORE_V0_CHANGE_MASK_NAME | - PW_CORE_V0_CHANGE_MASK_COOKIE; - compat_v2->send_types = false; - } - b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_INFO, &msg); - - n_items = info->props ? info->props->n_items : 0; - - if (info->change_mask & PW_CORE_CHANGE_MASK_PROPS) - change_mask |= PW_CORE_V0_CHANGE_MASK_PROPS; - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", info->id, - "l", change_mask, - "s", info->user_name, - "s", info->host_name, - "s", info->version, - "s", info->name, - "i", info->cookie, - "i", n_items, NULL); - - for (i = 0; i < n_items; i++) { - spa_pod_builder_add(b, - "s", info->props->items[i].key, - "s", info->props->items[i].value, NULL); - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void core_marshal_done(void *data, uint32_t id, int seq) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - - b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_DONE, NULL); - - spa_pod_builder_add_struct(b, "i", seq); - - pw_protocol_native_end_resource(resource, b); -} - -static void core_marshal_error(void *data, uint32_t id, int seq, int res, const char *error) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - - b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_ERROR, NULL); - - spa_pod_builder_add_struct(b, - "i", id, - "i", res, - "s", error); - - pw_protocol_native_end_resource(resource, b); -} - -static void core_marshal_remove_id(void *data, uint32_t id) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - - b = pw_protocol_native_begin_resource(resource, PW_CORE_V0_EVENT_REMOVE_ID, NULL); - - spa_pod_builder_add_struct(b, "i", id); - - pw_protocol_native_end_resource(resource, b); -} - -static int core_demarshal_client_update(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_dict props; - struct spa_pod_parser prs; - struct spa_pod_frame f; - uint32_t i; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_push_struct(&prs, &f) < 0 || - spa_pod_parser_get(&prs, - "i", &props.n_items, NULL) < 0) - return -EINVAL; - - props.items = alloca(props.n_items * sizeof(struct spa_dict_item)); - for (i = 0; i < props.n_items; i++) { - if (spa_pod_parser_get(&prs, - "s", &props.items[i].key, - "s", &props.items[i].value, - NULL) < 0) - return -EINVAL; - } - pw_impl_client_update_properties(client, &props); - return 0; -} - -static uint32_t parse_perms(const char *str) -{ - uint32_t perms = 0; - - while (*str != '\0') { - switch (*str++) { - case 'r': - perms |= PW_PERM_R; - break; - case 'w': - perms |= PW_PERM_W; - break; - case 'x': - perms |= PW_PERM_X; - break; - } - } - return perms; -} - -static int core_demarshal_permissions(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct spa_dict props; - struct spa_pod_parser prs; - struct spa_pod_frame f; - uint32_t i, n_permissions; - struct pw_permission *permissions, defperm = { 0, }; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_push_struct(&prs, &f) < 0 || - spa_pod_parser_get(&prs, "i", &props.n_items, NULL) < 0) - return -EINVAL; - - props.items = alloca(props.n_items * sizeof(struct spa_dict_item)); - - n_permissions = 0; - permissions = alloca(props.n_items * sizeof(struct pw_permission)); - - for (i = 0; i < props.n_items; i++) { - uint32_t id, perms; - const char *str; - - if (spa_pod_parser_get(&prs, - "s", &props.items[i].key, - "s", &props.items[i].value, - NULL) < 0) - return -EINVAL; - - str = props.items[i].value; - /* first set global permissions */ - if (spa_streq(props.items[i].key, PW_CORE_PERMISSIONS_GLOBAL)) { - size_t len; - - /* :[r][w][x] */ - len = strcspn(str, ":"); - if (len == 0) - continue; - id = atoi(str); - perms = parse_perms(str + len); - permissions[n_permissions++] = PW_PERMISSION_INIT(id, perms); - } else if (spa_streq(props.items[i].key, PW_CORE_PERMISSIONS_DEFAULT)) { - perms = parse_perms(str); - defperm = PW_PERMISSION_INIT(PW_ID_ANY, perms); - } - } - /* add default permission if set */ - if (defperm.id == PW_ID_ANY) - permissions[n_permissions++] = defperm; - - for (i = 0; i < n_permissions; i++) { - pw_log_debug("%d: %d: %08x", i, permissions[i].id, permissions[i].permissions); - } - - return pw_impl_client_update_permissions(resource->client, - n_permissions, permissions); -} - -static int core_demarshal_hello(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct spa_pod_parser prs; - void *ptr; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, - "P", &ptr) < 0) - return -EINVAL; - - return pw_resource_notify(resource, struct pw_core_methods, hello, 0, 2); -} - -static int core_demarshal_sync(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct spa_pod_parser prs; - uint32_t seq; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, "i", &seq) < 0) - return -EINVAL; - - return pw_resource_notify(resource, struct pw_core_methods, sync, 0, 0, seq); -} - -static int core_demarshal_get_registry(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct spa_pod_parser prs; - int32_t version, new_id; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, - "i", &version, - "i", &new_id) < 0) - return -EINVAL; - - return pw_resource_notify(resource, struct pw_core_methods, get_registry, 0, version, new_id); -} - -SPA_EXPORT -uint32_t pw_protocol_native0_type_from_v2(struct pw_impl_client *client, uint32_t type) -{ - void *t; - uint32_t index; - struct protocol_compat_v2 *compat_v2 = client->compat_v2; - - if ((t = pw_map_lookup(&compat_v2->types, type)) == NULL) - return SPA_ID_INVALID; - - index = PW_MAP_PTR_TO_ID(t); - if (index >= SPA_N_ELEMENTS(type_map)) - return SPA_ID_INVALID; - - return type_map[index].id; -} - -SPA_EXPORT -const char * pw_protocol_native0_name_from_v2(struct pw_impl_client *client, uint32_t type) -{ - void *t; - uint32_t index; - struct protocol_compat_v2 *compat_v2 = client->compat_v2; - - if ((t = pw_map_lookup(&compat_v2->types, type)) == NULL) - return NULL; - - index = PW_MAP_PTR_TO_ID(t); - if (index >= SPA_N_ELEMENTS(type_map)) - return NULL; - - return type_map[index].name; -} - -SPA_EXPORT -uint32_t pw_protocol_native0_name_to_v2(struct pw_impl_client *client, const char *name) -{ - uint32_t i; - /* match name to type table and return index */ - for (i = 0; i < SPA_N_ELEMENTS(type_map); i++) { - if (type_map[i].name != NULL && spa_streq(type_map[i].name, name)) - return i; - } - return SPA_ID_INVALID; -} - -SPA_EXPORT -uint32_t pw_protocol_native0_type_to_v2(struct pw_impl_client *client, - const struct spa_type_info *info, uint32_t type) -{ - const char *name; - - /** find full name of type in type_info */ - if ((name = spa_debug_type_find_name(info, type)) == NULL) - return SPA_ID_INVALID; - - return pw_protocol_native0_name_to_v2(client, name); -} - -struct spa_pod_prop_body0 { - uint32_t key; -#define SPA_POD_PROP0_RANGE_NONE 0 /**< no range */ -#define SPA_POD_PROP0_RANGE_MIN_MAX 1 /**< property has range */ -#define SPA_POD_PROP0_RANGE_STEP 2 /**< property has range with step */ -#define SPA_POD_PROP0_RANGE_ENUM 3 /**< property has enumeration */ -#define SPA_POD_PROP0_RANGE_FLAGS 4 /**< property has flags */ -#define SPA_POD_PROP0_RANGE_MASK 0xf /**< mask to select range type */ -#define SPA_POD_PROP0_FLAG_UNSET (1 << 4) /**< property value is unset */ -#define SPA_POD_PROP0_FLAG_OPTIONAL (1 << 5) /**< property value is optional */ -#define SPA_POD_PROP0_FLAG_READONLY (1 << 6) /**< property is readonly */ -#define SPA_POD_PROP0_FLAG_DEPRECATED (1 << 7) /**< property is deprecated */ -#define SPA_POD_PROP0_FLAG_INFO (1 << 8) /**< property is informational and is not - * used when filtering */ - uint32_t flags; - struct spa_pod value; - /* array with elements of value.size follows, - * first element is value/default, rest are alternatives */ -}; - -/* v2 iterates object as containing spa_pod */ -#define SPA_POD_OBJECT_BODY_FOREACH0(body, size, iter) \ - for ((iter) = SPA_PTROFF((body), sizeof(struct spa_pod_object_body), struct spa_pod); \ - spa_pod_is_inside(body, size, iter); \ - (iter) = spa_pod_next(iter)) - -#define SPA_POD_PROP_ALTERNATIVE_FOREACH0(body, _size, iter) \ - for ((iter) = SPA_PTROFF((body), (body)->value.size + \ - sizeof(struct spa_pod_prop_body0), __typeof__(*iter)); \ - (iter) <= SPA_PTROFF((body), (_size)-(body)->value.size, __typeof__(*iter)); \ - (iter) = SPA_PTROFF((iter), (body)->value.size, __typeof__(*iter))) - -#define SPA0_POD_PROP_N_VALUES(b,size) (((size) - sizeof(struct spa_pod_prop_body0)) / (b)->value.size) - -static int remap_from_v2(uint32_t type, void *body, uint32_t size, struct pw_impl_client *client, - struct spa_pod_builder *builder) -{ - int res = 0; - - switch (type) { - case SPA_TYPE_Id: - spa_pod_builder_id(builder, pw_protocol_native0_type_from_v2(client, *(int32_t*) body)); - break; - - /** choice was props in v2 */ - case SPA_TYPE_Choice: - { - struct spa_pod_prop_body0 *b = body; - struct spa_pod_frame f; - void *alt; - uint32_t key = pw_protocol_native0_type_from_v2(client, b->key); - enum spa_choice_type type; - - spa_pod_builder_prop(builder, key, 0); - - switch (b->flags & SPA_POD_PROP0_RANGE_MASK) { - default: - case SPA_POD_PROP0_RANGE_NONE: - type = SPA_CHOICE_None; - break; - case SPA_POD_PROP0_RANGE_MIN_MAX: - type = SPA_CHOICE_Range; - break; - case SPA_POD_PROP0_RANGE_STEP: - type = SPA_CHOICE_Step; - break; - case SPA_POD_PROP0_RANGE_ENUM: - type = SPA_CHOICE_Enum; - break; - case SPA_POD_PROP0_RANGE_FLAGS: - type = SPA_CHOICE_Flags; - break; - } - if (!SPA_FLAG_IS_SET(b->flags, SPA_POD_PROP0_FLAG_UNSET) && - SPA0_POD_PROP_N_VALUES(b, size) == 1) - type = SPA_CHOICE_None; - - spa_pod_builder_push_choice(builder, &f, type, 0); - - if (b->value.type == SPA_TYPE_Id) { - uint32_t id; - if ((res = spa_pod_get_id(&b->value, &id)) < 0) - goto done; - - spa_pod_builder_id(builder, pw_protocol_native0_type_from_v2(client, id)); - SPA_POD_PROP_ALTERNATIVE_FOREACH0(b, size, alt) - if ((res = remap_from_v2(b->value.type, alt, b->value.size, client, builder)) < 0) - break; - } else { - spa_pod_builder_raw(builder, &b->value, size - sizeof(struct spa_pod)); - } -done: - spa_pod_builder_pop(builder, &f); - break; - } - case SPA_TYPE_Object: - { - struct spa_pod_object_body *b = body; - struct spa_pod *p; - struct spa_pod_frame f; - uint32_t type, count = 0; - - /* type and id are switched */ - type = pw_protocol_native0_type_from_v2(client, b->id), - spa_pod_builder_push_object(builder, &f, type, - pw_protocol_native0_type_from_v2(client, b->type)); - - /* object contained pods in v2 */ - SPA_POD_OBJECT_BODY_FOREACH0(b, size, p) { - if (type == SPA_TYPE_OBJECT_Format && count < 2) { - uint32_t id; - if (spa_pod_get_id(p, &id) < 0) - continue; - id = pw_protocol_native0_type_from_v2(client, id); - - if (count == 0) { - spa_pod_builder_prop(builder, SPA_FORMAT_mediaType, 0); - spa_pod_builder_id(builder, id); - } - if (count == 1) { - spa_pod_builder_prop(builder, SPA_FORMAT_mediaSubtype, 0); - spa_pod_builder_id(builder, id); - } - count++; - continue; - } - if ((res = remap_from_v2(p->type, - SPA_POD_BODY(p), - p->size, - client, builder)) < 0) - break; - } - spa_pod_builder_pop(builder, &f); - break; - } - case SPA_TYPE_Struct: - { - struct spa_pod *b = body, *p; - struct spa_pod_frame f; - - spa_pod_builder_push_struct(builder, &f); - SPA_POD_FOREACH(b, size, p) - if ((res = remap_from_v2(p->type, SPA_POD_BODY(p), p->size, client, builder)) < 0) - break; - spa_pod_builder_pop(builder, &f); - break; - } - default: - break; - } - return res; -} - -static int remap_to_v2(struct pw_impl_client *client, const struct spa_type_info *info, - uint32_t type, void *body, uint32_t size, - struct spa_pod_builder *builder) -{ - int res; - - switch (type) { - case SPA_TYPE_Id: - spa_pod_builder_id(builder, pw_protocol_native0_type_to_v2(client, info, *(int32_t*) body)); - break; - - case SPA_TYPE_Object: - { - struct spa_pod_object_body *b = body; - struct spa_pod_prop *p; - struct spa_pod_frame f[2]; - uint32_t type; - const struct spa_type_info *ti, *ii; - - ti = spa_debug_type_find(info, b->type); - ii = ti ? spa_debug_type_find(ti->values, 0) : NULL; - - if (b->type == SPA_TYPE_COMMAND_Node || - b->type == SPA_TYPE_EVENT_Node) { - spa_pod_builder_push_object(builder, &f[0], 0, - pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, b->id)); - } else { - ii = ii ? spa_debug_type_find(ii->values, b->id) : NULL; - /* type and id are switched */ - type = pw_protocol_native0_type_to_v2(client, info, b->type), - spa_pod_builder_push_object(builder, &f[0], - pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, b->id), type); - } - - - info = ti ? ti->values : info; - - SPA_POD_OBJECT_BODY_FOREACH(b, size, p) { - uint32_t key, flags; - uint32_t n_vals, choice; - struct spa_pod *values; - - ii = spa_debug_type_find(info, p->key); - - values = spa_pod_get_values(&p->value, &n_vals, &choice); - - if (b->type == SPA_TYPE_OBJECT_Format && - (p->key == SPA_FORMAT_mediaType || - p->key == SPA_FORMAT_mediaSubtype)) { - uint32_t val; - - if (spa_pod_get_id(values, &val) < 0) - continue; - spa_pod_builder_id(builder, - pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, val)); - continue; - } - - flags = 0; - switch(choice) { - case SPA_CHOICE_None: - flags |= SPA_POD_PROP0_RANGE_NONE; - break; - case SPA_CHOICE_Range: - flags |= SPA_POD_PROP0_RANGE_MIN_MAX | SPA_POD_PROP0_FLAG_UNSET; - break; - case SPA_CHOICE_Step: - flags |= SPA_POD_PROP0_RANGE_STEP | SPA_POD_PROP0_FLAG_UNSET; - break; - case SPA_CHOICE_Enum: - flags |= SPA_POD_PROP0_RANGE_ENUM | SPA_POD_PROP0_FLAG_UNSET; - break; - case SPA_CHOICE_Flags: - flags |= SPA_POD_PROP0_RANGE_FLAGS | SPA_POD_PROP0_FLAG_UNSET; - break; - } - - key = pw_protocol_native0_type_to_v2(client, info, p->key); - - spa_pod_builder_push_choice(builder, &f[1], key, flags); - - if (values->type == SPA_TYPE_Id) { - uint32_t i, *id = SPA_POD_BODY(values); - - for (i = 0; i < n_vals; i++) { - spa_pod_builder_id(builder, - pw_protocol_native0_type_to_v2(client, ii ? ii->values : NULL, id[i])); - } - - } else { - spa_pod_builder_raw(builder, values, sizeof(struct spa_pod) + n_vals * values->size); - } - spa_pod_builder_pop(builder, &f[1]); - } - spa_pod_builder_pop(builder, &f[0]); - break; - } - case SPA_TYPE_Struct: - { - struct spa_pod *b = body, *p; - struct spa_pod_frame f; - - spa_pod_builder_push_struct(builder, &f); - SPA_POD_FOREACH(b, size, p) - if ((res = remap_to_v2(client, info, p->type, SPA_POD_BODY(p), p->size, builder)) < 0) - return res; - spa_pod_builder_pop(builder, &f); - break; - } - default: - break; - } - return 0; -} - - - - -SPA_EXPORT -struct spa_pod * pw_protocol_native0_pod_from_v2(struct pw_impl_client *client, const struct spa_pod *pod) -{ - uint8_t buffer[4096]; - struct spa_pod *copy; - struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, 4096); - int res; - - if (pod == NULL) - return NULL; - - if ((res = remap_from_v2(SPA_POD_TYPE(pod), - SPA_POD_BODY(pod), - SPA_POD_BODY_SIZE(pod), - client, &b)) < 0) { - errno = -res; - return NULL; - } - copy = spa_pod_copy(b.data); - return copy; -} - -SPA_EXPORT -int pw_protocol_native0_pod_to_v2(struct pw_impl_client *client, const struct spa_pod *pod, - struct spa_pod_builder *b) -{ - int res; - - if (pod == NULL) { - spa_pod_builder_none(b); - return 0; - } - - if ((res = remap_to_v2(client, pw_type_info(), - SPA_POD_TYPE(pod), - SPA_POD_BODY(pod), - SPA_POD_BODY_SIZE(pod), - b)) < 0) { - return -res; - } - return 0; -} - -static int core_demarshal_create_object(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_parser prs; - struct spa_pod_frame f; - uint32_t version, type, new_id, i; - const char *factory_name, *type_name; - struct spa_dict props; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_push_struct(&prs, &f) < 0 || - spa_pod_parser_get(&prs, - "s", &factory_name, - "I", &type, - "i", &version, - "i", &props.n_items, NULL) < 0) - return -EINVAL; - - props.items = alloca(props.n_items * sizeof(struct spa_dict_item)); - for (i = 0; i < props.n_items; i++) { - if (spa_pod_parser_get(&prs, - "s", &props.items[i].key, - "s", &props.items[i].value, NULL) < 0) - return -EINVAL; - } - if (spa_pod_parser_get(&prs, "i", &new_id, NULL) < 0) - return -EINVAL; - - type_name = pw_protocol_native0_name_from_v2(client, type); - if (type_name == NULL) - return -EINVAL; - - return pw_resource_notify(resource, struct pw_core_methods, create_object, 0, factory_name, - type_name, version, - &props, new_id); -} - -static int core_demarshal_destroy(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object, *r; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_parser prs; - uint32_t id; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, - "i", &id, NULL) < 0) - return -EINVAL; - - pw_log_debug("client %p: destroy resource %u", client, id); - - if ((r = pw_impl_client_find_resource(client, id)) == NULL) - goto no_resource; - - return pw_resource_notify(resource, struct pw_core_methods, destroy, 0, r); - -no_resource: - pw_log_error("client %p: unknown resource %u op:%u", client, id, msg->opcode); - pw_resource_errorf(resource, -ENOENT, "unknown resource %d op:%u", id, msg->opcode); - return 0; -} - -static int core_demarshal_update_types_server(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct protocol_compat_v2 *compat_v2 = client->compat_v2; - struct spa_pod_parser prs; - uint32_t first_id, n_types; - struct spa_pod_frame f; - const char **types; - uint32_t i; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_push_struct(&prs, &f) < 0 || - spa_pod_parser_get(&prs, - "i", &first_id, - "i", &n_types, - NULL) < 0) - return -EINVAL; - - if (first_id == 0) - compat_v2->send_types = true; - - types = alloca(n_types * sizeof(char *)); - for (i = 0; i < n_types; i++) { - if (spa_pod_parser_get(&prs, "s", &types[i], NULL) < 0) - return -EINVAL; - } - - for (i = 0; i < n_types; i++, first_id++) { - uint32_t type_id = pw_protocol_native0_find_type(client, types[i]); - if (type_id == SPA_ID_INVALID) - continue; - if (pw_map_insert_at(&compat_v2->types, first_id, PW_MAP_ID_TO_PTR(type_id)) < 0) - pw_log_error("can't add type %d->%d for client", first_id, type_id); - } - return 0; -} - -static void registry_marshal_global(void *data, uint32_t id, uint32_t permissions, - const char *type, uint32_t version, const struct spa_dict *props) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_builder *b; - struct spa_pod_frame f; - uint32_t i, n_items, parent_id; - uint32_t type_id; - const char *str; - - type_id = pw_protocol_native0_find_type(client, type); - if (type_id == SPA_ID_INVALID) - return; - - b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_V0_EVENT_GLOBAL, NULL); - - n_items = props ? props->n_items : 0; - - parent_id = 0; - if (props) { - if (spa_streq(type, PW_TYPE_INTERFACE_Port)) { - if ((str = spa_dict_lookup(props, "node.id")) != NULL) - parent_id = atoi(str); - } else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) { - if ((str = spa_dict_lookup(props, "device.id")) != NULL) - parent_id = atoi(str); - } else if (spa_streq(type, PW_TYPE_INTERFACE_Client) || - spa_streq(type, PW_TYPE_INTERFACE_Device) || - spa_streq(type, PW_TYPE_INTERFACE_Factory)) { - if ((str = spa_dict_lookup(props, "module.id")) != NULL) - parent_id = atoi(str); - } - } - - version = 0; - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", id, - "i", parent_id, - "i", permissions, - "I", type_id, - "i", version, - "i", n_items, NULL); - - for (i = 0; i < n_items; i++) { - spa_pod_builder_add(b, - "s", props->items[i].key, - "s", props->items[i].value, NULL); - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void registry_marshal_global_remove(void *data, uint32_t id) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - - b = pw_protocol_native_begin_resource(resource, PW_REGISTRY_V0_EVENT_GLOBAL_REMOVE, NULL); - - spa_pod_builder_add_struct(b, "i", id); - - pw_protocol_native_end_resource(resource, b); -} - -static int registry_demarshal_bind(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_parser prs; - uint32_t id, version, type, new_id; - const char *type_name; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, - "i", &id, - "I", &type, - "i", &version, - "i", &new_id) < 0) - return -EINVAL; - - type_name = pw_protocol_native0_name_from_v2(client, type); - if (type_name == NULL) - return -EINVAL; - - return pw_resource_notify(resource, struct pw_registry_methods, bind, 0, id, type_name, version, new_id); -} - -static void module_marshal_info(void *data, const struct pw_module_info *info) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - struct spa_pod_frame f; - uint32_t i, n_items; - - b = pw_protocol_native_begin_resource(resource, PW_MODULE_V0_EVENT_INFO, NULL); - - n_items = info->props ? info->props->n_items : 0; - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", info->id, - "l", info->change_mask, - "s", info->name, - "s", info->filename, - "s", info->args, - "i", n_items, NULL); - - for (i = 0; i < n_items; i++) { - spa_pod_builder_add(b, - "s", info->props->items[i].key, - "s", info->props->items[i].value, NULL); - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void factory_marshal_info(void *data, const struct pw_factory_info *info) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_builder *b; - struct spa_pod_frame f; - uint32_t i, n_items, type, version; - - type = pw_protocol_native0_find_type(client, info->type); - if (type == SPA_ID_INVALID) - return; - - b = pw_protocol_native_begin_resource(resource, PW_FACTORY_V0_EVENT_INFO, NULL); - - n_items = info->props ? info->props->n_items : 0; - - version = 0; - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", info->id, - "l", info->change_mask, - "s", info->name, - "I", type, - "i", version, - "i", n_items, NULL); - - for (i = 0; i < n_items; i++) { - spa_pod_builder_add(b, - "s", info->props->items[i].key, - "s", info->props->items[i].value, NULL); - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void node_marshal_info(void *data, const struct pw_node_info *info) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - struct spa_pod_frame f; - uint32_t i, n_items; - - b = pw_protocol_native_begin_resource(resource, PW_NODE_V0_EVENT_INFO, NULL); - - n_items = info->props ? info->props->n_items : 0; - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", info->id, - "l", info->change_mask, - "s", "node.name", - "i", info->max_input_ports, - "i", info->n_input_ports, - "i", info->max_output_ports, - "i", info->n_output_ports, - "i", info->state, - "s", info->error, - "i", n_items, NULL); - - for (i = 0; i < n_items; i++) { - spa_pod_builder_add(b, - "s", info->props->items[i].key, - "s", info->props->items[i].value, NULL); - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void node_marshal_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next, - const struct spa_pod *param) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_builder *b; - struct spa_pod_frame f; - - b = pw_protocol_native_begin_resource(resource, PW_NODE_V0_EVENT_PARAM, NULL); - - id = pw_protocol_native0_type_to_v2(client, spa_type_param, id), - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "I", id, - "i", index, - "i", next, - NULL); - pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)param, b); - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static int node_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_parser prs; - uint32_t id, index, num; - struct spa_pod *filter; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, - "I", &id, - "i", &index, - "i", &num, - "P", &filter) < 0) - return -EINVAL; - - id = pw_protocol_native0_type_from_v2(client, id); - filter = NULL; - - return pw_resource_notify(resource, struct pw_node_methods, enum_params, 0, - 0, id, index, num, filter); -} - -static void port_marshal_info(void *data, const struct pw_port_info *info) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - struct spa_pod_frame f; - uint32_t i, n_items; - uint64_t change_mask = 0; - const char *port_name; - - b = pw_protocol_native_begin_resource(resource, PW_PORT_V0_EVENT_INFO, NULL); - - n_items = info->props ? info->props->n_items : 0; - -#define PW_PORT_V0_CHANGE_MASK_NAME (1 << 0) -#define PW_PORT_V0_CHANGE_MASK_PROPS (1 << 1) -#define PW_PORT_V0_CHANGE_MASK_ENUM_PARAMS (1 << 2) - - change_mask |= PW_PORT_V0_CHANGE_MASK_NAME; - if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS) - change_mask |= PW_PORT_V0_CHANGE_MASK_PROPS; - if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS) - change_mask |= PW_PORT_V0_CHANGE_MASK_ENUM_PARAMS; - - port_name = NULL; - if (info->props != NULL) - port_name = spa_dict_lookup(info->props, "port.name"); - if (port_name == NULL) - port_name = "port.name"; - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", info->id, - "l", change_mask, - "s", port_name, - "i", n_items, NULL); - - for (i = 0; i < n_items; i++) { - spa_pod_builder_add(b, - "s", info->props->items[i].key, - "s", info->props->items[i].value, NULL); - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void port_marshal_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next, - const struct spa_pod *param) -{ - struct pw_resource *resource = data; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_builder *b; - struct spa_pod_frame f; - - b = pw_protocol_native_begin_resource(resource, PW_PORT_V0_EVENT_PARAM, NULL); - - id = pw_protocol_native0_type_to_v2(client, spa_type_param, id), - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "I", id, - "i", index, - "i", next, - NULL); - pw_protocol_native0_pod_to_v2(client, (struct spa_pod *)param, b); - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static int port_demarshal_enum_params(void *object, const struct pw_protocol_native_message *msg) -{ - struct pw_resource *resource = object; - struct pw_impl_client *client = pw_resource_get_client(resource); - struct spa_pod_parser prs; - uint32_t id, index, num; - struct spa_pod *filter; - - spa_pod_parser_init(&prs, msg->data, msg->size); - if (spa_pod_parser_get_struct(&prs, - "I", &id, - "i", &index, - "i", &num, - "P", &filter) < 0) - return -EINVAL; - - id = pw_protocol_native0_type_from_v2(client, id); - filter = NULL; - - return pw_resource_notify(resource, struct pw_port_methods, enum_params, 0, - 0, id, index, num, filter); -} - -static void client_marshal_info(void *data, const struct pw_client_info *info) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - struct spa_pod_frame f; - uint32_t i, n_items; - - b = pw_protocol_native_begin_resource(resource, PW_CLIENT_V0_EVENT_INFO, NULL); - - n_items = info->props ? info->props->n_items : 0; - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", info->id, - "l", info->change_mask, - "i", n_items, NULL); - - for (i = 0; i < n_items; i++) { - spa_pod_builder_add(b, - "s", info->props->items[i].key, - "s", info->props->items[i].value, NULL); - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static void client_marshal_permissions(void *data, uint32_t index, uint32_t n_permissions, - const struct pw_permission *permissions) -{ -} - - -static void link_marshal_info(void *data, const struct pw_link_info *info) -{ - struct pw_resource *resource = data; - struct spa_pod_builder *b; - struct spa_pod_frame f; - uint32_t i, n_items; - - b = pw_protocol_native_begin_resource(resource, PW_LINK_V0_EVENT_INFO, NULL); - - n_items = info->props ? info->props->n_items : 0; - - spa_pod_builder_push_struct(b, &f); - spa_pod_builder_add(b, - "i", info->id, - "l", info->change_mask, - "i", info->output_node_id, - "i", info->output_port_id, - "i", info->input_node_id, - "i", info->input_port_id, - "P", info->format, - "i", n_items, NULL); - - for (i = 0; i < n_items; i++) { - spa_pod_builder_add(b, - "s", info->props->items[i].key, - "s", info->props->items[i].value, NULL); - } - spa_pod_builder_pop(b, &f); - - pw_protocol_native_end_resource(resource, b); -} - -static const struct pw_protocol_native_demarshal pw_protocol_native_core_method_demarshal[PW_CORE_V0_METHOD_NUM] = { - [PW_CORE_V0_METHOD_HELLO] = { &core_demarshal_hello, 0, }, - [PW_CORE_V0_METHOD_UPDATE_TYPES] = { &core_demarshal_update_types_server, 0, }, - [PW_CORE_V0_METHOD_SYNC] = { &core_demarshal_sync, 0, }, - [PW_CORE_V0_METHOD_GET_REGISTRY] = { &core_demarshal_get_registry, 0, }, - [PW_CORE_V0_METHOD_CLIENT_UPDATE] = { &core_demarshal_client_update, 0, }, - [PW_CORE_V0_METHOD_PERMISSIONS] = { &core_demarshal_permissions, 0, }, - [PW_CORE_V0_METHOD_CREATE_OBJECT] = { &core_demarshal_create_object, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, }, - [PW_CORE_V0_METHOD_DESTROY] = { &core_demarshal_destroy, 0, } -}; - -static const struct pw_core_events pw_protocol_native_core_event_marshal = { - PW_VERSION_CORE_EVENTS, - .info = &core_marshal_info, - .done = &core_marshal_done, - .error = &core_marshal_error, - .remove_id = &core_marshal_remove_id, -}; - -static const struct pw_protocol_marshal pw_protocol_native_core_marshal = { - PW_TYPE_INTERFACE_Core, - PW_VERSION_CORE_V0, - PW_CORE_V0_METHOD_NUM, - PW_CORE_EVENT_NUM, - 0, - NULL, - pw_protocol_native_core_method_demarshal, - &pw_protocol_native_core_event_marshal, - NULL -}; - -static const struct pw_protocol_native_demarshal pw_protocol_native_registry_method_demarshal[] = { - [PW_REGISTRY_V0_METHOD_BIND] = { ®istry_demarshal_bind, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, }, -}; - -static const struct pw_registry_events pw_protocol_native_registry_event_marshal = { - PW_VERSION_REGISTRY_EVENTS, - .global = ®istry_marshal_global, - .global_remove = ®istry_marshal_global_remove, -}; - -static const struct pw_protocol_marshal pw_protocol_native_registry_marshal = { - PW_TYPE_INTERFACE_Registry, - PW_VERSION_REGISTRY_V0, - PW_REGISTRY_V0_METHOD_NUM, - PW_REGISTRY_EVENT_NUM, - 0, - NULL, - pw_protocol_native_registry_method_demarshal, - &pw_protocol_native_registry_event_marshal, - NULL -}; - -static const struct pw_module_events pw_protocol_native_module_event_marshal = { - PW_VERSION_MODULE_EVENTS, - .info = &module_marshal_info, -}; - -static const struct pw_protocol_marshal pw_protocol_native_module_marshal = { - PW_TYPE_INTERFACE_Module, - PW_VERSION_MODULE_V0, - 0, - PW_MODULE_EVENT_NUM, - 0, - NULL, NULL, - &pw_protocol_native_module_event_marshal, - NULL -}; - -static const struct pw_factory_events pw_protocol_native_factory_event_marshal = { - PW_VERSION_FACTORY_EVENTS, - .info = &factory_marshal_info, -}; - -static const struct pw_protocol_marshal pw_protocol_native_factory_marshal = { - PW_TYPE_INTERFACE_Factory, - PW_VERSION_FACTORY_V0, - 0, - PW_FACTORY_EVENT_NUM, - 0, - NULL, NULL, - &pw_protocol_native_factory_event_marshal, - NULL, -}; - -static const struct pw_protocol_native_demarshal pw_protocol_native_node_method_demarshal[] = { - [PW_NODE_V0_METHOD_ENUM_PARAMS] = { &node_demarshal_enum_params, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, }, -}; - -static const struct pw_node_events pw_protocol_native_node_event_marshal = { - PW_VERSION_NODE_EVENTS, - .info = &node_marshal_info, - .param = &node_marshal_param, -}; - -static const struct pw_protocol_marshal pw_protocol_native_node_marshal = { - PW_TYPE_INTERFACE_Node, - PW_VERSION_NODE_V0, - PW_NODE_V0_METHOD_NUM, - PW_NODE_EVENT_NUM, - 0, - NULL, - pw_protocol_native_node_method_demarshal, - &pw_protocol_native_node_event_marshal, - NULL -}; - - -static const struct pw_protocol_native_demarshal pw_protocol_native_port_method_demarshal[] = { - [PW_PORT_V0_METHOD_ENUM_PARAMS] = { &port_demarshal_enum_params, 0, PW_PROTOCOL_NATIVE_FLAG_REMAP, }, -}; - -static const struct pw_port_events pw_protocol_native_port_event_marshal = { - PW_VERSION_PORT_EVENTS, - .info = &port_marshal_info, - .param = &port_marshal_param, -}; - -static const struct pw_protocol_marshal pw_protocol_native_port_marshal = { - PW_TYPE_INTERFACE_Port, - PW_VERSION_PORT_V0, - PW_PORT_V0_METHOD_NUM, - PW_PORT_EVENT_NUM, - 0, - NULL, - pw_protocol_native_port_method_demarshal, - &pw_protocol_native_port_event_marshal, - NULL -}; - -static const struct pw_client_events pw_protocol_native_client_event_marshal = { - PW_VERSION_CLIENT_EVENTS, - .info = &client_marshal_info, - .permissions = &client_marshal_permissions, -}; - -static const struct pw_protocol_marshal pw_protocol_native_client_marshal = { - PW_TYPE_INTERFACE_Client, - PW_VERSION_CLIENT_V0, - 0, - PW_CLIENT_EVENT_NUM, - 0, - NULL, NULL, - &pw_protocol_native_client_event_marshal, - NULL, -}; - -static const struct pw_link_events pw_protocol_native_link_event_marshal = { - PW_VERSION_LINK_EVENTS, - .info = &link_marshal_info, -}; - -static const struct pw_protocol_marshal pw_protocol_native_link_marshal = { - PW_TYPE_INTERFACE_Link, - PW_VERSION_LINK_V0, - 0, - PW_LINK_EVENT_NUM, - 0, - NULL, NULL, - &pw_protocol_native_link_event_marshal, - NULL -}; - -void pw_protocol_native0_init(struct pw_protocol *protocol) -{ - pw_protocol_add_marshal(protocol, &pw_protocol_native_core_marshal); - pw_protocol_add_marshal(protocol, &pw_protocol_native_registry_marshal); - pw_protocol_add_marshal(protocol, &pw_protocol_native_module_marshal); - pw_protocol_add_marshal(protocol, &pw_protocol_native_node_marshal); - pw_protocol_add_marshal(protocol, &pw_protocol_native_port_marshal); - pw_protocol_add_marshal(protocol, &pw_protocol_native_factory_marshal); - pw_protocol_add_marshal(protocol, &pw_protocol_native_client_marshal); - pw_protocol_add_marshal(protocol, &pw_protocol_native_link_marshal); -} diff --git a/src/modules/module-protocol-native/v0/typemap.h b/src/modules/module-protocol-native/v0/typemap.h deleted file mode 100644 index 980cc41cc..000000000 --- a/src/modules/module-protocol-native/v0/typemap.h +++ /dev/null @@ -1,292 +0,0 @@ -enum spa_node0_event { - SPA_NODE0_EVENT_START = SPA_TYPE_VENDOR_PipeWire, - SPA_NODE0_EVENT_RequestClockUpdate, -}; - -enum spa_node0_command { - SPA_NODE0_COMMAND_START = SPA_TYPE_VENDOR_PipeWire, - SPA_NODE0_COMMAND_ClockUpdate, -}; - -static const struct type_info { - const char *type; - const char *name; - uint32_t id; -} type_map[] = { - { "Spa:Interface:TypeMap", SPA_TYPE_INFO_INTERFACE_BASE, 0, }, - { "Spa:Interface:Log", SPA_TYPE_INTERFACE_Log, 0, }, - { "Spa:Interface:Loop", SPA_TYPE_INTERFACE_Loop, 0, }, - { "Spa:Interface:LoopControl", SPA_TYPE_INTERFACE_LoopControl, 0, }, - { "Spa:Interface:LoopUtils", SPA_TYPE_INTERFACE_LoopUtils, 0, }, - { "PipeWire:Interface:Core", PW_TYPE_INTERFACE_Core, 0, }, - { "PipeWire:Interface:Registry", PW_TYPE_INTERFACE_Registry, 0, }, - { "PipeWire:Interface:Node", PW_TYPE_INTERFACE_Node, 0, }, - { "PipeWire:Interface:Port", PW_TYPE_INTERFACE_Port,0, }, - { "PipeWire:Interface:Factory", PW_TYPE_INTERFACE_Factory, 0, }, - { "PipeWire:Interface:Link", PW_TYPE_INTERFACE_Link, 0, }, - { "PipeWire:Interface:Client", PW_TYPE_INTERFACE_Client, 0, }, - { "PipeWire:Interface:Module", PW_TYPE_INTERFACE_Module, 0, }, - { "PipeWire:Interface:Device", PW_TYPE_INTERFACE_Device, 0, }, - - { "PipeWire:Interface:Metadata", PW_TYPE_INTERFACE_Metadata, 0, }, - { "PipeWire:Interface:Session", PW_TYPE_INTERFACE_Session, 0, }, - { "PipeWire:Interface:Endpoint", PW_TYPE_INTERFACE_Endpoint, 0, }, - { "PipeWire:Interface:EndpointStream", PW_TYPE_INTERFACE_EndpointStream, 0, }, - { "PipeWire:Interface:EndpointLink", PW_TYPE_INTERFACE_EndpointLink, 0, }, - - { "PipeWire:Interface:ClientNode", PW_TYPE_INTERFACE_ClientNode, 0, }, - { "PipeWire:Interface:ClientSession", PW_TYPE_INTERFACE_ClientSession, 0, }, - { "PipeWire:Interface:ClientEndpoint", PW_TYPE_INTERFACE_ClientEndpoint, 0, }, - - { "Spa:Interface:Node", SPA_TYPE_INTERFACE_Node, 0, }, - { "Spa:Interface:Clock", }, - { "Spa:Interface:Monitor", }, - { "Spa:Interface:Device", SPA_TYPE_INTERFACE_Device, 0, }, - { "Spa:POD:Object:Param:Format", SPA_TYPE_INFO_Format, SPA_TYPE_OBJECT_Format, }, - { "Spa:POD:Object:Param:Props", SPA_TYPE_INFO_Props, SPA_TYPE_OBJECT_Props, }, - { "Spa:Pointer:IO:Buffers", }, - { "Spa:Pointer:IO:Control:Range", }, - { "Spa:Pointer:IO:Prop", }, - { "Spa:Enum:ParamId:List", }, - { "Spa:POD:Object:Param:List", }, - { "Spa:POD:Object:Param:List:id", }, - { "Spa:Enum:ParamId:PropInfo", SPA_TYPE_INFO_PARAM_ID_BASE "PropInfo", SPA_PARAM_PropInfo, }, - { "Spa:POD:Object:Param:PropInfo", SPA_TYPE_INFO_PROP_INFO_BASE, SPA_PROP_INFO_START, }, - { "Spa:POD:Object:Param:PropInfo:id", SPA_TYPE_INFO_PROP_INFO_BASE "id", SPA_PROP_INFO_id, }, - { "Spa:POD:Object:Param:PropInfo:name", SPA_TYPE_INFO_PROP_INFO_BASE "name", SPA_PROP_INFO_name, }, - { "Spa:POD:Object:Param:PropInfo:type", SPA_TYPE_INFO_PROP_INFO_BASE "typ", SPA_PROP_INFO_type, }, - { "Spa:POD:Object:Param:PropInfo:labels", SPA_TYPE_INFO_PROP_INFO_BASE "labels", SPA_PROP_INFO_labels, }, - { "Spa:Enum:ParamId:Props", SPA_TYPE_INFO_PARAM_ID_BASE "Props", SPA_PARAM_Props, }, - { "Spa:Enum:ParamId:EnumFormat", SPA_TYPE_INFO_PARAM_ID_BASE "EnumFormat", SPA_PARAM_EnumFormat,}, - { "Spa:Enum:ParamId:Format", SPA_TYPE_INFO_PARAM_ID_BASE "Format", SPA_PARAM_Format, }, - { "Spa:Enum:ParamId:Buffers", SPA_TYPE_INFO_PARAM_ID_BASE "Buffers", SPA_PARAM_Buffers }, - { "Spa:Enum:ParamId:Meta", SPA_TYPE_INFO_PARAM_ID_BASE "Meta", SPA_PARAM_Meta, }, - { "Spa:Pointer:Meta:Header", SPA_TYPE_INFO_META_BASE "Header", SPA_META_Header, }, - { "Spa:Pointer:Meta:VideoCrop", SPA_TYPE_INFO_META_REGION_BASE "VideoCrop", SPA_META_VideoCrop, }, - { "Spa:Pointer:Meta:VideoDamage", SPA_TYPE_INFO_META_ARRAY_REGION_BASE "VideoDamage", SPA_META_VideoDamage, }, - { "Spa:Pointer:Meta:Bitmap", SPA_TYPE_INFO_META_BASE "Bitmap", SPA_META_Bitmap, }, - { "Spa:Pointer:Meta:Cursor", SPA_TYPE_INFO_META_BASE "Cursor", SPA_META_Cursor, }, - { "Spa:Enum:DataType:MemPtr", SPA_TYPE_INFO_DATA_BASE "MemPtr", SPA_DATA_MemPtr, }, - { "Spa:Enum:DataType:Fd:MemFd", SPA_TYPE_INFO_DATA_FD_BASE "MemFd", SPA_DATA_MemFd, }, - { "Spa:Enum:DataType:Fd:DmaBuf", SPA_TYPE_INFO_DATA_FD_BASE "DmaBuf", SPA_DATA_DmaBuf, }, - { "Spa:POD:Object:Event:Node:Error", SPA_TYPE_INFO_NODE_EVENT_BASE "Error", SPA_NODE_EVENT_Error, }, - { "Spa:POD:Object:Event:Node:Buffering", SPA_TYPE_INFO_NODE_EVENT_BASE "Buffering", SPA_NODE_EVENT_Buffering, }, - { "Spa:POD:Object:Event:Node:RequestRefresh", SPA_TYPE_INFO_NODE_EVENT_BASE "RequestRefresh", SPA_NODE_EVENT_RequestRefresh, }, - { "Spa:POD:Object:Event:Node:RequestClockUpdate", SPA_TYPE_INFO_NODE_EVENT_BASE "RequestClockUpdate", SPA_NODE0_EVENT_RequestClockUpdate, }, - { "Spa:POD:Object:Command:Node", SPA_TYPE_INFO_COMMAND_BASE "Node", SPA_TYPE_COMMAND_Node,}, - { "Spa:POD:Object:Command:Node:Suspend", SPA_TYPE_INFO_NODE_COMMAND_BASE "Suspend", SPA_NODE_COMMAND_Suspend,}, - { "Spa:POD:Object:Command:Node:Pause", SPA_TYPE_INFO_NODE_COMMAND_BASE "Pause", SPA_NODE_COMMAND_Pause, }, - { "Spa:POD:Object:Command:Node:Start", SPA_TYPE_INFO_NODE_COMMAND_BASE "Start", SPA_NODE_COMMAND_Start, }, - { "Spa:POD:Object:Command:Node:Enable", SPA_TYPE_INFO_NODE_COMMAND_BASE "Enable", SPA_NODE_COMMAND_Enable, }, - { "Spa:POD:Object:Command:Node:Disable", SPA_TYPE_INFO_NODE_COMMAND_BASE "Disable", SPA_NODE_COMMAND_Disable, }, - { "Spa:POD:Object:Command:Node:Flush", SPA_TYPE_INFO_NODE_COMMAND_BASE "Flush", SPA_NODE_COMMAND_Flush, }, - { "Spa:POD:Object:Command:Node:Drain", SPA_TYPE_INFO_NODE_COMMAND_BASE "Drain", SPA_NODE_COMMAND_Drain, }, - { "Spa:POD:Object:Command:Node:Marker", SPA_TYPE_INFO_NODE_COMMAND_BASE "Marker", SPA_NODE_COMMAND_Marker, }, - { "Spa:POD:Object:Command:Node:ClockUpdate", SPA_TYPE_INFO_NODE_COMMAND_BASE "ClockUpdate", SPA_NODE0_COMMAND_ClockUpdate, }, - { "Spa:POD:Object:Event:Monitor:Added", }, - { "Spa:POD:Object:Event:Monitor:Removed", }, - { "Spa:POD:Object:Event:Monitor:Changed", }, - { "Spa:POD:Object:MonitorItem", }, - { "Spa:POD:Object:MonitorItem:id", }, - { "Spa:POD:Object:MonitorItem:flags", }, - { "Spa:POD:Object:MonitorItem:state", }, - { "Spa:POD:Object:MonitorItem:name", }, - { "Spa:POD:Object:MonitorItem:class", }, - { "Spa:POD:Object:MonitorItem:info", }, - { "Spa:POD:Object:MonitorItem:factory", }, - { "Spa:POD:Object:Param:Buffers", SPA_TYPE_INFO_PARAM_Buffers, SPA_TYPE_OBJECT_ParamBuffers, }, - { "Spa:POD:Object:Param:Buffers:size", SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "size", SPA_PARAM_BUFFERS_size, }, - { "Spa:POD:Object:Param:Buffers:stride", SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "stride", SPA_PARAM_BUFFERS_stride, }, - { "Spa:POD:Object:Param:Buffers:buffers", SPA_TYPE_INFO_PARAM_BUFFERS_BASE "buffers", SPA_PARAM_BUFFERS_buffers, }, - { "Spa:POD:Object:Param:Buffers:align",SPA_TYPE_INFO_PARAM_BLOCK_INFO_BASE "align", SPA_PARAM_BUFFERS_align, }, - { "Spa:POD:Object:Param:Meta", SPA_TYPE_INFO_PARAM_Meta, SPA_TYPE_OBJECT_ParamMeta, }, - { "Spa:POD:Object:Param:Meta:type", SPA_TYPE_INFO_PARAM_META_BASE "type", SPA_PARAM_META_type, }, - { "Spa:POD:Object:Param:Meta:size", SPA_TYPE_INFO_PARAM_META_BASE "size", SPA_PARAM_META_size, }, - { "Spa:POD:Object:Param:IO:id", }, - { "Spa:POD:Object:Param:IO:size", }, - { "Spa:Enum:ParamId:IO:Buffers", }, - { "Spa:POD:Object:Param:IO:Buffers", }, - { "Spa:Enum:ParamId:IO:Control", }, - { "Spa:POD:Object:Param:IO:Control", }, - { "Spa:Enum:ParamId:IO:Props:In", }, - { "Spa:Enum:ParamId:IO:Props:Out", }, - { "Spa:POD:Object:Param:IO:Prop", }, - { "Spa:Interface:DBus", SPA_TYPE_INFO_INTERFACE_BASE "DBus", 0, }, - { "Spa:Enum:MediaType:audio", SPA_TYPE_INFO_MEDIA_TYPE_BASE "audio", SPA_MEDIA_TYPE_audio, }, - { "Spa:Enum:MediaType:video", SPA_TYPE_INFO_MEDIA_TYPE_BASE "video", SPA_MEDIA_TYPE_video, }, - { "Spa:Enum:MediaType:image", SPA_TYPE_INFO_MEDIA_TYPE_BASE "image", SPA_MEDIA_TYPE_image, }, - { "Spa:Enum:MediaType:binary", SPA_TYPE_INFO_MEDIA_TYPE_BASE "binary", SPA_MEDIA_TYPE_binary, }, - { "Spa:Enum:MediaType:stream", SPA_TYPE_INFO_MEDIA_TYPE_BASE "stream", SPA_MEDIA_TYPE_stream, }, - { "Spa:Enum:MediaType:application", SPA_TYPE_INFO_MEDIA_TYPE_BASE "application", SPA_MEDIA_TYPE_application, }, - { "Spa:Enum:MediaSubtype:raw", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "raw", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:dsp", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dsp", SPA_MEDIA_SUBTYPE_dsp, }, - { "Spa:Enum:MediaSubtype:control", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "control", SPA_MEDIA_SUBTYPE_control, }, - { "Spa:Enum:MediaSubtype:h264", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h264", SPA_MEDIA_SUBTYPE_h264, }, - { "Spa:Enum:MediaSubtype:mjpg", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mjpg", SPA_MEDIA_SUBTYPE_mjpg, }, - { "Spa:Enum:MediaSubtype:dv", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "dv", SPA_MEDIA_SUBTYPE_dv, }, - { "Spa:Enum:MediaSubtype:mpegts", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpegts", SPA_MEDIA_SUBTYPE_mpegts, }, - { "Spa:Enum:MediaSubtype:h263", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "h263", SPA_MEDIA_SUBTYPE_h263, }, - { "Spa:Enum:MediaSubtype:mpeg1", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg1", SPA_MEDIA_SUBTYPE_mpeg1, }, - { "Spa:Enum:MediaSubtype:mpeg2", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg2", SPA_MEDIA_SUBTYPE_mpeg2, }, - { "Spa:Enum:MediaSubtype:mpeg4", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mpeg4", SPA_MEDIA_SUBTYPE_mpeg4, }, - { "Spa:Enum:MediaSubtype:xvid", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "xvid", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:vc1", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vc1", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:vp8", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp8", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:vp9", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vp9", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:jpeg", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "jpeg", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:bayer", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "bayer", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:mp3", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "mp3", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:aac", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "aac", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:vorbis", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "vorbis", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:wma", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "wma", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:ra", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "ra", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:sbc", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "sbc", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:adpcm", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "adpcm", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:g723", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g723", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:g726", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g726", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:g729", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "g729", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:amr", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "amr", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:gsm", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "gsm", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:Enum:MediaSubtype:midi", SPA_TYPE_INFO_MEDIA_SUBTYPE_BASE "midi", SPA_MEDIA_SUBTYPE_raw, }, - { "Spa:POD:Object:Param:Format:Video:format", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "format", SPA_FORMAT_VIDEO_format,}, - { "Spa:POD:Object:Param:Format:Video:size", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "size", SPA_FORMAT_VIDEO_size,}, - { "Spa:POD:Object:Param:Format:Video:framerate", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "framerate", SPA_FORMAT_VIDEO_framerate}, - { "Spa:POD:Object:Param:Format:Video:max-framerate", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "maxFramerate", SPA_FORMAT_VIDEO_maxFramerate}, - { "Spa:POD:Object:Param:Format:Video:views", SPA_TYPE_INFO_FORMAT_VIDEO_BASE "views", SPA_FORMAT_VIDEO_views}, - { "Spa:POD:Object:Param:Format:Video:interlace-mode", }, - { "Spa:POD:Object:Param:Format:Video:pixel-aspect-ratio", }, - { "Spa:POD:Object:Param:Format:Video:multiview-mode", }, - { "Spa:POD:Object:Param:Format:Video:multiview-flags", }, - { "Spa:POD:Object:Param:Format:Video:chroma-site", }, - { "Spa:POD:Object:Param:Format:Video:color-range", }, - { "Spa:POD:Object:Param:Format:Video:color-matrix", }, - { "Spa:POD:Object:Param:Format:Video:transfer-function", }, - { "Spa:POD:Object:Param:Format:Video:color-primaries", }, - { "Spa:POD:Object:Param:Format:Video:profile", }, - { "Spa:POD:Object:Param:Format:Video:level", }, - { "Spa:POD:Object:Param:Format:Video:stream-format", }, - { "Spa:POD:Object:Param:Format:Video:alignment", }, - { "Spa:POD:Object:Param:Format:Audio:format", SPA_TYPE_INFO_FORMAT_AUDIO_BASE "format", SPA_FORMAT_AUDIO_format,}, - { "Spa:POD:Object:Param:Format:Audio:flags", }, - { "Spa:POD:Object:Param:Format:Audio:layout", }, - { "Spa:POD:Object:Param:Format:Audio:rate", }, - { "Spa:POD:Object:Param:Format:Audio:channels", }, - { "Spa:POD:Object:Param:Format:Audio:channel-mask", }, - { "Spa:Enum:VideoFormat:encoded", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "encoded", SPA_VIDEO_FORMAT_ENCODED,}, - { "Spa:Enum:VideoFormat:I420", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420", SPA_VIDEO_FORMAT_I420,}, - { "Spa:Enum:VideoFormat:YV12", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YV12", SPA_VIDEO_FORMAT_YV12,}, - { "Spa:Enum:VideoFormat:YUY2", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUY2", SPA_VIDEO_FORMAT_YUY2,}, - { "Spa:Enum:VideoFormat:UYVY", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVY", SPA_VIDEO_FORMAT_UYVY,}, - { "Spa:Enum:VideoFormat:AYUV", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV", SPA_VIDEO_FORMAT_AYUV,}, - { "Spa:Enum:VideoFormat:RGBx", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx", SPA_VIDEO_FORMAT_RGBx,}, - { "Spa:Enum:VideoFormat:BGRx", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx", SPA_VIDEO_FORMAT_BGRx,}, - { "Spa:Enum:VideoFormat:xRGB", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB", SPA_VIDEO_FORMAT_xRGB,}, - { "Spa:Enum:VideoFormat:xBGR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR", SPA_VIDEO_FORMAT_xBGR,}, - { "Spa:Enum:VideoFormat:RGBA", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA", SPA_VIDEO_FORMAT_RGBA,}, - { "Spa:Enum:VideoFormat:BGRA", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA", SPA_VIDEO_FORMAT_BGRA,}, - { "Spa:Enum:VideoFormat:ARGB", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB", SPA_VIDEO_FORMAT_ARGB,}, - { "Spa:Enum:VideoFormat:ABGR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR", SPA_VIDEO_FORMAT_ABGR,}, - { "Spa:Enum:VideoFormat:RGB", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB", SPA_VIDEO_FORMAT_RGB,}, - { "Spa:Enum:VideoFormat:BGR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR", SPA_VIDEO_FORMAT_BGR,}, - { "Spa:Enum:VideoFormat:Y41B", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y41B", SPA_VIDEO_FORMAT_Y41B,}, - { "Spa:Enum:VideoFormat:Y42B", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y42B", SPA_VIDEO_FORMAT_Y42B,}, - { "Spa:Enum:VideoFormat:YVYU", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVYU", SPA_VIDEO_FORMAT_YVYU,}, - { "Spa:Enum:VideoFormat:Y444", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444", SPA_VIDEO_FORMAT_Y444,}, - { "Spa:Enum:VideoFormat:v210", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v210", SPA_VIDEO_FORMAT_v210,}, - { "Spa:Enum:VideoFormat:v216", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v216", SPA_VIDEO_FORMAT_v216,}, - { "Spa:Enum:VideoFormat:NV12", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12", SPA_VIDEO_FORMAT_NV12,}, - { "Spa:Enum:VideoFormat:NV21", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV21", SPA_VIDEO_FORMAT_NV21,}, - { "Spa:Enum:VideoFormat:GRAY8", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY8", SPA_VIDEO_FORMAT_GRAY8,}, - { "Spa:Enum:VideoFormat:GRAY16_BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_BE", SPA_VIDEO_FORMAT_GRAY16_BE,}, - { "Spa:Enum:VideoFormat:GRAY16_LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GRAY16_LE", SPA_VIDEO_FORMAT_GRAY16_LE,}, - { "Spa:Enum:VideoFormat:v308", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "v308", SPA_VIDEO_FORMAT_v308,}, - { "Spa:Enum:VideoFormat:RGB16", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB16", SPA_VIDEO_FORMAT_RGB16,}, - { "Spa:Enum:VideoFormat:BGR16", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR16", SPA_VIDEO_FORMAT_BGR16,}, - { "Spa:Enum:VideoFormat:RGB15", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB15", SPA_VIDEO_FORMAT_RGB15,}, - { "Spa:Enum:VideoFormat:BGR15", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGR15", SPA_VIDEO_FORMAT_BGR15,}, - { "Spa:Enum:VideoFormat:UYVP", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "UYVP", SPA_VIDEO_FORMAT_UYVP,}, - { "Spa:Enum:VideoFormat:A420", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420", SPA_VIDEO_FORMAT_A420,}, - { "Spa:Enum:VideoFormat:RGB8P", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGB8P", SPA_VIDEO_FORMAT_RGB8P,}, - { "Spa:Enum:VideoFormat:YUV9", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YUV9", SPA_VIDEO_FORMAT_YUV9,}, - { "Spa:Enum:VideoFormat:YVU9", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "YVU9", SPA_VIDEO_FORMAT_YVU9,}, - { "Spa:Enum:VideoFormat:IYU1", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU1", SPA_VIDEO_FORMAT_IYU1,}, - { "Spa:Enum:VideoFormat:ARGB64", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB64", SPA_VIDEO_FORMAT_ARGB64,}, - { "Spa:Enum:VideoFormat:AYUV64", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "AYUV64", SPA_VIDEO_FORMAT_AYUV64,}, - { "Spa:Enum:VideoFormat:r210", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "r210", SPA_VIDEO_FORMAT_r210,}, - { "Spa:Enum:VideoFormat:I420_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10BE", SPA_VIDEO_FORMAT_I420_10BE,}, - { "Spa:Enum:VideoFormat:I420_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_10LE", SPA_VIDEO_FORMAT_I420_10LE,}, - { "Spa:Enum:VideoFormat:I422_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10BE", SPA_VIDEO_FORMAT_I422_10BE,}, - { "Spa:Enum:VideoFormat:I422_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_10LE", SPA_VIDEO_FORMAT_I422_10LE,}, - { "Spa:Enum:VideoFormat:Y444_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10BE", SPA_VIDEO_FORMAT_Y444_10BE,}, - { "Spa:Enum:VideoFormat:Y444_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_10LE", SPA_VIDEO_FORMAT_Y444_10LE,}, - { "Spa:Enum:VideoFormat:GBR", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR", SPA_VIDEO_FORMAT_GBR,}, - { "Spa:Enum:VideoFormat:GBR_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10BE", SPA_VIDEO_FORMAT_GBR_10BE,}, - { "Spa:Enum:VideoFormat:GBR_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_10LE", SPA_VIDEO_FORMAT_GBR_10LE,}, - { "Spa:Enum:VideoFormat:NV16", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV16", SPA_VIDEO_FORMAT_NV16,}, - { "Spa:Enum:VideoFormat:NV24", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV24", SPA_VIDEO_FORMAT_NV24,}, - { "Spa:Enum:VideoFormat:NV12_64Z32", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV12_64Z32", SPA_VIDEO_FORMAT_NV12_64Z32,}, - { "Spa:Enum:VideoFormat:A420_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10BE", SPA_VIDEO_FORMAT_A420_10BE,}, - { "Spa:Enum:VideoFormat:A420_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A420_10LE", SPA_VIDEO_FORMAT_A420_10LE,}, - { "Spa:Enum:VideoFormat:A422_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10BE", SPA_VIDEO_FORMAT_A422_10BE,}, - { "Spa:Enum:VideoFormat:A422_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A422_10LE", SPA_VIDEO_FORMAT_A422_10LE,}, - { "Spa:Enum:VideoFormat:A444_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10BE", SPA_VIDEO_FORMAT_A444_10BE,}, - { "Spa:Enum:VideoFormat:A444_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "A444_10LE", SPA_VIDEO_FORMAT_A444_10LE,}, - { "Spa:Enum:VideoFormat:NV61", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "NV61", SPA_VIDEO_FORMAT_NV61,}, - { "Spa:Enum:VideoFormat:P010_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10BE", SPA_VIDEO_FORMAT_P010_10BE,}, - { "Spa:Enum:VideoFormat:P010_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "P010_10LE", SPA_VIDEO_FORMAT_P010_10LE,}, - { "Spa:Enum:VideoFormat:IYU2", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "IYU2", SPA_VIDEO_FORMAT_IYU2,}, - { "Spa:Enum:VideoFormat:VYUY", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "VYUY", SPA_VIDEO_FORMAT_VYUY,}, - { "Spa:Enum:VideoFormat:GBRA", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA", SPA_VIDEO_FORMAT_GBRA,}, - { "Spa:Enum:VideoFormat:GBRA_10BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10BE", SPA_VIDEO_FORMAT_GBRA_10BE,}, - { "Spa:Enum:VideoFormat:GBRA_10LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_10LE", SPA_VIDEO_FORMAT_GBRA_10LE,}, - { "Spa:Enum:VideoFormat:GBR_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12BE", SPA_VIDEO_FORMAT_GBR_12BE,}, - { "Spa:Enum:VideoFormat:GBR_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBR_12LE", SPA_VIDEO_FORMAT_GBR_12LE,}, - { "Spa:Enum:VideoFormat:GBRA_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12BE", SPA_VIDEO_FORMAT_GBRA_12BE,}, - { "Spa:Enum:VideoFormat:GBRA_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "GBRA_12LE", SPA_VIDEO_FORMAT_GBRA_12LE,}, - { "Spa:Enum:VideoFormat:I420_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12BE", SPA_VIDEO_FORMAT_I420_12BE,}, - { "Spa:Enum:VideoFormat:I420_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I420_12LE", SPA_VIDEO_FORMAT_I420_12LE,}, - { "Spa:Enum:VideoFormat:I422_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12BE", SPA_VIDEO_FORMAT_I422_12BE,}, - { "Spa:Enum:VideoFormat:I422_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "I422_12LE", SPA_VIDEO_FORMAT_I422_12LE,}, - { "Spa:Enum:VideoFormat:Y444_12BE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12BE", SPA_VIDEO_FORMAT_Y444_12BE,}, - { "Spa:Enum:VideoFormat:Y444_12LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "Y444_12LE", SPA_VIDEO_FORMAT_Y444_12LE,}, - { "Spa:Enum:VideoFormat:xRGB_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xRGB_210LE", SPA_VIDEO_FORMAT_xRGB_210LE,}, - { "Spa:Enum:VideoFormat:xBGR_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "xBGR_210LE", SPA_VIDEO_FORMAT_xBGR_210LE,}, - { "Spa:Enum:VideoFormat:RGBx_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBx_102LE", SPA_VIDEO_FORMAT_RGBx_102LE,}, - { "Spa:Enum:VideoFormat:BGRx_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRx_102LE", SPA_VIDEO_FORMAT_BGRx_102LE,}, - { "Spa:Enum:VideoFormat:ARGB_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ARGB_210LE", SPA_VIDEO_FORMAT_ARGB_210LE,}, - { "Spa:Enum:VideoFormat:ABGR_210LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "ABGR_210LE", SPA_VIDEO_FORMAT_ABGR_210LE,}, - { "Spa:Enum:VideoFormat:RGBA_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "RGBA_102LE", SPA_VIDEO_FORMAT_RGBA_102LE,}, - { "Spa:Enum:VideoFormat:BGRA_102LE", SPA_TYPE_INFO_VIDEO_FORMAT_BASE "BGRA_102LE", SPA_VIDEO_FORMAT_BGRA_102LE,}, - { "Spa:Enum:AudioFormat:ENCODED", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "ENCODED", SPA_AUDIO_FORMAT_ENCODED,}, - { "Spa:Enum:AudioFormat:S8", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S8", SPA_AUDIO_FORMAT_S8, }, - { "Spa:Enum:AudioFormat:U8", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U8", SPA_AUDIO_FORMAT_U8, }, - { "Spa:Enum:AudioFormat:S16LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16_LE", SPA_AUDIO_FORMAT_S16_LE, }, - { "Spa:Enum:AudioFormat:U16LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16_LE", SPA_AUDIO_FORMAT_U16_LE, }, - { "Spa:Enum:AudioFormat:S24_32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32_LE", SPA_AUDIO_FORMAT_S24_32_LE, }, - { "Spa:Enum:AudioFormat:U24_32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32_LE", SPA_AUDIO_FORMAT_U24_32_LE, }, - { "Spa:Enum:AudioFormat:S32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32_LE", SPA_AUDIO_FORMAT_S32_LE, }, - { "Spa:Enum:AudioFormat:U32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32_LE", SPA_AUDIO_FORMAT_U32_LE, }, - { "Spa:Enum:AudioFormat:S24LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_LE", SPA_AUDIO_FORMAT_S24_LE, }, - { "Spa:Enum:AudioFormat:U24LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_LE", SPA_AUDIO_FORMAT_U24_LE, }, - { "Spa:Enum:AudioFormat:S20LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20_LE", SPA_AUDIO_FORMAT_S20_LE, }, - { "Spa:Enum:AudioFormat:U20LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20_LE", SPA_AUDIO_FORMAT_U20_LE, }, - { "Spa:Enum:AudioFormat:S18LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18_LE", SPA_AUDIO_FORMAT_S18_LE, }, - { "Spa:Enum:AudioFormat:U18LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18_LE", SPA_AUDIO_FORMAT_U18_LE, }, - { "Spa:Enum:AudioFormat:F32LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32_LE", SPA_AUDIO_FORMAT_F32_LE, }, - { "Spa:Enum:AudioFormat:F64LE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64_LE", SPA_AUDIO_FORMAT_F64_LE, }, - { "Spa:Enum:AudioFormat:S16BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S16_BE", SPA_AUDIO_FORMAT_S16_BE, }, - { "Spa:Enum:AudioFormat:U16BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U16_BE", SPA_AUDIO_FORMAT_U16_BE, }, - { "Spa:Enum:AudioFormat:S24_32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_32_BE", SPA_AUDIO_FORMAT_S24_32_BE, }, - { "Spa:Enum:AudioFormat:U24_32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_32_BE", SPA_AUDIO_FORMAT_U24_32_BE, }, - { "Spa:Enum:AudioFormat:S32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S32_BE", SPA_AUDIO_FORMAT_S32_BE, }, - { "Spa:Enum:AudioFormat:U32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U32_BE", SPA_AUDIO_FORMAT_U32_BE, }, - { "Spa:Enum:AudioFormat:S24BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S24_BE", SPA_AUDIO_FORMAT_S24_BE, }, - { "Spa:Enum:AudioFormat:U24BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U24_BE", SPA_AUDIO_FORMAT_U24_BE, }, - { "Spa:Enum:AudioFormat:S20BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S20_BE", SPA_AUDIO_FORMAT_S20_BE, }, - { "Spa:Enum:AudioFormat:U20BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U20_BE", SPA_AUDIO_FORMAT_U20_BE, }, - { "Spa:Enum:AudioFormat:S18BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "S18_BE", SPA_AUDIO_FORMAT_S18_BE, }, - { "Spa:Enum:AudioFormat:U18BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "U18_BE", SPA_AUDIO_FORMAT_U18_BE, }, - { "Spa:Enum:AudioFormat:F32BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32_BE", SPA_AUDIO_FORMAT_F32_BE, }, - { "Spa:Enum:AudioFormat:F64BE", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F64_BE", SPA_AUDIO_FORMAT_F64_BE, }, - { "Spa:Enum:AudioFormat:F32P", SPA_TYPE_INFO_AUDIO_FORMAT_BASE "F32P", SPA_AUDIO_FORMAT_F32P, }, -}; diff --git a/src/modules/module-protocol-pulse.c b/src/modules/module-protocol-pulse.c index ea5eb6cbe..4c82d97c0 100644 --- a/src/modules/module-protocol-pulse.c +++ b/src/modules/module-protocol-pulse.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,8 +12,6 @@ #include #include -#include "config.h" - #include #include diff --git a/src/modules/module-protocol-pulse/client.c b/src/modules/module-protocol-pulse/client.c index 454ffe7b2..b189fdbe7 100644 --- a/src/modules/module-protocol-pulse/client.c +++ b/src/modules/module-protocol-pulse/client.c @@ -87,7 +87,7 @@ bool client_detach(struct client *client) if (server->wait_clients > 0 && --server->wait_clients == 0) { int mask = server->source->mask; SPA_FLAG_SET(mask, SPA_IO_IN); - pw_loop_update_io(impl->loop, server->source, mask); + pw_loop_update_io(impl->main_loop, server->source, mask); } client->server = NULL; @@ -112,7 +112,7 @@ void client_disconnect(struct client *client) pw_map_for_each(&client->streams, client_free_stream, client); if (client->source) { - pw_loop_destroy_source(impl->loop, client->source); + pw_loop_destroy_source(impl->main_loop, client->source); client->source = NULL; } @@ -207,7 +207,7 @@ int client_queue_message(struct client *client, struct message *msg) uint32_t mask = client->source->mask; if (!SPA_FLAG_IS_SET(mask, SPA_IO_OUT)) { SPA_FLAG_SET(mask, SPA_IO_OUT); - pw_loop_update_io(impl->loop, client->source, mask); + pw_loop_update_io(impl->main_loop, client->source, mask); } client->new_msg_since_last_flush = true; @@ -278,7 +278,7 @@ int client_flush_messages(struct client *client) if (SPA_FLAG_IS_SET(mask, SPA_IO_OUT)) { SPA_FLAG_CLEAR(mask, SPA_IO_OUT); - pw_loop_update_io(client->impl->loop, client->source, mask); + pw_loop_update_io(client->impl->main_loop, client->source, mask); } } else { if (res != -EAGAIN && res != -EWOULDBLOCK) diff --git a/src/modules/module-protocol-pulse/collect.c b/src/modules/module-protocol-pulse/collect.c index 189008019..300eee262 100644 --- a/src/modules/module-protocol-pulse/collect.c +++ b/src/modules/module-protocol-pulse/collect.c @@ -169,7 +169,7 @@ uint32_t collect_profile_info(struct pw_manager_object *card, struct card_info * SPA_PARAM_PROFILE_description, SPA_POD_OPT_String(&pi->description), SPA_PARAM_PROFILE_priority, SPA_POD_OPT_Int(&pi->priority), SPA_PARAM_PROFILE_available, SPA_POD_OPT_Id(&pi->available), - SPA_PARAM_PROFILE_classes, SPA_POD_OPT_Pod(&classes)) < 0) { + SPA_PARAM_PROFILE_classes, SPA_POD_OPT_PodStruct(&classes)) < 0) { continue; } if (pi->description == NULL) @@ -246,7 +246,7 @@ static void collect_device_info(struct pw_manager_object *device, struct pw_mana SPA_TYPE_OBJECT_ParamRoute, NULL, SPA_PARAM_ROUTE_index, SPA_POD_Int(&index), SPA_PARAM_ROUTE_device, SPA_POD_Int(&dev), - SPA_PARAM_ROUTE_props, SPA_POD_OPT_Pod(&props)) < 0) + SPA_PARAM_ROUTE_props, SPA_POD_OPT_PodObject(&props)) < 0) continue; if (dev != dev_info->device) continue; @@ -369,7 +369,9 @@ uint32_t collect_port_info(struct pw_manager_object *card, struct card_info *car n = 0; spa_list_for_each(p, &card->param_list, link) { - struct spa_pod *devices = NULL, *profiles = NULL; + int32_t *devices = NULL, *profiles = NULL; + uint32_t devices_size = 0, devices_type = 0, n_devices = 0; + uint32_t profiles_size = 0, profiles_type = 0, n_profiles = 0; struct port_info *pi; if (p->id != SPA_PARAM_EnumRoute) @@ -387,16 +389,24 @@ uint32_t collect_port_info(struct pw_manager_object *card, struct card_info *car SPA_PARAM_ROUTE_priority, SPA_POD_OPT_Int(&pi->priority), SPA_PARAM_ROUTE_available, SPA_POD_OPT_Id(&pi->available), SPA_PARAM_ROUTE_info, SPA_POD_OPT_Pod(&pi->info), - SPA_PARAM_ROUTE_devices, SPA_POD_OPT_Pod(&devices), - SPA_PARAM_ROUTE_profiles, SPA_POD_OPT_Pod(&profiles)) < 0) + SPA_PARAM_ROUTE_devices, SPA_POD_OPT_Array(&devices_size, + &devices_type, &n_devices, &devices), + SPA_PARAM_ROUTE_profiles, SPA_POD_OPT_Array(&profiles_size, + &profiles_type, &n_profiles, &profiles)) < 0) continue; if (pi->description == NULL) pi->description = pi->name; - if (devices) - pi->devices = spa_pod_get_array(devices, &pi->n_devices); - if (profiles) - pi->profiles = spa_pod_get_array(profiles, &pi->n_profiles); + if (devices && devices_size == sizeof(pi->devices[0]) && + devices_type == SPA_TYPE_Int) { + pi->devices = (uint32_t*)devices; + pi->n_devices = n_devices; + } + if (profiles && profiles_size == sizeof(pi->profiles[0]) && + profiles_type == SPA_TYPE_Int) { + pi->profiles = (uint32_t*)profiles; + pi->n_profiles = n_profiles; + } if (dev_info != NULL) { if (pi->direction != dev_info->direction) @@ -524,8 +534,11 @@ uint32_t collect_transport_codec_info(struct pw_manager_object *card, if (iid != SPA_PROP_bluetoothAudioCodec) continue; - if (SPA_POD_CHOICE_TYPE(type) != SPA_CHOICE_Enum || - SPA_POD_TYPE(SPA_POD_CHOICE_CHILD(type)) != SPA_TYPE_Int) + if (type->pod.size < sizeof(struct spa_pod_choice_body) + + 2 * sizeof(int32_t) || + type->body.type != SPA_CHOICE_Enum || + type->body.child.type != SPA_TYPE_Int || + type->body.child.size != sizeof(int32_t)) continue; /* diff --git a/src/modules/module-protocol-pulse/defs.h b/src/modules/module-protocol-pulse/defs.h index fa47c3d81..378644bda 100644 --- a/src/modules/module-protocol-pulse/defs.h +++ b/src/modules/module-protocol-pulse/defs.h @@ -109,8 +109,8 @@ static inline int res_to_err(int res) case -ENOTSUP: case -EPROTONOSUPPORT: case -ESOCKTNOSUPPORT: return ERR_NOTSUPPORTED; case -ENOSYS: return ERR_NOTIMPLEMENTED; case -EIO: return ERR_IO; - case -EBUSY: return ERR_BUSY; - case -ENFILE: case -EMFILE: return ERR_INTERNAL; + case -EBUSY: case -EADDRINUSE: case -EAGAIN: return ERR_BUSY; + case -ENFILE: case -EMFILE: case -ENOMEM: return ERR_INTERNAL; } return ERR_UNKNOWN; } diff --git a/src/modules/module-protocol-pulse/format.c b/src/modules/module-protocol-pulse/format.c index 7ce9757c5..24ef024f9 100644 --- a/src/modules/module-protocol-pulse/format.c +++ b/src/modules/module-protocol-pulse/format.c @@ -12,6 +12,8 @@ #include "format.h" +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS + static const struct format audio_formats[] = { [SAMPLE_U8] = { SAMPLE_U8, SPA_AUDIO_FORMAT_U8, "u8", 1 }, [SAMPLE_ALAW] = { SAMPLE_ALAW, SPA_AUDIO_FORMAT_ALAW, "alaw", 1 }, @@ -296,9 +298,9 @@ uint32_t channel_pa2id(enum channel_position channel) return audio_channels[channel].channel; } -const char *channel_id2name(uint32_t channel) +const char *channel_id2name(uint32_t channel, char *buf, size_t size) { - return spa_type_audio_channel_to_short_name(channel); + return spa_type_audio_channel_make_short_name(channel, buf, size, "UNK"); } uint32_t channel_name2id(const char *name) @@ -346,16 +348,17 @@ uint32_t channel_paname2id(const char *name, size_t size) } -void channel_map_to_positions(const struct channel_map *map, uint32_t *pos) +void channel_map_to_positions(const struct channel_map *map, uint32_t *pos, uint32_t max_pos) { - int i; - for (i = 0; i < map->channels; i++) + uint32_t i, channels = SPA_MIN(map->channels, max_pos); + for (i = 0; i < channels; i++) pos[i] = map->map[i]; } void positions_to_channel_map(const uint32_t *pos, uint32_t channels, struct channel_map *map) { uint32_t i; + channels = SPA_MIN(channels, CHANNELS_MAX); for (i = 0; i < channels; i++) map->map[i] = pos[i]; map->channels = channels; @@ -430,7 +433,7 @@ void channel_map_parse(const char *str, struct channel_map *map) }; } else { channels = map->channels = 0; - while (*p && channels < SPA_AUDIO_MAX_CHANNELS) { + while (*p && channels < CHANNELS_MAX) { uint32_t chname; if ((len = strcspn(p, ",")) == 0) @@ -447,8 +450,9 @@ void channel_map_parse(const char *str, struct channel_map *map) void channel_map_parse_position(const char *str, struct channel_map *map) { - uint32_t channels = 0, position[SPA_AUDIO_MAX_CHANNELS]; - spa_audio_parse_position(str, strlen(str), position, &channels); + uint32_t channels = 0, position[CHANNELS_MAX]; + spa_audio_parse_position_n(str, strlen(str), position, + SPA_N_ELEMENTS(position), &channels); positions_to_channel_map(position, channels, map); } @@ -531,8 +535,7 @@ int format_parse_param(const struct spa_pod *param, bool collect, info.info.raw.rate = 48000; if (info.info.raw.format == 0 || info.info.raw.rate == 0 || - info.info.raw.channels == 0 || - info.info.raw.channels > SPA_AUDIO_MAX_CHANNELS) + info.info.raw.channels == 0) return -ENOTSUP; } break; @@ -576,11 +579,11 @@ int format_parse_param(const struct spa_pod *param, bool collect, if (info.info.raw.rate) ss->rate = info.info.raw.rate; if (info.info.raw.channels) - ss->channels = info.info.raw.channels; + ss->channels = SPA_MIN(info.info.raw.channels, CHANNELS_MAX); } if (map) { if (info.info.raw.channels) { - map->channels = info.info.raw.channels; + map->channels = SPA_MIN(info.info.raw.channels, CHANNELS_MAX); for (i = 0; i < map->channels; i++) map->map[i] = info.info.raw.position[i]; } @@ -630,8 +633,8 @@ const struct spa_pod *format_build_param(struct spa_pod_builder *b, uint32_t id, SPA_FORMAT_AUDIO_channels, SPA_POD_Int(spec->channels), 0); if (map && map->channels == spec->channels) { - uint32_t positions[SPA_AUDIO_MAX_CHANNELS]; - channel_map_to_positions(map, positions); + uint32_t positions[spec->channels]; + channel_map_to_positions(map, positions, spec->channels); spa_pod_builder_add(b, SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), SPA_TYPE_Id, spec->channels, positions), 0); @@ -654,13 +657,13 @@ int format_info_from_spec(struct format_info *info, const struct sample_spec *ss pw_properties_setf(info->props, "format.channels", "%d", ss->channels); if (map && map->channels == ss->channels) { char chmap[1024] = ""; - int i, o, r; - uint32_t aux = 0; + int r; + uint32_t aux = 0, i, o; for (i = 0, o = 0; i < map->channels; i++) { r = snprintf(chmap+o, sizeof(chmap)-o, "%s%s", i == 0 ? "" : ",", channel_id2paname(map->map[i], &aux)); - if (r < 0 || o + r >= (int)sizeof(chmap)) + if (r < 0 || o + r >= sizeof(chmap)) return -ENOSPC; o += r; } @@ -682,7 +685,7 @@ static int add_int(struct format_info *info, const char *k, struct spa_pod *para return -ENOENT; val = spa_pod_get_values(&prop->value, &n_values, &choice); - if (val->type != SPA_TYPE_Int) + if (!spa_pod_is_int(val)) return -ENOTSUP; if (n_values == 0) @@ -766,9 +769,7 @@ static int format_info_iec958_from_param(struct format_info *info, struct spa_po if ((info->props = pw_properties_new(NULL, NULL)) == NULL) return -errno; - add_int(info, "format.rate", param, SPA_FORMAT_AUDIO_rate); - - return 0; + return add_int(info, "format.rate", param, SPA_FORMAT_AUDIO_rate); } int format_info_from_param(struct format_info *info, struct spa_pod *param, uint32_t index) diff --git a/src/modules/module-protocol-pulse/format.h b/src/modules/module-protocol-pulse/format.h index d564b1822..50bb8e2f8 100644 --- a/src/modules/module-protocol-pulse/format.h +++ b/src/modules/module-protocol-pulse/format.h @@ -190,13 +190,13 @@ void sample_spec_fix(struct sample_spec *ss, struct channel_map *map, struct spa_dict *props); uint32_t channel_pa2id(enum channel_position channel); -const char *channel_id2name(uint32_t channel); +const char *channel_id2name(uint32_t channel, char *buf, size_t size); uint32_t channel_name2id(const char *name); enum channel_position channel_id2pa(uint32_t id, uint32_t *aux); const char *channel_id2paname(uint32_t id, uint32_t *aux); uint32_t channel_paname2id(const char *name, size_t size); -void channel_map_to_positions(const struct channel_map *map, uint32_t *pos); +void channel_map_to_positions(const struct channel_map *map, uint32_t *pos, uint32_t max_pos); void channel_map_parse(const char *str, struct channel_map *map); bool channel_map_valid(const struct channel_map *map); void channel_map_parse_position(const char *str, struct channel_map *map); diff --git a/src/modules/module-protocol-pulse/internal.h b/src/modules/module-protocol-pulse/internal.h index 6e7eba7ba..906ae6ed1 100644 --- a/src/modules/module-protocol-pulse/internal.h +++ b/src/modules/module-protocol-pulse/internal.h @@ -47,7 +47,7 @@ struct stats { }; struct impl { - struct pw_loop *loop; + struct pw_loop *main_loop; struct pw_context *context; struct spa_hook context_listener; @@ -59,6 +59,7 @@ struct impl { struct spa_hook_list hooks; struct spa_list servers; + struct pw_timer_queue *timer_queue; struct pw_work_queue *work_queue; struct spa_list cleanup_clients; diff --git a/src/modules/module-protocol-pulse/manager.c b/src/modules/module-protocol-pulse/manager.c index ec8b0963d..5b597f4eb 100644 --- a/src/modules/module-protocol-pulse/manager.c +++ b/src/modules/module-protocol-pulse/manager.c @@ -27,6 +27,7 @@ struct manager { struct pw_manager this; struct pw_loop *loop; + struct pw_timer_queue *timer_queue; struct spa_hook core_listener; struct spa_hook registry_listener; @@ -48,7 +49,7 @@ struct object_data { struct object *object; const char *key; size_t size; - struct spa_source *timer; + struct pw_timer timer; }; struct object { @@ -178,10 +179,7 @@ static void object_reset_params(struct object *o) static void object_data_free(struct object_data *d) { spa_list_remove(&d->link); - if (d->timer) { - pw_loop_destroy_source(d->object->manager->loop, d->timer); - d->timer = NULL; - } + pw_timer_queue_cancel(&d->timer); free(d); } @@ -752,6 +750,7 @@ struct pw_manager *pw_manager_new(struct pw_core *core) context = pw_core_get_context(core); m->loop = pw_context_get_main_loop(context); + m->timer_queue = pw_context_get_timer_queue(context); spa_hook_list_init(&m->hooks); @@ -888,7 +887,7 @@ done: return SPA_PTROFF(d, sizeof(struct object_data), void); } -static void object_data_timeout(void *data, uint64_t count) +static void object_data_timeout(void *data) { struct object_data *d = data; struct object *o = d->object; @@ -897,11 +896,6 @@ static void object_data_timeout(void *data, uint64_t count) pw_log_debug("manager:%p object id:%d data '%s' lifetime ends", m, o->this.id, d->key); - if (d->timer) { - pw_loop_destroy_source(m->loop, d->timer); - d->timer = NULL; - } - manager_emit_object_data_timeout(m, &o->this, d->key); } @@ -911,7 +905,6 @@ void *pw_manager_object_add_temporary_data(struct pw_manager_object *obj, const struct object *o = SPA_CONTAINER_OF(obj, struct object, this); struct object_data *d; void *data; - struct timespec timeout = {0}, interval = {0}; data = pw_manager_object_add_data(obj, key, size); if (data == NULL) @@ -919,14 +912,8 @@ void *pw_manager_object_add_temporary_data(struct pw_manager_object *obj, const d = SPA_PTROFF(data, -sizeof(struct object_data), void); - if (d->timer == NULL) - d->timer = pw_loop_add_timer(o->manager->loop, object_data_timeout, d); - if (d->timer == NULL) - return NULL; - - timeout.tv_sec = lifetime_nsec / SPA_NSEC_PER_SEC; - timeout.tv_nsec = lifetime_nsec % SPA_NSEC_PER_SEC; - pw_loop_update_timer(o->manager->loop, d->timer, &timeout, &interval, false); + pw_timer_queue_add(o->manager->timer_queue, &d->timer, NULL, + lifetime_nsec, object_data_timeout, d); return data; } diff --git a/src/modules/module-protocol-pulse/manager.h b/src/modules/module-protocol-pulse/manager.h index 5e831b39b..6dc15b305 100644 --- a/src/modules/module-protocol-pulse/manager.h +++ b/src/modules/module-protocol-pulse/manager.h @@ -5,10 +5,6 @@ #ifndef PIPEWIRE_MANAGER_H #define PIPEWIRE_MANAGER_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -16,6 +12,10 @@ extern "C" { #include +#ifdef __cplusplus +extern "C" { +#endif + struct client; struct pw_manager_object; diff --git a/src/modules/module-protocol-pulse/message-handler.c b/src/modules/module-protocol-pulse/message-handler.c index 47b14ab21..fdbe3ac5d 100644 --- a/src/modules/module-protocol-pulse/message-handler.c +++ b/src/modules/module-protocol-pulse/message-handler.c @@ -93,7 +93,19 @@ static int core_object_message_handler(struct client *client, struct pw_manager_ { pw_log_debug(": core %p object message:'%s' params:'%s'", o, message, params); - if (spa_streq(message, "list-handlers")) { + if (spa_streq(message, "help")) { + fprintf(response, + "/core []\n" + "available commands:\n" + " help this help\n" + " list-handlers show available object handlers\n" + " pipewire-pulse:malloc-info show malloc_info\n" + " pipewire-pulse:malloc-trim run malloc_trim\n" + " pipewire-pulse:log-level update log level with \n" + " pipewire-pulse:list-modules list all module names\n" + " pipewire-pulse:describe-module describe module info for " + ); + } else if (spa_streq(message, "list-handlers")) { bool first = true; fputc('[', response); @@ -118,6 +130,16 @@ static int core_object_message_handler(struct client *client, struct pw_manager_ } else if (spa_streq(message, "pipewire-pulse:log-level")) { int res = pw_log_set_level_string(params); fprintf(response, "%d", res); + } else if (spa_streq(message, "pipewire-pulse:list-modules")) { + bool first = true; + const struct module_info *i = NULL; + fputc('[', response); + while ((i = module_info_next(client->impl, i)) != NULL) { + fprintf(response, "%s{\"name\":\"%s\"}", + first ? "" : ",\n", i->name); + first = false; + } + fputc(']', response); } else if (spa_streq(message, "pipewire-pulse:describe-module")) { const struct module_info *i = module_info_find(client->impl, params); diff --git a/src/modules/module-protocol-pulse/message.c b/src/modules/module-protocol-pulse/message.c index dbebc437e..19b4f82d6 100644 --- a/src/modules/module-protocol-pulse/message.c +++ b/src/modules/module-protocol-pulse/message.c @@ -761,11 +761,13 @@ int message_dump(enum spa_log_level level, const char *prefix, struct message *m case TAG_CHANNEL_MAP: { struct channel_map map; + char pos[8]; if ((res = read_channel_map(m, &map)) < 0) return res; pw_log(level, "%s %u: channelmap: channels:%u", prefix, o, map.channels); for (i = 0; i < map.channels; i++) - pw_log(level, "%s %d: %s", prefix, i, channel_id2name(map.map[i])); + pw_log(level, "%s %d: %s", prefix, i, + channel_id2name(map.map[i], pos, sizeof(pos))); break; } case TAG_CVOLUME: diff --git a/src/modules/module-protocol-pulse/module.c b/src/modules/module-protocol-pulse/module.c index 0d45399c3..2b47d93e9 100644 --- a/src/modules/module-protocol-pulse/module.c +++ b/src/modules/module-protocol-pulse/module.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -204,7 +205,7 @@ int module_args_to_audioinfo_keys(struct impl *impl, struct pw_properties *props } if (key_channels && (str = pw_properties_get(props, key_channels)) != NULL) { info->channels = pw_properties_parse_int(str); - if (info->channels == 0 || info->channels > SPA_AUDIO_MAX_CHANNELS) { + if (info->channels == 0 || info->channels > CHANNELS_MAX) { pw_log_error("invalid %s '%s'", key_channels, str); return -EINVAL; } @@ -214,7 +215,7 @@ int module_args_to_audioinfo_keys(struct impl *impl, struct pw_properties *props struct channel_map map; channel_map_parse(str, &map); - if (map.channels == 0 || map.channels > SPA_AUDIO_MAX_CHANNELS) { + if (map.channels == 0 || map.channels > CHANNELS_MAX) { pw_log_error("invalid %s '%s'", key_channel_map, str); return -EINVAL; } @@ -226,14 +227,15 @@ int module_args_to_audioinfo_keys(struct impl *impl, struct pw_properties *props info->channels, map.channels); return -EINVAL; } - channel_map_to_positions(&map, info->position); + channel_map_to_positions(&map, info->position, SPA_N_ELEMENTS(info->position)); pw_properties_set(props, key_channel_map, NULL); } else { if (info->channels == 0) info->channels = impl->defs.sample_spec.channels; if (info->channels == impl->defs.channel_map.channels) { - channel_map_to_positions(&impl->defs.channel_map, info->position); + channel_map_to_positions(&impl->defs.channel_map, + info->position, SPA_N_ELEMENTS(info->position)); } else if (info->channels == 1) { info->position[0] = SPA_AUDIO_CHANNEL_MONO; } else if (info->channels == 2) { @@ -281,32 +283,40 @@ void audioinfo_to_properties(struct spa_audio_info_raw *info, struct pw_properti if (info->rate) pw_properties_setf(props, SPA_KEY_AUDIO_RATE, "%u", info->rate); if (info->channels) { - char *s, *p; + char *s, *p, pos[8]; pw_properties_setf(props, SPA_KEY_AUDIO_CHANNELS, "%u", info->channels); p = s = alloca(info->channels * 8); for (i = 0; i < info->channels; i++) p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ", ", - channel_id2name(info->position[i])); + channel_id2name(info->position[i], pos, sizeof(pos))); pw_properties_setf(props, SPA_KEY_AUDIO_POSITION, "[ %s ]", s); } } -const struct module_info *module_info_find(struct impl *impl, const char *name) +const struct module_info *module_info_next(struct impl *impl, const struct module_info *info) { extern const struct module_info __start_pw_mod_pulse_modules[]; extern const struct module_info __stop_pw_mod_pulse_modules[]; - const struct module_info *info = __start_pw_mod_pulse_modules; + if (info == NULL) + info = __start_pw_mod_pulse_modules; + else + info++; + if (info == __stop_pw_mod_pulse_modules) + return NULL; + return info; +} - for (; info < __stop_pw_mod_pulse_modules; info++) { +const struct module_info *module_info_find(struct impl *impl, const char *name) +{ + const struct module_info *info = NULL; + + while ((info = module_info_next(impl, info)) != NULL) { if (spa_streq(info->name, name)) return info; } - - spa_assert(info == __stop_pw_mod_pulse_modules); - return NULL; } diff --git a/src/modules/module-protocol-pulse/module.h b/src/modules/module-protocol-pulse/module.h index fdc45e46e..6bae8d9a5 100644 --- a/src/modules/module-protocol-pulse/module.h +++ b/src/modules/module-protocol-pulse/module.h @@ -62,6 +62,7 @@ struct module { #define module_emit_loaded(m,r) spa_hook_list_call(&m->listener_list, struct module_events, loaded, 0, r) #define module_emit_destroy(m) spa_hook_list_call(&(m)->listener_list, struct module_events, destroy, 0) +const struct module_info *module_info_next(struct impl *impl, const struct module_info *info); const struct module_info *module_info_find(struct impl *impl, const char *name); struct module *module_create(struct impl *impl, const char *name, const char *args); diff --git a/src/modules/module-protocol-pulse/modules/module-combine-sink.c b/src/modules/module-protocol-pulse/modules/module-combine-sink.c index 4951b4e6b..962c22293 100644 --- a/src/modules/module-protocol-pulse/modules/module-combine-sink.c +++ b/src/modules/module-protocol-pulse/modules/module-combine-sink.c @@ -71,7 +71,7 @@ struct module_combine_sink_data { struct pw_properties *combine_props; struct pw_properties *stream_props; - struct spa_source *sinks_timeout; + struct pw_timer sinks_timeout; unsigned int sinks_pending; unsigned int load_emitted:1; @@ -128,7 +128,7 @@ static const struct pw_manager_events manager_events = { .added = manager_added, }; -static void on_sinks_timeout(void *d, uint64_t count) +static void on_sinks_timeout(void *d) { struct module_combine_sink_data *data = d; @@ -214,13 +214,10 @@ static int module_combine_sink_load(struct module *module) pw_manager_add_listener(data->manager, &data->manager_listener, &manager_events, data); - data->sinks_timeout = pw_loop_add_timer(module->impl->loop, on_sinks_timeout, data); - if (data->sinks_timeout) { - struct timespec timeout = {0}; - timeout.tv_sec = TIMEOUT_SINKS_MSEC / 1000; - timeout.tv_nsec = (TIMEOUT_SINKS_MSEC % 1000) * SPA_NSEC_PER_MSEC; - pw_loop_update_timer(module->impl->loop, data->sinks_timeout, &timeout, NULL, false); - } + pw_timer_queue_add(module->impl->timer_queue, &data->sinks_timeout, + NULL, TIMEOUT_SINKS_MSEC * SPA_NSEC_PER_MSEC, + on_sinks_timeout, data); + return data->load_emitted ? 0 : SPA_RESULT_RETURN_ASYNC(0); } @@ -228,8 +225,7 @@ static int module_combine_sink_unload(struct module *module) { struct module_combine_sink_data *d = module->user_data; - if (d->sinks_timeout != NULL) - pw_loop_destroy_source(module->impl->loop, d->sinks_timeout); + pw_timer_queue_cancel(&d->sinks_timeout); if (d->mod != NULL) { spa_hook_remove(&d->mod_listener); diff --git a/src/modules/module-protocol-pulse/modules/module-gsettings.c b/src/modules/module-protocol-pulse/modules/module-gsettings.c index 1968cc101..2b7648528 100644 --- a/src/modules/module-protocol-pulse/modules/module-gsettings.c +++ b/src/modules/module-protocol-pulse/modules/module-gsettings.c @@ -197,7 +197,7 @@ static void handle_module_group(struct module_gsettings_data *d, gchar *name) snprintf(p, sizeof(p), "args%i", i); info.args[i] = g_settings_get_string(settings, p); } - pw_loop_invoke(impl->loop, do_handle_info, 0, + pw_loop_invoke(impl->main_loop, do_handle_info, 0, &info, sizeof(info), false, d); g_object_unref(G_OBJECT(settings)); diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-recv.c b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c index 1777caf61..f81e19047 100644 --- a/src/modules/module-protocol-pulse/modules/module-rtp-recv.c +++ b/src/modules/module-protocol-pulse/modules/module-rtp-recv.c @@ -26,7 +26,8 @@ static const char *const pulse_module_options = "sink= " "sap_address= " - "latency_msec= "; + "latency_msec= " + "stream_properties= "; #define NAME "rtp-recv" @@ -141,6 +142,8 @@ static int module_rtp_recv_prepare(struct module * const module) pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT, str); if ((str = pw_properties_get(props, "latency_msec")) != NULL) pw_properties_set(stream_props, "sess.latency.msec", str); + if ((str = pw_properties_get(props, "stream_properties")) != NULL) + module_args_add_props(stream_props, str); d->module = module; d->stream_props = stream_props; diff --git a/src/modules/module-protocol-pulse/modules/module-rtp-send.c b/src/modules/module-protocol-pulse/modules/module-rtp-send.c index e84962956..aa4015d12 100644 --- a/src/modules/module-protocol-pulse/modules/module-rtp-send.c +++ b/src/modules/module-protocol-pulse/modules/module-rtp-send.c @@ -36,6 +36,7 @@ static const char *const pulse_module_options = "ttl= " "inhibit_auto_suspend= " "stream_name= " + "stream_properties= " "enable_opus="; #define NAME "rtp-send" @@ -199,6 +200,8 @@ static int module_rtp_send_prepare(struct module * const module) pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT, str); } } + if ((str = pw_properties_get(props, "stream_properties")) != NULL) + module_args_add_props(stream_props, str); if (module_args_to_audioinfo_keys(module->impl, props, "format", "rate", "channels", "channel_map", &info) < 0) { diff --git a/src/modules/module-protocol-pulse/modules/module-stream-restore.c b/src/modules/module-protocol-pulse/modules/module-stream-restore.c index bdb9cfd7d..a33ab6fa2 100644 --- a/src/modules/module-protocol-pulse/modules/module-stream-restore.c +++ b/src/modules/module-protocol-pulse/modules/module-stream-restore.c @@ -295,9 +295,11 @@ static int do_extension_stream_restore_write(struct module *module, struct clien fprintf(f, " ]"); } if (map.channels > 0) { + char pos[8]; fprintf(f, ", \"channels\": ["); for (i = 0; i < map.channels; i++) - fprintf(f, "%s\"%s\"", (i == 0 ? " ":", "), channel_id2name(map.map[i])); + fprintf(f, "%s\"%s\"", (i == 0 ? " ":", "), + channel_id2name(map.map[i], pos, sizeof(pos))); fprintf(f, " ]"); } if (device_name != NULL && device_name[0] && diff --git a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c index 414299467..d4425a3fe 100644 --- a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c +++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c @@ -637,7 +637,6 @@ static const struct impl_events impl_events = { static int module_zeroconf_publish_load(struct module *module) { struct module_zeroconf_publish_data *data = module->user_data; - struct pw_loop *loop; int error; data->core = pw_context_connect(module->impl->context, NULL, 0); @@ -650,8 +649,7 @@ static int module_zeroconf_publish_load(struct module *module) &data->core_listener, &core_events, data); - loop = pw_context_get_main_loop(module->impl->context); - data->avahi_poll = pw_avahi_poll_new(loop); + data->avahi_poll = pw_avahi_poll_new(module->impl->context); data->client = avahi_client_new(data->avahi_poll, AVAHI_CLIENT_NO_FAIL, client_callback, data, &error); diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index cb66f75e2..6c2ba43c6 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -621,7 +621,7 @@ static int reply_create_playback_stream(struct stream *stream, struct pw_manager TAG_INVALID); } - stream->create_tag = SPA_ID_INVALID; + stream_created(stream); return client_queue_message(client, reply); } @@ -783,7 +783,7 @@ static int reply_create_record_stream(struct stream *stream, struct pw_manager_o TAG_INVALID); } - stream->create_tag = SPA_ID_INVALID; + stream_created(stream); return client_queue_message(client, reply); } @@ -1127,6 +1127,25 @@ static void stream_state_changed(void *data, enum pw_stream_state old, break; } + /* Don't emit suspended if we are creating a corked stream, as that will have a quick + * RUNNING/SUSPENDED transition for initial negotiation */ + if (stream->create_tag == SPA_ID_INVALID && !stream->corked) { + if (old == PW_STREAM_STATE_PAUSED && state == PW_STREAM_STATE_STREAMING && + stream->is_suspended) { + stream_send_suspended(stream, false); + stream->is_suspended = false; + } + if (old == PW_STREAM_STATE_STREAMING && state == PW_STREAM_STATE_PAUSED && + !stream->is_suspended) { + if (stream->fail_on_suspend) { + stream->killed = true; + destroy_stream = true; + } else { + stream_send_suspended(stream, true); + } + stream->is_suspended = true; + } + } if (destroy_stream) { pw_work_queue_add(impl->work_queue, stream, 0, do_destroy_stream, NULL); @@ -1379,6 +1398,7 @@ static void stream_process(void *data) if (stream->direction == PW_DIRECTION_OUTPUT) { int32_t avail = spa_ringbuffer_get_read_index(&stream->ring, &index); + bool empty = false; minreq = buffer->requested * stream->frame_size; if (minreq == 0) @@ -1391,6 +1411,7 @@ static void stream_process(void *data) /* underrun, produce a silence buffer */ size = SPA_MIN(d->maxsize, minreq); sample_spec_silence(&stream->ss, p, size); + empty = true; if (stream->draining && !stream->corked) { stream->draining = false; @@ -1406,6 +1427,7 @@ static void stream_process(void *data) stream->buffer, MAXLENGTH, index % MAXLENGTH, p, avail); + empty = false; } index += size; pd.read_inc = size; @@ -1446,6 +1468,7 @@ static void stream_process(void *data) d->chunk->offset = 0; d->chunk->stride = stream->frame_size; d->chunk->size = size; + SPA_FLAG_UPDATE(d->chunk->flags, SPA_CHUNK_FLAG_EMPTY, empty); buffer->size = size / stream->frame_size; } else { int32_t filled = spa_ringbuffer_get_write_index(&stream->ring, &index); @@ -1484,7 +1507,7 @@ static void stream_process(void *data) pw_stream_get_time_n(stream->stream, &pd.pwt, sizeof(pd.pwt)); - pw_loop_invoke(impl->loop, + pw_loop_invoke(impl->main_loop, do_process_done, 1, &pd, sizeof(pd), false, stream); } @@ -1729,6 +1752,7 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui stream->muted_set = muted_set; stream->is_underrun = true; stream->underrun_for = -1; + stream->fail_on_suspend = fail_on_suspend; pw_properties_set(props, "pulse.corked", corked ? "true" : "false"); @@ -1756,6 +1780,9 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui PW_KEY_TARGET_OBJECT, "%u", sink_index); } + if (dont_inhibit_auto_suspend) + pw_properties_set(props, PW_KEY_NODE_PASSIVE, "true"); + stream->stream = pw_stream_new(client->core, name, props); props = NULL; if (stream->stream == NULL) @@ -2001,6 +2028,7 @@ static int do_create_record_stream(struct client *client, uint32_t command, uint stream->volume_set = volume_set; stream->muted = muted; stream->muted_set = muted_set; + stream->fail_on_suspend = fail_on_suspend; if (client->quirks & QUIRK_REMOVE_CAPTURE_DONT_MOVE) no_move = false; @@ -2048,6 +2076,8 @@ static int do_create_record_stream(struct client *client, uint32_t command, uint pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true"); } + if (dont_inhibit_auto_suspend) + pw_properties_set(props, PW_KEY_NODE_PASSIVE, "true"); stream->stream = pw_stream_new(client->core, name, props); props = NULL; @@ -5492,8 +5522,9 @@ struct pw_protocol_pulse *pw_protocol_pulse_new(struct pw_context *context, spa_list_init(&impl->cleanup_clients); spa_list_init(&impl->free_messages); - impl->loop = pw_context_get_main_loop(context); + impl->main_loop = pw_context_get_main_loop(context); impl->work_queue = pw_context_get_work_queue(context); + impl->timer_queue = pw_context_get_timer_queue(context); if (props == NULL) props = pw_properties_new(NULL, NULL); diff --git a/src/modules/module-protocol-pulse/pulse-server.h b/src/modules/module-protocol-pulse/pulse-server.h index df3e40848..dfcfd15f1 100644 --- a/src/modules/module-protocol-pulse/pulse-server.h +++ b/src/modules/module-protocol-pulse/pulse-server.h @@ -5,13 +5,13 @@ #ifndef PIPEWIRE_PROTOCOL_PULSE_H #define PIPEWIRE_PROTOCOL_PULSE_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - #define PW_PROTOCOL_PULSE_DEFAULT_PORT 4713 #define PW_PROTOCOL_PULSE_DEFAULT_SOCKET "native" diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c index fe7d47ab0..4e744e33f 100644 --- a/src/modules/module-protocol-pulse/server.c +++ b/src/modules/module-protocol-pulse/server.c @@ -31,6 +31,7 @@ #include #include +#include "network-utils.h" #include "client.h" #include "commands.h" #include "defs.h" @@ -375,6 +376,7 @@ on_connect(void *data, int fd, uint32_t mask) struct client *client = NULL; const char *client_access = NULL; const char *error_reason = NULL; + char ipname[256]; pid_t pid; length = sizeof(name); @@ -384,7 +386,7 @@ on_connect(void *data, int fd, uint32_t mask) if (server->n_clients > 0) { int m = server->source->mask; SPA_FLAG_CLEAR(m, SPA_IO_IN); - pw_loop_update_io(impl->loop, server->source, m); + pw_loop_update_io(impl->main_loop, server->source, m); server->wait_clients++; } } @@ -404,7 +406,7 @@ on_connect(void *data, int fd, uint32_t mask) pw_log_debug("server %p: new client %p fd:%d", server, client, client_fd); - client->source = pw_loop_add_io(impl->loop, + client->source = pw_loop_add_io(impl->main_loop, client_fd, SPA_IO_ERR | SPA_IO_HUP | SPA_IO_IN, true, on_client_data, client); @@ -418,10 +420,18 @@ on_connect(void *data, int fd, uint32_t mask) if (client->props == NULL) goto error; + pw_properties_setf(client->props, "pulse.server.type", "%s", server->addr.ss_family == AF_UNIX ? "unix" : "tcp"); + if (server->addr.ss_family != AF_UNIX) { + uint16_t port = 0; + if (pw_net_get_ip(&name, ipname, sizeof(ipname), NULL, &port) >= 0) + pw_properties_setf(client->props, + "pulse.server.peer", "%s:%d", ipname, port); + } + client->routes = pw_properties_new(NULL, NULL); if (client->routes == NULL) goto error; @@ -430,7 +440,7 @@ on_connect(void *data, int fd, uint32_t mask) client_access = server->client_access; if (server->addr.ss_family == AF_UNIX) { - spa_autofree char *app_id = NULL, *snap_app_id = NULL, *devices = NULL; + spa_autofree char *app_id = NULL, *snap_app_id = NULL, *devices = NULL, *instance_id = NULL; #ifdef HAVE_SNAP pw_sandbox_access_t snap_access; #endif @@ -441,7 +451,7 @@ on_connect(void *data, int fd, uint32_t mask) pw_log_warn("setsockopt(SO_PRIORITY) failed: %m"); #endif pid = get_client_pid(client, client_fd); - if (pid != 0 && pw_check_flatpak(pid, &app_id, &devices) == 1) { + if (pid != 0 && pw_check_flatpak(pid, &app_id, &instance_id, &devices) == 1) { /* * XXX: we should really use Portal client access here * @@ -464,6 +474,8 @@ on_connect(void *data, int fd, uint32_t mask) client_access = "flatpak"; pw_properties_set(client->props, "pipewire.access.portal.app_id", app_id); + pw_properties_set(client->props, "pipewire.access.portal.instance_id", + instance_id); if (devices && (spa_streq(devices, "all") || spa_strstartswith(devices, "all;") || @@ -947,7 +959,7 @@ static int server_start(struct server *server, const struct sockaddr_storage *ad if (fd < 0) return fd; - server->source = pw_loop_add_io(impl->loop, fd, SPA_IO_IN, true, on_connect, server); + server->source = pw_loop_add_io(impl->main_loop, fd, SPA_IO_IN, true, on_connect, server); if (server->source == NULL) { res = -errno; pw_log_error("server %p: can't create server source: %m", impl); @@ -1098,7 +1110,7 @@ void server_free(struct server *server) spa_hook_list_call(&impl->hooks, struct impl_events, server_stopped, 0, server); if (server->source) - pw_loop_destroy_source(impl->loop, server->source); + pw_loop_destroy_source(impl->main_loop, server->source); if (server->addr.ss_family == AF_UNIX && !server->activated) unlink(((const struct sockaddr_un *) &server->addr)->sun_path); diff --git a/src/modules/module-protocol-pulse/snap-policy.c b/src/modules/module-protocol-pulse/snap-policy.c index 2921f06de..774763f9a 100644 --- a/src/modules/module-protocol-pulse/snap-policy.c +++ b/src/modules/module-protocol-pulse/snap-policy.c @@ -2,9 +2,7 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Canonical Ltd. */ /* SPDX-License-Identifier: MIT */ -#ifdef HAVE_CONFIG_H #include -#endif #include #include diff --git a/src/modules/module-protocol-pulse/stream.c b/src/modules/module-protocol-pulse/stream.c index e3b8b7b0f..496bcb52c 100644 --- a/src/modules/module-protocol-pulse/stream.c +++ b/src/modules/module-protocol-pulse/stream.c @@ -40,6 +40,21 @@ static int parse_frac(struct pw_properties *props, const char *key, return 0; } +static void create_stream_timeout(void *user_data) +{ + struct stream *stream = user_data; + + if (stream->create_tag != SPA_ID_INVALID) { + pw_log_warn("[%s] timeout on stream %p channel:%d", stream->client->name, stream, stream->channel); + + /* Don't try to signal anything to the client, it's already killed the stream on its end */ + stream->drain_tag = 0; + stream->killed = false; + + stream_free(stream); + } +} + struct stream *stream_new(struct client *client, enum stream_type type, uint32_t create_tag, const struct sample_spec *ss, const struct channel_map *map, const struct buffer_attr *attr) @@ -89,6 +104,11 @@ struct stream *stream_new(struct client *client, enum stream_type type, uint32_t spa_assert_not_reached(); } + /* Time out if we don't get a link and can't send a reply to create in 35s. Client will time out in + * 30s and clean up its stream anyway. */ + pw_timer_queue_add(stream->impl->timer_queue, &stream->timer, NULL, + 35 * SPA_NSEC_PER_SEC, create_stream_timeout, stream); + return stream; error_errno: @@ -99,6 +119,15 @@ error_errno: return NULL; } +void stream_created(struct stream *stream) +{ + struct client *client = stream->client; + pw_log_debug("client %p: stream %p channel:%d", client, stream, stream->channel); + + stream->create_tag = SPA_ID_INVALID; + pw_timer_queue_cancel(&stream->timer); +} + void stream_free(struct stream *stream) { struct client *client = stream->client; @@ -106,6 +135,8 @@ void stream_free(struct stream *stream) pw_log_debug("client %p: stream %p channel:%d", client, stream, stream->channel); + pw_timer_queue_cancel(&stream->timer); + if (stream->drain_tag) reply_error(client, -1, stream->drain_tag, -ENOENT); @@ -118,7 +149,7 @@ void stream_free(struct stream *stream) /* force processing of all pending messages before we destroy * the stream */ - pw_loop_invoke(impl->loop, NULL, 0, NULL, 0, false, client); + pw_loop_invoke(impl->main_loop, NULL, 0, NULL, 0, false, client); pw_stream_destroy(stream->stream); } @@ -314,6 +345,31 @@ int stream_send_started(struct stream *stream) return client_queue_message(client, reply); } +int stream_send_suspended(struct stream *stream, bool suspended) +{ + struct client *client = stream->client; + struct impl *impl = client->impl; + struct message *reply; + uint32_t command; + + pw_log_debug("client %p [%s]: stream %p SUSPENDED %d channel:%u", + client, client->name, stream, suspended, stream->channel); + + command = stream->direction == PW_DIRECTION_OUTPUT ? + COMMAND_PLAYBACK_STREAM_SUSPENDED : + COMMAND_RECORD_STREAM_SUSPENDED; + + reply = message_alloc(impl, -1, 0); + message_put(reply, + TAG_U32, command, + TAG_U32, -1, + TAG_U32, stream->channel, + TAG_BOOLEAN, suspended, + TAG_INVALID); + + return client_queue_message(client, reply); +} + int stream_send_request(struct stream *stream) { struct client *client = stream->client; diff --git a/src/modules/module-protocol-pulse/stream.h b/src/modules/module-protocol-pulse/stream.h index 64ecea680..022f0ee74 100644 --- a/src/modules/module-protocol-pulse/stream.h +++ b/src/modules/module-protocol-pulse/stream.h @@ -50,6 +50,7 @@ struct stream { struct pw_stream *stream; struct spa_hook stream_listener; + struct pw_timer timer; struct spa_io_position *position; struct spa_ringbuffer ring; @@ -98,11 +99,14 @@ struct stream { unsigned int pending:1; unsigned int is_idle:1; unsigned int is_paused:1; + unsigned int fail_on_suspend:1; + unsigned int is_suspended:1; }; struct stream *stream_new(struct client *client, enum stream_type type, uint32_t create_tag, const struct sample_spec *ss, const struct channel_map *map, const struct buffer_attr *attr); +void stream_created(struct stream *stream); void stream_free(struct stream *stream); void stream_flush(struct stream *stream); uint32_t stream_pop_missing(struct stream *stream); @@ -114,6 +118,7 @@ int stream_send_underflow(struct stream *stream, int64_t offset); int stream_send_overflow(struct stream *stream); int stream_send_killed(struct stream *stream); int stream_send_started(struct stream *stream); +int stream_send_suspended(struct stream *stream, bool suspended); int stream_send_request(struct stream *stream); int stream_update_minreq(struct stream *stream, uint32_t minreq); int stream_send_moved(struct stream *stream, uint32_t peer_index, const char *peer_name); diff --git a/src/modules/module-protocol-pulse/volume.c b/src/modules/module-protocol-pulse/volume.c index b47a1d026..e53f967ec 100644 --- a/src/modules/module-protocol-pulse/volume.c +++ b/src/modules/module-protocol-pulse/volume.c @@ -53,7 +53,7 @@ int volume_parse_param(const struct spa_pod *param, struct volume_info *info, bo if (monitor) continue; info->volume.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - info->volume.values, SPA_AUDIO_MAX_CHANNELS); + info->volume.values, SPA_N_ELEMENTS(info->volume.values)); SPA_FLAG_UPDATE(info->flags, VOLUME_HW_VOLUME, prop->flags & SPA_POD_PROP_FLAG_HARDWARE); break; @@ -68,7 +68,7 @@ int volume_parse_param(const struct spa_pod *param, struct volume_info *info, bo if (!monitor) continue; info->volume.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - info->volume.values, SPA_AUDIO_MAX_CHANNELS); + info->volume.values, SPA_N_ELEMENTS(info->volume.values)); SPA_FLAG_CLEAR(info->flags, VOLUME_HW_VOLUME); break; case SPA_PROP_volumeBase: @@ -84,7 +84,7 @@ int volume_parse_param(const struct spa_pod *param, struct volume_info *info, bo } case SPA_PROP_channelMap: info->map.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id, - info->map.map, SPA_AUDIO_MAX_CHANNELS); + info->map.map, SPA_N_ELEMENTS(info->map.map)); break; default: break; diff --git a/src/modules/module-protocol-simple.c b/src/modules/module-protocol-simple.c index bbed3b5f7..ffc225b98 100644 --- a/src/modules/module-protocol-simple.c +++ b/src/modules/module-protocol-simple.c @@ -72,6 +72,7 @@ * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_LATENCY * - \ref PW_KEY_NODE_RATE @@ -665,8 +666,9 @@ static int make_tcp_socket(struct server *server, const char *name, const char * const char *ifaddress) { struct sockaddr_storage addr; - int res, fd, on; + int res, on; socklen_t len = 0; + spa_autoclose int fd = -1; if ((res = pw_net_parse_address_port(name, ifaddress, DEFAULT_PORT, &addr, &len)) < 0) { pw_log_error("%p: can't parse address %s: %s", server, @@ -693,26 +695,24 @@ static int make_tcp_socket(struct server *server, const char *name, const char * if (bind(fd, (struct sockaddr *) &addr, len) < 0) { res = -errno; pw_log_error("%p: bind() failed: %m", server); - goto error_close; + goto error; } if (listen(fd, 5) < 0) { res = -errno; pw_log_error("%p: listen() failed: %m", server); - goto error_close; + goto error; } if (getsockname(fd, (struct sockaddr *)&addr, &len) < 0) { res = -errno; pw_log_error("%p: getsockname() failed: %m", server); - goto error_close; + goto error; } server->type = SERVER_TYPE_TCP; server->addr = addr; - return fd; + return spa_steal_fd(fd); -error_close: - close(fd); error: return res; } @@ -816,13 +816,14 @@ static int calc_frame_size(struct spa_audio_info_raw *info) case SPA_AUDIO_FORMAT_F64_OE: return res * 8; default: - return 0; + return -ENOTSUP; } } static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + int res; + if ((res = spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), @@ -831,7 +832,9 @@ static int parse_audio_info(const struct pw_properties *props, struct spa_audio_ SPA_KEY_AUDIO_FORMAT, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, - SPA_KEY_AUDIO_POSITION, NULL); + SPA_KEY_AUDIO_LAYOUT, + SPA_KEY_AUDIO_POSITION, NULL)) < 0) + return res; return calc_frame_size(info); } @@ -852,6 +855,7 @@ static int parse_params(struct impl *impl) const char *str; struct spa_json it[1]; char value[512]; + int res; pw_properties_fetch_bool(impl->props, "capture", &impl->capture); pw_properties_fetch_bool(impl->props, "playback", &impl->playback); @@ -886,6 +890,7 @@ static int parse_params(struct impl *impl) copy_props(impl, PW_KEY_AUDIO_FORMAT); copy_props(impl, PW_KEY_AUDIO_RATE); copy_props(impl, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, SPA_KEY_AUDIO_POSITION); copy_props(impl, PW_KEY_NODE_RATE); copy_props(impl, PW_KEY_NODE_NAME); @@ -895,19 +900,20 @@ static int parse_params(struct impl *impl) copy_props(impl, PW_KEY_NODE_VIRTUAL); copy_props(impl, PW_KEY_NODE_NETWORK); - impl->capture_frame_size = parse_audio_info(impl->capture_props, &impl->capture_info); - if (impl->capture_frame_size == 0) { + if ((res = parse_audio_info(impl->capture_props, &impl->capture_info)) <= 0) { pw_log_error("unsupported capture audio format:%d channels:%d", impl->capture_info.format, impl->capture_info.channels); return -EINVAL; } + impl->capture_frame_size = res; - impl->playback_frame_size = parse_audio_info(impl->playback_props, &impl->playback_info); - if (impl->playback_frame_size == 0) { + if ((res = parse_audio_info(impl->playback_props, &impl->playback_info)) <= 0) { pw_log_error("unsupported playback audio format:%d channels:%d", impl->playback_info.format, impl->playback_info.channels); return -EINVAL; } + impl->playback_frame_size = res; + if (impl->capture_info.rate != 0 && pw_properties_get(impl->capture_props, PW_KEY_NODE_RATE) == NULL) pw_properties_setf(impl->capture_props, PW_KEY_NODE_RATE, diff --git a/src/modules/module-pulse-tunnel.c b/src/modules/module-pulse-tunnel.c index 32d01122a..fe496fa33 100644 --- a/src/modules/module-pulse-tunnel.c +++ b/src/modules/module-pulse-tunnel.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -14,8 +16,6 @@ #include #include -#include "config.h" - #include #include #include @@ -72,6 +72,7 @@ * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_LATENCY * - \ref PW_KEY_NODE_NAME @@ -117,6 +118,8 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define DEFAULT_CHANNELS 2 #define DEFAULT_POSITION "[ FL FR ]" +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS + #define MODULE_USAGE "( remote.name= ] " \ "( node.latency= ] " \ "( node.name= ] " \ @@ -148,6 +151,7 @@ static const struct spa_dict_item module_props[] = { struct impl { struct pw_context *context; struct pw_loop *main_loop; + struct pw_timer_queue *timer_queue; #define MODE_SINK 0 #define MODE_SOURCE 1 @@ -194,7 +198,7 @@ struct impl { bool do_disconnect:1; bool stopping; - struct spa_source *timer; + struct pw_timer timer; uint32_t reconnect_interval_ms; bool recovering; }; @@ -294,11 +298,11 @@ static void stream_param_changed(void *d, uint32_t id, const struct spa_pod *par { struct pa_cvolume volume; uint32_t n; - float vols[SPA_AUDIO_MAX_CHANNELS]; + float vols[MAX_CHANNELS]; if ((n = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - vols, SPA_AUDIO_MAX_CHANNELS)) > 0) { - volume.channels = n; + vols, SPA_N_ELEMENTS(vols))) > 0) { + volume.channels = SPA_MIN(PA_CHANNELS_MAX, n); for (n = 0; n < volume.channels; n++) volume.values[n] = pa_sw_volume_from_linear(vols[n]); @@ -525,7 +529,7 @@ static void cleanup_streams(struct impl *impl) pw_stream_destroy(impl->stream); } -static void on_timer_event(void *data, uint64_t expirations) +static void on_timer_event(void *data) { struct impl *impl = data; cleanup_streams(impl); @@ -538,13 +542,9 @@ do_schedule_recovery(struct spa_loop *loop, { struct impl *impl = user_data; if (impl->reconnect_interval_ms > 0) { - struct timespec value; - uint64_t timestamp; - - timestamp = impl->reconnect_interval_ms * SPA_NSEC_PER_MSEC; - value.tv_sec = timestamp / SPA_NSEC_PER_SEC; - value.tv_nsec = timestamp % SPA_NSEC_PER_SEC; - pw_loop_update_timer(impl->main_loop, impl->timer, &value, NULL, false); + pw_timer_queue_add(impl->timer_queue, &impl->timer, + NULL, impl->reconnect_interval_ms * SPA_NSEC_PER_MSEC, + on_timer_event, impl); } else { if (impl->module) pw_impl_module_schedule_destroy(impl->module); @@ -755,7 +755,8 @@ static int create_pulse_stream(struct impl *impl) map.channels = impl->info.channels; for (i = 0; i < map.channels; i++) - map.map[i] = (pa_channel_position_t)channel_id2pa(impl->info.position[i], &aux); + map.map[i] = (pa_channel_position_t)channel_id2pa( + impl->info.position[i], &aux); snprintf(stream_name, sizeof(stream_name), _("Tunnel for %s@%s"), pw_get_user_name(), pw_get_host_name()); @@ -834,11 +835,12 @@ do_stream_sync_volumes(struct spa_loop *loop, struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); struct spa_pod_frame f[1]; struct spa_pod *param; - uint32_t i; - float vols[SPA_AUDIO_MAX_CHANNELS]; - float soft_vols[SPA_AUDIO_MAX_CHANNELS]; + uint32_t i, channels; + float vols[MAX_CHANNELS]; + float soft_vols[MAX_CHANNELS]; - for (i = 0; i < impl->volume.channels; i++) { + channels = SPA_MIN(impl->volume.channels, MAX_CHANNELS); + for (i = 0; i < channels; i++) { vols[i] = (float)pa_sw_volume_to_linear(impl->volume.values[i]); soft_vols[i] = 1.0f; } @@ -851,10 +853,10 @@ do_stream_sync_volumes(struct spa_loop *loop, spa_pod_builder_prop(&b, SPA_PROP_channelVolumes, 0); spa_pod_builder_array(&b, sizeof(float), SPA_TYPE_Float, - impl->volume.channels, vols); + channels, vols); spa_pod_builder_prop(&b, SPA_PROP_softVolumes, 0); spa_pod_builder_array(&b, sizeof(float), SPA_TYPE_Float, - impl->volume.channels, soft_vols); + channels, soft_vols); param = spa_pod_builder_pop(&b, &f[0]); pw_stream_set_param(impl->stream, SPA_PARAM_Props, param); @@ -1028,8 +1030,7 @@ static void impl_destroy(struct impl *impl) pw_properties_free(impl->stream_props); pw_properties_free(impl->props); - if (impl->timer) - pw_loop_destroy_source(impl->main_loop, impl->timer); + pw_timer_queue_cancel(&impl->timer); free(impl->buffer); free(impl); @@ -1048,9 +1049,9 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), @@ -1059,6 +1060,7 @@ static void parse_audio_info(const struct pw_properties *props, struct spa_audio SPA_KEY_AUDIO_FORMAT, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -1143,6 +1145,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->module = module; impl->context = context; impl->main_loop = pw_context_get_main_loop(context); + impl->timer_queue = pw_context_get_timer_queue(context); spa_ringbuffer_init(&impl->ring); impl->buffer = calloc(1, RINGBUFFER_SIZE); @@ -1164,13 +1167,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->reconnect_interval_ms = pw_properties_get_uint32(props, "reconnect.interval.ms", 0); - impl->timer = pw_loop_add_timer(impl->main_loop, on_timer_event, impl); - if (impl->timer == NULL) { - res = -errno; - pw_log_error("can't create timer source: %m"); - goto error; - } - impl->latency_msec = pw_properties_get_uint32(props, "pulse.latency", DEFAULT_LATENCY_MSEC); if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL) @@ -1189,6 +1185,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_FORMAT); copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); @@ -1198,7 +1195,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_NODE_NETWORK); copy_props(impl, props, PW_KEY_MEDIA_CLASS); - parse_audio_info(impl->stream_props, &impl->info); + if ((res = parse_audio_info(impl->stream_props, &impl->info)) < 0) { + pw_log_error("can't parse format: %s", spa_strerror(res)); + goto error; + } impl->frame_size = calc_frame_size(&impl->info); if (impl->frame_size == 0) { diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c index c794bc076..53032d0be 100644 --- a/src/modules/module-raop-discover.c +++ b/src/modules/module-raop-discover.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,8 +12,6 @@ #include #include -#include "config.h" - #include #include #include @@ -45,7 +45,7 @@ * * Options specific to the behavior of this module * - * - `roap.discover-local` = allow discovery of local services as well. + * - `raop.discover-local` = allow discovery of local services as well. * false by default. * - `raop.latency.ms` = latency for all streams in microseconds. This * can be overwritten in the stream rules. @@ -60,7 +60,7 @@ * context.modules = [ * { name = libpipewire-module-raop-discover * args = { - * #roap.discover-local = false; + * #raop.discover-local = false; * #raop.latency.ms = 1000 * stream.rules = [ * { matches = [ @@ -71,7 +71,7 @@ * #raop.domain = "" * #raop.device = "" * #raop.transport = "udp" | "tcp" - * #raop.encryption.type = "RSA" | "auth_setup" | "none" + * #raop.encryption.type = "none" | "RSA" | "auth_setup" | "fp_sap25" * #raop.audio.codec = "PCM" | "ALAC" | "AAC" | "AAC-ELD" * #audio.channels = 2 * #audio.format = "S16" | "S24" | "S32" @@ -244,10 +244,12 @@ static void pw_properties_from_avahi_string(const char *key, const char *value, * 3 = FairPlay, * 4 = MFiSAP (/auth-setup), * 5 = FairPlay SAPv2.5 */ - if (str_in_list(value, ",", "1")) - value = "RSA"; + if (str_in_list(value, ",", "5")) + value = "fp_sap25"; else if (str_in_list(value, ",", "4")) value = "auth_setup"; + else if (str_in_list(value, ",", "1")) + value = "RSA"; else value = "none"; pw_properties_set(props, "raop.encryption.type", value); @@ -559,10 +561,8 @@ static int start_client(struct impl *impl) static int start_avahi(struct impl *impl) { - struct pw_loop *loop; - loop = pw_context_get_main_loop(impl->context); - impl->avahi_poll = pw_avahi_poll_new(loop); + impl->avahi_poll = pw_avahi_poll_new(impl->context); return start_client(impl); } diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c index 9d9406811..e217ff5b2 100644 --- a/src/modules/module-raop-sink.c +++ b/src/modules/module-raop-sink.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -27,8 +29,6 @@ #include #include -#include "config.h" - #include #include #include @@ -38,7 +38,6 @@ #include #include #include -#include #include #include @@ -83,6 +82,7 @@ * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_NAME * - \ref PW_KEY_NODE_DESCRIPTION @@ -159,6 +159,7 @@ PW_LOG_TOPIC(mod_topic, "mod." NAME); #define RAOP_LATENCY_MS 250 #define DEFAULT_LATENCY_MS 1500 +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define VOLUME_MAX 0.0 #define VOLUME_MIN -30.0 #define VOLUME_MUTE -144.0 @@ -197,6 +198,7 @@ enum { CRYPTO_NONE, CRYPTO_RSA, CRYPTO_AUTH_SETUP, + CRYPTO_FP_SAP25, }; enum { CODEC_PCM, @@ -212,6 +214,7 @@ struct impl { struct pw_impl_module *module; struct pw_loop *loop; + struct pw_timer_queue *timer_queue; struct spa_hook module_listener; @@ -245,7 +248,7 @@ struct impl { uint16_t control_port; int control_fd; struct spa_source *control_source; - struct spa_source *feedback_timer; + struct pw_timer feedback_timer; uint16_t timing_port; int timing_fd; @@ -271,9 +274,6 @@ struct impl { bool mute; float volume; - struct spa_latency_info latency_info; - struct spa_process_latency_info process_latency; - struct spa_ringbuffer ring; uint8_t buffer[BUFFER_SIZE]; @@ -442,6 +442,7 @@ static int write_codec_pcm(void *dst, void *frames, uint32_t n_frames) bit_writer(&bp, &bpos, *(d + 2), 8); d += 4; } + bit_writer(&bp, &bpos, 7, 3); /* end tag */ return bp - b + 1; } @@ -838,12 +839,16 @@ static int rtsp_send_volume(struct impl *impl) return rtsp_send(impl, "SET_PARAMETER", "text/parameters", header, rtsp_log_reply_status); } -static void rtsp_do_post_feedback(void *data, uint64_t expirations) +static void rtsp_do_post_feedback(void *data) { struct impl *impl = data; pw_rtsp_client_url_send(impl->rtsp, "/feedback", "POST", &impl->headers->dict, NULL, NULL, 0, rtsp_log_reply_status, impl); + + pw_timer_queue_add(impl->timer_queue, &impl->feedback_timer, + &impl->feedback_timer.timeout, 2 * SPA_NSEC_PER_SEC, + rtsp_do_post_feedback, impl); } static uint32_t msec_to_samples(struct impl *impl, uint32_t msec) @@ -851,31 +856,12 @@ static uint32_t msec_to_samples(struct impl *impl, uint32_t msec) return (uint64_t) msec * impl->rate / 1000; } -static void update_latency(struct impl *impl) -{ - uint32_t n_params = 0; - const struct spa_pod *params[3]; - uint8_t buffer[1024]; - struct spa_pod_builder b; - struct spa_latency_info latency; - - spa_pod_builder_init(&b, buffer, sizeof(buffer)); - - latency = SPA_LATENCY_INFO(PW_DIRECTION_INPUT); - - spa_process_latency_info_add(&impl->process_latency, &latency); - params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &latency); - params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &impl->latency_info); - params[n_params++] = spa_process_latency_build(&b, SPA_PARAM_ProcessLatency, &impl->process_latency); - rtp_stream_update_params(impl->stream, params, n_params); -} - static int rtsp_record_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content) { struct impl *impl = data; const char *str; char progress[128]; - struct timespec timeout, interval; + struct spa_process_latency_info process_latency; pw_log_info("record status: %d", status); switch (status) { @@ -886,16 +872,12 @@ static int rtsp_record_reply(void *data, int status, const struct spa_dict *head return 0; } - timeout.tv_sec = 2; - timeout.tv_nsec = 0; - interval.tv_sec = 2; - interval.tv_nsec = 0; - // feedback timer is only needed for auth_setup encryption - if (impl->encryption == CRYPTO_AUTH_SETUP && !impl->feedback_timer) { - - impl->feedback_timer = pw_loop_add_timer(impl->loop, rtsp_do_post_feedback, impl); - pw_loop_update_timer(impl->loop, impl->feedback_timer, &timeout, &interval, false); + if (impl->encryption == CRYPTO_FP_SAP25) { + pw_timer_queue_cancel(&impl->feedback_timer); + pw_timer_queue_add(impl->timer_queue, &impl->feedback_timer, + NULL, 2 * SPA_NSEC_PER_SEC, + rtsp_do_post_feedback, impl); } if ((str = spa_dict_lookup(headers, "Audio-Latency")) != NULL) { @@ -903,14 +885,14 @@ static int rtsp_record_reply(void *data, int status, const struct spa_dict *head if (spa_atou32(str, &l, 0)) impl->latency = SPA_MAX(l, impl->latency); } - impl->process_latency.rate = impl->latency + msec_to_samples(impl, RAOP_LATENCY_MS); + spa_zero(process_latency); + process_latency.rate = impl->latency + msec_to_samples(impl, RAOP_LATENCY_MS); - update_latency(impl); + rtp_stream_update_process_latency(impl->stream, &process_latency); rtp_stream_set_first(impl->stream); impl->sync = 0; - impl->sync_period = impl->rate / (impl->mtu / impl->stride); impl->recording = true; rtsp_send_volume(impl); @@ -1239,6 +1221,7 @@ static int rtsp_do_announce(struct impl *impl) switch (impl->encryption) { case CRYPTO_NONE: + case CRYPTO_FP_SAP25: sdp = spa_aprintf("v=0\r\n" "o=iTunes %s 0 IN IP%d %s\r\n" "s=iTunes\r\n" @@ -1252,6 +1235,7 @@ static int rtsp_do_announce(struct impl *impl) if (!sdp) return -errno; break; + case CRYPTO_AUTH_SETUP: sdp = spa_aprintf("v=0\r\n" "o=iTunes %s 0 IN IP%d %s\r\n" @@ -1331,10 +1315,12 @@ static int rtsp_post_auth_setup_reply(void *data, int status, const struct spa_d static int rtsp_do_post_auth_setup(struct impl *impl) { - static const unsigned char content[33] = - "\x01" - "\x59\x02\xed\xe9\x0d\x4e\xf2\xbd\x4c\xb6\x8a\x63\x30\x03\x82\x07" - "\xa9\x4d\xbd\x50\xd8\xaa\x46\x5b\x5d\x8c\x01\x2a\x0c\x7e\x1d\x4e"; + static const uint8_t content[33] = { + 0x01, + 0x59, 0x02, 0xed, 0xe9, 0x0d, 0x4e, 0xf2, 0xbd, + 0x4c, 0xb6, 0x8a, 0x63, 0x30, 0x03, 0x82, 0x07, + 0xa9, 0x4d, 0xbd, 0x50, 0xd8, 0xaa, 0x46, 0x5b, + 0x5d, 0x8c, 0x01, 0x2a, 0x0c, 0x7e, 0x1d, 0x4e }; return pw_rtsp_client_url_send(impl->rtsp, "/auth-setup", "POST", &impl->headers->dict, "application/octet-stream", content, sizeof(content), @@ -1492,10 +1478,8 @@ static void connection_cleanup(struct impl *impl) close(impl->timing_fd); impl->timing_fd = -1; } - if (impl->feedback_timer != NULL) { - pw_loop_destroy_source(impl->loop, impl->feedback_timer); - impl->feedback_timer = NULL; - } + pw_timer_queue_cancel(&impl->feedback_timer); + free(impl->auth_method); impl->auth_method = NULL; free(impl->realm); @@ -1541,14 +1525,12 @@ static void stream_destroy(void *d) impl->stream = NULL; } -static void stream_state_changed(void *data, bool started, const char *error) +static void stream_report_error(void *data, const char *error) { struct impl *impl = data; - if (error) { pw_log_error("stream error: %s", error); pw_impl_module_schedule_destroy(impl->module); - return; } } @@ -1632,11 +1614,11 @@ static void stream_props_changed(struct impl *impl, uint32_t id, const struct sp case SPA_PROP_channelVolumes: { uint32_t i, n_vols; - float vols[SPA_AUDIO_MAX_CHANNELS], volume; - float soft_vols[SPA_AUDIO_MAX_CHANNELS]; + float vols[MAX_CHANNELS], volume; + float soft_vols[MAX_CHANNELS]; if ((n_vols = spa_pod_copy_array(&prop->value, SPA_TYPE_Float, - vols, SPA_AUDIO_MAX_CHANNELS)) > 0) { + vols, SPA_N_ELEMENTS(vols))) > 0) { volume = 0.0f; for (i = 0; i < n_vols; i++) { volume += vols[i]; @@ -1667,32 +1649,6 @@ static void stream_props_changed(struct impl *impl, uint32_t id, const struct sp rtp_stream_set_param(impl->stream, id, param); } -static void param_latency_changed(struct impl *impl, const struct spa_pod *param) -{ - struct spa_latency_info latency; - - if (param == NULL || spa_latency_parse(param, &latency) < 0) - return; - if (latency.direction == SPA_DIRECTION_OUTPUT) - impl->latency_info = latency; - - update_latency(impl); -} - -static void param_process_latency_changed(struct impl *impl, const struct spa_pod *param) -{ - struct spa_process_latency_info info; - - if (param == NULL) - spa_zero(info); - else if (spa_process_latency_parse(param, &info) < 0) - return; - if (spa_process_latency_info_compare(&impl->process_latency, &info) == 0) - return; - impl->process_latency = info; - update_latency(impl); -} - static void stream_param_changed(void *data, uint32_t id, const struct spa_pod *param) { struct impl *impl = data; @@ -1708,12 +1664,6 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod * if (param != NULL) stream_props_changed(impl, id, param); break; - case SPA_PARAM_Latency: - param_latency_changed(impl, param); - break; - case SPA_PARAM_ProcessLatency: - param_process_latency_changed(impl, param); - break; default: break; } @@ -1722,7 +1672,7 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod * static const struct rtp_stream_events stream_events = { RTP_VERSION_STREAM_EVENTS, .destroy = stream_destroy, - .state_changed = stream_state_changed, + .report_error = stream_report_error, .param_changed = stream_param_changed, .send_packet = stream_send_packet }; @@ -1842,8 +1792,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->module = module; impl->context = context; impl->loop = pw_context_get_main_loop(context); - - impl->latency_info = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); + impl->timer_queue = pw_context_get_timer_queue(context); ip = pw_properties_get(props, "raop.ip"); port = pw_properties_get(props, "raop.port"); @@ -1875,6 +1824,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->encryption = CRYPTO_RSA; else if (spa_streq(str, "auth_setup")) impl->encryption = CRYPTO_AUTH_SETUP; + else if (spa_streq(str, "fp_sap25")) + impl->encryption = CRYPTO_FP_SAP25; else { pw_log_error( "can't handle encryption type %s", str); res = -EINVAL; @@ -1909,8 +1860,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->rate = RAOP_RATE; impl->latency = msec_to_samples(impl, RAOP_LATENCY_MS); impl->stride = RAOP_STRIDE; - impl->mtu = impl->stride * impl->psamples; - impl->sync_period = impl->rate / impl->psamples; if ((str = pw_properties_get(props, "raop.latency.ms")) == NULL) str = SPA_STRINGIFY(DEFAULT_LATENCY_MS); @@ -1936,10 +1885,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true"); if (pw_properties_get(props, PW_KEY_MEDIA_CLASS) == NULL) pw_properties_set(props, PW_KEY_MEDIA_CLASS, "Audio/Sink"); - if (pw_properties_get(props, PW_KEY_MEDIA_FORMAT) == NULL) - pw_properties_setf(props, PW_KEY_MEDIA_FORMAT, "%d", SPA_AUDIO_FORMAT_S16_LE); if (pw_properties_get(props, "net.mtu") == NULL) - pw_properties_setf(props, "net.mtu", "%d", impl->mtu); + pw_properties_set(props, "net.mtu", "1448"); if (pw_properties_get(props, "rtp.sender-ts-offset") == NULL) pw_properties_setf(props, "rtp.sender-ts-offset", "%d", 0); if (pw_properties_get(props, "sess.ts-direct") == NULL) @@ -1955,6 +1902,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_FORMAT); copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_DEVICE_ICON_NAME); copy_props(impl, props, PW_KEY_NODE_NAME); @@ -1975,6 +1923,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, "sess.ts-refclk"); copy_props(impl, props, "sess.ts-direct"); + impl->mtu = pw_properties_get_uint32(impl->props, "net.mtu", 1448); + impl->sync_period = impl->rate / (impl->mtu / impl->stride); impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { str = pw_properties_get(props, PW_KEY_REMOTE_NAME); diff --git a/src/modules/module-raop/rtsp-client.h b/src/modules/module-raop/rtsp-client.h index 1b832cbcc..bfb4a4fe2 100644 --- a/src/modules/module-raop/rtsp-client.h +++ b/src/modules/module-raop/rtsp-client.h @@ -5,14 +5,14 @@ #ifndef PIPEWIRE_RTSP_CLIENT_H #define PIPEWIRE_RTSP_CLIENT_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include +#ifdef __cplusplus +extern "C" { +#endif + struct pw_rtsp_client; struct pw_rtsp_client_events { diff --git a/src/modules/module-roc-sink.c b/src/modules/module-roc-sink.c index dcd736848..7b834e3eb 100644 --- a/src/modules/module-roc-sink.c +++ b/src/modules/module-roc-sink.c @@ -3,12 +3,12 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Sanchayan Maity */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include -#include "config.h" - #include #include #include diff --git a/src/modules/module-roc-source.c b/src/modules/module-roc-source.c index b5a5ea99b..0da4560e7 100644 --- a/src/modules/module-roc-source.c +++ b/src/modules/module-roc-source.c @@ -3,12 +3,12 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Sanchayan Maity */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include -#include "config.h" - #include #include #include diff --git a/src/modules/module-rt.c b/src/modules/module-rt.c index b2d0739cf..2475ca6fc 100644 --- a/src/modules/module-rt.c +++ b/src/modules/module-rt.c @@ -26,6 +26,8 @@ SOFTWARE. ***/ +#include "config.h" + #include #include #include @@ -44,8 +46,6 @@ #include #include -#include "config.h" - #include #include @@ -136,11 +136,14 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic #define REALTIME_POLICY SCHED_FIFO -#ifdef SCHED_RESET_ON_FORK -#define PW_SCHED_RESET_ON_FORK SCHED_RESET_ON_FORK -#else + /* FreeBSD compat */ -#define PW_SCHED_RESET_ON_FORK 0 +#ifndef SCHED_RESET_ON_FORK +#define SCHED_RESET_ON_FORK 0 +#endif + +#ifndef RLIMIT_RTTIME +#define RLIMIT_RTTIME 15 #endif #define MIN_NICE_LEVEL -20 @@ -192,7 +195,7 @@ struct thread { struct impl *impl; struct spa_list link; pthread_t thread; - pid_t pid; + pid_t tid; void *(*start)(void*); void *arg; }; @@ -203,7 +206,7 @@ struct impl { struct spa_thread_utils thread_utils; - pid_t main_pid; + pid_t main_tid; struct rlimit rl; int nice_level; int rt_prio; @@ -240,10 +243,6 @@ struct impl { #endif }; -#ifndef RLIMIT_RTTIME -#define RLIMIT_RTTIME 15 -#endif - static pthread_mutex_t rlimit_lock = PTHREAD_MUTEX_INITIALIZER; static pid_t _gettid(void) @@ -483,6 +482,9 @@ static void module_destroy(void *data) pw_thread_loop_destroy(impl->thread_loop); if (impl->rtkit_bus) pw_rtkit_bus_free(impl->rtkit_bus); + + pthread_cond_destroy(&impl->cond); + pthread_mutex_destroy(&impl->lock); #endif free(impl); @@ -567,8 +569,8 @@ static bool check_realtime_privileges(struct impl *impl) spa_zero(new_sched_params); new_sched_params.sched_priority = SPA_CLAMP((int)priority, min, max); new_policy = REALTIME_POLICY; - if ((old_policy & PW_SCHED_RESET_ON_FORK) != 0) - new_policy |= PW_SCHED_RESET_ON_FORK; + if ((old_policy & SCHED_RESET_ON_FORK) != 0) + new_policy |= SCHED_RESET_ON_FORK; /* Disable RLIMIT_RTTIME in a thread safe way and hope that the application * doesn't also set RLIMIT_RTTIME while trying new_policy. */ @@ -595,14 +597,6 @@ static bool check_realtime_privileges(struct impl *impl) return ret; } -static int sched_set_nice(pid_t pid, int nice_level) -{ - if (setpriority(PRIO_PROCESS, pid, nice_level) == 0) - return 0; - else - return -errno; -} - static int set_nice(struct impl *impl, int nice_level, bool warn) { int res = 0; @@ -614,12 +608,14 @@ static int set_nice(struct impl *impl, int nice_level, bool warn) nice_level, impl->min_nice_level); nice_level = impl->min_nice_level; } - res = pw_rtkit_make_high_priority(impl, impl->main_pid, nice_level); + res = pw_rtkit_make_high_priority(impl, impl->main_tid, nice_level); } else #endif - if (impl->rlimits_enabled) - res = sched_set_nice(impl->main_pid, nice_level); + if (impl->rlimits_enabled) { + if (setpriority(PRIO_PROCESS, impl->main_tid, nice_level) < 0) + res = -errno; + } else res = -ENOTSUP; @@ -627,9 +623,6 @@ static int set_nice(struct impl *impl, int nice_level, bool warn) if (warn) pw_log_warn("could not set nice-level to %d: %s", nice_level, spa_strerror(res)); - } else if (res > 0) { - pw_log_info("main thread setting nice level to %d: %s", - nice_level, spa_strerror(-res)); } else { pw_log_info("main thread nice level set to %d", nice_level); @@ -673,7 +666,7 @@ static int acquire_rt_sched(struct spa_thread *thread, int priority) spa_zero(sp); sp.sched_priority = priority; - if ((err = pthread_setschedparam(pt, REALTIME_POLICY | PW_SCHED_RESET_ON_FORK, &sp)) != 0) { + if ((err = pthread_setschedparam(pt, REALTIME_POLICY | SCHED_RESET_ON_FORK, &sp)) != 0) { pw_log_warn("could not make thread %p realtime: %s", thread, strerror(err)); return -err; } @@ -689,7 +682,7 @@ static int impl_drop_rt_generic(void *object, struct spa_thread *thread) int err; spa_zero(sp); - if ((err = pthread_setschedparam(pt, SCHED_OTHER | PW_SCHED_RESET_ON_FORK, &sp)) != 0) { + if ((err = pthread_setschedparam(pt, SCHED_OTHER | SCHED_RESET_ON_FORK, &sp)) != 0) { pw_log_debug("thread %p: SCHED_OTHER|SCHED_RESET_ON_FORK failed: %s", thread, strerror(err)); return -err; @@ -716,7 +709,7 @@ static void *custom_start(void *data) struct impl *impl = this->impl; pthread_mutex_lock(&impl->lock); - this->pid = _gettid(); + this->tid = _gettid(); pthread_cond_broadcast(&impl->cond); pthread_mutex_unlock(&impl->lock); @@ -762,7 +755,7 @@ static int impl_join(void *object, struct spa_thread *thread, void **retval) struct thread *thr; int res; - res = pthread_join(pt, retval); + res = pw_thread_utils_join(thread, retval); pthread_mutex_lock(&impl->lock); if ((thr = find_thread_by_pt(impl, pt)) != NULL) { @@ -775,7 +768,7 @@ static int impl_join(void *object, struct spa_thread *thread, void **retval) } -static int get_rtkit_priority_range(struct impl *impl, int *min, int *max) +static void get_rtkit_priority_range(struct impl *impl, int *min, int *max) { if (min) *min = 1; @@ -784,23 +777,22 @@ static int get_rtkit_priority_range(struct impl *impl, int *min, int *max) if (*max < 1) *max = 1; } - return 0; } static int impl_get_rt_range(void *object, const struct spa_dict *props, int *min, int *max) { struct impl *impl = object; - int res; + int res = 0; if (impl->use_rtkit) - res = get_rtkit_priority_range(impl, min, max); + get_rtkit_priority_range(impl, min, max); else res = get_rt_priority_range(min, max); return res; } struct rt_params { - pid_t pid; + pid_t tid; int priority; }; @@ -810,12 +802,11 @@ static int do_make_realtime(struct spa_loop *loop, bool async, uint32_t seq, struct impl *impl = user_data; const struct rt_params *params = data; int err, min, max, priority = params->priority; - pid_t pid = params->pid; + pid_t pid = params->tid; pw_log_debug("rtkit realtime"); - if ((err = get_rtkit_priority_range(impl, &min, &max)) < 0) - return err; + get_rtkit_priority_range(impl, &min, &max); if (priority < min || priority > max) { pw_log_info("clamping requested priority %d for thread %d " @@ -848,20 +839,21 @@ static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority struct thread *thr; spa_zero(sp); - if (pthread_setschedparam(pt, SCHED_OTHER | PW_SCHED_RESET_ON_FORK, &sp) == 0) { + if (pthread_setschedparam(pt, SCHED_OTHER | SCHED_RESET_ON_FORK, &sp) == 0) { pw_log_debug("SCHED_OTHER|SCHED_RESET_ON_FORK worked."); } - params.priority = priority; - pthread_mutex_lock(&impl->lock); - if ((thr = find_thread_by_pt(impl, pt)) != NULL) - params.pid = thr->pid; - else - params.pid = _gettid(); + if ((thr = find_thread_by_pt(impl, pt)) != NULL) { + params.priority = priority; + params.tid = thr->tid; - res = pw_loop_invoke(pw_thread_loop_get_loop(impl->thread_loop), + res = pw_loop_invoke(pw_thread_loop_get_loop(impl->thread_loop), do_make_realtime, 0, ¶ms, sizeof(params), false, impl); + } + else { + res = -ESRCH; + } pthread_mutex_unlock(&impl->lock); return res; @@ -870,15 +862,6 @@ static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority } } -static const struct spa_thread_utils_methods impl_thread_utils = { - SPA_VERSION_THREAD_UTILS_METHODS, - .create = impl_create, - .join = impl_join, - .get_rt_range = impl_get_rt_range, - .acquire_rt = impl_acquire_rt, - .drop_rt = impl_drop_rt_generic, -}; - #else /* HAVE_DBUS */ static struct spa_thread *impl_create(void *object, const struct spa_dict *props, @@ -909,6 +892,8 @@ static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority return acquire_rt_sched(thread, priority); } +#endif /* HAVE_DBUS */ + static const struct spa_thread_utils_methods impl_thread_utils = { SPA_VERSION_THREAD_UTILS_METHODS, .create = impl_create, @@ -917,23 +902,19 @@ static const struct spa_thread_utils_methods impl_thread_utils = { .acquire_rt = impl_acquire_rt, .drop_rt = impl_drop_rt_generic, }; -#endif /* HAVE_DBUS */ - #ifdef HAVE_DBUS -static int check_rtkit(struct impl *impl, struct pw_context *context, bool *can_use_rtkit) +static bool check_rtkit(struct pw_context *context) { const struct pw_properties *context_props; const char *str; - *can_use_rtkit = true; - if ((context_props = pw_context_get_properties(context)) != NULL && (str = pw_properties_get(context_props, "support.dbus")) != NULL && !pw_properties_parse_bool(str)) - *can_use_rtkit = false; + return false; - return 0; + return true; } static int rtkit_get_bus(struct impl *impl) @@ -1024,7 +1005,8 @@ static int do_rtkit_setup(struct spa_loop *loop, bool async, uint32_t seq, } #endif /* HAVE_DBUS */ -static int set_uclamp(int uclamp_min, int uclamp_max, pid_t pid) { +static int set_uclamp(int uclamp_min, int uclamp_max, pid_t pid) +{ #ifdef __linux__ int ret; struct sched_attr { @@ -1040,7 +1022,7 @@ static int set_uclamp(int uclamp_min, int uclamp_max, pid_t pid) { uint32_t sched_util_max; } attr; - ret = syscall(SYS_sched_getattr, pid, &attr, sizeof(struct sched_attr), 0); + ret = syscall(SYS_sched_getattr, pid, &attr, sizeof(attr), 0); if (ret) { pw_log_warn("Could not retrieve scheduler attributes: %d", -errno); return -errno; @@ -1073,7 +1055,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) { struct pw_context *context = pw_impl_module_get_context(module); struct impl *impl; - struct pw_properties *props; int res = 0; PW_LOG_TOPIC_INIT(mod_topic); @@ -1084,7 +1065,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_log_debug("module %p: new", impl); - props = args ? pw_properties_new_string(args) : pw_properties_new(NULL, NULL); + spa_autoptr(pw_properties) props = args ? pw_properties_new_string(args) : pw_properties_new(NULL, NULL); if (!props) { res = -errno; goto error; @@ -1104,7 +1085,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->rl.rlim_cur = impl->rt_time_soft; impl->rl.rlim_max = impl->rt_time_hard; - impl->main_pid = _gettid(); + impl->main_tid = _gettid(); bool can_use_rtkit = false, use_rtkit = false; @@ -1119,9 +1100,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pthread_mutex_init(&impl->lock, NULL); pthread_cond_init(&impl->cond, NULL); - if ((res = check_rtkit(impl, context, &can_use_rtkit)) < 0) - goto error; - + can_use_rtkit = check_rtkit(context); #endif /* If the user has permissions to use regular realtime scheduling, as well as * the nice level we want, then we'll use that instead of RTKit */ @@ -1148,7 +1127,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) } if (impl->uclamp_min || impl->uclamp_max < 1024) - set_uclamp(impl->uclamp_min, impl->uclamp_max, impl->main_pid); + set_uclamp(impl->uclamp_min, impl->uclamp_max, impl->main_tid); #ifdef HAVE_DBUS impl->use_rtkit = use_rtkit; @@ -1174,12 +1153,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) do_rtkit_setup, 0, NULL, 0, false, impl); pw_log_debug("initialized using RTKit"); - } else { + } else +#endif + { pw_log_debug("initialized using regular realtime scheduling"); } -#else - pw_log_debug("initialized using regular realtime scheduling"); -#endif impl->thread_utils.iface = SPA_INTERFACE_INIT( SPA_TYPE_INTERFACE_ThreadUtils, @@ -1194,7 +1172,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); pw_impl_module_update_properties(module, &props->dict); - goto done; + return 0; error: #ifdef HAVE_DBUS @@ -1204,8 +1182,6 @@ error: pw_rtkit_bus_free(impl->rtkit_bus); #endif free(impl); -done: - pw_properties_free(props); return res; } diff --git a/src/modules/module-rtp-sap.c b/src/modules/module-rtp-sap.c index 9eeb85024..27df24816 100644 --- a/src/modules/module-rtp-sap.c +++ b/src/modules/module-rtp-sap.c @@ -156,6 +156,7 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define DEFAULT_LOOP false #define MAX_SDP 2048 +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS #define USAGE "( local.ifname= ) " \ "( sap.ip= ) " \ @@ -247,10 +248,21 @@ struct node { struct session *session; }; +struct igmp_recovery { + struct pw_timer timer; + int socket_fd; + struct sockaddr_storage mcast_addr; + socklen_t mcast_len; + uint32_t if_index; + bool is_ipv6; + uint32_t deadline; +}; + struct impl { struct pw_properties *props; struct pw_loop *loop; + struct pw_timer_queue *timer_queue; struct pw_impl_module *module; struct spa_hook module_listener; @@ -263,7 +275,11 @@ struct impl { struct pw_registry *registry; struct spa_hook registry_listener; - struct spa_source *timer; + struct pw_timer sap_send_timer; + + /* This timer is used when the first start_sap() call fails because + * of an ENODEV error (see the start_sap() code for details) */ + struct pw_timer start_sap_retry_timer; char *ifname; uint32_t ttl; @@ -279,6 +295,10 @@ struct impl { struct spa_source *sap_source; uint32_t cleanup_interval; + /* IGMP recovery (triggers when no SAP packets are + * received after the recovery deadline is reached) */ + struct igmp_recovery igmp_recovery; + uint32_t max_sessions; uint32_t n_sessions; struct spa_list sessions; @@ -286,7 +306,7 @@ struct impl { char *extra_attrs_preamble; char *extra_attrs_end; - char *ptp_mgmt_socket; + char *ptp_mgmt_socket_path; int ptp_fd; uint32_t ptp_seq; uint8_t clock_id[8]; @@ -320,6 +340,7 @@ static const struct format_info *find_audio_format_info(const char *mime) return NULL; } +static int start_sap(struct impl *impl); static int send_sap(struct impl *impl, struct session *sess, bool bye); @@ -381,7 +402,7 @@ static bool is_multicast(struct sockaddr *sa, socklen_t salen) return false; } -static int make_unix_socket(const char *path) { +static int make_unix_ptp_mgmt_socket(const char *path) { struct sockaddr_un addr; spa_autoclose int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); @@ -417,7 +438,7 @@ static int make_send_socket( af = src->ss_family; if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) { - pw_log_error("socket failed: %m"); + pw_log_error("socket() failed: %m"); return -errno; } if (bind(fd, (struct sockaddr*)src, src_len) < 0) { @@ -449,6 +470,9 @@ static int make_send_socket( pw_log_warn("setsockopt(IPV6_MULTICAST_HOPS) failed: %m"); } } + + pw_log_info("sender socket up and running"); + return fd; error: close(fd); @@ -456,7 +480,7 @@ error: } static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen, - char *ifname) + char *ifname, struct igmp_recovery *igmp_recovery) { int af, fd, val, res; struct ifreq req; @@ -466,13 +490,13 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen, af = sa->ss_family; if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) { - pw_log_error("socket failed: %m"); + pw_log_error("socket() failed: %m"); return -errno; } val = 1; if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) { res = -errno; - pw_log_error("setsockopt failed: %m"); + pw_log_error("setsockopt() failed: %m"); goto error; } spa_zero(req); @@ -526,6 +550,16 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen, goto error; } + /* Store multicast info for recovery */ + igmp_recovery->socket_fd = fd; + igmp_recovery->mcast_addr = ba; + igmp_recovery->mcast_len = salen; + igmp_recovery->if_index = req.ifr_ifindex; + igmp_recovery->is_ipv6 = (af == AF_INET6); + pw_log_debug("stored %s multicast info: socket_fd=%d, " + "if_index=%d", igmp_recovery->is_ipv6 ? + "IPv6" : "IPv4", fd, req.ifr_ifindex); + if (bind(fd, (struct sockaddr*)&ba, salen) < 0) { res = -errno; pw_log_error("bind() failed: %m"); @@ -538,16 +572,24 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen, goto error; } } + + pw_log_info("receiver socket up and running"); + return fd; error: close(fd); return res; } -static void update_ts_refclk(struct impl *impl) +static bool update_ts_refclk(struct impl *impl) { - if (!impl->ptp_mgmt_socket || impl->ptp_fd < 0) - return; + if (!impl->ptp_mgmt_socket_path) + return false; + if (impl->ptp_fd < 0) { + impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path); + if (impl->ptp_fd < 0) + return false; + } // Read if something is left in the socket int avail; @@ -579,13 +621,19 @@ static void update_ts_refclk(struct impl *impl) if (write(impl->ptp_fd, &req, sizeof(req)) == -1) { pw_log_warn("Failed to send PTP management request: %m"); - return; + if (errno != ENOTCONN) + return false; + close(impl->ptp_fd); + impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path); + if (impl->ptp_fd > -1) + pw_log_info("Reopened PTP management socket"); + return false; } uint8_t buf[sizeof(struct ptp_management_msg) + sizeof(struct ptp_parent_data_set)]; if (read(impl->ptp_fd, &buf, sizeof(buf)) == -1) { pw_log_warn("Failed to receive PTP management response: %m"); - return; + return false; } struct ptp_management_msg res = *(struct ptp_management_msg *)buf; @@ -594,27 +642,27 @@ static void update_ts_refclk(struct impl *impl) if ((res.ver & 0x0f) != 2) { pw_log_warn("PTP major version is %d, expected 2", res.ver); - return; + return false; } if ((res.major_sdo_id_message_type & 0x0f) != PTP_MESSAGE_TYPE_MANAGEMENT) { pw_log_warn("PTP management returned type %x, expected management", res.major_sdo_id_message_type); - return; + return false; } if (res.action != PTP_MGMT_ACTION_RESPONSE) { pw_log_warn("PTP management returned action %d, expected response", res.action); - return; + return false; } if (be16toh(res.tlv_type_be) != PTP_TLV_TYPE_MGMT) { pw_log_warn("PTP management returned tlv type %d, expected management", be16toh(res.tlv_type_be)); - return; + return false; } if (be16toh(res.management_id_be) != PTP_MGMT_ID_PARENT_DATA_SET) { pw_log_warn("PTP management returned ID %d, expected PARENT_DATA_SET", be16toh(res.management_id_be)); - return; + return false; } uint16_t data_len = be16toh(res.management_message_length_be) - 2; @@ -637,7 +685,8 @@ static void update_ts_refclk(struct impl *impl) ); uint8_t *gmid = parent.gm_clock_id; - if (memcmp(gmid, impl->gm_id, 8) != 0) + bool gmid_changed = false; + if (memcmp(gmid, impl->gm_id, 8) != 0) { pw_log_info( "GM ID: IEEE1588-2008:%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X:%d", gmid[0], @@ -650,23 +699,35 @@ static void update_ts_refclk(struct impl *impl) gmid[7], 0 /* domain */ ); + gmid_changed = true; + } // When GM is not equal to own clock we are clocked by external master pw_log_debug("Synced to GM: %s", (memcmp(cid, gmid, 8) != 0) ? "true" : "false"); memcpy(impl->clock_id, cid, 8); memcpy(impl->gm_id, gmid, 8); + return gmid_changed; } -static int make_sdp(struct impl *impl, struct session *sess, char *buffer, size_t buffer_size) +static uint16_t generate_hash(uint16_t prev) { - char src_addr[64], dst_addr[64], dst_ttl[8]; + uint16_t hash = pw_rand32(); + if (hash == prev) hash++; + if (hash == 0) hash++; + return hash; +} + +static int make_sdp(struct impl *impl, struct session *sess, char *buffer, size_t buffer_size, bool new) +{ + char src_addr[64], dst_addr[64], dst_ttl[8], ptime[32]; struct sdp_info *sdp = &sess->info; bool src_ip4, dst_ip4; bool multicast; - const char *user_name; + const char *user_name, *str; struct spa_strbuf buf; int res; + struct pw_properties *props = sess->props; if ((res = pw_net_get_ip(&impl->src_addr, src_addr, sizeof(src_addr), &src_ip4, NULL)) < 0) return res; @@ -674,6 +735,30 @@ static int make_sdp(struct impl *impl, struct session *sess, char *buffer, size_ if ((res = pw_net_get_ip(&sdp->dst_addr, dst_addr, sizeof(dst_addr), &dst_ip4, NULL)) < 0) return res; + if (new) { + /* update the version and hash */ + sdp->hash = generate_hash(sdp->hash); + if ((str = pw_properties_get(props, "sess.id")) != NULL) { + if (!spa_atou32(str, &sdp->session_id, 10)) { + pw_log_error("Invalid session id: %s (must be a uint32)", str); + return -EINVAL; + } + sdp->t_ntp = pw_properties_get_uint32(props, "rtp.ntp", + (uint32_t) time(NULL) + 2208988800U + impl->n_sessions); + } else { + sdp->session_id = (uint32_t) time(NULL) + 2208988800U + impl->n_sessions; + sdp->t_ntp = pw_properties_get_uint32(props, "rtp.ntp", sdp->session_id); + } + if ((str = pw_properties_get(props, "sess.version")) != NULL) { + if (!spa_atou32(str, &sdp->session_version, 10)) { + pw_log_error("Invalid session version: %s (must be a uint32)", str); + return -EINVAL; + } + } else { + sdp->session_version = sdp->t_ntp; + } + } + if ((user_name = pw_get_user_name()) == NULL) user_name = "-"; @@ -738,7 +823,7 @@ static int make_sdp(struct impl *impl, struct session *sess, char *buffer, size_ if (sdp->ptime > 0) spa_strbuf_append(&buf, - "a=ptime:%.6g\n", sdp->ptime); + "a=ptime:%s\n", spa_dtoa(ptime, sizeof(ptime), sdp->ptime)); if (sdp->framecount > 0) spa_strbuf_append(&buf, @@ -835,7 +920,7 @@ static int send_sap(struct impl *impl, struct session *sess, bool bye) * socket needs to be open for us to get the interface address (which * happens above. So let's create the SDP now, if needed. */ if (!sess->has_sdp) { - res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp)); + res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp), true); if (res != 0) { pw_log_error("Failed to create SDP: %s", spa_strerror(res)); return res; @@ -883,18 +968,121 @@ static int send_sap(struct impl *impl, struct session *sess, bool bye) return res; } -static void on_timer_event(void *data, uint64_t expirations) +static void on_igmp_recovery_timer_event(void *data) +{ + struct impl *impl = data; + char addr[128]; + int res = 0; + + /* Only attempt recovery if we have a valid socket and multicast address */ + if (impl->igmp_recovery.socket_fd < 0) { + pw_log_debug("no socket, skipping IGMP recovery"); + goto finish; + } + + pw_net_get_ip(&impl->igmp_recovery.mcast_addr, addr, sizeof(addr), NULL, NULL); + pw_log_info("IGMP recovery triggered for %s", addr); + + /* Force IGMP membership refresh by leaving the group first, then rejoin */ + if (impl->igmp_recovery.is_ipv6) { + struct ipv6_mreq mr6; + memset(&mr6, 0, sizeof(mr6)); + mr6.ipv6mr_multiaddr = ((struct sockaddr_in6*)&impl->igmp_recovery.mcast_addr)->sin6_addr; + mr6.ipv6mr_interface = impl->igmp_recovery.if_index; + + /* Leave the group first */ + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP, + &mr6, sizeof(mr6)); + if (SPA_LIKELY(res == 0)) { + pw_log_info("left IPv6 multicast group"); + } else { + if (errno == EADDRNOTAVAIL) { + pw_log_info("attempted to leave IPv6 multicast group, but " + "membership was already silently dropped"); + } else { + pw_log_warn("failed to leave IPv6 multicast group: %m"); + } + } + + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, + &mr6, sizeof(mr6)); + if (res < 0) { + pw_log_warn("failed to re-join IPv6 multicast group: %m"); + } else { + pw_log_info("re-joined IPv6 multicast group successfully"); + } + } else { + struct ip_mreqn mr4; + memset(&mr4, 0, sizeof(mr4)); + mr4.imr_multiaddr = ((struct sockaddr_in*)&impl->igmp_recovery.mcast_addr)->sin_addr; + mr4.imr_ifindex = impl->igmp_recovery.if_index; + + /* Leave the group first */ + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, + &mr4, sizeof(mr4)); + if (SPA_LIKELY(res == 0)) { + pw_log_info("left IPv4 multicast group"); + } else { + if (errno == EADDRNOTAVAIL) { + pw_log_info("attempted to leave IPv4 multicast group, but " + "membership was already silently dropped"); + } else { + pw_log_warn("failed to leave IPv4 multicast group: %m"); + } + } + + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, + &mr4, sizeof(mr4)); + if (res < 0) { + pw_log_warn("failed to re-join IPv4 multicast group: %m"); + } else { + pw_log_info("re-joined IPv4 multicast group successfully"); + } + } + +finish: + /* If rejoining failed, try again in 1 second. This can happen + * for example when the network interface went down, and is not + * yet up and running again, and ENODEV is returned as a result. + * Otherwise, continue with the usual deadline. */ + pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer, + &impl->igmp_recovery.timer.timeout, + ((res == 0) ? impl->igmp_recovery.deadline : 1) * SPA_NSEC_PER_SEC, + on_igmp_recovery_timer_event, impl); +} + +static void rearm_igmp_recovery_timer(struct impl *impl) +{ + pw_timer_queue_cancel(&impl->igmp_recovery.timer); + pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer, + NULL, impl->igmp_recovery.deadline * SPA_NSEC_PER_SEC, + on_igmp_recovery_timer_event, impl); +} + +static void on_sap_send_timer_event(void *data) { struct impl *impl = data; struct session *sess, *tmp; uint64_t timestamp, interval; + bool clk_changed; + int res; timestamp = get_time_nsec(impl); interval = impl->cleanup_interval * SPA_NSEC_PER_SEC; - update_ts_refclk(impl); + clk_changed = update_ts_refclk(impl); spa_list_for_each_safe(sess, tmp, &impl->sessions, link) { if (sess->announce) { + if (clk_changed) { + // The clock has changed: Send bye and create new SDP. + send_sap(impl, sess, 1); + + res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp), true); + if (res != 0) + pw_log_error("Failed to create SDP: %s", spa_strerror(res)); + else + sess->has_sdp = true; + } send_sap(impl, sess, 0); } else { if (sess->timestamp + interval < timestamp) { @@ -905,6 +1093,16 @@ static void on_timer_event(void *data, uint64_t expirations) } } + pw_timer_queue_add(impl->timer_queue, &impl->sap_send_timer, + &impl->sap_send_timer.timeout, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC, + on_sap_send_timer_event, impl); +} + +static void on_start_sap_retry_timer_event(void *data) +{ + struct impl *impl = data; + pw_log_debug("trying again to start SAP send after previous attempt failed with ENODEV"); + start_sap(impl); } static struct session *session_find(struct impl *impl, const struct sdp_info *info) @@ -912,6 +1110,7 @@ static struct session *session_find(struct impl *impl, const struct sdp_info *in struct session *sess; spa_list_for_each(sess, &impl->sessions, link) { if (info->hash == sess->info.hash && + info->dst_port == sess->info.dst_port && spa_streq(info->origin, sess->info.origin)) return sess; } @@ -1021,42 +1220,19 @@ static struct session *session_new_announce(struct impl *impl, struct node *node /* see if we can make an SDP, will fail for the first session because we * haven't got the SAP socket open yet */ - res = make_sdp(impl, sess, buffer, sizeof(buffer)); + res = make_sdp(impl, sess, buffer, sizeof(buffer), false); /* we had no sdp or something changed */ - if (res == 0 && (!sess->has_sdp || strcmp(buffer, sess->sdp) != 0)) { + if (!sess->has_sdp || ((res == 0) && strcmp(buffer, sess->sdp) != 0)) { /* send bye on the old session */ send_sap(impl, sess, 1); - /* update the version and hash */ - sdp->hash = pw_rand32(); - if ((str = pw_properties_get(props, "sess.id")) != NULL) { - if (!spa_atou32(str, &sdp->session_id, 10)) { - pw_log_error("Invalid session id: %s (must be a uint32)", str); - goto error_free; - } - sdp->t_ntp = pw_properties_get_uint32(props, "rtp.ntp", - (uint32_t) time(NULL) + 2208988800U + impl->n_sessions); - } else { - sdp->session_id = (uint32_t) time(NULL) + 2208988800U + impl->n_sessions; - sdp->t_ntp = pw_properties_get_uint32(props, "rtp.ntp", sdp->session_id); - } - if ((str = pw_properties_get(props, "sess.version")) != NULL) { - if (!spa_atou32(str, &sdp->session_version, 10)) { - pw_log_error("Invalid session version: %s (must be a uint32)", str); - goto error_free; - } - } else { - sdp->session_version = sdp->t_ntp; - } - /* make an updated SDP for sending, this should not actually fail */ - res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp)); - - if (res == 0) - sess->has_sdp = true; - else + res = make_sdp(impl, sess, sess->sdp, sizeof(sess->sdp), true); + if (res != 0) pw_log_error("Failed to create SDP: %s", spa_strerror(res)); + else + sess->has_sdp = true; } send_sap(impl, sess, 0); @@ -1373,7 +1549,7 @@ static int parse_sdp_i(struct impl *impl, char *c, struct sdp_info *info) c[strcspn(c, " ")] = '\0'; uint32_t channels; - if (sscanf(c, "%u", &channels) != 1 || channels <= 0 || channels > SPA_AUDIO_MAX_CHANNELS) + if (sscanf(c, "%u", &channels) != 1 || channels <= 0 || channels > MAX_CHANNELS) return 0; c += strcspn(c, "\0"); @@ -1478,6 +1654,8 @@ static int parse_sdp(struct impl *impl, char *sdp, struct sdp_info *info) int count = 0, res = 0; size_t l; + spa_zero(*info); + while (*s) { if ((l = strcspn(s, "\r\n")) < 2) goto too_short; @@ -1523,12 +1701,15 @@ static int parse_sdp(struct impl *impl, char *sdp, struct sdp_info *info) return 0; too_short: pw_log_warn("SDP: line starting with `%.6s...' too short", s); + clear_sdp_info(info); return -EINVAL; invalid_version: pw_log_warn("SDP: invalid first version line `%*s'", (int)l, s); + clear_sdp_info(info); return -EINVAL; error: pw_log_warn("SDP: error: %s", spa_strerror(res)); + clear_sdp_info(info); return res; } @@ -1570,7 +1751,6 @@ static int parse_sap(struct impl *impl, void *data, size_t len) pw_log_debug("got SAP: %s %s", mime, sdp); - spa_zero(info); if ((res = parse_sdp(impl, sdp, &info)) < 0) return res; @@ -1610,30 +1790,70 @@ on_sap_io(void *data, int fd, uint32_t mask) buffer[len] = 0; if ((res = parse_sap(impl, buffer, len)) < 0) pw_log_warn("error parsing SAP: %s", spa_strerror(res)); + + rearm_igmp_recovery_timer(impl); } } static int start_sap(struct impl *impl) { - int fd = -1, res; - struct timespec value, interval; + int fd = -1, res = 0; char addr[128] = "invalid"; - pw_log_info("starting SAP timer"); - impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl); - if (impl->timer == NULL) { - res = -errno; - pw_log_error("can't create timer source: %m"); + pw_log_info("starting SAP send timer"); + /* start_sap() might be called more than once. See the make_recv_socket() + * call below for why that can happen. In such a case, the timer was + * started already. The easiest way of handling it is to just cancel it. + * Such cases are not expected to occur often, so canceling and then + * adding the timer again is acceptable. */ + pw_timer_queue_cancel(&impl->sap_send_timer); + if ((res = pw_timer_queue_add(impl->timer_queue, &impl->sap_send_timer, + NULL, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC, + on_sap_send_timer_event, impl)) < 0) { + pw_log_error("can't add SAP send timer: %s", spa_strerror(res)); goto error; } - value.tv_sec = 0; - value.tv_nsec = 1; - interval.tv_sec = SAP_INTERVAL_SEC; - interval.tv_nsec = 0; - pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false); + if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname, + &(impl->igmp_recovery))) < 0) { + /* If make_recv_socket() tries to create a socket and join to a multicast + * group while the network interfaces are not ready yet to do so + * (usually because a network manager component is still setting up + * those network interfaces), ENODEV will be returned. This is essentially + * a race condition. There is no discernible way to be notified when the + * network interfaces are ready for that operation, so the next best + * approach is to essentially do a form of polling by retrying the + * start_sap() call after some time. The start_sap_retry_timer exists + * precisely for that purpose. This means that ENODEV is not treated as + * an error, but instead, it triggers the creation of that timer. */ + if (fd == -ENODEV) { + pw_log_warn("failed to create receiver socket because network device " + "is not ready and present yet; will try again"); - if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname)) < 0) - return fd; + pw_timer_queue_cancel(&impl->start_sap_retry_timer); + /* Use a 1-second retry interval. The network interfaces + * are likely to be up and running then. */ + pw_timer_queue_add(impl->timer_queue, &impl->start_sap_retry_timer, + NULL, 1 * SPA_NSEC_PER_SEC, + on_start_sap_retry_timer_event, impl); + + /* It is important to return 0 in this case. Otherwise, the nonzero return + * value will later be propagated through the core as an error. */ + res = 0; + goto finish; + } else { + pw_log_error("failed to create socket: %s", spa_strerror(-fd)); + /* If ENODEV was returned earlier, and the start_sap_retry_timer + * was consequently created, but then a non-ENODEV error occurred, + * the timer must be stopped and removed. */ + pw_timer_queue_cancel(&impl->start_sap_retry_timer); + res = fd; + goto error; + } + } + + /* Cleanup the timer in case ENODEV occurred earlier, and this time, + * the socket creation succeeded. */ + pw_timer_queue_cancel(&impl->start_sap_retry_timer); pw_net_get_ip(&impl->sap_addr, addr, sizeof(addr), NULL, NULL); pw_log_info("starting SAP listener on %s", addr); @@ -1644,11 +1864,15 @@ static int start_sap(struct impl *impl) goto error; } - return 0; + rearm_igmp_recovery_timer(impl); + +finish: + return res; + error: if (fd > 0) close(fd); - return res; + goto finish; } static void node_event_info(void *data, const struct pw_node_info *info) @@ -1778,8 +2002,9 @@ static void impl_destroy(struct impl *impl) if (impl->core && impl->do_disconnect) pw_core_disconnect(impl->core); - if (impl->timer) - pw_loop_destroy_source(impl->loop, impl->timer); + pw_timer_queue_cancel(&impl->sap_send_timer); + pw_timer_queue_cancel(&impl->start_sap_retry_timer); + pw_timer_queue_cancel(&impl->igmp_recovery.timer); if (impl->sap_source) pw_loop_destroy_source(impl->loop, impl->sap_source); @@ -1793,7 +2018,7 @@ static void impl_destroy(struct impl *impl) free(impl->extra_attrs_preamble); free(impl->extra_attrs_end); - free(impl->ptp_mgmt_socket); + free(impl->ptp_mgmt_socket_path); free(impl->ifname); free(impl); } @@ -1843,8 +2068,12 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) return -errno; impl->sap_fd = -1; + impl->ptp_fd = -1; spa_list_init(&impl->sessions); + impl->igmp_recovery.socket_fd = -1; + impl->igmp_recovery.if_index = -1; + if (args == NULL) args = ""; @@ -1858,16 +2087,17 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->module = module; impl->loop = pw_context_get_main_loop(context); + impl->timer_queue = pw_context_get_timer_queue(context); str = pw_properties_get(props, "local.ifname"); impl->ifname = str ? strdup(str) : NULL; str = pw_properties_get(props, "ptp.management-socket"); - impl->ptp_mgmt_socket = str ? strdup(str) : NULL; + impl->ptp_mgmt_socket_path = str ? strdup(str) : NULL; // TODO: support UDP management access as well - if (impl->ptp_mgmt_socket) - impl->ptp_fd = make_unix_socket(impl->ptp_mgmt_socket); + if (impl->ptp_mgmt_socket_path) + impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path); if ((str = pw_properties_get(props, "sap.ip")) == NULL) str = DEFAULT_SAP_IP; @@ -1879,6 +2109,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->cleanup_interval = pw_properties_get_uint32(impl->props, "sap.cleanup.sec", DEFAULT_CLEANUP_SEC); + /* We will use half of the cleanup interval for IGMP deadline, minimum 1 second */ + impl->igmp_recovery.deadline = SPA_MAX(impl->cleanup_interval / 2, 1u); + pw_log_info("using IGMP deadline of %" PRIu32 " second(s)", + impl->igmp_recovery.deadline); + impl->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL); impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP); impl->max_sessions = pw_properties_get_uint32(props, "sap.max-sessions", DEFAULT_MAX_SESSIONS); diff --git a/src/modules/module-rtp-session.c b/src/modules/module-rtp-session.c index ab5e1d8a7..63470c539 100644 --- a/src/modules/module-rtp-session.c +++ b/src/modules/module-rtp-session.c @@ -82,6 +82,7 @@ * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_NAME * - \ref PW_KEY_NODE_DESCRIPTION @@ -148,6 +149,7 @@ PW_LOG_TOPIC(mod_topic, "mod." NAME); "( audio.rate= ) " \ "( audio.channels= ) "\ "( audio.position= ) " \ + "( audio.layout= ) " \ "( stream.props= { key=value ... } ) " static const struct spa_dict_item module_info[] = { @@ -204,7 +206,7 @@ struct session { #define SESSION_STATE_ESTABLISHED 4 int state; int ck_count; - uint64_t next_time; + struct pw_timer timer; uint32_t ctrl_initiator; uint32_t data_initiator; @@ -237,15 +239,13 @@ struct impl { struct pw_loop *loop; struct pw_loop *data_loop; + struct pw_timer_queue *timer_queue; struct pw_core *core; struct spa_hook core_listener; struct spa_hook core_proxy_listener; unsigned int do_disconnect:1; - struct spa_source *timer; - uint64_t next_time; - struct spa_source *ctrl_source; struct spa_source *data_source; @@ -276,34 +276,19 @@ static ssize_t send_packet(int fd, struct msghdr *msg) return n; } -static uint64_t current_time_ns(void) +static uint64_t current_time_ns(struct timespec *ts) { - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return SPA_TIMESPEC_TO_NSEC(&ts); + clock_gettime(CLOCK_MONOTONIC, ts); + return SPA_TIMESPEC_TO_NSEC(ts); } -static void set_timeout(struct impl *impl, uint64_t time) -{ - struct itimerspec ts; - ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; - ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; - ts.it_interval.tv_sec = 0; - ts.it_interval.tv_nsec = 0; - pw_loop_update_timer(impl->loop, impl->timer, &ts.it_value, &ts.it_interval, true); - impl->next_time = time; -} +static void send_apple_midi_cmd_ck0(struct session *sess); -static void schedule_timeout(struct impl *impl) +static void on_timer_event(void *data) { - struct session *sess; - uint64_t next_time = 0; - spa_list_for_each(sess, &impl->sessions, link) { - if (next_time == 0 || - (sess->next_time != 0 && sess->next_time < next_time)) - next_time = sess->next_time; - } - set_timeout(impl, next_time); + struct session *sess = data; + pw_log_debug("timeout"); + send_apple_midi_cmd_ck0(sess); } static void send_apple_midi_cmd_ck0(struct session *sess) @@ -312,14 +297,14 @@ static void send_apple_midi_cmd_ck0(struct session *sess) struct iovec iov[3]; struct msghdr msg; struct rtp_apple_midi_ck hdr; - uint64_t current_time, ts; + struct timespec now; + uint64_t timeout, ts; spa_zero(hdr); hdr.cmd = htonl(APPLE_MIDI_CMD_CK); hdr.ssrc = htonl(sess->ssrc); - current_time = current_time_ns(); - ts = current_time / 10000; + ts = current_time_ns(&now) / 10000; hdr.ts1_h = htonl(ts >> 32); hdr.ts1_l = htonl(ts); @@ -335,11 +320,15 @@ static void send_apple_midi_cmd_ck0(struct session *sess) send_packet(impl->data_source->fd, &msg); if (sess->ck_count++ < 8) - sess->next_time = current_time + SPA_NSEC_PER_SEC; + timeout = 1; else if (sess->ck_count++ < 16) - sess->next_time = current_time + 2 * SPA_NSEC_PER_SEC; + timeout = 2; else - sess->next_time = current_time + 5 * SPA_NSEC_PER_SEC; + timeout = 5; + + pw_timer_queue_add(impl->timer_queue, &sess->timer, + &now, timeout * SPA_NSEC_PER_SEC, + on_timer_event, sess); } static void session_update_state(struct session *sess, int state) @@ -355,12 +344,10 @@ static void session_update_state(struct session *sess, int state) if (sess->we_initiated) { sess->ck_count = 0; send_apple_midi_cmd_ck0(sess); - schedule_timeout(sess->impl); } break; case SESSION_STATE_INIT: - sess->next_time = 0; - schedule_timeout(sess->impl); + pw_timer_queue_cancel(&sess->timer); break; default: break; @@ -478,18 +465,23 @@ static void send_destroy(void *data) { } -static void send_state_changed(void *data, bool started, const char *error) +static void send_open_connection(void *data, int *result) { struct session *sess = data; + sess->sending = true; + if (result) + *result = 1; + session_establish(sess); +} - if (started) { - sess->sending = true; - session_establish(sess); - } else { - sess->sending = false; - if (!sess->receiving) - session_stop(sess); - } +static void send_close_connection(void *data, int *result) +{ + struct session *sess = data; + sess->sending = false; + if (result) + *result = 1; + if (!sess->receiving) + session_stop(sess); } static void send_send_packet(void *data, struct iovec *iov, size_t iovlen) @@ -516,17 +508,24 @@ static void send_send_packet(void *data, struct iovec *iov, size_t iovlen) static void recv_destroy(void *data) { } -static void recv_state_changed(void *data, bool started, const char *error) + +static void recv_open_connection(void *data, int *result) { struct session *sess = data; - if (started) { - sess->receiving = true; - session_establish(sess); - } else { - sess->receiving = false; - if (!sess->sending) - session_stop(sess); - } + sess->receiving = true; + if (result) + *result = 1; + session_establish(sess); +} + +static void recv_close_connection(void *data, int *result) +{ + struct session *sess = data; + sess->receiving = false; + if (result) + *result = 1; + if (!sess->sending) + session_stop(sess); } static void recv_send_feedback(void *data, uint32_t seqnum) @@ -560,14 +559,16 @@ static void recv_send_feedback(void *data, uint32_t seqnum) static const struct rtp_stream_events send_stream_events = { RTP_VERSION_STREAM_EVENTS, .destroy = send_destroy, - .state_changed = send_state_changed, + .open_connection = send_open_connection, + .close_connection = send_close_connection, .send_packet = send_send_packet, }; static const struct rtp_stream_events recv_stream_events = { RTP_VERSION_STREAM_EVENTS, .destroy = recv_destroy, - .state_changed = recv_state_changed, + .open_connection = recv_open_connection, + .close_connection = recv_close_connection, .send_feedback = recv_send_feedback, }; @@ -584,9 +585,10 @@ static void free_session(struct session *sess) { struct impl *impl = sess->impl; - pw_loop_invoke(impl->data_loop, do_unlink_session, 1, NULL, 0, true, sess); + pw_loop_locked(impl->data_loop, do_unlink_session, 1, NULL, 0, sess); sess->impl->n_sessions--; + pw_timer_queue_cancel(&sess->timer); if (sess->send) rtp_stream_destroy(sess->send); @@ -845,8 +847,9 @@ static void parse_apple_midi_cmd_ck(struct impl *impl, bool ctrl, uint8_t *buffe struct msghdr msg; struct rtp_apple_midi_ck reply; struct session *sess; - uint64_t now, t1, t2, t3; + uint64_t ts, t1, t2, t3; uint32_t ssrc = ntohl(hdr->ssrc); + struct timespec now; sess = find_session_by_ssrc(impl, ssrc); if (sess == NULL) { @@ -856,7 +859,7 @@ static void parse_apple_midi_cmd_ck(struct impl *impl, bool ctrl, uint8_t *buffe pw_log_trace("got CK count %d", hdr->count); - now = current_time_ns() / 10000; + ts = current_time_ns(&now) / 10000; reply = *hdr; reply.ssrc = htonl(sess->ssrc); reply.count++; @@ -868,11 +871,11 @@ static void parse_apple_midi_cmd_ck(struct impl *impl, bool ctrl, uint8_t *buffe switch (hdr->count) { case 0: - t2 = now; + t2 = ts; break; case 1: t2 = ((uint64_t)ntohl(hdr->ts2_h) << 32) | ntohl(hdr->ts2_l); - t3 = now; + t3 = ts; break; case 2: t3 = ((uint64_t)ntohl(hdr->ts3_h) << 32) | ntohl(hdr->ts3_l); @@ -1042,8 +1045,11 @@ on_data_io(void *data, int fd, uint32_t mask) if (sess == NULL) goto unknown_ssrc; - if (sess->data_ready && sess->receiving) - rtp_stream_receive_packet(sess->recv, buffer, len); + if (sess->data_ready && sess->receiving) { + uint64_t current_time = rtp_stream_get_nsec(sess->recv); + rtp_stream_receive_packet(sess->recv, buffer, len, + current_time); + } } } return; @@ -1209,8 +1215,6 @@ static void impl_destroy(struct impl *impl) if (impl->core && impl->do_disconnect) pw_core_disconnect(impl->core); - if (impl->timer) - pw_loop_destroy_source(impl->loop, impl->timer); if (impl->ctrl_source) pw_loop_destroy_source(impl->loop, impl->ctrl_source); if (impl->data_source) @@ -1327,6 +1331,12 @@ static struct service *make_service(struct impl *impl, const struct service_info } else if (spa_streq(key, "channels")) { k = PW_KEY_AUDIO_CHANNELS; mask |= 1<<3; + } else if (spa_streq(key, "position")) { + pw_properties_set(props, + SPA_KEY_AUDIO_POSITION, value); + } else if (spa_streq(key, "layout")) { + pw_properties_set(props, + SPA_KEY_AUDIO_LAYOUT, value); } else if (spa_streq(key, "channelnames")) { pw_properties_set(props, PW_KEY_NODE_CHANNELNAMES, value); @@ -1585,6 +1595,8 @@ static int make_announce(struct impl *impl) txt = avahi_string_list_add_pair(txt, "channels", str); if ((str = pw_properties_get(impl->stream_props, SPA_KEY_AUDIO_POSITION)) != NULL) txt = avahi_string_list_add_pair(txt, "position", str); + if ((str = pw_properties_get(impl->stream_props, SPA_KEY_AUDIO_LAYOUT)) != NULL) + txt = avahi_string_list_add_pair(txt, "layout", str); if ((str = pw_properties_get(impl->stream_props, PW_KEY_NODE_CHANNELNAMES)) != NULL) txt = avahi_string_list_add_pair(txt, "channelnames", str); if (impl->ts_refclk != NULL) { @@ -1632,24 +1644,6 @@ static void client_callback(AvahiClient *c, AvahiClientState state, void *userda } } -static void on_timer_event(void *data, uint64_t expirations) -{ - struct impl *impl = data; - struct session *sess; - uint64_t current_time = impl->next_time; - - pw_log_debug("timeout"); - spa_list_for_each(sess, &impl->sessions, link) { - if (sess->state != SESSION_STATE_ESTABLISHED) - continue; - if (sess->next_time < current_time) - continue; - - send_apple_midi_cmd_ck0(sess); - } - schedule_timeout(impl); -} - static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) { const char *str; @@ -1667,7 +1661,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) struct pw_properties *props = NULL, *stream_props = NULL; uint16_t port; const char *str; - struct timespec value, interval; int res = 0; PW_LOG_TOPIC_INIT(mod_topic); @@ -1705,6 +1698,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->context = context; impl->loop = pw_context_get_main_loop(context); impl->data_loop = pw_context_acquire_loop(context, &props->dict); + impl->timer_queue = pw_context_get_timer_queue(context); pw_properties_set(props, PW_KEY_NODE_LOOP_NAME, impl->data_loop->name); @@ -1718,6 +1712,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_FORMAT); copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); @@ -1806,22 +1801,10 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) &impl->core_listener, &core_events, impl); - impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl); - if (impl->timer == NULL) { - res = -errno; - pw_log_error("can't create timer source: %m"); - goto out; - } - value.tv_sec = 0; - value.tv_nsec = 1; - interval.tv_sec = 1; - interval.tv_nsec = 0; - pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false); - if ((res = setup_apple_session(impl)) < 0) goto out; - impl->avahi_poll = pw_avahi_poll_new(impl->loop); + impl->avahi_poll = pw_avahi_poll_new(impl->context); if ((impl->client = avahi_client_new(impl->avahi_poll, AVAHI_CLIENT_NO_FAIL, client_callback, impl, diff --git a/src/modules/module-rtp-sink.c b/src/modules/module-rtp-sink.c index 7abd4d531..c77fc067a 100644 --- a/src/modules/module-rtp-sink.c +++ b/src/modules/module-rtp-sink.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -76,6 +77,7 @@ * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_NAME * - \ref PW_KEY_NODE_DESCRIPTION @@ -149,6 +151,7 @@ PW_LOG_TOPIC(mod_topic, "mod." NAME); "( audio.rate= ) " \ "( audio.channels= ) " \ "( audio.position= ) " \ + "( audio.layout= ) " \ "( aes67.driver-group= ) " \ "( stream.props= { key=value ... } ) " @@ -175,6 +178,8 @@ struct impl { struct pw_properties *stream_props; struct rtp_stream *stream; + struct spa_ratelimit rate_limit; + unsigned int do_disconnect:1; char *ifname; @@ -279,6 +284,13 @@ static void stream_destroy(void *d) impl->stream = NULL; } +static inline uint64_t get_time_ns(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return SPA_TIMESPEC_TO_NSEC(&ts); +} + static void stream_send_packet(void *data, struct iovec *iov, size_t iovlen) { struct impl *impl = data; @@ -293,32 +305,56 @@ static void stream_send_packet(void *data, struct iovec *iov, size_t iovlen) msg.msg_flags = 0; n = sendmsg(impl->rtp_fd, &msg, MSG_NOSIGNAL); - if (n < 0) - pw_log_warn("sendmsg() failed: %m"); + if (n < 0) { + int suppressed; + if ((suppressed = spa_ratelimit_test(&impl->rate_limit, get_time_ns())) >= 0) + pw_log_warn("(%d suppressed) sendmsg() failed: %m", suppressed); + } } -static void stream_state_changed(void *data, bool started, const char *error) +static void stream_report_error(void *data, const char *error) { struct impl *impl = data; - if (error) { pw_log_error("stream error: %s", error); pw_impl_module_schedule_destroy(impl->module); - } else if (started) { - int res; + } +} - if ((res = make_socket(&impl->src_addr, impl->src_len, - &impl->dst_addr, impl->dst_len, - impl->mcast_loop, impl->ttl, impl->dscp, - impl->ifname)) < 0) { - pw_log_error("can't make socket: %s", spa_strerror(res)); - rtp_stream_set_error(impl->stream, res, "Can't make socket"); - return; - } - impl->rtp_fd = res; - } else { +static void stream_open_connection(void *data, int *result) +{ + int res; + struct impl *impl = data; + + if ((res = make_socket(&impl->src_addr, impl->src_len, + &impl->dst_addr, impl->dst_len, + impl->mcast_loop, impl->ttl, impl->dscp, + impl->ifname)) < 0) { + pw_log_error("can't make socket: %s", spa_strerror(res)); + rtp_stream_set_error(impl->stream, res, "Can't make socket"); + if (result) + *result = res; + return; + } + + if (result) + *result = 1; + + impl->rtp_fd = res; +} + +static void stream_close_connection(void *data, int *result) +{ + struct impl *impl = data; + + if (impl->rtp_fd > 0) { + if (result) + *result = 1; close(impl->rtp_fd); impl->rtp_fd = -1; + } else { + if (result) + *result = 0; } } @@ -404,7 +440,9 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod * static const struct rtp_stream_events stream_events = { RTP_VERSION_STREAM_EVENTS, .destroy = stream_destroy, - .state_changed = stream_state_changed, + .report_error = stream_report_error, + .open_connection = stream_open_connection, + .close_connection = stream_close_connection, .param_changed = stream_param_changed, .send_packet = stream_send_packet, }; @@ -429,8 +467,10 @@ static void impl_destroy(struct impl *impl) if (impl->core && impl->do_disconnect) pw_core_disconnect(impl->core); - if (impl->rtp_fd != -1) + if (impl->rtp_fd != -1) { + pw_log_info("closing socket with FD %d as part of shutdown", impl->rtp_fd); close(impl->rtp_fd); + } pw_properties_free(impl->stream_props); pw_properties_free(impl->props); @@ -516,6 +556,9 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) } impl->stream_props = stream_props; + impl->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; + impl->rate_limit.burst = 1; + impl->module = module; impl->context = context; impl->loop = pw_context_get_main_loop(context); @@ -537,6 +580,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_FORMAT); copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c index 98a26065a..8e227b53b 100644 --- a/src/modules/module-rtp-source.c +++ b/src/modules/module-rtp-source.c @@ -15,12 +15,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -34,11 +36,12 @@ /** \page page_module_rtp_source RTP source * - * The `rtp-source` module creates a PipeWire source that receives audio - * and midi RTP packets. + * The `rtp-source` module creates a PipeWire source that receives audio RTP packets. + * These RTP packets may contain raw PCM data, Opus encoded audio, or midi audio. * * This module is usually loaded from the \ref page_module_rtp_sap so that the - * source.ip and source.port and format parameters matches that of the sender. + * source.ip and source.port and format parameters matches that of the sender that + * is announced via SAP. * * ## Module Name * @@ -56,6 +59,8 @@ * - `sess.latency.msec = `: target network latency in milliseconds, default 100 * - `sess.ignore-ssrc = `: ignore SSRC, default false * - `sess.media = `: the media type audio|midi|opus, default audio + * - `sess.ts-direct = `: use direct timestamp mode, default false + * (see the Buffer Modes section below) * - `stream.may-pause = `: pause the stream when no data is reveived, default false * - `stream.props = {}`: properties to be passed to the stream * @@ -67,6 +72,7 @@ * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_NAME * - \ref PW_KEY_MEDIA_CLASS @@ -103,6 +109,47 @@ * ] *\endcode * + * ## Buffer modes + * + * RTP source nodes created by this module use an internal ring buffer. Received RTP audio + * data is written into this ring buffer. When the node's process callback is run, it reads + * from that ring buffer and provides audio data from it to the graph. + * + * The `sess.ts-direct` option controls the _buffer mode_, which defines how this ring buffer + * is used. The RTP source nodes created by this module can operate in one of two of these + * buffer modes. In both modes, the RTP source node uses the timestamps of incoming RTP + * packets to write into the ring buffer (more specifically, at the position + * `timestamp + latency from the sess.latency.msec option`). The modes are: + * + * -# *Constant latency mode*: This is the default mode. It is used when `sess.ts-direct` + * is set to false. `sess.latency.msec` then defines the ideal fill level of the ring + * buffer. If the fill level is above or below this, then a DLL is used to adjust the + * consumption of the buffer contents. If the fill level is below a critical value + * (that's the amount of data that is needed in a cycle), or if the fill level equals + * the total buffer size (meaning that no more data can be fed into the buffer), the + * buffer contents are resynchronized, meaning that the existing contents are thrown + * away, and the ring buffer is reset. This buffer mode is useful for when a constant + * latency is desired, and the actual moment playback starts is unimportant (meaning + * that playback is not necessarily in sync with other devices). This mode requires + * no special graph driver. + * -# *Direct timestamp mode*: This is an alternate mode, used when `sess.ts-direct` is + * set to true. In this mode, ring buffer over- and underrun and fill level are not + * directly tracked; instead, they are handled implicitly. There is no constant latency + * maintained. The current time (more specifically, the \ref spa_io_clock::position field + * of \ref spa_io_position::clock) is directly used during playback to retrieve audio + * data. This assumes that a graph driver is used whose time is somehow synchronized + * to the sender's. Since the current time is directly used as an offset within the + * ring buffer, the correct data is always pulled from the ring buffer, that is, the + * data that shall be played now, in sync with the sender (and with other receivers). + * This buffer mode is useful for when receivers shall play in sync with each other, + * and shall use one common synchronized time, provided through the \ref spa_io_clock . + * `sess.latency.msec` functions as a configurable assumed maximum transport delay + * instead of a constant latency quantity in this mode. The DLL is not used in this + * mode, since the graph driver is assumed to be synchronized to the sender, as said, + * so any output sinks in the graph will already adjust their consumption pace to + * match the pace of the graph driver. + * AES67 sessions use this mode, for example. + * * \since 0.3.60 */ @@ -111,6 +158,9 @@ PW_LOG_TOPIC(mod_topic, "mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic +#define DEFAULT_IGMP_CHECK_INTERVAL_SEC 5 +#define DEFAULT_IGMP_DEADLINE_SEC 30 + #define DEFAULT_CLEANUP_SEC 60 #define DEFAULT_SOURCE_IP "224.0.0.56" @@ -126,6 +176,7 @@ PW_LOG_TOPIC(mod_topic, "mod." NAME); "( audio.rate= ) " \ "( audio.channels= ) " \ "( audio.position= ) " \ + "( audio.layout= ) " \ "( stream.props= { key=value ... } ) " static const struct spa_dict_item module_info[] = { @@ -135,25 +186,57 @@ static const struct spa_dict_item module_info[] = { { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; +struct igmp_recovery { + struct pw_timer timer; + int socket_fd; + struct sockaddr_storage mcast_addr; + socklen_t mcast_len; + uint32_t if_index; + bool is_ipv6; + /* This is the interval the recovery timer runs at. The timer + * checks at each interval if recovery is required. This value + * is defined by the igmp.check.interval.sec property. */ + uint32_t check_interval; + /* This is the deadline for packets to arrive. If the deadline + * is exceeded, an IGMP recovery is attempted. This value is + * defined by the igmp.deadline.sec property. */ + uint32_t deadline; +}; + struct impl { struct pw_impl_module *module; struct spa_hook module_listener; struct pw_properties *props; struct pw_context *context; - struct pw_loop *loop; + struct pw_loop *main_loop; struct pw_loop *data_loop; + struct pw_timer_queue *timer_queue; struct pw_core *core; struct spa_hook core_listener; struct spa_hook core_proxy_listener; unsigned int do_disconnect:1; + struct spa_ratelimit rate_limit; + char *ifname; bool always_process; uint32_t cleanup_interval; - struct spa_source *timer; + /* IGMP recovery (triggers when no RTP packets are + * received after the recovery deadline is reached) */ + struct igmp_recovery igmp_recovery; + + /* Monotonic timestamp of the last time a packet was + * received. This is accessed with atomic accessors + * to avoid race conditions. */ + uint64_t last_packet_time; + + struct pw_timer standby_timer; + /* This timer is used when the first stream_start() call fails because + * of an ENODEV error (see the stream_start() code for details) */ + struct pw_timer stream_start_retry_timer; struct pw_properties *stream_props; struct rtp_stream *stream; @@ -166,7 +249,11 @@ struct impl { uint8_t *buffer; size_t buffer_size; - bool receiving; +#define STATE_IDLE 0 +#define STATE_PROBE 1 +#define STATE_RECEIVING 2 +#define STATE_STOPPING 3 + int state; bool may_pause; bool standby; bool waiting; @@ -198,6 +285,10 @@ on_rtp_io(void *data, int fd, uint32_t mask) { struct impl *impl = data; ssize_t len; + int suppressed; + uint64_t current_time; + + current_time = rtp_stream_get_nsec(impl->stream); if (mask & SPA_IO_IN) { if ((len = recv(fd, impl->buffer, impl->buffer_size, 0)) < 0) @@ -207,26 +298,169 @@ on_rtp_io(void *data, int fd, uint32_t mask) goto short_packet; if (SPA_LIKELY(impl->stream)) { - if (rtp_stream_receive_packet(impl->stream, impl->buffer, len) < 0) + if (rtp_stream_receive_packet(impl->stream, impl->buffer, len, + current_time) < 0) goto receive_error; } - if (!impl->receiving) { - impl->receiving = true; - pw_loop_invoke(impl->loop, do_start, 1, NULL, 0, false, impl); + /* Update last packet timestamp for IGMP recovery. + * The recovery timer will check this to see if recovery + * is necessary. Do this _before_ invoking do_start() + * in case the stream is waking up from standby. */ + SPA_ATOMIC_STORE(impl->last_packet_time, current_time); + + if (SPA_ATOMIC_LOAD(impl->state) != STATE_RECEIVING) { + if (!SPA_ATOMIC_CAS(impl->state, STATE_PROBE, STATE_RECEIVING)) { + if (SPA_ATOMIC_CAS(impl->state, STATE_IDLE, STATE_RECEIVING)) + pw_loop_invoke(impl->main_loop, do_start, 1, NULL, 0, false, impl); + } } } return; receive_error: - pw_log_warn("recv error: %m"); + if ((suppressed = spa_ratelimit_test(&impl->rate_limit, current_time)) >= 0) + pw_log_warn("(%d suppressed) recv() error: %m", suppressed); return; short_packet: - pw_log_warn("short packet of len %zd received", len); + if ((suppressed = spa_ratelimit_test(&impl->rate_limit, current_time)) >= 0) + pw_log_warn("(%d suppressed) short packet of len %zd received", + suppressed, len); return; } -static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname) +static int rejoin_igmp_group(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + /* IMPORTANT: This must be run from within the data loop. */ + + int res; + struct impl *impl = user_data; + uint64_t current_time; + + /* Force IGMP membership refresh by leaving the group first, then rejoin */ + if (impl->igmp_recovery.is_ipv6) { + struct ipv6_mreq mr6; + memset(&mr6, 0, sizeof(mr6)); + mr6.ipv6mr_multiaddr = ((struct sockaddr_in6*)&impl->igmp_recovery.mcast_addr)->sin6_addr; + mr6.ipv6mr_interface = impl->igmp_recovery.if_index; + + /* Leave the group first */ + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP, + &mr6, sizeof(mr6)); + if (SPA_LIKELY(res == 0)) { + pw_log_info("left IPv6 multicast group"); + } else { + if (errno == EADDRNOTAVAIL) { + pw_log_info("attempted to leave IPv6 multicast group, but " + "membership was already silently dropped"); + } else { + pw_log_warn("failed to leave IPv6 multicast group: %m"); + } + } + + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, + &mr6, sizeof(mr6)); + if (res < 0) { + pw_log_warn("failed to re-join IPv6 multicast group: %m"); + } else { + pw_log_info("re-joined IPv6 multicast group successfully"); + } + } else { + struct ip_mreqn mr4; + memset(&mr4, 0, sizeof(mr4)); + mr4.imr_multiaddr = ((struct sockaddr_in*)&impl->igmp_recovery.mcast_addr)->sin_addr; + mr4.imr_ifindex = impl->igmp_recovery.if_index; + + /* Leave the group first */ + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, + &mr4, sizeof(mr4)); + if (SPA_LIKELY(res == 0)) { + pw_log_info("left IPv4 multicast group"); + } else { + if (errno == EADDRNOTAVAIL) { + pw_log_info("attempted to leave IPv4 multicast group, but " + "membership was already silently dropped"); + } else { + pw_log_warn("failed to leave IPv4 multicast group: %m"); + } + } + + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, + &mr4, sizeof(mr4)); + if (res < 0) { + pw_log_warn("failed to re-join IPv4 multicast group: %m"); + } else { + pw_log_info("re-joined IPv4 multicast group successfully"); + } + } + + current_time = rtp_stream_get_nsec(impl->stream); + SPA_ATOMIC_STORE(impl->last_packet_time, current_time); + + return res; +} + +static void on_igmp_recovery_timer_event(void *data) +{ + int res; + struct impl *impl = data; + char addr[128]; + uint64_t current_time, elapsed_seconds, last_packet_time; + + /* Only attempt recovery if we have a valid socket and multicast address */ + if (SPA_UNLIKELY(impl->igmp_recovery.socket_fd < 0)) { + pw_log_trace("no socket, skipping IGMP recovery"); + goto finish; + } + + /* This check if performed even if standby = false or + * receiving != STATE_RECEIVING , because the very reason + * for these states may be that the receiver socket was + * silently kicked out of the IGMP group (which causes data + * to no longer arrive, thus leading to these states). */ + + current_time = rtp_stream_get_nsec(impl->stream); + last_packet_time = SPA_ATOMIC_LOAD(impl->last_packet_time); + elapsed_seconds = (current_time - last_packet_time) / SPA_NSEC_PER_SEC; + + /* Only trigger recovery if enough time has elapsed since last packet */ + if (elapsed_seconds < impl->igmp_recovery.deadline) { + pw_log_trace("IGMP recovery check: %" PRIu64 " seconds elapsed, " + "need %" PRIu32 " seconds", elapsed_seconds, + impl->igmp_recovery.deadline); + goto finish; + } + + pw_net_get_ip(&impl->igmp_recovery.mcast_addr, addr, sizeof(addr), NULL, NULL); + pw_log_info("starting IGMP recovery for %s", addr); + + /* Run the actual recovery in the data loop, since recovery involves + * rejoining the socket to the IGMP group. By running this in the + * data loop, race conditions due to stray packets causing an on_rtp_io() + * invocation at the same time when the IGMP group rejoining takes place + * is avoided, since on_rtp_io() too runs in the data loop. + * This is a blocking call to make sure the rejoin attempt was fully + * done by the time this callback ends. (rejoin_igmp_group() does not + * do work that takes a long time to finish. )*/ + res = pw_loop_locked(impl->data_loop, rejoin_igmp_group, 1, NULL, 0, impl); + + if (SPA_LIKELY(res == 0)) { + pw_log_info("IGMP recovery for %s finished", addr); + } else { + pw_log_error("error while finishing IGMP recovery for %s: %s", + addr, spa_strerror(res)); + } + +finish: + pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer, + &impl->igmp_recovery.timer.timeout, + impl->igmp_recovery.check_interval * SPA_NSEC_PER_SEC, + on_igmp_recovery_timer_event, impl); +} + +static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname, + struct igmp_recovery *igmp_recovery) { int af, fd, val, res; struct ifreq req; @@ -306,6 +540,16 @@ static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname) goto error; } + /* Store multicast info for recovery */ + igmp_recovery->socket_fd = fd; + igmp_recovery->mcast_addr = ba; + igmp_recovery->mcast_len = salen; + igmp_recovery->if_index = req.ifr_ifindex; + igmp_recovery->is_ipv6 = (af == AF_INET6); + pw_log_debug("stored %s multicast info: socket_fd=%d, " + "if_index=%d", igmp_recovery->is_ipv6 ? + "IPv6" : "IPv4", fd, req.ifr_ifindex); + if (bind(fd, (struct sockaddr*)&ba, salen) < 0) { res = -errno; pw_log_error("bind() failed: %m"); @@ -324,38 +568,119 @@ error: return res; } -static int stream_start(struct impl *impl) +static void stream_report_error(void *data, const char *error) { + struct impl *impl = data; + if (error) { + pw_log_error("stream error: %s", error); + pw_impl_module_schedule_destroy(impl->module); + } +} + +static void stream_open_connection(void *data, int *result); + +static void on_open_connection_retry_timer_event(void *data) +{ + struct impl *impl = data; + pw_log_debug("trying again to open connection after previous attempt failed with ENODEV"); + stream_open_connection(impl, NULL); +} + +static void stream_open_connection(void *data, int *result) +{ + int res = 0; int fd; + struct impl *impl = data; if (impl->source != NULL) - return 0; + goto finish; pw_log_info("starting RTP listener"); if ((fd = make_socket((const struct sockaddr *)&impl->src_addr, - impl->src_len, impl->ifname)) < 0) { - pw_log_error("failed to create socket: %m"); - return -errno; + impl->src_len, impl->ifname, + &(impl->igmp_recovery))) < 0) { + /* If make_socket() tries to create a socket and join to a multicast + * group while the network interfaces are not ready yet to do so + * (usually because a network manager component is still setting up + * those network interfaces), ENODEV will be returned. This is essentially + * a race condition. There is no discernible way to be notified when the + * network interfaces are ready for that operation, so the next best + * approach is to essentially do a form of polling by retrying the + * stream_start() call after some time. The stream_start_retry_timer exists + * precisely for that purpose. This means that ENODEV is not treated as + * an error, but instead, it triggers the creation of that timer. */ + if (fd == -ENODEV) { + pw_log_warn("failed to create socket because network device is not ready " + "and present yet; will try again"); + + pw_timer_queue_cancel(&impl->stream_start_retry_timer); + /* Use a 1-second retry interval. The network interfaces + * are likely to be up and running then. */ + pw_timer_queue_add(impl->timer_queue, &impl->stream_start_retry_timer, + NULL, 1 * SPA_NSEC_PER_SEC, + on_open_connection_retry_timer_event, impl); + + /* It is important to return 0 in this case. Otherwise, the nonzero return + * value will later be propagated through the core as an error. */ + res = 0; + goto finish; + } else { + pw_log_error("failed to create socket: %s", spa_strerror(fd)); + /* If ENODEV was returned earlier, and the stream_start_retry_timer + * was consequently created, but then a non-ENODEV error occurred, + * the timer must be stopped and removed. */ + pw_timer_queue_cancel(&impl->stream_start_retry_timer); + res = fd; + goto finish; + } } + /* Cleanup the timer in case ENODEV occurred earlier, and this time, + * the socket creation succeeded. */ + pw_timer_queue_cancel(&impl->stream_start_retry_timer); + impl->source = pw_loop_add_io(impl->data_loop, fd, SPA_IO_IN, true, on_rtp_io, impl); if (impl->source == NULL) { pw_log_error("can't create io source: %m"); close(fd); - return -errno; + res = -errno; + goto finish; } - return 0; + + if ((res = pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer, + NULL, impl->igmp_recovery.check_interval * SPA_NSEC_PER_SEC, + on_igmp_recovery_timer_event, impl)) < 0) { + pw_log_error("can't add timer: %s", spa_strerror(res)); + goto finish; + } + +finish: + if (res != 0) { + pw_log_error("failed to start RTP stream: %s", spa_strerror(res)); + rtp_stream_set_error(impl->stream, res, "Can't start RTP stream"); + } + + if (result) + *result = res; } -static void stream_stop(struct impl *impl) +static void stream_close_connection(void *data, int *result) { + struct impl *impl = data; + + if (result) + *result = 0; + if (!impl->source) return; pw_log_info("stopping RTP listener"); + pw_timer_queue_cancel(&impl->stream_start_retry_timer); + pw_timer_queue_cancel(&impl->igmp_recovery.timer); + pw_loop_destroy_source(impl->data_loop, impl->source); impl->source = NULL; } @@ -366,25 +691,6 @@ static void stream_destroy(void *d) impl->stream = NULL; } -static void stream_state_changed(void *data, bool started, const char *error) -{ - struct impl *impl = data; - int res; - - if (error) { - pw_log_error("stream error: %s", error); - pw_impl_module_schedule_destroy(impl->module); - } else if (started) { - if ((res = stream_start(impl)) < 0) { - pw_log_error("failed to start RTP stream: %s", spa_strerror(res)); - rtp_stream_set_error(impl->stream, res, "Can't start RTP stream"); - } - } else { - if (!impl->always_process && !impl->standby) - stream_stop(impl); - } -} - static void stream_props_changed(struct impl *impl, uint32_t id, const struct spa_pod *param) { struct spa_pod_object *obj = (struct spa_pod_object *)param; @@ -449,17 +755,20 @@ static void stream_param_changed(void *data, uint32_t id, const struct spa_pod * static const struct rtp_stream_events stream_events = { RTP_VERSION_STREAM_EVENTS, .destroy = stream_destroy, - .state_changed = stream_state_changed, + .report_error = stream_report_error, + .open_connection = stream_open_connection, + .close_connection = stream_close_connection, .param_changed = stream_param_changed, }; -static void on_timer_event(void *data, uint64_t expirations) +static void on_standby_timer_event(void *data) { struct impl *impl = data; - pw_log_debug("timer %d", impl->receiving); + pw_log_debug("standby timer event; state: %d standby: %d waiting: %d", + impl->state, impl->standby, impl->waiting); - if (!impl->receiving) { + if (SPA_ATOMIC_CAS(impl->state, STATE_PROBE, STATE_STOPPING)) { if (!impl->standby) { struct spa_dict_item item[1]; @@ -474,10 +783,15 @@ static void on_timer_event(void *data, uint64_t expirations) rtp_stream_set_active(impl->stream, false); } //pw_impl_module_schedule_destroy(impl->module); + SPA_ATOMIC_STORE(impl->state, STATE_IDLE); } else { pw_log_debug("timeout, keeping active RTP source"); + SPA_ATOMIC_CAS(impl->state, STATE_RECEIVING, STATE_PROBE); } - impl->receiving = false; + + pw_timer_queue_add(impl->timer_queue, &impl->standby_timer, + &impl->standby_timer.timeout, impl->cleanup_interval * SPA_NSEC_PER_SEC, + on_standby_timer_event, impl); } static void core_destroy(void *d) @@ -502,8 +816,9 @@ static void impl_destroy(struct impl *impl) if (impl->core && impl->do_disconnect) pw_core_disconnect(impl->core); - if (impl->timer) - pw_loop_destroy_source(impl->loop, impl->timer); + pw_timer_queue_cancel(&impl->standby_timer); + pw_timer_queue_cancel(&impl->stream_start_retry_timer); + pw_timer_queue_cancel(&impl->igmp_recovery.timer); if (impl->data_loop) pw_context_release_loop(impl->context, impl->data_loop); @@ -559,7 +874,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) struct pw_context *context = pw_impl_module_get_context(module); struct impl *impl; const char *str, *sess_name; - struct timespec value, interval; struct pw_properties *props, *stream_props; int64_t ts_offset; char addr[128]; @@ -585,9 +899,13 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->module = module; impl->context = context; - impl->loop = pw_context_get_main_loop(context); + impl->main_loop = pw_context_get_main_loop(context); + impl->timer_queue = pw_context_get_timer_queue(context); impl->data_loop = pw_context_acquire_loop(context, &props->dict); + impl->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; + impl->rate_limit.burst = 1; + if ((sess_name = pw_properties_get(props, "sess.name")) == NULL) sess_name = pw_get_host_name(); @@ -607,6 +925,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_FORMAT); copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); @@ -665,9 +984,20 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) * till we make it (or get timed out) */ pw_properties_set(stream_props, "rtp.receiving", "true"); - impl->cleanup_interval = pw_properties_get_uint32(props, + impl->cleanup_interval = pw_properties_get_uint32(stream_props, "cleanup.sec", DEFAULT_CLEANUP_SEC); + impl->igmp_recovery.check_interval = SPA_MAX(pw_properties_get_uint32(stream_props, + "igmp.check.interval.sec", + DEFAULT_IGMP_CHECK_INTERVAL_SEC), 1u); + pw_log_info("using IGMP check interval of %" PRIu32 " second(s)", + impl->igmp_recovery.check_interval); + + impl->igmp_recovery.deadline = SPA_MAX(pw_properties_get_uint32(stream_props, + "igmp.deadline.sec", DEFAULT_IGMP_DEADLINE_SEC), 5u); + pw_log_info("using IGMP deadline of %" PRIu32 " second(s)", + impl->igmp_recovery.deadline); + impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { str = pw_properties_get(props, PW_KEY_REMOTE_NAME); @@ -691,17 +1021,12 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) &impl->core_listener, &core_events, impl); - impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl); - if (impl->timer == NULL) { - res = -errno; - pw_log_error("can't create timer source: %m"); + if ((res = pw_timer_queue_add(impl->timer_queue, &impl->standby_timer, + NULL, impl->cleanup_interval * SPA_NSEC_PER_SEC, + on_standby_timer_event, impl)) < 0) { + pw_log_error("can't add timer: %s", spa_strerror(res)); goto out; } - value.tv_sec = impl->cleanup_interval; - value.tv_nsec = 0; - interval.tv_sec = impl->cleanup_interval; - interval.tv_nsec = 0; - pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false); impl->stream = rtp_stream_new(impl->core, PW_DIRECTION_OUTPUT, pw_properties_copy(stream_props), diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index e89712a32..0af38d649 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -2,13 +2,35 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +static inline void +set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size, + uint32_t offset, struct iovec *iov, uint32_t len) +{ + iov[0].iov_len = SPA_MIN(len, size - offset); + iov[0].iov_base = SPA_PTROFF(buffer, offset, void); + iov[1].iov_len = len - iov[0].iov_len; + iov[1].iov_base = buffer; +} + +static void ringbuffer_clear(struct spa_ringbuffer *rbuf SPA_UNUSED, + void *buffer, uint32_t size, + uint32_t offset, uint32_t len) +{ + struct iovec iov[2]; + set_iovec(rbuf, buffer, size, offset, iov, len); + memset(iov[0].iov_base, 0, iov[0].iov_len); + memset(iov[1].iov_base, 0, iov[1].iov_len); +} + static void rtp_audio_process_playback(void *data) { struct impl *impl = data; struct pw_buffer *buf; struct spa_data *d; + struct pw_time pwt; uint32_t wanted, timestamp, target_buffer, stride, maxsize; - int32_t avail; + uint32_t device_delay; + int32_t avail, flags = 0; if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) { pw_log_info("Out of stream buffers: %m"); @@ -21,48 +43,153 @@ static void rtp_audio_process_playback(void *data) maxsize = d[0].maxsize / stride; wanted = buf->requested ? SPA_MIN(buf->requested, maxsize) : maxsize; - if (impl->io_position && impl->direct_timestamp) { - /* in direct mode, read directly from the timestamp index, - * because sender and receiver are in sync, this would keep - * target_buffer of samples available. */ - spa_ringbuffer_read_update(&impl->ring, - impl->io_position->clock.position); - } - avail = spa_ringbuffer_get_read_index(&impl->ring, ×tamp); + pw_stream_get_time_n(impl->stream, &pwt, sizeof(pwt)); - target_buffer = impl->target_buffer; + /* Negative delay is used rarely, mostly for the combine stream. + * There, the delay is used as an offset value between streams. + * Here, negative delay values make no sense. It is safe to clamp + * delay values to 0 (see docs), so do that here. */ + device_delay = SPA_MAX(pwt.delay, 0LL); - if (avail < (int32_t)wanted) { - enum spa_log_level level; - memset(d[0].data, 0, wanted * stride); - if (impl->have_sync) { - impl->have_sync = false; - level = SPA_LOG_LEVEL_INFO; + /* IMPORTANT: In the explanations below, sometimes, "reading/writing from/to the + * ring buffer at a position X" is mentioned. To be exact, that buffer is actually + * impl->buffer. And since X can be a timestamp whose value is far higher than the + * buffer size (and the fact that impl->buffer is a _ring_ buffer), reads and writes + * actually first do a modulo operation to the position to implement a ring buffer + * index wrap-around. (Wrap-around when reading / writing the data bytes is + * handled by the spa_ringbuffer code; this is about the wrap around of the + * read or write index itself.) */ + + if (impl->direct_timestamp) { + /* In direct timestamp mode, the focus lies on synchronized playback, not + * on a constant latency. The ring buffer fill level is not of interest + * here. The code in rtp_audio_receive() writes to the ring buffer at + * position (RTP timestamp + target_buffer), just like in the constant + * latency mode. Crucially however, in direct timestamp mode, it is assumed + * that the RTP timestamps are based on the same synchronized clock that + * runs the graph driver here, so the clock position is using the same + * time base as these timestamps. + * + * If the transport delay from the sender to this receiver were zero, then + * the data with the given RTP timestamp could in theory be played right + * away, since that timestamp would equal the clock position (or, in other + * words, it would be the present time). Since the transport takes some + * time, writing the data at the position (RTP timestamp + target_buffer) + * shifts the timestamp into the future sufficiently enough that no data + * is lost. (target_buffer corresponds to the `sess.latency.msec` RTP + * source module option, and that option has to be chosen by the user + * to be of a sensible size - high enough to at least match the maximum + * transport delay, but not too high to not risk too much latency + * Also, `sess.latency.msec` must be the same value across all RTP + * source nodes that shall play in sync.) + * + * When the code here reads from the position defined by the current + * clock position, it is then guaranteed that the data is accessed in + * sync with other RTP source nodes which also run in the direct + * timestamp mode, since all of them shift the timestamp by the same + * `sess.latency.msec` into the future. + * + * "Fill level" makes no sense in this mode, since a constant latency + * is not important in this mode, so no DLL is needed. Also, matching + * the pace of the synchronized clock is done by having the graph + * driver be synchronized to that clock, which will in turn cause + * any output sinks to adjust their DLLs (or similar control loop + * mechanisms) to match the pace of their data consumption with the + * pace of the driver. */ + + if (impl->io_position) { + /* Shift clock position by stream delay to compensate + * for processing and output delay. */ + timestamp = impl->io_position->clock.position + device_delay; + spa_ringbuffer_read_update(&impl->ring, timestamp); } else { - level = SPA_LOG_LEVEL_DEBUG; + /* In the unlikely case that no spa_io_position pointer + * was passed yet by PipeWire to this node, resort to a + * default behavior: just use the current read index. + * This most likely is not in sync with other nodes, + * but _something_ is needed as read index until the + * spa_io_position is available. */ + spa_ringbuffer_get_read_index(&impl->ring, ×tamp); + } + + spa_ringbuffer_read_data(&impl->ring, + impl->buffer, + impl->actual_max_buffer_size, + (timestamp * stride) % impl->actual_max_buffer_size, + d[0].data, wanted * stride); + + /* Clear the bytes that were just retrieved. Since the fill level + * is not tracked in this buffer mode, it is possible that as soon + * as actual playback ends, the RTP source node re-reads old data. + * Make sure it reads silence when no actual new data is present + * and the RTP source node still runs. Do this by filling the + * region of the retrieved data with null bytes. */ + ringbuffer_clear(&impl->ring, + impl->buffer, + impl->actual_max_buffer_size, + (timestamp * stride) % impl->actual_max_buffer_size, + wanted * stride); + + if (!impl->io_position) { + /* In the unlikely case that no spa_io_position pointer + * was passed yet by PipeWire to this node, monotonically + * increment the read index like this to not consume from + * the same position in the ring buffer over and over again. */ + timestamp += wanted; + spa_ringbuffer_read_update(&impl->ring, timestamp); } - pw_log(level, "underrun %d/%u < %u", - avail, target_buffer, wanted); } else { - double error, corr; - if (impl->first) { - if ((uint32_t)avail > target_buffer) { - uint32_t skip = avail - target_buffer; - pw_log_debug("first: avail:%d skip:%u target:%u", - avail, skip, target_buffer); - timestamp += skip; + /* In the constant delay mode, it is assumed that the ring buffer fill + * level matches impl->target_buffer. If not, check for over- and + * underruns. Adjust the DLL as needed. If the over/underruns are too + * severe, resynchronize. */ + + avail = spa_ringbuffer_get_read_index(&impl->ring, ×tamp); + + /* Reduce target buffer by the delay amount to start playback sooner. + * This compensates for the delay to the device. */ + if (SPA_UNLIKELY(impl->target_buffer < device_delay)) { + pw_log_error("Delay to device (%" PRIu32 ") is higher than " + "the target buffer size (%" PRIu32 ")", device_delay, + impl->target_buffer); + target_buffer = 0; + } else { + target_buffer = impl->target_buffer - device_delay; + } + + if (avail < (int32_t)wanted) { + enum spa_log_level level; + memset(d[0].data, 0, wanted * stride); + flags |= SPA_CHUNK_FLAG_EMPTY; + + if (impl->have_sync) { + impl->have_sync = false; + level = SPA_LOG_LEVEL_INFO; + } else { + level = SPA_LOG_LEVEL_DEBUG; + } + pw_log(level, "receiver read underrun %d/%u < %u", + avail, target_buffer, wanted); + } else { + double error, corr; + if (impl->first) { + if ((uint32_t)avail > target_buffer) { + uint32_t skip = avail - target_buffer; + pw_log_debug("first: avail:%d skip:%u target:%u", + avail, skip, target_buffer); + timestamp += skip; + avail = target_buffer; + } + impl->first = false; + } else if (avail > (int32_t)SPA_MIN(target_buffer * 8, BUFFER_SIZE / stride)) { + pw_log_warn("receiver read overrun %u > %u", avail, target_buffer * 8); + timestamp += avail - target_buffer; avail = target_buffer; } - impl->first = false; - } else if (avail > (int32_t)SPA_MIN(target_buffer * 8, BUFFER_SIZE / stride)) { - pw_log_warn("overrun %u > %u", avail, target_buffer * 8); - timestamp += avail - target_buffer; - avail = target_buffer; - } - if (!impl->direct_timestamp) { - /* when not using direct timestamp and clocks are not - * in sync, try to adjust our playback rate to keep the - * requested target_buffer bytes in the ringbuffer */ + + /* when the speed of the sender clock and our clock are + * not in sync, try to adjust our playback rate to keep + * the requested target_buffer bytes in the ringbuffer */ double in_flight = 0; struct spa_io_position *pos = impl->io_position; @@ -85,25 +212,29 @@ static void rtp_audio_process_playback(void *data) target_buffer, error, corr); pw_stream_set_rate(impl->stream, 1.0 / corr); - } - spa_ringbuffer_read_data(&impl->ring, - impl->buffer, - BUFFER_SIZE, - (timestamp * stride) & BUFFER_MASK, - d[0].data, wanted * stride); - timestamp += wanted; - spa_ringbuffer_read_update(&impl->ring, timestamp); + spa_ringbuffer_read_data(&impl->ring, + impl->buffer, + impl->actual_max_buffer_size, + (timestamp * stride) % impl->actual_max_buffer_size, + d[0].data, wanted * stride); + + timestamp += wanted; + spa_ringbuffer_read_update(&impl->ring, timestamp); + } } + + d[0].chunk->offset = 0; d[0].chunk->size = wanted * stride; d[0].chunk->stride = stride; - d[0].chunk->offset = 0; + d[0].chunk->flags = flags; buf->size = wanted; pw_stream_queue_buffer(impl->stream, buf); } -static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len) +static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len, + uint64_t current_time) { struct rtp_header *hdr; ssize_t hlen, plen; @@ -132,7 +263,10 @@ static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len) if (impl->have_seq && impl->seq != seq) { pw_log_info("unexpected seq (%d != %d) SSRC:%u", seq, impl->seq, hdr->ssrc); - impl->have_sync = false; + /* No need to resynchronize here. If packets arrive out of + * order, then they are still written in order into the ring + * buffer, since they are written according to where the + * RTP timestamp points to. */ } impl->seq = seq + 1; impl->have_seq = true; @@ -140,7 +274,7 @@ static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len) timestamp = ntohl(hdr->timestamp) - impl->ts_offset; impl->receiving = true; - impl->last_recv_timestamp = pw_stream_get_nsec(impl->stream); + impl->last_recv_timestamp = current_time; plen = len - hlen; samples = plen / stride; @@ -170,20 +304,48 @@ static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len) write, expected_write); } - if (filled + samples > BUFFER_SIZE / stride) { - pw_log_debug("capture overrun %u + %u > %u", filled, samples, + /* Write overrun only makes sense in constant delay mode. See the + * RTP source module documentation and the rtp_audio_process_playback() + * code for an explanation why. */ + if (!impl->direct_timestamp && (filled + samples > BUFFER_SIZE / stride)) { + pw_log_debug("receiver write overrun %u + %u > %u", filled, samples, BUFFER_SIZE / stride); impl->have_sync = false; } else { pw_log_trace("got samples:%u", samples); spa_ringbuffer_write_data(&impl->ring, impl->buffer, - BUFFER_SIZE, - (write * stride) & BUFFER_MASK, + impl->actual_max_buffer_size, + (write * stride) % impl->actual_max_buffer_size, &buffer[hlen], (samples * stride)); - write += samples; - spa_ringbuffer_write_update(&impl->ring, write); + + /* Only update the write index if data was actually _appended_. + * If packets arrived out of order, then it may be that parts + * of the ring buffer further ahead were written to first, and + * now, unwritten parts preceding those other parts were now + * written to. For example, if previously, 10 samples were + * written to index 100, even though 10 samples were expected + * to be written at index 90, then there is a "hole" at index + * 90. If now, the packet that contains data for index 90 + * arrived, then this data will be _inserted_ at index 90, + * and not _appended_. In this example, `expected_write` would + * be 100 (since `expected_write` is the current write index), + * `write` would be 90, `samples` would be 10. In this case, + * the inequality below does not hold, so data is being + * _inserted_. By contrast, during normal operation, `write` + * and `expected_write` are equal, so the inequality below + * _does_ hold, meaning that data is being appended. + * + * (Note that this write index update is only important if + * the constant delay mode is active, or if no spa_io_position + * was not provided yet. See the rtp_audio_process_playback() + * code for more about this.) */ + if (expected_write < (write + samples)) { + write += samples; + spa_ringbuffer_write_update(&impl->ring, write); + } } + return 0; short_packet: @@ -215,17 +377,7 @@ static void set_timer(struct impl *impl, uint64_t time, uint64_t itime) ts.it_interval.tv_nsec = itime % SPA_NSEC_PER_SEC; spa_system_timerfd_settime(impl->data_loop->system, impl->timer->fd, SPA_FD_TIMER_ABSTIME, &ts, NULL); - impl->timer_running = time != 0 && itime != 0; -} - -static inline void -set_iovec(struct spa_ringbuffer *rbuf, void *buffer, uint32_t size, - uint32_t offset, struct iovec *iov, uint32_t len) -{ - iov[0].iov_len = SPA_MIN(len, size - offset); - iov[0].iov_base = SPA_PTROFF(buffer, offset, void); - iov[1].iov_len = len - iov[0].iov_len; - iov[1].iov_base = buffer; + set_timer_running(impl, time != 0 && itime != 0); } static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets, uint64_t set_timestamp) @@ -234,19 +386,31 @@ static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets, uin uint32_t stride, timestamp; struct iovec iov[3]; struct rtp_header header; + bool insufficient_data; avail = spa_ringbuffer_get_read_index(&impl->ring, ×tamp); tosend = impl->psamples; - if (avail < tosend) - if (impl->started) + insufficient_data = (avail < tosend); + if (insufficient_data) { + /* There is insufficient data for even a single full packet. + * Handle this depending on the current state. */ + + if (get_internal_stream_state(impl) == RTP_STREAM_INTERNAL_STATE_STARTED) { + /* If the stream is started, just try again later, + * when more data comes in. Enough data for covering + * the psamples amount might be available by then. */ goto done; - else { - /* send last packet before emitting state_changed */ + } else { + /* There is not enough data for a full packet, but the + * stream is no longer in the started state, so the + * remaining data needs to be flushed out now. */ tosend = avail; num_packets = 1; } - else + } else { + /* There is sufficient data for one or more full packets. */ num_packets = SPA_MIN(num_packets, (uint32_t)(avail / tosend)); + } stride = impl->stride; @@ -267,8 +431,8 @@ static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets, uin header.timestamp = htonl(impl->ts_offset + (set_timestamp ? set_timestamp : timestamp)); set_iovec(&impl->ring, - impl->buffer, BUFFER_SIZE, - (timestamp * stride) & BUFFER_MASK, + impl->buffer, impl->actual_max_buffer_size, + (timestamp * stride) % impl->actual_max_buffer_size, &iov[1], tosend * stride); pw_log_trace("sending %d packet:%d ts_offset:%d timestamp:%d", @@ -283,22 +447,39 @@ static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets, uin num_packets--; } spa_ringbuffer_read_update(&impl->ring, timestamp); + done: - if (impl->timer_running) { - if (impl->started) { - if (avail < tosend) { + if (is_timer_running(impl)) { + if (get_internal_stream_state(impl) != RTP_STREAM_INTERNAL_STATE_STOPPING) { + /* If the stream isn't being stopped, and instead is running, + * keep the timer running if there was sufficient data to + * produce at least one packet. That's because by the time + * the next timer expiration happens, there might be enough + * data available for even more packets. However, if there + * wasn't sufficient data for even one packet, stop the + * timer, since it is likely then that input has ceased + * (at least for now). */ + if (insufficient_data) { set_timer(impl, 0, 0); } } else if (avail <= 0) { - bool started = false; - - /* the stream has been stopped and all packets have been sent */ + /* All packets were sent, and the stream is in the stopping + * state. This means that stream_stop() was called while this + * timer was still sending out remaining packets, and thus, + * stream_stop() could not immediately change the stream to the + * stopping state. Now that all packets have gone out, finish + * the stopping state change. */ set_timer(impl, 0, 0); - pw_loop_invoke(impl->main_loop, do_emit_state_changed, SPA_ID_INVALID, &started, sizeof started, false, impl); + pw_loop_invoke(impl->main_loop, do_finish_stopping_state, SPA_ID_INVALID, NULL, 0, false, impl); } } } +static void rtp_audio_stop_timer(struct impl *impl) +{ + set_timer(impl, 0, 0); +} + static void rtp_audio_flush_timeout(struct impl *impl, uint64_t expirations) { if (expirations > 1) @@ -311,7 +492,7 @@ static void rtp_audio_process_capture(void *data) struct impl *impl = data; struct pw_buffer *buf; struct spa_data *d; - uint32_t offs, size, timestamp, expected_timestamp, stride; + uint32_t offs, size, actual_timestamp, expected_timestamp, stride; int32_t filled, wanted; uint32_t pending, num_queued; struct spa_io_position *pos; @@ -338,7 +519,7 @@ static void rtp_audio_process_capture(void *data) pos = impl->io_position; if (SPA_LIKELY(pos)) { uint32_t rate = pos->clock.rate.denom; - timestamp = pos->clock.position * impl->rate / rate; + actual_timestamp = pos->clock.position * impl->rate / rate; next_nsec = pos->clock.next_nsec; quantum = (uint64_t)(pos->clock.duration * SPA_NSEC_PER_SEC / (rate * pos->clock.rate_diff)); @@ -350,18 +531,45 @@ static void rtp_audio_process_capture(void *data) impl->sink_quantum = (uint64_t)(pos->clock.duration * SPA_NSEC_PER_SEC / rate); } } else { - timestamp = expected_timestamp; + actual_timestamp = expected_timestamp; next_nsec = 0; quantum = 0; } + /* First do the synchronization checks (if the sender is in sync already.) */ + + if (impl->have_sync) { + if (SPA_FLAG_IS_SET(pos->clock.flags, SPA_IO_CLOCK_FLAG_DISCONT)) { + pw_log_info("IO clock reports discontinuity; resynchronizing"); + impl->have_sync = false; + } else if (SPA_ABS((int64_t)expected_timestamp - (int64_t)actual_timestamp) > (int64_t)(pos->clock.duration)) { + /* Normally, expected and actual timestamp should be in sync, and deviate + * only minimally at most. If a major deviation occurs, then most likely + * the driver clock has experienced an unexpected jump. Note that the + * cycle duration in samples is used, and not the value of "quantum". + * That value is given in nanoseconds, not samples. Also, the timestamps + * themselves are not affected by rate_diff. See the documentation + * "Driver architecture and workflow" for an explanation why not. */ + pw_log_warn("timestamp: expected %u != actual %u", expected_timestamp, actual_timestamp); + impl->have_sync = false; + } else if (filled + wanted > (int32_t)SPA_MIN(impl->target_buffer * 8, BUFFER_SIZE / stride)) { + pw_log_warn("sender write overrun %u + %u > %u/%u", filled, wanted, + impl->target_buffer * 8, BUFFER_SIZE / stride); + impl->have_sync = false; + filled = 0; + } + } + + /* Next, (re)synchronize. If the sender was in sync, but the checks above detected + * that resynchronization is needed, then this will be done immediately below. */ + if (!impl->have_sync) { - pw_log_info("sync to timestamp:%u seq:%u ts_offset:%u SSRC:%u", - timestamp, impl->seq, impl->ts_offset, impl->ssrc); - impl->ring.readindex = impl->ring.writeindex = timestamp; + pw_log_info("(re)sync to timestamp:%u seq:%u ts_offset:%u SSRC:%u", + actual_timestamp, impl->seq, impl->ts_offset, impl->ssrc); + impl->ring.readindex = impl->ring.writeindex = actual_timestamp; memset(impl->buffer, 0, BUFFER_SIZE); impl->have_sync = true; - expected_timestamp = timestamp; + expected_timestamp = actual_timestamp; filled = 0; if (impl->separate_sender) { @@ -369,24 +577,14 @@ static void rtp_audio_process_capture(void *data) * refill the buffer */ impl->refilling = true; } - } else { - if (SPA_ABS((int)expected_timestamp - (int)timestamp) > (int)quantum) { - pw_log_warn("expected %u != timestamp %u", expected_timestamp, timestamp); - impl->have_sync = false; - } else if (filled + wanted > (int32_t)SPA_MIN(impl->target_buffer * 8, BUFFER_SIZE / stride)) { - pw_log_warn("overrun %u + %u > %u/%u", filled, wanted, - impl->target_buffer * 8, BUFFER_SIZE / stride); - impl->have_sync = false; - filled = 0; - } } pw_log_trace("writing %u samples at %u", wanted, expected_timestamp); spa_ringbuffer_write_data(&impl->ring, impl->buffer, - BUFFER_SIZE, - (expected_timestamp * stride) & BUFFER_MASK, + impl->actual_max_buffer_size, + (expected_timestamp * stride) % impl->actual_max_buffer_size, SPA_PTROFF(d[0].data, offs, void), wanted * stride); expected_timestamp += wanted; spa_ringbuffer_write_update(&impl->ring, expected_timestamp); @@ -617,6 +815,7 @@ static int rtp_audio_init(struct impl *impl, struct pw_core *core, enum spa_dire impl->stream_events.process = rtp_audio_process_playback; impl->receive_rtp = rtp_audio_receive; + impl->stop_timer = rtp_audio_stop_timer; impl->flush_timeout = rtp_audio_flush_timeout; setup_ptp_sender(impl, core, direction, ptp_driver); diff --git a/src/modules/module-rtp/midi.c b/src/modules/module-rtp/midi.c index 1b7f9ad65..5fbdf3b63 100644 --- a/src/modules/module-rtp/midi.c +++ b/src/modules/module-rtp/midi.c @@ -2,6 +2,12 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include +#include + +/* TODO: Direct timestamp mode here may require a rework. See audio.c for a reference. + * Also check out the usage of actual_max_buffer_size in audio.c. */ + static void rtp_midi_process_playback(void *data) { struct impl *impl = data; @@ -51,7 +57,8 @@ static void rtp_midi_process_playback(void *data) goto done; /* the ringbuffer contains series of sequences, one for each - * received packet */ + * received packet. This is not in shared mem so we can safely use + * the iterators here. */ SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) { /* try to render with given delay */ uint32_t target = c->offset + impl->target_buffer; @@ -83,9 +90,10 @@ complete: pw_log_warn("overflow buffer %u %u", b.state.offset, maxsize); b.state.offset = 0; } + d[0].chunk->offset = 0; d[0].chunk->size = b.state.offset; d[0].chunk->stride = 1; - d[0].chunk->offset = 0; + d[0].chunk->flags = 0; done: pw_stream_queue_buffer(impl->stream, buf); } @@ -95,12 +103,15 @@ static int parse_varlen(uint8_t *p, uint32_t avail, uint32_t *result) uint32_t value = 0, offs = 0; while (offs < avail) { uint8_t b = p[offs++]; + if (value > (UINT32_MAX >> 7)) + return -ERANGE; value = (value << 7) | (b & 0x7f); - if ((b & 0x80) == 0) - break; + if ((b & 0x80) == 0) { + *result = value; + return offs; + } } - *result = value; - return offs; + return -EINVAL; } static int get_midi_size(uint8_t *p, uint32_t avail) @@ -108,6 +119,8 @@ static int get_midi_size(uint8_t *p, uint32_t avail) int size; uint32_t offs = 0, value; + if (avail < 1) + return -EINVAL; switch (p[offs++]) { case 0xc0 ... 0xdf: size = 2; @@ -119,8 +132,11 @@ static int get_midi_size(uint8_t *p, uint32_t avail) case 0xff: case 0xf0: case 0xf7: - size = parse_varlen(&p[offs], avail - offs, &value); - size += value + 1; + if ((size = parse_varlen(&p[offs], avail - offs, &value)) < 0) + return size; + if ((unsigned int)(INT_MAX - size - 1) > value) + return -EINVAL; + size += (int)value + 1; break; default: return -EINVAL; @@ -129,7 +145,11 @@ static int get_midi_size(uint8_t *p, uint32_t avail) } static int parse_journal(struct impl *impl, uint8_t *packet, uint16_t seq, uint32_t len) { - struct rtp_midi_journal *j = (struct rtp_midi_journal*)packet; + struct rtp_midi_journal *j; + + if (len < sizeof(*j)) + return -EINVAL; + j = (struct rtp_midi_journal*)packet; uint16_t seqnum = ntohs(j->checkpoint_seqnum); rtp_stream_emit_send_feedback(impl, seqnum); return 0; @@ -155,7 +175,7 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti uint16_t seq, uint32_t payload_offset, uint32_t plen) { uint32_t write; - struct rtp_midi_header *hdr; + struct rtp_midi_header hdr; int32_t filled; struct spa_pod_builder b; struct spa_pod_frame f[1]; @@ -163,6 +183,8 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti uint32_t offs = payload_offset, len, end; bool first = true; + if (plen <= payload_offset) + return -EINVAL; if (impl->direct_timestamp) { /* in direct timestamp we attach the RTP timestamp directly on the * midi events and render them in the corresponding cycle */ @@ -218,18 +240,25 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti return -ENOSPC; } - hdr = (struct rtp_midi_header *)&packet[offs++]; - len = hdr->len; - if (hdr->b) { - len = (len << 8) | hdr->len_b; - offs++; + SPA_STATIC_ASSERT(sizeof hdr == 2); + memcpy(&hdr, &packet[offs++], 1); + if (hdr.b) { + if (offs >= plen) { + pw_log_warn("invalid packet: no room for long length byte"); + return -EINVAL; + } + hdr.len_b = packet[offs++]; + len = (hdr.len << 8) | hdr.len_b; + } else { + hdr.len_b = 0; + len = hdr.len; } - end = len + offs; - if (end > plen) { - pw_log_warn("invalid packet %d > %d", end, plen); + if (plen - offs < len) { + pw_log_warn("invalid packet %" PRIu64 " > %" PRIu32, (uint64_t)offs + len, plen); return -EINVAL; } - if (hdr->j) + end = len + offs; + if (hdr.j) parse_journal(impl, &packet[end], seq, plen - end); ptr = SPA_PTROFF(impl->buffer, write & BUFFER_MASK2, void); @@ -246,17 +275,23 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti uint8_t *d; size_t s; - if (first && !hdr->z) + if (first && !hdr.z) delta = 0; - else - offs += parse_varlen(&packet[offs], end - offs, &delta); + else { + size = parse_varlen(&packet[offs], end - offs, &delta); + if (size < 0) { + pw_log_warn("invalid offset at offset %u", offs); + return size; + } + offs += size; + } timestamp += (uint32_t)(delta * impl->corr); size = get_midi_size(&packet[offs], end - offs); - if (size <= 0 || offs + size > end) { + if (size <= 0 || (unsigned int)size > end - offs) { pw_log_warn("invalid size (%08x) %d (%u %u)", packet[offs], size, offs, end); - break; + return -EINVAL; } d = &packet[offs]; @@ -273,21 +308,25 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti offs += size; first = false; } - spa_pod_builder_pop(&b, &f[0]); - + if (spa_pod_builder_pop(&b, &f[0]) == NULL) { + pw_log_warn("overflow"); + return -ENOSPC; + } write += b.state.offset; spa_ringbuffer_write_update(&impl->ring, write); return 0; } -static int rtp_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len) +static int rtp_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len, + uint64_t current_time) { struct rtp_header *hdr; ssize_t hlen; uint16_t seq; uint32_t timestamp; + SPA_STATIC_ASSERT(sizeof(struct rtp_header) == 12); if (len < 12) goto short_packet; @@ -296,7 +335,7 @@ static int rtp_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len) goto invalid_version; hlen = 12 + hdr->cc * 4; - if (hlen > len) + if (hlen >= len) goto invalid_len; if (impl->have_ssrc && impl->ssrc != hdr->ssrc) @@ -339,31 +378,41 @@ unexpected_ssrc: return -EINVAL; } -static int write_event(uint8_t *p, uint32_t value, void *ev, uint32_t size) +static int write_event(uint8_t *p, uint32_t buffer_size, uint32_t value, void *ev, uint32_t size) { uint64_t buffer; uint8_t b; - int count = 0; + unsigned int count = 0; + if (buffer_size <= size) + return -ENOSPC; buffer = value & 0x7f; while ((value >>= 7)) { + if (buffer > (UINT64_MAX >> 8)) + return -ERANGE; buffer <<= 8; buffer |= ((value & 0x7f) | 0x80); } do { + if (count >= buffer_size) + return -ENOSPC; b = buffer & 0xff; p[count++] = b; buffer >>= 8; } while (b & 0x80); + if (buffer_size - size < count || + count + size > (unsigned int)INT_MAX) + return -ENOSPC; memcpy(&p[count], ev, size); - return count + size; + return (int)(count + size); } static void rtp_midi_flush_packets(struct impl *impl, - struct spa_pod_sequence *sequence, uint32_t timestamp, uint32_t rate) + struct spa_pod_parser *parser, uint32_t timestamp, uint32_t rate) { - struct spa_pod_control *c; + struct spa_pod_control c; + const void *c_body; struct rtp_header header; struct rtp_midi_header midi_header; struct iovec iov[3]; @@ -386,57 +435,64 @@ static void rtp_midi_flush_packets(struct impl *impl, prev_offset = len = base = 0; max_size = impl->payload_size - sizeof(midi_header); - SPA_POD_SEQUENCE_FOREACH(sequence, c) { + while (spa_pod_parser_get_control_body(parser, &c, &c_body) >= 0) { uint32_t delta, offset; uint8_t event[16]; - size_t size; + int size; + size_t c_size = c.value.size; + uint64_t state = 0; - if (c->type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_UMP) continue; - size = spa_ump_to_midi(SPA_POD_BODY(&c->value), - SPA_POD_BODY_SIZE(&c->value), event, sizeof(event)); - if (size <= 0) - continue; + while (c_size > 0) { + size = spa_ump_to_midi((const uint32_t **)&c_body, &c_size, event, sizeof(event), &state); + if (size <= 0) + break; - offset = c->offset * impl->rate / rate; + offset = c.offset * impl->rate / rate; - if (len > 0 && (len + size > max_size || - offset - base > impl->psamples)) { - /* flush packet when we have one and when it's either - * too large or has too much data. */ - if (len < 16) { - midi_header.b = 0; - midi_header.len = len; - iov[1].iov_len = sizeof(midi_header) - 1; - } else { - midi_header.b = 1; - midi_header.len = (len >> 8) & 0xf; - midi_header.len_b = len & 0xff; - iov[1].iov_len = sizeof(midi_header); + if (len > 0 && (len + size > max_size || + offset - base > impl->psamples)) { + /* flush packet when we have one and when it's either + * too large or has too much data. */ + if (len < 16) { + midi_header.b = 0; + midi_header.len = len; + iov[1].iov_len = sizeof(midi_header) - 1; + } else { + midi_header.b = 1; + midi_header.len = (len >> 8) & 0xf; + midi_header.len_b = len & 0xff; + iov[1].iov_len = sizeof(midi_header); + } + iov[2].iov_len = len; + + pw_log_trace("sending %d timestamp:%d %u %u", + len, timestamp + base, + offset, impl->psamples); + rtp_stream_emit_send_packet(impl, iov, 3); + + impl->seq++; + len = 0; } - iov[2].iov_len = len; + if ((unsigned int)size > BUFFER_SIZE || len > BUFFER_SIZE - size) { + pw_log_error("Buffer overflow prevented!"); + return; // FIXME: what to do instead? + } + if (len == 0) { + /* start new packet */ + base = prev_offset = offset; + header.sequence_number = htons(impl->seq); + header.timestamp = htonl(impl->ts_offset + timestamp + base); - pw_log_trace("sending %d timestamp:%d %u %u", - len, timestamp + base, - offset, impl->psamples); - rtp_stream_emit_send_packet(impl, iov, 3); - - impl->seq++; - len = 0; - } - if (len == 0) { - /* start new packet */ - base = prev_offset = offset; - header.sequence_number = htons(impl->seq); - header.timestamp = htonl(impl->ts_offset + timestamp + base); - - memcpy(&impl->buffer[len], event, size); - len += size; - } else { - delta = offset - prev_offset; - prev_offset = offset; - len += write_event(&impl->buffer[len], delta, event, size); + memcpy(&impl->buffer[len], event, size); + len += size; + } else { + delta = offset - prev_offset; + prev_offset = offset; + len += write_event(&impl->buffer[len], BUFFER_SIZE - len, delta, event, size); + } } } if (len > 0) { @@ -464,9 +520,11 @@ static void rtp_midi_process_capture(void *data) struct impl *impl = data; struct pw_buffer *buf; struct spa_data *d; - uint32_t offs, size, timestamp, rate; - struct spa_pod *pod; - void *ptr; + uint32_t timestamp, rate; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + const void *seq_body; if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) { pw_log_info("Out of stream buffers: %m"); @@ -474,9 +532,6 @@ static void rtp_midi_process_capture(void *data) } d = buf->buffer->datas; - offs = SPA_MIN(d[0].chunk->offset, d[0].maxsize); - size = SPA_MIN(d[0].chunk->size, d[0].maxsize - offs); - if (SPA_LIKELY(impl->io_position)) { rate = impl->io_position->clock.rate.denom; timestamp = impl->io_position->clock.position * impl->rate / rate; @@ -485,11 +540,10 @@ static void rtp_midi_process_capture(void *data) timestamp = 0; } - ptr = SPA_PTROFF(d[0].data, offs, void); - if ((pod = spa_pod_from_data(ptr, size, 0, size)) == NULL) - goto done; - if (!spa_pod_is_sequence(pod)) + spa_pod_parser_init_from_data(&parser, d[0].data, d[0].maxsize, + d[0].chunk->offset, d[0].chunk->size); + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) goto done; if (!impl->have_sync) { @@ -498,7 +552,7 @@ static void rtp_midi_process_capture(void *data) impl->have_sync = true; } - rtp_midi_flush_packets(impl, (struct spa_pod_sequence*)pod, timestamp, rate); + rtp_midi_flush_packets(impl, &parser, timestamp, rate); done: pw_stream_queue_buffer(impl->stream, buf); diff --git a/src/modules/module-rtp/opus.c b/src/modules/module-rtp/opus.c index 83857c80b..d13a4efaf 100644 --- a/src/modules/module-rtp/opus.c +++ b/src/modules/module-rtp/opus.c @@ -7,6 +7,9 @@ #include #include +/* TODO: Direct timestamp mode here may require a rework. See audio.c for a reference. + * Also check out the usage of actual_max_buffer_size in audio.c. */ + static void rtp_opus_process_playback(void *data) { struct impl *impl = data; @@ -87,15 +90,17 @@ static void rtp_opus_process_playback(void *data) timestamp += wanted; spa_ringbuffer_read_update(&impl->ring, timestamp); } + d[0].chunk->offset = 0; d[0].chunk->size = wanted * stride; d[0].chunk->stride = stride; - d[0].chunk->offset = 0; + d[0].chunk->flags = 0; buf->size = wanted; pw_stream_queue_buffer(impl->stream, buf); } -static int rtp_opus_receive(struct impl *impl, uint8_t *buffer, ssize_t len) +static int rtp_opus_receive(struct impl *impl, uint8_t *buffer, ssize_t len, + uint64_t current_time) { struct rtp_header *hdr; ssize_t hlen, plen; @@ -317,12 +322,25 @@ static void rtp_opus_process_capture(void *data) rtp_opus_flush_packets(impl); } +static void rtp_opus_deinit(struct impl *impl, enum spa_direction direction) +{ + if (impl->stream_data) { + if (direction == SPA_DIRECTION_INPUT) + opus_multistream_encoder_destroy(impl->stream_data); + else + opus_multistream_decoder_destroy(impl->stream_data); + } +} + static int rtp_opus_init(struct impl *impl, enum spa_direction direction) { int err; - unsigned char mapping[64]; + unsigned char mapping[255]; uint32_t i; + if (impl->info.info.opus.channels > 255) + return -EINVAL; + if (impl->psamples >= 2880) impl->psamples = 2880; else if (impl->psamples >= 1920) @@ -339,6 +357,7 @@ static int rtp_opus_init(struct impl *impl, enum spa_direction direction) for (i = 0; i < impl->info.info.opus.channels; i++) mapping[i] = i; + impl->deinit = rtp_opus_deinit; impl->receive_rtp = rtp_opus_receive; if (direction == SPA_DIRECTION_INPUT) { impl->stream_events.process = rtp_opus_process_capture; diff --git a/src/modules/module-rtp/rtp.h b/src/modules/module-rtp/rtp.h index 1fd3bedef..24111dc67 100644 --- a/src/modules/module-rtp/rtp.h +++ b/src/modules/module-rtp/rtp.h @@ -32,7 +32,7 @@ struct rtp_header { uint16_t sequence_number; uint32_t timestamp; uint32_t ssrc; - uint32_t csrc[0]; + uint32_t csrc[]; } __attribute__ ((packed)); struct rtp_payload { diff --git a/src/modules/module-rtp/stream.c b/src/modules/module-rtp/stream.c index f4d3fa3b8..e19d88a1f 100644 --- a/src/modules/module-rtp/stream.c +++ b/src/modules/module-rtp/stream.c @@ -2,23 +2,25 @@ /* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include -#include "config.h" - #include #include @@ -34,14 +36,45 @@ PW_LOG_TOPIC_EXTERN(mod_topic); #define BUFFER_SIZE2 (BUFFER_SIZE>>1) #define BUFFER_MASK2 (BUFFER_SIZE2-1) +/* IMPORTANT: When using calls that have return values, like + * rtp_stream_emit_open_connection, callers must set the variables + * that receive the return values to a default value, because in + * cases where the callback is not actually set, no call is made, + * and thus, uninitialized return variables remain uninitialized.*/ #define rtp_stream_emit(s,m,v,...) spa_hook_list_call(&s->listener_list, \ struct rtp_stream_events, m, v, ##__VA_ARGS__) #define rtp_stream_emit_destroy(s) rtp_stream_emit(s, destroy, 0) -#define rtp_stream_emit_state_changed(s,n,e) rtp_stream_emit(s, state_changed,0,n,e) +#define rtp_stream_emit_report_error(s,e) rtp_stream_emit(s, report_error, 0,e) +#define rtp_stream_emit_open_connection(s,r) rtp_stream_emit(s, open_connection, 0,r) +#define rtp_stream_emit_close_connection(s,r) rtp_stream_emit(s, close_connection, 0,r) #define rtp_stream_emit_param_changed(s,i,p) rtp_stream_emit(s, param_changed,0,i,p) #define rtp_stream_emit_send_packet(s,i,l) rtp_stream_emit(s, send_packet,0,i,l) #define rtp_stream_emit_send_feedback(s,seq) rtp_stream_emit(s, send_feedback,0,seq) +enum rtp_stream_internal_state { + /* The state when the stream is idle / stopped. The background + * timer that may be used for sending out buffered data + * must not be running in this state. If the separate PTP sender + * is being used, then that one must be inactive in this state. + * Set at the end of stream_stop() and when the stream is created. */ + RTP_STREAM_INTERNAL_STATE_STOPPED, + /* Temporary state that is set at the beginning of stream_stop(). + * If a full stop is possible, stream_stop() immediately moves on + * to the STOPPED state. However, if the timer is running (because it + * is still sending out buffered data), the state remains set to + * STOPPING until the timer has sent out all data, at which point + * the timer finishes the change to the STOPPED state. */ + RTP_STREAM_INTERNAL_STATE_STOPPING, + /* Temporary state that is set at the beginning of stream_start(). + * It is mainly used for preventing do_finish_stopping_state() + * from setting a stopped state. See do_finish_stopping_state() + * for details. */ + RTP_STREAM_INTERNAL_STATE_STARTING, + /* The state when the stream has been started. It is set at the + * end of stream_start(). */ + RTP_STREAM_INTERNAL_STATE_STARTED +}; + struct impl { struct spa_audio_info info; struct spa_audio_info stream_info; @@ -57,10 +90,12 @@ struct impl { const struct format_info *format_info; + enum spa_direction direction; void *stream_data; uint32_t rate; uint32_t stride; + uint32_t actual_max_buffer_size; uint8_t payload; uint32_t ssrc; uint16_t seq; @@ -91,18 +126,46 @@ struct impl { unsigned direct_timestamp:1; unsigned always_process:1; - unsigned started:1; unsigned have_sync:1; unsigned receiving:1; unsigned first:1; + /* IMPORTANT: Do NOT access this value directly. Use the atomic + * set_internal_stream_state() / get_internal_stream_state() accessors, + * since the state is accessed by both the dataloop and mainloop. To + * prevent memory visibility issues, atomic accessors need to be used. + * + * Also, its type here is uint32_t. See the explanation about atomic + * access below for the reason why. */ + uint32_t internal_state; + struct pw_loop *main_loop; struct pw_loop *data_loop; struct spa_source *timer; - bool timer_running; + /* IMPORTANT: Do NOT access this value directly. Use the atomic + * set_timer_running() / is_timer_running() accessors, since the + * flag is accessed by both the dataloop and mainloop. To prevent + * memory visibility issues, atomic accessors need to be used. + * + * Also, its type here is uint8_t. See the explanation about atomic + * access below for the reason why. */ + uint8_t timer_running; - int (*receive_rtp)(struct impl *impl, uint8_t *buffer, ssize_t len); + int (*receive_rtp)(struct impl *impl, uint8_t *buffer, ssize_t len, + uint64_t current_time); + /* Used for resetting the ring buffer before the stream starts, to prevent + * reading from uninitialized memory. This can otherwise happen in direct + * timestamp mode when the read index is set to an uninitialized location. + * This is a function pointer to allow customizations in case resetting + * requires filling the ring buffer with something other than nullbytes + * (this can happen with DSD for example). */ + void (*reset_ringbuffer)(struct impl *impl); + /* Called by stream_start() to stop any running timer before continuing to + * start the stream. This is necessary, because by that point, any remaining + * buffered data is stale, and the timer would keep sending it out. */ + void (*stop_timer)(struct impl *impl); void (*flush_timeout)(struct impl *impl, uint64_t expirations); + void (*deinit)(struct impl *impl, enum spa_direction direction); /* * pw_filter where the filter would be driven at the PTP clock @@ -124,14 +187,103 @@ struct impl { /* And some bookkeping for the sender processing */ uint64_t rtp_base_ts; uint32_t rtp_last_ts; + + /* The process latency, set by on_stream_param_changed(). */ + struct spa_process_latency_info process_latency; }; -static int do_emit_state_changed(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) -{ - struct impl *impl = user_data; - bool *started = (bool *)data; +/* Atomic internal_state accessors. + * + * These are necessary because internal_state may be accessed by both + * the dataloop (in the flush_timeout and do_finish_stopping_state()) + * and the mainloop (in stream_start() and stream_stop()). Even though + * stream_start() and stream_stop() may not necessarily run at the + * same time when the dataloop is active, there is still a potential + * memory visibility issue if the state is set in one loop but that + * change is not yet propagated to other CPU cores, causing the other + * loop (which runs in a separate thread) to still see the old state. + * + * Also, since GCC __atomic built-ins (which the SPA macros use) are + * designed to work with integral scalar or pointer type that is 1, + * 2, 4, or 8 bytes in length, impl->internal_state is of type uint33_t. + * This guarantee a correct size for the built-ins. The accessors take + * care of casting from/to rtp_stream_internal_state . The relevant + * GCC manual page for this is: + * https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html + */ + +static inline enum rtp_stream_internal_state get_internal_stream_state(struct impl *impl) { + return (enum rtp_stream_internal_state)SPA_ATOMIC_LOAD(impl->internal_state); +} + +static inline void set_internal_stream_state(struct impl *impl, enum rtp_stream_internal_state state) { + SPA_ATOMIC_STORE(impl->internal_state, (uint32_t)state); +} + +/* Similar to the atomic internal_state accessors, these safeguard + * the timer_running flag, which can be accessed both by stream_stop() + * and the flush_timeout, which are called in separate threads. + * Since timer_running and internal_state are accessed independently, + * they are treated as two independent atomic variables instead of two + * resources under a common mutex. */ + +static inline bool is_timer_running(struct impl *impl) { + return (bool)SPA_ATOMIC_LOAD(impl->timer_running); +} + +static inline void set_timer_running(struct impl *impl, bool running) { + SPA_ATOMIC_STORE(impl->timer_running, (uint8_t)(running ? 1 : 0)); +} + +static int do_finish_stopping_state(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) +{ + int res = 0; + struct impl *impl = user_data; + enum rtp_stream_internal_state cur_state = get_internal_stream_state(impl); + + /* The checks here cover a corner case that can happen when the + * following conditions are met (in order): + * + * 1. Stream is stopped via stream_stop(), but the timer is still + * running, meaning that internal_state stays at STOPPING. + * 2. The timer manages to invoke do_finish_stopping_state() + * asynchronously, meaning that the invocation is queued. + * 3. Immediately afterwards, the state is started again via + * stream_start(). That call stops the timer, but does not + * undo the do_finish_stopping_state() invocation. + * The internal_state is set to STARTED. + * 4. The queued do_finish_stopping_state() invocation takes + * place, and it tries to set the internal_state to STOPPED. + * + * In such a case, the STARTED state would be set again to STOPPED, + * even though the stream has been started and is running. + * + * To fix this, check if the current internal state is STOPPING. + * This is the only case where setting the state to STOPPED makes + * sense, since that is why this do_finish_stopping_state() exists - + * to finish a stopping procedure that could not be finished in + * stream_stop() immediately. If the stream is restarted, then this + * delayed stop is no longer needed. Canceling the queued invocation + * is not possible (PipeWire has no cancellation API for this), + * so this approach needs to be used instead. */ + + switch (cur_state) { + case RTP_STREAM_INTERNAL_STATE_STOPPING: + pw_log_debug("setting \"stopped\" state after timer expired"); + break; + default: + pw_log_debug("\"stopped\" state change event emission was scheduled, " + "but the current state is not \"stopping\"; ignoring " + "scheduled request"); + return 0; + } + + rtp_stream_emit_close_connection(impl, &res); + if (res > 0) + pw_log_debug("closed connection"); + else if (res < 0) + pw_log_error("error while closing connection: %s", spa_strerror(res)); - rtp_stream_emit_state_changed(impl, *started, NULL); return 0; } @@ -180,12 +332,63 @@ static void stream_destroy(void *d) static int stream_start(struct impl *impl) { - if (impl->started) + int res; + enum rtp_stream_internal_state cur_state; + + cur_state = get_internal_stream_state(impl); + + if (cur_state == RTP_STREAM_INTERNAL_STATE_STARTED) return 0; impl->first = true; - rtp_stream_emit_state_changed(impl, true, NULL); + set_internal_stream_state(impl, RTP_STREAM_INTERNAL_STATE_STARTING); + + /* Stop the timer now (if the timer is used). Any lingering timer + * will try to send data that is stale at this point, especially + * after the ring buffer contents get reset. Worse, the timer might + * emit a "stopped" state change after a "started" state change + * is emitted here, causing undefined behavior. */ + if (impl->stop_timer) + impl->stop_timer(impl); + + res = 0; + rtp_stream_emit_close_connection(impl, &res); + + /* A leftover connection only makes sense if the stream was in the + * STOPPING state prior to this stream_start() call, because then, + * the previous stream_stop() call could not finish stopping the + * stream, and had to leave the connection open so the timer can + * finish sending out packets. If stream_start() was called before + * the timer finished, then the stream is still in the STOPPING + * state, was thus not properly stopped, and the connection is still + * there. This is not an error, but a consequence of restarting the + * stream early enough. + * If however the state prior to this stream_start() call was + * anything other than STOPPING, then something is wrong. */ + if (res > 0) { + if (cur_state != RTP_STREAM_INTERNAL_STATE_STOPPING) { + pw_log_warn("there was already an open connection, " + "even though none was expected"); + } else { + pw_log_debug("closed leftover connection since a scheduled " + "\"stopped\" state change was cancelled " + "and we are still in the \"stopping\" state"); + } + } else if (res < 0) { + pw_log_error("error while closing leftover connection: %s", spa_strerror(res)); + } + + impl->reset_ringbuffer(impl); + + res = 0; + rtp_stream_emit_open_connection(impl, &res); + if (res > 0) { + pw_log_debug("opened new connection"); + } else if (res < 0) { + pw_log_error("could not open connection: %s", spa_strerror(res)); + return res; + } if (impl->separate_sender) { struct spa_dict_item items[1]; @@ -197,20 +400,46 @@ static int stream_start(struct impl *impl) pw_log_info("activated pw_filter for separate sender"); } - impl->started = true; + set_internal_stream_state(impl, RTP_STREAM_INTERNAL_STATE_STARTED); + pw_log_info("stream started"); return 0; } static int stream_stop(struct impl *impl) { - if (!impl->started) - return 0; + bool timer_running; - /* if timer is running, the state changed event must be emitted by the timer after all packets have been sent */ - if (!impl->timer_running) - rtp_stream_emit_state_changed(impl, false, NULL); + switch (get_internal_stream_state(impl)) { + case RTP_STREAM_INTERNAL_STATE_STOPPING: + case RTP_STREAM_INTERNAL_STATE_STOPPED: + return 0; + default: + break; + } + set_internal_stream_state(impl, RTP_STREAM_INTERNAL_STATE_STOPPING); + + timer_running = is_timer_running(impl); + + /* Proper stop is only possible if the timer is currently not running, + * because a stop involves closing the connection. If the timer is still + * running, it needs an open connection for sending out remaining packets. */ + if (!timer_running) { + int res; + pw_log_info("closing connection as part of stopping the stream"); + rtp_stream_emit_close_connection(impl, &res); + if (res > 0) { + pw_log_debug("closed connection"); + } else if (res < 0) { + pw_log_error("error while closing connection: %s", spa_strerror(res)); + } + } else { + pw_log_info("cannot close connection yet - timer is still running"); + } + + /* Stopping the separate sender can be done even if the timer is still + * running because it has no dependency on said timer. */ if (impl->separate_sender) { struct spa_dict_item items[1]; items[0] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_ALWAYS_PROCESS, "false"); @@ -221,7 +450,14 @@ static int stream_stop(struct impl *impl) pw_filter_set_active(impl->ptp_sender, false); } - impl->started = false; + /* Only switch to STOPPED if the stream could _actually_ be stopped, + * meaning that the timer was no longer running, and the connection + * could be closed. */ + if (!timer_running) { + set_internal_stream_state(impl, RTP_STREAM_INTERNAL_STATE_STOPPED); + pw_log_info("stream stopped"); + } + return 0; } @@ -251,10 +487,70 @@ static void on_stream_state_changed(void *d, enum pw_stream_state old, } } +static void update_latency_params(struct impl *impl) +{ + uint32_t n_params = 0; + const struct spa_pod *params[2]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + struct spa_latency_info main_latency; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + /* main_latency is the latency in the direction indicated by impl->direction. + * In RTP streams, this consists solely of the process latency. (In theory, + * PipeWire SPA nodes could have additional latencies on top of the process + * latency, but this is not the case here.) The other direction is already + * handled by pw_stream. + * + * The main_latncy is passed as updated SPA_PARAM_Latency params to the stream. + * That way, the stream always gets information of latency for _both_ directions; + * the direction indicated by impl->direction is covered by main_latency, and + * the opposite direction is already taken care of by the default pw_stream + * param handling. + * + * The process latency is also passed on as an SPA_PARAM_ProcessLatency param. + */ + + main_latency = SPA_LATENCY_INFO(impl->direction); + spa_process_latency_info_add(&impl->process_latency, &main_latency); + + params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &main_latency); + params[n_params++] = spa_process_latency_build(&b, SPA_PARAM_ProcessLatency, + &impl->process_latency); + + pw_stream_update_params(impl->stream, params, n_params); +} + +static void param_process_latency_changed(struct impl *impl, const struct spa_pod *param) +{ + struct spa_process_latency_info process_latency; + + if (param == NULL) + spa_zero(process_latency); + + else if (spa_process_latency_parse(param, &process_latency) < 0) + return; + if (spa_process_latency_info_compare(&impl->process_latency, &process_latency) == 0) + return; + + impl->process_latency = process_latency; + + update_latency_params(impl); +} + static void on_stream_param_changed (void *d, uint32_t id, const struct spa_pod *param) { struct impl *impl = d; - rtp_stream_emit_param_changed(impl, id, param); + + switch (id) { + case SPA_PARAM_ProcessLatency: + param_process_latency_changed(impl, param); + break; + default: + rtp_stream_emit_param_changed(impl, id, param); + break; + } }; static const struct pw_stream_events stream_events = { @@ -274,9 +570,9 @@ static const struct format_info *find_audio_format_info(const struct spa_audio_i return NULL; } -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), @@ -285,6 +581,7 @@ static void parse_audio_info(const struct pw_properties *props, struct spa_audio SPA_KEY_AUDIO_FORMAT, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -303,8 +600,13 @@ static void on_flush_timeout(void *d, uint64_t expirations) impl->flush_timeout(d, expirations); } +static void default_reset_ringbuffer(struct impl *impl) +{ + spa_memzero(impl->buffer, sizeof(impl->buffer)); +} + struct rtp_stream *rtp_stream_new(struct pw_core *core, - enum pw_direction direction, struct pw_properties *props, + enum spa_direction direction, struct pw_properties *props, const struct rtp_stream_events *events, void *data) { struct impl *impl; @@ -314,10 +616,11 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, struct spa_pod_builder b; uint32_t n_params, min_samples, max_samples; float min_ptime, max_ptime; - const struct spa_pod *params[1]; + const struct spa_pod *params[3]; enum pw_stream_flags flags; float latency_msec; int res; + bool process_latency_from_sess; impl = calloc(1, sizeof(*impl)); if (impl == NULL) { @@ -325,7 +628,9 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, goto out; } impl->first = true; + set_internal_stream_state(impl, RTP_STREAM_INTERNAL_STATE_STOPPED); spa_hook_list_init(&impl->listener_list); + impl->direction = direction; impl->stream_events = stream_events; impl->context = pw_core_get_context(core); impl->main_loop = pw_context_get_main_loop(impl->context); @@ -337,6 +642,8 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, goto out; } + impl->reset_ringbuffer = default_reset_ringbuffer; + if ((str = pw_properties_get(props, "sess.media")) == NULL) str = "audio"; @@ -370,7 +677,10 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, switch (impl->info.media_subtype) { case SPA_MEDIA_SUBTYPE_raw: - parse_audio_info(props, &impl->info.info.raw); + if ((res = parse_audio_info(props, &impl->info.info.raw)) < 0) { + pw_log_error("can't parse format: %s", spa_strerror(res)); + goto out; + } impl->stream_info = impl->info; impl->format_info = find_audio_format_info(&impl->info); if (impl->format_info == NULL) { @@ -390,7 +700,7 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, res = -EINVAL; goto out; } - pw_properties_set(props, PW_KEY_FORMAT_DSP, "32 bit raw UMP"); + pw_properties_set(props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); impl->stride = impl->format_info->size; impl->rate = pw_properties_get_uint32(props, "midi.rate", 10000); if (impl->rate == 0) @@ -399,7 +709,10 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, case SPA_MEDIA_SUBTYPE_opus: impl->stream_info.media_type = SPA_MEDIA_TYPE_audio; impl->stream_info.media_subtype = SPA_MEDIA_SUBTYPE_raw; - parse_audio_info(props, &impl->stream_info.info.raw); + if ((res = parse_audio_info(props, &impl->stream_info.info.raw)) < 0) { + pw_log_error("can't parse format: %s", spa_strerror(res)); + goto out; + } impl->stream_info.info.raw.format = SPA_AUDIO_FORMAT_F32; impl->info.info.opus.rate = impl->stream_info.info.raw.rate; impl->info.info.opus.channels = impl->stream_info.info.raw.channels; @@ -420,6 +733,46 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, break; } + /* Limit the actual maximum buffer size to the maximum integer multiple + * amount of impl->stride that fits within BUFFER_SIZE. This is important + * to prevent corner cases where the read pointer wrapped around at the + * same time when the IO clock experiences a discontinuity. + * + * If the BUFFER_SIZE constant is not an integer multiple of impl->stride, + * pointer wrap-arounds will result in positions that exhibit a nonzero + * impl->stride division rest. Also, the write and read pointers are normally + * increased monotonically and contiguously. But, if a discontinuity is + * detected, these pointers may be resynchronized. Importantly, sometimes + * only one of them may be resynchronized, while the other retains its existing + * synchronization. (For example, the read and write side may use different + * discontinuity thresholds.) + * + * What then can happen is that the resynchronized pointer exhibits a _different_ + * impl->stride division than the other pointer. Once the resynchronization takes + * place, that pointer is again monotonically increased from then on, so those + * division rests will stay different. This then means that the read and write + * operations will not be aligned properly. For example, a write operation might + * write to position 20 in the ring buffer, but the read operation might read + * from position 22, and doing so with a stride value of 6. The end result is + * invalid data. + * + * One way to visualize this is to think of the ring buffer as a grid. The grid + * cell size equals impl->stride. If BUFFER_SIZE is not an integer multiple of + * impl->stride, it means that the very last grid cell will have a size that is + * smaller than impl->stride. The unaligned read/write operations mean that the + * operations will not be done at the same grid cell boundaries, so for example + * the read operation might think that a cell starts at byte 2, while the write + * operation might think that the same cell starts at byte 4. + * + * By limiting the actual maximum buffer size to the maximum integer multiple + * amount of impl->stride that fits within BUFFER_SIZE, this is avoided, since + * then, all grid cells are guaranteed to have the size impl->stride, so the + * aforementioned division rest will always be zero. + */ + impl->actual_max_buffer_size = SPA_ROUND_DOWN(BUFFER_SIZE, impl->stride); + pw_log_debug("possible / actual max buffer size: %" PRIu32 " / %" PRIu32, + (uint32_t)BUFFER_SIZE, impl->actual_max_buffer_size); + pw_properties_setf(props, "rtp.mime", "%s", impl->format_info->mime); if (pw_properties_get(props, PW_KEY_NODE_VIRTUAL) == NULL) @@ -557,6 +910,8 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, pw_properties_set(props, "rtp.ts-refclk", str); } + process_latency_from_sess = pw_properties_get_bool(props, "process.latency.from.sess", false); + spa_dll_init(&impl->dll); spa_dll_set_bw(&impl->dll, SPA_DLL_BW_MIN, 128, impl->rate); impl->corr = 1.0; @@ -599,6 +954,33 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, goto out; } + if (process_latency_from_sess) { + /* If process.latency.from.sess is set to true, then the sess.latency.msec + * quantity is to be set as the process latency at startup. But since the + * sess.latency.msec value is converted to impl->target_buffer, and that + * quantity in turn is subjected to constraint checks (see above), it is + * possible that the _actual_ session latency no longer equals the value + * of sess.latency.msec by the time this location is reached. To take into + * account these constraint adjustments, convert back the impl->target_buffer + * to nanoseconds, and use that as the process latency. + * + * Then, just like how update_latency_params() does it, construct the + * SPA_PARAM_Latency and SPA_PARAM_ProcessLatency params to let the new + * pw_stream know of these latency figures right from the start. */ + + struct spa_latency_info latency; + + impl->process_latency.ns = (int64_t)(impl->target_buffer * 1e9 / impl->rate); + pw_log_debug("set process latency to %" PRId64 " based on sess.latency.msec " + "value %f", impl->process_latency.ns, latency_msec); + + latency = SPA_LATENCY_INFO(impl->direction); + spa_process_latency_info_add(&(impl->process_latency), &latency); + params[n_params++] = spa_latency_build(&b, SPA_PARAM_Latency, &latency); + params[n_params++] = spa_process_latency_build(&b, SPA_PARAM_ProcessLatency, + &(impl->process_latency)); + } + pw_stream_add_listener(impl->stream, &impl->stream_listener, &impl->stream_events, impl); @@ -631,6 +1013,12 @@ void rtp_stream_destroy(struct rtp_stream *s) rtp_stream_emit_destroy(impl); + if (impl->deinit) + impl->deinit(impl, impl->direction); + + if (impl->ptp_sender) + pw_filter_destroy(impl->ptp_sender); + if (impl->stream) pw_stream_destroy(impl->stream); @@ -650,10 +1038,17 @@ int rtp_stream_update_properties(struct rtp_stream *s, const struct spa_dict *di return pw_stream_update_properties(impl->stream, dict); } -int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len) +int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len, + uint64_t current_time) { struct impl *impl = (struct impl*)s; - return impl->receive_rtp(impl, buffer, len); + return impl->receive_rtp(impl, buffer, len, current_time); +} + +uint64_t rtp_stream_get_nsec(struct rtp_stream *s) +{ + struct impl *impl = (struct impl*)s; + return pw_stream_get_nsec(impl->stream); } uint64_t rtp_stream_get_time(struct rtp_stream *s, uint32_t *rate) @@ -721,3 +1116,17 @@ int rtp_stream_update_params(struct rtp_stream *s, struct impl *impl = (struct impl*)s; return pw_stream_update_params(impl->stream, params, n_params); } + +void rtp_stream_update_process_latency(struct rtp_stream *s, + const struct spa_process_latency_info *process_latency) +{ + struct impl *impl = (struct impl*)s; + + if (spa_process_latency_info_compare(&impl->process_latency, process_latency) == 0) + return; + + spa_memcpy(&(impl->process_latency), process_latency, + sizeof(const struct spa_process_latency_info)); + + update_latency_params(impl); +} diff --git a/src/modules/module-rtp/stream.h b/src/modules/module-rtp/stream.h index 83cf0da34..2c6e6dea5 100644 --- a/src/modules/module-rtp/stream.h +++ b/src/modules/module-rtp/stream.h @@ -15,6 +15,7 @@ struct rtp_stream; #define DEFAULT_RATE 48000 #define DEFAULT_CHANNELS 2 #define DEFAULT_POSITION "[ FL FR ]" +#define DEFAULT_LAYOUT "Stereo" #define ERROR_MSEC 2.0f #define DEFAULT_SESS_LATENCY 100.0f @@ -35,7 +36,17 @@ struct rtp_stream_events { void (*destroy) (void *data); - void (*state_changed) (void *data, bool started, const char *error); + void (*report_error) (void *data, const char *error); + + /* Requests the network connection to be opened. If result is non-NULL, + * the call sets it to >0 in case of success, and a negative errno error + * code in case of failure. (Result value 0 is unused.) */ + void (*open_connection) (void *data, int *result); + + /* Requests the network connection to be closed. If result is non-NULL, + * the call sets it to >0 in case of success, 0 if the connection was + * already closed, and a negative errno error code in case of failure. */ + void (*close_connection) (void *data, int *result); void (*param_changed) (void *data, uint32_t id, const struct spa_pod *param); @@ -52,7 +63,10 @@ void rtp_stream_destroy(struct rtp_stream *s); int rtp_stream_update_properties(struct rtp_stream *s, const struct spa_dict *dict); -int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len); +int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len, + uint64_t current_time); + +uint64_t rtp_stream_get_nsec(struct rtp_stream *s); uint64_t rtp_stream_get_time(struct rtp_stream *s, uint32_t *rate); @@ -72,6 +86,9 @@ int rtp_stream_update_params(struct rtp_stream *stream, const struct spa_pod **params, uint32_t n_params); +void rtp_stream_update_process_latency(struct rtp_stream *stream, + const struct spa_process_latency_info *process_latency); + #ifdef __cplusplus } #endif diff --git a/src/modules/module-session-manager/protocol-native.c b/src/modules/module-session-manager/protocol-native.c index 98be24429..01be455ae 100644 --- a/src/modules/module-session-manager/protocol-native.c +++ b/src/modules/module-session-manager/protocol-native.c @@ -1284,7 +1284,7 @@ static int endpoint_link_proxy_demarshal_subscribe_params(void *object, SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_proxy_notify(proxy, struct pw_endpoint_link_methods, @@ -1304,7 +1304,7 @@ static int endpoint_link_resource_demarshal_subscribe_params(void *object, SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_resource_notify(resource, struct pw_endpoint_link_methods, @@ -1806,7 +1806,7 @@ static int endpoint_stream_proxy_demarshal_subscribe_params(void *object, SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_proxy_notify(proxy, struct pw_endpoint_stream_methods, @@ -1826,7 +1826,7 @@ static int endpoint_stream_resource_demarshal_subscribe_params(void *object, SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_resource_notify(resource, struct pw_endpoint_stream_methods, @@ -2320,7 +2320,7 @@ static int endpoint_proxy_demarshal_subscribe_params(void *object, SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_proxy_notify(proxy, struct pw_endpoint_methods, @@ -2340,7 +2340,7 @@ static int endpoint_resource_demarshal_subscribe_params(void *object, SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_resource_notify(resource, struct pw_endpoint_methods, @@ -2842,7 +2842,7 @@ static int session_proxy_demarshal_subscribe_params(void *object, SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_proxy_notify(proxy, struct pw_session_methods, @@ -2862,7 +2862,7 @@ static int session_resource_demarshal_subscribe_params(void *object, SPA_POD_Array(&csize, &ctype, &n_ids, &ids)) < 0) return -EINVAL; - if (ctype != SPA_TYPE_Id) + if (ctype != SPA_TYPE_Id || csize != sizeof(uint32_t)) return -EINVAL; return pw_resource_notify(resource, struct pw_session_methods, diff --git a/src/modules/module-snapcast-discover.c b/src/modules/module-snapcast-discover.c index a11409169..596d5677b 100644 --- a/src/modules/module-snapcast-discover.c +++ b/src/modules/module-snapcast-discover.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2024 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -17,8 +19,6 @@ #include #include -#include "config.h" - #include #include #include @@ -166,13 +166,13 @@ static const struct spa_dict_item module_props[] = { struct impl { struct pw_context *context; - struct pw_loop *loop; struct pw_impl_module *module; struct spa_hook module_listener; struct pw_properties *properties; bool discover_local; + struct pw_loop *loop; AvahiPoll *avahi_poll; AvahiClient *client; @@ -503,9 +503,11 @@ static int add_snapcast_stream(struct impl *impl, struct tunnel *t, return -ENOENT; } -static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + int res; + + if ((res = spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), @@ -514,12 +516,15 @@ static void parse_audio_info(struct pw_properties *props, struct spa_audio_info_ SPA_KEY_AUDIO_FORMAT, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, - SPA_KEY_AUDIO_POSITION, NULL); + SPA_KEY_AUDIO_LAYOUT, + SPA_KEY_AUDIO_POSITION, NULL)) < 0) + return res; pw_properties_set(props, PW_KEY_AUDIO_FORMAT, spa_type_audio_format_to_short_name(info->format)); pw_properties_setf(props, PW_KEY_AUDIO_RATE, "%d", info->rate); pw_properties_setf(props, PW_KEY_AUDIO_CHANNELS, "%d", info->channels); + return res; } static int create_stream(struct impl *impl, struct pw_properties *props, @@ -545,7 +550,10 @@ static int create_stream(struct impl *impl, struct pw_properties *props, if ((str = pw_properties_get(props, "capture.props")) == NULL) pw_properties_set(props, "capture.props", "{ media.class = Audio/Sink }"); - parse_audio_info(props, &t->audio_info); + if ((res = parse_audio_info(props, &t->audio_info)) < 0) { + pw_log_error("Can't parse format: %s", spa_strerror(res)); + goto done; + } if ((f = open_memstream(&args, &size)) == NULL) { res = -errno; @@ -850,10 +858,8 @@ static int start_client(struct impl *impl) static int start_avahi(struct impl *impl) { - struct pw_loop *loop; - loop = pw_context_get_main_loop(impl->context); - impl->avahi_poll = pw_avahi_poll_new(loop); + impl->avahi_poll = pw_avahi_poll_new(impl->context); return start_client(impl); } diff --git a/src/modules/module-spa-device-factory.c b/src/modules/module-spa-device-factory.c index eb8d2436a..a3b08a285 100644 --- a/src/modules/module-spa-device-factory.c +++ b/src/modules/module-spa-device-factory.c @@ -2,13 +2,13 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include #include -#include "config.h" - #include #include "pipewire/impl.h" diff --git a/src/modules/module-spa-node-factory.c b/src/modules/module-spa-node-factory.c index 0f9a97020..c7fedd47e 100644 --- a/src/modules/module-spa-node-factory.c +++ b/src/modules/module-spa-node-factory.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -9,8 +11,6 @@ #include -#include "config.h" - #include "pipewire/impl.h" #include "spa/spa-node.h" diff --git a/src/modules/module-vban-recv.c b/src/modules/module-vban-recv.c index 902057965..2ee724305 100644 --- a/src/modules/module-vban-recv.c +++ b/src/modules/module-vban-recv.c @@ -75,6 +75,7 @@ * Options with well-known behavior: * * - \ref PW_KEY_REMOTE_NAME + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_MEDIA_NAME * - \ref PW_KEY_MEDIA_CLASS @@ -166,8 +167,9 @@ struct impl { struct pw_properties *props; struct pw_context *context; - struct pw_loop *loop; + struct pw_loop *main_loop; struct pw_loop *data_loop; + struct pw_timer_queue *timer_queue; struct pw_core *core; struct spa_hook core_listener; @@ -180,7 +182,7 @@ struct impl { struct pw_properties *stream_props; - struct spa_source *timer; + struct pw_timer timer; uint16_t src_port; struct sockaddr_storage src_addr; @@ -457,7 +459,7 @@ static struct stream *make_stream(struct impl *impl, const struct vban_header *h stream->salen = salen; spa_list_append(&impl->streams, &stream->link); - pw_loop_invoke(impl->loop, do_setup_stream, 1, NULL, 0, false, stream); + pw_loop_invoke(impl->main_loop, do_setup_stream, 1, NULL, 0, false, stream); return stream; } @@ -562,7 +564,7 @@ static void destroy_stream(struct stream *s) free(s); } -static void on_timer_event(void *data, uint64_t expirations) +static void on_timer_event(void *data) { struct impl *impl = data; struct stream *s; @@ -576,6 +578,9 @@ static void on_timer_event(void *data, uint64_t expirations) } s->receiving = false; } + pw_timer_queue_add(impl->timer_queue, &impl->timer, + &impl->timer.timeout, impl->cleanup_interval * SPA_NSEC_PER_SEC, + on_timer_event, impl); } static void core_destroy(void *d) @@ -602,8 +607,7 @@ static void impl_destroy(struct impl *impl) if (impl->core && impl->do_disconnect) pw_core_disconnect(impl->core); - if (impl->timer) - pw_loop_destroy_source(impl->loop, impl->timer); + pw_timer_queue_cancel(&impl->timer); if (impl->data_loop) pw_context_release_loop(impl->context, impl->data_loop); @@ -658,7 +662,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) struct pw_context *context = pw_impl_module_get_context(module); struct impl *impl; const char *str; - struct timespec value, interval; struct pw_properties *props, *stream_props; int res = 0; @@ -681,7 +684,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->module = module; impl->context = context; - impl->loop = pw_context_get_main_loop(context); + impl->main_loop = pw_context_get_main_loop(context); + impl->timer_queue = pw_context_get_timer_queue(context); impl->data_loop = pw_context_acquire_loop(context, &props->dict); spa_list_init(&impl->streams); @@ -691,6 +695,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_properties_update_string(stream_props, str, strlen(str)); copy_props(impl, props, PW_KEY_NODE_LOOP_NAME); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); @@ -747,17 +752,12 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) &impl->core_listener, &core_events, impl); - impl->timer = pw_loop_add_timer(impl->loop, on_timer_event, impl); - if (impl->timer == NULL) { - res = -errno; - pw_log_error("can't create timer source: %m"); + if ((res = pw_timer_queue_add(impl->timer_queue, &impl->timer, + NULL, impl->cleanup_interval * SPA_NSEC_PER_SEC, + on_timer_event, impl)) < 0) { + pw_log_error("can't add timer: %s", spa_strerror(res)); goto out; } - value.tv_sec = impl->cleanup_interval; - value.tv_nsec = 0; - interval.tv_sec = impl->cleanup_interval; - interval.tv_nsec = 0; - pw_loop_update_timer(impl->loop, impl->timer, &value, &interval, false); if ((res = listen_start(impl)) < 0) { pw_log_error("failed to start VBAN stream: %s", spa_strerror(res)); diff --git a/src/modules/module-vban-send.c b/src/modules/module-vban-send.c index 5fc6793a1..a3ea7c760 100644 --- a/src/modules/module-vban-send.c +++ b/src/modules/module-vban-send.c @@ -68,6 +68,7 @@ * - \ref PW_KEY_AUDIO_FORMAT * - \ref PW_KEY_AUDIO_RATE * - \ref PW_KEY_AUDIO_CHANNELS + * - \ref SPA_KEY_AUDIO_LAYOUT * - \ref SPA_KEY_AUDIO_POSITION * - \ref PW_KEY_NODE_NAME * - \ref PW_KEY_NODE_DESCRIPTION @@ -417,6 +418,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) copy_props(impl, props, PW_KEY_AUDIO_FORMAT); copy_props(impl, props, PW_KEY_AUDIO_RATE); copy_props(impl, props, PW_KEY_AUDIO_CHANNELS); + copy_props(impl, props, SPA_KEY_AUDIO_LAYOUT); copy_props(impl, props, SPA_KEY_AUDIO_POSITION); copy_props(impl, props, PW_KEY_NODE_NAME); copy_props(impl, props, PW_KEY_NODE_DESCRIPTION); diff --git a/src/modules/module-vban/midi.c b/src/modules/module-vban/midi.c index f820b6839..f460bd274 100644 --- a/src/modules/module-vban/midi.c +++ b/src/modules/module-vban/midi.c @@ -48,7 +48,8 @@ static void vban_midi_process_playback(void *data) goto done; /* the ringbuffer contains series of sequences, one for each - * received packet */ + * received packet. This is not share mem so we can use the + * iterator. */ SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) { #if 0 /* try to render with given delay */ @@ -218,9 +219,10 @@ static int vban_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len) } static void vban_midi_flush_packets(struct impl *impl, - struct spa_pod_sequence *sequence, uint32_t timestamp, uint32_t rate) + struct spa_pod_parser *parser, uint32_t timestamp, uint32_t rate) { - struct spa_pod_control *c; + struct spa_pod_control c; + const void *c_body; struct vban_header header; struct iovec iov[2]; uint32_t len; @@ -234,31 +236,35 @@ static void vban_midi_flush_packets(struct impl *impl, len = 0; - SPA_POD_SEQUENCE_FOREACH(sequence, c) { + while (spa_pod_parser_get_control_body(parser, &c, &c_body) >= 0) { int size; uint8_t event[16]; + uint64_t state = 0; + size_t c_size = c.value.size; - if (c->type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_UMP) continue; - size = spa_ump_to_midi(SPA_POD_BODY(&c->value), - SPA_POD_BODY_SIZE(&c->value), event, sizeof(event)); - if (size <= 0) - continue; + while (c_size > 0) { + size = spa_ump_to_midi((const uint32_t**)&c_body, + &c_size, event, sizeof(event), &state); + if (size <= 0) + break; - if (len == 0) { - /* start new packet */ - header.n_frames++; - } else if (len + size > impl->mtu) { - /* flush packet when we have one and when it's too large */ - iov[1].iov_len = len; + if (len == 0) { + /* start new packet */ + header.n_frames++; + } else if (len + size > impl->mtu) { + /* flush packet when we have one and when it's too large */ + iov[1].iov_len = len; - pw_log_debug("sending %d", len); - vban_stream_emit_send_packet(impl, iov, 2); - len = 0; + pw_log_debug("sending %d", len); + vban_stream_emit_send_packet(impl, iov, 2); + len = 0; + } + memcpy(&impl->buffer[len], event, size); + len += size; } - memcpy(&impl->buffer[len], event, size); - len += size; } if (len > 0) { /* flush last packet */ @@ -275,9 +281,11 @@ static void vban_midi_process_capture(void *data) struct impl *impl = data; struct pw_buffer *buf; struct spa_data *d; - uint32_t offs, size, timestamp, rate; - struct spa_pod *pod; - void *ptr; + uint32_t timestamp, rate; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + const void *seq_body; if ((buf = pw_stream_dequeue_buffer(impl->stream)) == NULL) { pw_log_debug("Out of stream buffers: %m"); @@ -285,9 +293,6 @@ static void vban_midi_process_capture(void *data) } d = buf->buffer->datas; - offs = SPA_MIN(d[0].chunk->offset, d[0].maxsize); - size = SPA_MIN(d[0].chunk->size, d[0].maxsize - offs); - if (SPA_LIKELY(impl->io_position)) { rate = impl->io_position->clock.rate.denom; timestamp = impl->io_position->clock.position * impl->rate / rate; @@ -296,11 +301,9 @@ static void vban_midi_process_capture(void *data) timestamp = 0; } - ptr = SPA_PTROFF(d[0].data, offs, void); - - if ((pod = spa_pod_from_data(ptr, size, 0, size)) == NULL) - goto done; - if (!spa_pod_is_sequence(pod)) + spa_pod_parser_init_from_data(&parser, d[0].data, d[0].maxsize, + d[0].chunk->offset, d[0].chunk->size); + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) goto done; if (!impl->have_sync) { @@ -309,7 +312,7 @@ static void vban_midi_process_capture(void *data) impl->have_sync = true; } - vban_midi_flush_packets(impl, (struct spa_pod_sequence*)pod, timestamp, rate); + vban_midi_flush_packets(impl, &parser, timestamp, rate); done: pw_stream_queue_buffer(impl->stream, buf); diff --git a/src/modules/module-vban/stream.c b/src/modules/module-vban/stream.c index efa5af370..da3469583 100644 --- a/src/modules/module-vban/stream.c +++ b/src/modules/module-vban/stream.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2023 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include @@ -18,8 +20,6 @@ #include #include -#include "config.h" - #include #include @@ -185,9 +185,9 @@ static const struct format_info *find_audio_format_info(const struct spa_audio_i return NULL; } -static void parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) +static int parse_audio_info(const struct pw_properties *props, struct spa_audio_info_raw *info) { - spa_audio_info_raw_init_dict_keys(info, + return spa_audio_info_raw_init_dict_keys(info, &SPA_DICT_ITEMS( SPA_DICT_ITEM(SPA_KEY_AUDIO_FORMAT, DEFAULT_FORMAT), SPA_DICT_ITEM(SPA_KEY_AUDIO_RATE, SPA_STRINGIFY(DEFAULT_RATE)), @@ -196,6 +196,7 @@ static void parse_audio_info(const struct pw_properties *props, struct spa_audio SPA_KEY_AUDIO_FORMAT, SPA_KEY_AUDIO_RATE, SPA_KEY_AUDIO_CHANNELS, + SPA_KEY_AUDIO_LAYOUT, SPA_KEY_AUDIO_POSITION, NULL); } @@ -274,9 +275,14 @@ struct vban_stream *vban_stream_new(struct pw_core *core, switch (impl->info.media_subtype) { case SPA_MEDIA_SUBTYPE_raw: - parse_audio_info(props, &impl->info.info.raw); - if (SPA_FLAG_IS_SET(impl->info.info.raw.flags, SPA_AUDIO_FLAG_UNPOSITIONED)) + if ((res = parse_audio_info(props, &impl->info.info.raw)) < 0) { + pw_log_error("can't parse format: %s", spa_strerror(res)); + goto out; + } + if (SPA_FLAG_IS_SET(impl->info.info.raw.flags, SPA_AUDIO_FLAG_UNPOSITIONED)) { default_layout(impl->info.info.raw.channels, impl->info.info.raw.position); + SPA_FLAG_CLEAR(impl->info.info.raw.flags, SPA_AUDIO_FLAG_UNPOSITIONED); + } impl->stream_info = impl->info; impl->format_info = find_audio_format_info(&impl->info); if (impl->format_info == NULL) { @@ -298,7 +304,7 @@ struct vban_stream *vban_stream_new(struct pw_core *core, impl->header.format_bit = impl->format_info->format_bit; if ((str = pw_properties_get(props, "sess.name")) == NULL) str = "Stream1"; - strcpy(impl->header.stream_name, str); + snprintf(impl->header.stream_name, sizeof(impl->header.stream_name), "%s", str); break; case SPA_MEDIA_SUBTYPE_control: impl->stream_info = impl->info; @@ -307,7 +313,7 @@ struct vban_stream *vban_stream_new(struct pw_core *core, res = -EINVAL; goto out; } - pw_properties_set(props, PW_KEY_FORMAT_DSP, "32 bit raw UMP"); + pw_properties_set(props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); impl->stride = impl->format_info->size; impl->rate = pw_properties_get_uint32(props, "midi.rate", 10000); if (impl->rate == 0) @@ -319,7 +325,7 @@ struct vban_stream *vban_stream_new(struct pw_core *core, impl->header.format_bit = impl->format_info->format_bit; if ((str = pw_properties_get(props, "sess.name")) == NULL) str = "Midi1"; - strcpy(impl->header.stream_name, str); + snprintf(impl->header.stream_name, sizeof(impl->header.stream_name), "%s", str); break; default: spa_assert_not_reached(); diff --git a/src/modules/module-x11-bell.c b/src/modules/module-x11-bell.c index 1617c20c8..3e3660775 100644 --- a/src/modules/module-x11-bell.c +++ b/src/modules/module-x11-bell.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,8 +12,6 @@ #include #include -#include "config.h" - #include #include diff --git a/src/modules/module-zeroconf-discover.c b/src/modules/module-zeroconf-discover.c index 68edd605c..b324db403 100644 --- a/src/modules/module-zeroconf-discover.c +++ b/src/modules/module-zeroconf-discover.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -10,8 +12,6 @@ #include #include -#include "config.h" - #include #include #include @@ -188,17 +188,17 @@ static void pw_properties_from_avahi_string(const char *key, const char *value, else if (spa_streq(key, "channel_map")) { struct channel_map channel_map; uint32_t i, pos[CHANNELS_MAX]; - char *p, *s; + char *p, *s, buf[8]; spa_zero(channel_map); channel_map_parse(value, &channel_map); - channel_map_to_positions(&channel_map, pos); + channel_map_to_positions(&channel_map, pos, CHANNELS_MAX); p = s = alloca(4 + channel_map.channels * 8); p += spa_scnprintf(p, 2, "["); for (i = 0; i < channel_map.channels; i++) p += spa_scnprintf(p, 8, "%s%s", i == 0 ? "" : ",", - channel_id2name(pos[i])); + channel_id2name(pos[i], buf, sizeof(buf))); p += spa_scnprintf(p, 2, "]"); pw_properties_set(props, SPA_KEY_AUDIO_POSITION, s); } @@ -487,10 +487,7 @@ static int start_client(struct impl *impl) static int start_avahi(struct impl *impl) { - struct pw_loop *loop; - - loop = pw_context_get_main_loop(impl->context); - impl->avahi_poll = pw_avahi_poll_new(loop); + impl->avahi_poll = pw_avahi_poll_new(impl->context); return start_client(impl); } diff --git a/src/modules/module-zeroconf-discover/avahi-poll.c b/src/modules/module-zeroconf-discover/avahi-poll.c index ab54888f6..4ead6427a 100644 --- a/src/modules/module-zeroconf-discover/avahi-poll.c +++ b/src/modules/module-zeroconf-discover/avahi-poll.c @@ -8,7 +8,9 @@ struct impl { AvahiPoll api; + struct pw_context *context; struct pw_loop *loop; + struct pw_timer_queue *timer_queue; }; struct AvahiWatch { @@ -22,7 +24,7 @@ struct AvahiWatch { struct AvahiTimeout { struct impl *impl; - struct spa_source *source; + struct pw_timer timer; AvahiTimeoutCallback callback; void *userdata; }; @@ -72,7 +74,10 @@ static AvahiWatch* watch_new(const AvahiPoll *api, int fd, AvahiWatchEvent event w->userdata = userdata; w->source = pw_loop_add_io(impl->loop, fd, to_pw_events(event), false, watch_callback, w); - + if (w->source == NULL) { + free(w); + return NULL; + } return w; } @@ -96,17 +101,40 @@ static void watch_free(AvahiWatch *w) free(w); } -static void timeout_callback(void *data, uint64_t expirations) +static void timeout_callback(void *data) { AvahiTimeout *w = data; w->callback(w, w->userdata); } +static int schedule_timeout(AvahiTimeout *t, const struct timeval *tv) +{ + struct timeval now; + int64_t timeout_ns; + + if (tv == NULL) + return 0; + + /* Get current REALTIME (same clock domain as Avahi) */ + if (gettimeofday(&now, NULL) < 0) + return -errno; + + /* Calculate relative timeout: target - now */ + timeout_ns = ((int64_t)tv->tv_sec - now.tv_sec) * SPA_NSEC_PER_SEC + + ((int64_t)tv->tv_usec - now.tv_usec) * 1000UL; + + /* Ensure minimum timeout */ + if (timeout_ns <= 0) + timeout_ns = 1; + + return pw_timer_queue_add(t->impl->timer_queue, &t->timer, NULL, + timeout_ns, timeout_callback, t); +} + static AvahiTimeout* timeout_new(const AvahiPoll *api, const struct timeval *tv, AvahiTimeoutCallback callback, void *userdata) { struct impl *impl = api->userdata; - struct timespec value; AvahiTimeout *w; w = calloc(1, sizeof(*w)); @@ -116,35 +144,27 @@ static AvahiTimeout* timeout_new(const AvahiPoll *api, const struct timeval *tv, w->impl = impl; w->callback = callback; w->userdata = userdata; - w->source = pw_loop_add_timer(impl->loop, timeout_callback, w); - if (tv != NULL) { - value.tv_sec = tv->tv_sec; - value.tv_nsec = tv->tv_usec * 1000UL; - pw_loop_update_timer(impl->loop, w->source, &value, NULL, true); + if (schedule_timeout(w, tv) < 0) { + free(w); + return NULL; } + return w; } static void timeout_update(AvahiTimeout *t, const struct timeval *tv) { - struct impl *impl = t->impl; - struct timespec value, *v = NULL; + /* Cancel the existing timer */ + pw_timer_queue_cancel(&t->timer); - if (tv != NULL) { - value.tv_sec = tv->tv_sec; - value.tv_nsec = tv->tv_usec * 1000UL; - if (value.tv_sec == 0 && value.tv_nsec == 0) - value.tv_nsec = 1; - v = &value; - } - pw_loop_update_timer(impl->loop, t->source, v, NULL, true); + /* Schedule new timeout if provided */ + schedule_timeout(t, tv); } static void timeout_free(AvahiTimeout *t) { - struct impl *impl = t->impl; - pw_loop_destroy_source(impl->loop, t->source); + pw_timer_queue_cancel(&t->timer); free(t); } @@ -158,7 +178,7 @@ static const AvahiPoll avahi_poll_api = { .timeout_free = timeout_free, }; -AvahiPoll* pw_avahi_poll_new(struct pw_loop *loop) +AvahiPoll* pw_avahi_poll_new(struct pw_context *context) { struct impl *impl; @@ -166,7 +186,10 @@ AvahiPoll* pw_avahi_poll_new(struct pw_loop *loop) if (impl == NULL) return NULL; - impl->loop = loop; + impl->context = context; + impl->loop = pw_context_get_main_loop(context); + impl->timer_queue = pw_context_get_timer_queue(context); + impl->api = avahi_poll_api; impl->api.userdata = impl; diff --git a/src/modules/module-zeroconf-discover/avahi-poll.h b/src/modules/module-zeroconf-discover/avahi-poll.h index f651c5fd8..fce9b9d1b 100644 --- a/src/modules/module-zeroconf-discover/avahi-poll.h +++ b/src/modules/module-zeroconf-discover/avahi-poll.h @@ -4,8 +4,8 @@ #include -#include +#include -AvahiPoll* pw_avahi_poll_new(struct pw_loop *loop); +AvahiPoll* pw_avahi_poll_new(struct pw_context *context); void pw_avahi_poll_free(AvahiPoll *p); diff --git a/src/pipewire/array.h b/src/pipewire/array.h index ca00e7b9b..4af60a961 100644 --- a/src/pipewire/array.h +++ b/src/pipewire/array.h @@ -5,14 +5,14 @@ #ifndef PIPEWIRE_ARRAY_H #define PIPEWIRE_ARRAY_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef PW_API_ARRAY #define PW_API_ARRAY static inline #endif diff --git a/src/pipewire/buffers.c b/src/pipewire/buffers.c index e73adaaec..db1a01551 100644 --- a/src/pipewire/buffers.c +++ b/src/pipewire/buffers.c @@ -3,7 +3,7 @@ /* SPDX-License-Identifier: MIT */ #include -#include +#include #include #include #include @@ -16,8 +16,8 @@ PW_LOG_TOPIC_EXTERN(log_buffers); #define PW_LOG_TOPIC_DEFAULT log_buffers -#define MAX_ALIGN 32 -#define MAX_BLOCKS 64u +#define MAX_ALIGN 32u +#define MAX_BLOCKS 256u struct port { struct spa_node *node; @@ -40,37 +40,37 @@ static int alloc_buffers(struct pw_mempool *pool, { struct spa_buffer **buffers; void *skel, *data; - uint32_t i; + uint32_t i, j; struct spa_data *datas; struct pw_memblock *m; struct spa_buffer_alloc_info info = { 0, }; - if (!SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_SHARED)) - SPA_FLAG_SET(info.flags, SPA_BUFFER_ALLOC_FLAG_INLINE_ALL); - datas = alloca(sizeof(struct spa_data) * n_datas); for (i = 0; i < n_datas; i++) { struct spa_data *d = &datas[i]; - spa_zero(*d); - if (data_sizes[i] > 0) { - /* we allocate memory */ - d->type = SPA_DATA_MemPtr; - d->maxsize = data_sizes[i]; - SPA_FLAG_SET(d->flags, SPA_DATA_FLAG_READWRITE); - } else { - /* client allocates memory. Set the mask of possible - * types in the type field */ - d->type = data_types[i]; - d->maxsize = 0; - } + d->type = data_types[i]; + d->maxsize = data_sizes[i]; if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_DYNAMIC)) SPA_FLAG_SET(d->flags, SPA_DATA_FLAG_DYNAMIC); + /* if we alloc, we know it will be READWRITE */ + if (!SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_NO_MEM)) + SPA_FLAG_SET(d->flags, SPA_DATA_FLAG_READWRITE); } + /* propagate NO_MEM flag to NO_DATA for the buffer alloc. This ensures, + * it does not try to set the data pointer in spa_data. */ + if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_NO_MEM)) + SPA_FLAG_SET(info.flags, SPA_BUFFER_ALLOC_FLAG_NO_DATA); + + /* if we don't share buffers, we can inline all meta/chunk/data with the + * skeleton */ + if (!SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_SHARED)) + SPA_FLAG_SET(info.flags, SPA_BUFFER_ALLOC_FLAG_INLINE_ALL); spa_buffer_alloc_fill_info(&info, n_metas, metas, n_datas, datas, data_aligns); + /* allocate the skeleton, depending on SHARED flag, meta/chunk/data is included */ buffers = calloc(1, info.max_align + n_buffers * (sizeof(struct spa_buffer *) + info.skel_size)); if (buffers == NULL) return -errno; @@ -79,7 +79,7 @@ static int alloc_buffers(struct pw_mempool *pool, skel = SPA_PTR_ALIGN(skel, info.max_align, void); if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_SHARED)) { - /* pointer to buffer structures */ + /* For shared data we use MemFd for meta/chunk/data */ m = pw_mempool_alloc(pool, PW_MEMBLOCK_FLAG_READWRITE | PW_MEMBLOCK_FLAG_SEAL | @@ -90,7 +90,6 @@ static int alloc_buffers(struct pw_mempool *pool, free(buffers); return -errno; } - data = m->map->ptr; } else { m = NULL; @@ -99,8 +98,23 @@ static int alloc_buffers(struct pw_mempool *pool, pw_log_debug("%p: layout buffers skel:%p data:%p n_buffers:%d buffers:%p", allocation, skel, data, n_buffers, buffers); + spa_buffer_alloc_layout_array(&info, n_buffers, buffers, skel, data); + if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_SHARED) && + !SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_NO_MEM)) { + /* set the fd and mappoffset into our shared memory */ + for (i = 0; i < n_buffers; i++) { + struct spa_buffer *buf = buffers[i]; + for (j = 0; j < buf->n_datas; j++) { + struct spa_data *d = &buf->datas[j]; + d->fd = m->fd; + d->mapoffset = SPA_PTRDIFF(d->data, data); + SPA_FLAG_SET(d->flags, SPA_DATA_FLAG_MAPPABLE); + } + } + } + allocation->mem = m; allocation->n_buffers = n_buffers; allocation->buffers = buffers; @@ -188,7 +202,7 @@ int pw_buffers_negotiate(struct pw_context *context, uint32_t flags, struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); uint32_t i, j, offset, n_params, n_metas; struct spa_meta *metas; - uint32_t max_buffers, blocks; + uint32_t min_buffers, max_buffers, blocks; size_t minsize, stride, align; uint32_t *data_sizes; int32_t *data_strides; @@ -221,12 +235,12 @@ int pw_buffers_negotiate(struct pw_context *context, uint32_t flags, if ((res = param_filter(result, &input, &output, SPA_PARAM_Meta, &b)) > 0) n_params += res; - metas = alloca(sizeof(struct spa_meta) * n_params); + metas = alloca(sizeof(struct spa_meta) * n_params * 2); n_metas = 0; params = alloca(n_params * sizeof(struct spa_pod *)); for (i = 0, offset = 0; i < n_params; i++) { - uint32_t type, size; + uint32_t type, size, features = 0; params[i] = SPA_PTROFF(buffer, offset, struct spa_pod); spa_pod_fixate(params[i]); @@ -239,7 +253,8 @@ int pw_buffers_negotiate(struct pw_context *context, uint32_t flags, if (spa_pod_parse_object(params[i], SPA_TYPE_OBJECT_ParamMeta, NULL, SPA_PARAM_META_type, SPA_POD_Id(&type), - SPA_PARAM_META_size, SPA_POD_Int(&size)) < 0) { + SPA_PARAM_META_size, SPA_POD_Int(&size), + SPA_PARAM_META_features, SPA_POD_OPT_Int(&features)) < 0) { pw_log_warn("%p: invalid Meta param", result); continue; } @@ -250,9 +265,15 @@ int pw_buffers_negotiate(struct pw_context *context, uint32_t flags, metas[n_metas].type = type; metas[n_metas].size = size; n_metas++; + if (features != 0) { + metas[n_metas].type = SPA_META_TYPE_FEATURES(type, features); + metas[n_metas].size = 0; + n_metas++; + } } max_buffers = context->settings.link_max_buffers; + min_buffers = 1; align = pw_properties_get_uint32(context->properties, PW_KEY_CPU_MAX_ALIGN, MAX_ALIGN); @@ -306,17 +327,36 @@ int pw_buffers_negotiate(struct pw_context *context, uint32_t flags, } if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_ASYNC)) - max_buffers = SPA_MAX(2u, max_buffers); + min_buffers += 1; - if (SPA_FLAG_IS_SET(flags, PW_BUFFERS_FLAG_SHARED_MEM)) { - if (types != SPA_ID_INVALID) - SPA_FLAG_CLEAR(types, 1<sc_pagesize); + } + else + return -ENOTSUP; + } data_sizes = alloca(sizeof(uint32_t) * blocks); data_strides = alloca(sizeof(int32_t) * blocks); diff --git a/src/pipewire/buffers.h b/src/pipewire/buffers.h index 9c1ae2b78..ff39b8be4 100644 --- a/src/pipewire/buffers.h +++ b/src/pipewire/buffers.h @@ -25,9 +25,10 @@ extern "C" { #define PW_BUFFERS_FLAG_NONE 0 #define PW_BUFFERS_FLAG_NO_MEM (1<<0) /**< don't allocate buffer memory */ -#define PW_BUFFERS_FLAG_SHARED (1<<1) /**< buffers can be shared */ +#define PW_BUFFERS_FLAG_SHARED (1<<1) /**< buffers can be shared, This makes sure that + * the data types on the buffers don't contain + * any MemPtr, only fd based memory. */ #define PW_BUFFERS_FLAG_DYNAMIC (1<<2) /**< buffers have dynamic data */ -#define PW_BUFFERS_FLAG_SHARED_MEM (1<<3) /**< buffers need shared memory */ #define PW_BUFFERS_FLAG_IN_PRIORITY (1<<4) /**< input parameters have priority */ #define PW_BUFFERS_FLAG_ASYNC (1<<5) /**< one of the nodes is async */ diff --git a/src/pipewire/client.h b/src/pipewire/client.h index 7798e212d..7f9749bf0 100644 --- a/src/pipewire/client.h +++ b/src/pipewire/client.h @@ -5,10 +5,6 @@ #ifndef PIPEWIRE_CLIENT_H #define PIPEWIRE_CLIENT_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -16,6 +12,10 @@ extern "C" { #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_client Client * Client interface */ diff --git a/src/pipewire/conf.c b/src/pipewire/conf.c index 387a3ab9e..c29d45648 100644 --- a/src/pipewire/conf.c +++ b/src/pipewire/conf.c @@ -441,7 +441,7 @@ static bool check_override(struct pw_properties *conf, const char *name, int lev continue; if (sscanf(it->key, "override.%d.%d.config.name", &lev, &idx) != 2) continue; - if (lev < level) + if (lev > level) return false; } return true; diff --git a/src/pipewire/context.c b/src/pipewire/context.c index e81de9126..ddcdd2cba 100644 --- a/src/pipewire/context.c +++ b/src/pipewire/context.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -229,6 +230,7 @@ static int setup_data_loops(struct impl *impl) pw_properties_clear(pr); pw_properties_update(pr, &this->properties->dict); pw_properties_set(pr, PW_KEY_LIBRARY_NAME_SYSTEM, lib_name); + pw_properties_set(pr, "loop.prio-inherit", "true"); while ((l = spa_json_object_next(&it[1], key, sizeof(key), &val)) > 0) { if (spa_json_is_container(val, l)) @@ -261,6 +263,7 @@ static int setup_data_loops(struct impl *impl) } for (i = 0; i < impl->n_data_loops; i++) { pw_properties_setf(pr, SPA_KEY_THREAD_NAME, "data-loop.%d", i); + pw_properties_set(pr, "loop.prio-inherit", "true"); impl->data_loops[i].impl = pw_data_loop_new(&pr->dict); if (impl->data_loops[i].impl == NULL) { res = -errno; @@ -298,6 +301,88 @@ static void data_loop_stop(struct impl *impl, struct data_loop *loop) loop->started = false; } +static int adjust_rlimit(int resource, const char *name, int value) +{ + struct rlimit rlim, highest, fixed; + + rlim = (struct rlimit) { .rlim_cur = value, .rlim_max = value, }; + + if (setrlimit(resource, &rlim) >= 0) { + pw_log_info("set rlimit %s to %d", name, value); + return 0; + } + if (errno != EPERM) + return -errno; + + /* So we failed to set the desired setrlimit, then let's try + * to get as close as we can */ + if (getrlimit(resource, &highest) < 0) + return -errno; + + /* If the hard limit is unbounded anyway, then the EPERM had other reasons, + * let's propagate the original EPERM then */ + if (highest.rlim_max == RLIM_INFINITY) + return -EPERM; + + fixed = (struct rlimit) { + .rlim_cur = SPA_MIN(rlim.rlim_cur, highest.rlim_max), + .rlim_max = SPA_MIN(rlim.rlim_max, highest.rlim_max), + }; + + /* Shortcut things if we wouldn't change anything. */ + if (fixed.rlim_cur == highest.rlim_cur && + fixed.rlim_max == highest.rlim_max) + return 0; + + pw_log_info("set rlimit %s to %d/%d instead of %d", name, + (int)fixed.rlim_cur, (int)fixed.rlim_max, value); + if (setrlimit(resource, &fixed) < 0) + return -errno; + + return 0; +} + +static int adjust_rlimits(const struct spa_dict *dict) +{ + const struct spa_dict_item *it; + static const char* rlimit_table[] = { + [RLIMIT_AS] = "as", + [RLIMIT_CORE] = "core", + [RLIMIT_CPU] = "cpu", + [RLIMIT_DATA] = "data", + [RLIMIT_FSIZE] = "fsize", + [RLIMIT_LOCKS] = "locks", + [RLIMIT_MEMLOCK] = "memlock", + [RLIMIT_MSGQUEUE] = "msgqueue", + [RLIMIT_NICE] = "nice", + [RLIMIT_NOFILE] = "nofile", + [RLIMIT_NPROC] = "nproc", + [RLIMIT_RSS] = "rss", + [RLIMIT_RTPRIO] = "rtprio", + [RLIMIT_RTTIME] = "rttime", + [RLIMIT_SIGPENDING] = "sigpending", + [RLIMIT_STACK] = "stack", + }; + int res; + spa_dict_for_each(it, dict) { + if (!spa_strstartswith(it->key, "rlimit.")) + continue; + for (size_t i = 0; i < SPA_N_ELEMENTS(rlimit_table); i++) { + const char *name = rlimit_table[i]; + int64_t val; + if (!spa_streq(it->key+7, name)) + continue; + if (!spa_atoi64(it->value, &val, 0)) { + pw_log_warn("invalid number %s", it->value); + } else if ((res = adjust_rlimit(i, name, val)) < 0) + pw_log_warn("can't set rlimit %s to %s: %s", + name, it->value, spa_strerror(res)); + break; + } + } + return 0; +} + /** Create a new context object * * \param main_loop the main loop to use @@ -418,6 +503,7 @@ struct pw_context *pw_context_new(struct pw_loop *main_loop, else pw_log_info("%p: mlockall succeeded", impl); } + adjust_rlimits(&properties->dict); pw_settings_init(this); this->settings = this->defaults; @@ -580,6 +666,8 @@ void pw_context_destroy(struct pw_context *context) if (context->work_queue) pw_work_queue_destroy(context->work_queue); + if (context->timer_queue) + pw_timer_queue_destroy(context->timer_queue); pw_properties_free(context->properties); pw_properties_free(context->conf); @@ -750,6 +838,14 @@ struct pw_work_queue *pw_context_get_work_queue(struct pw_context *context) return context->work_queue; } +SPA_EXPORT +struct pw_timer_queue *pw_context_get_timer_queue(struct pw_context *context) +{ + if (context->timer_queue == NULL) + context->timer_queue = pw_timer_queue_new(context->main_loop); + return context->timer_queue; +} + SPA_EXPORT struct pw_mempool *pw_context_get_mempool(struct pw_context *context) { @@ -886,194 +982,6 @@ SPA_PRINTF_FUNC(7, 8) int pw_context_debug_port_params(struct pw_context *this, return 0; } -/** Find a common format between two ports - * - * \param context a context object - * \param output an output port - * \param input an input port - * \param props extra properties - * \param n_format_filters number of format filters - * \param format_filters array of format filters - * \param[out] format the common format between the ports - * \param builder builder to use for processing - * \param[out] error an error when something is wrong - * \return a common format of NULL on error - * - * Find a common format between the given ports. The format will - * be restricted to a subset given with the format filters. - */ -int pw_context_find_format(struct pw_context *context, - struct pw_impl_port *output, - uint32_t output_mix, - struct pw_impl_port *input, - uint32_t input_mix, - struct pw_properties *props, - uint32_t n_format_filters, - struct spa_pod **format_filters, - struct spa_pod **format, - struct spa_pod_builder *builder, - char **error) -{ - uint32_t out_state, in_state; - int res; - uint32_t iidx = 0, oidx = 0; - struct spa_pod_builder fb = { 0 }; - uint8_t fbuf[4096]; - struct spa_pod *filter; - struct spa_node *in_node, *out_node; - uint32_t in_port, out_port; - - out_state = output->state; - in_state = input->state; - - if (output_mix == SPA_ID_INVALID) { - out_node = output->node->node; - out_port = output->port_id; - } else { - out_node = output->mix; - out_port = output_mix; - } - if (input_mix == SPA_ID_INVALID) { - in_node = input->node->node; - in_port = input->port_id; - } else { - in_node = input->mix; - in_port = input_mix; - } - - pw_log_debug("%p: finding best format %d %d", context, out_state, in_state); - - /* when a port is configured but the node is idle, we can reconfigure with a different format */ - if (out_state > PW_IMPL_PORT_STATE_CONFIGURE && output->node->info.state == PW_NODE_STATE_IDLE) - out_state = PW_IMPL_PORT_STATE_CONFIGURE; - if (in_state > PW_IMPL_PORT_STATE_CONFIGURE && input->node->info.state == PW_NODE_STATE_IDLE) - in_state = PW_IMPL_PORT_STATE_CONFIGURE; - - pw_log_debug("%p: states %d %d", context, out_state, in_state); - - if (in_state == PW_IMPL_PORT_STATE_CONFIGURE && out_state > PW_IMPL_PORT_STATE_CONFIGURE) { - /* only input needs format */ - spa_pod_builder_init(&fb, fbuf, sizeof(fbuf)); - if ((res = spa_node_port_enum_params_sync(out_node, - output->direction, out_port, - SPA_PARAM_Format, &oidx, - NULL, &filter, &fb)) != 1) { - if (res < 0) - *error = spa_aprintf("error get output format: %s", spa_strerror(res)); - else - *error = spa_aprintf("no output formats"); - goto error; - } - pw_log_debug("%p: Got output format:", context); - pw_log_format(SPA_LOG_LEVEL_DEBUG, filter); - - if ((res = spa_node_port_enum_params_sync(in_node, - input->direction, in_port, - SPA_PARAM_EnumFormat, &iidx, - filter, format, builder)) <= 0) { - if (res == -ENOENT || res == 0) { - pw_log_debug("%p: no input format filter, using output format: %s", - context, spa_strerror(res)); - - uint32_t offset = builder->state.offset; - res = spa_pod_builder_raw_padded(builder, filter, SPA_POD_SIZE(filter)); - if (res < 0) { - *error = spa_aprintf("failed to add pod"); - goto error; - } - - *format = spa_pod_builder_deref(builder, offset); - } else { - *error = spa_aprintf("error input enum formats: %s", spa_strerror(res)); - goto error; - } - } - } else if (out_state >= PW_IMPL_PORT_STATE_CONFIGURE && in_state > PW_IMPL_PORT_STATE_CONFIGURE) { - /* only output needs format */ - spa_pod_builder_init(&fb, fbuf, sizeof(fbuf)); - if ((res = spa_node_port_enum_params_sync(in_node, - input->direction, in_port, - SPA_PARAM_Format, &iidx, - NULL, &filter, &fb)) != 1) { - if (res < 0) - *error = spa_aprintf("error get input format: %s", spa_strerror(res)); - else - *error = spa_aprintf("no input format"); - goto error; - } - pw_log_debug("%p: Got input format:", context); - pw_log_format(SPA_LOG_LEVEL_DEBUG, filter); - - if ((res = spa_node_port_enum_params_sync(out_node, - output->direction, out_port, - SPA_PARAM_EnumFormat, &oidx, - filter, format, builder)) <= 0) { - if (res == -ENOENT || res == 0) { - pw_log_debug("%p: no output format filter, using input format: %s", - context, spa_strerror(res)); - - uint32_t offset = builder->state.offset; - res = spa_pod_builder_raw_padded(builder, filter, SPA_POD_SIZE(filter)); - if (res < 0) { - *error = spa_aprintf("failed to add pod"); - goto error; - } - - *format = spa_pod_builder_deref(builder, offset); - } else { - *error = spa_aprintf("error output enum formats: %s", spa_strerror(res)); - goto error; - } - } - } else if (in_state == PW_IMPL_PORT_STATE_CONFIGURE && out_state == PW_IMPL_PORT_STATE_CONFIGURE) { - again: - /* both ports need a format */ - pw_log_debug("%p: do enum input %d", context, iidx); - spa_pod_builder_init(&fb, fbuf, sizeof(fbuf)); - if ((res = spa_node_port_enum_params_sync(in_node, - input->direction, in_port, - SPA_PARAM_EnumFormat, &iidx, - NULL, &filter, &fb)) != 1) { - if (res == -ENOENT) { - pw_log_debug("%p: no input filter", context); - filter = NULL; - } else { - if (res < 0) - *error = spa_aprintf("error input enum formats: %s", spa_strerror(res)); - else - *error = spa_aprintf("no more input formats"); - goto error; - } - } - pw_log_debug("%p: enum output %d with filter: %p", context, oidx, filter); - pw_log_format(SPA_LOG_LEVEL_DEBUG, filter); - - if ((res = spa_node_port_enum_params_sync(out_node, - output->direction, out_port, - SPA_PARAM_EnumFormat, &oidx, - filter, format, builder)) != 1) { - if (res == 0 && filter != NULL) { - oidx = 0; - goto again; - } - *error = spa_aprintf("error output enum formats: %s", spa_strerror(res)); - goto error; - } - - pw_log_debug("%p: Got filtered:", context); - pw_log_format(SPA_LOG_LEVEL_DEBUG, *format); - } else { - res = -EBADF; - *error = spa_aprintf("error bad node state"); - goto error; - } - return res; -error: - if (res == 0) - res = -EINVAL; - return res; -} - static int ensure_state(struct pw_impl_node *node, bool running) { enum pw_node_state state = node->info.state; @@ -1277,11 +1185,25 @@ static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, pw_log_debug(" next node %p: '%s' runnable:%u %p %p %p", n, n->name, n->runnable, n->groups, n->link_groups, sync); } - spa_list_for_each(n, collect, sort_link) - if (!n->driving && n->runnable) { + /* All non-driver runnable nodes (ie. reachable with a non-passive link) now make + * all linked nodes up and downstream runnable as well */ + spa_list_for_each(n, collect, sort_link) { + if (!n->driver && n->runnable) { run_nodes(context, n, collect, PW_DIRECTION_OUTPUT, 0); run_nodes(context, n, collect, PW_DIRECTION_INPUT, 0); } + } + /* now we might have made a driver runnable, if the node is not runnable at this point + * it means it was linked to the driver with passives links and some other node + * made the driver active. If the node is a leaf it can not be activated in any other + * way and we will also make it, and all its peers, runnable */ + spa_list_for_each(n, collect, sort_link) { + if (!n->driver && n->driver_node->runnable && !n->runnable && n->leaf && n->active) { + n->runnable = true; + run_nodes(context, n, collect, PW_DIRECTION_OUTPUT, 0); + run_nodes(context, n, collect, PW_DIRECTION_INPUT, 0); + } + } return 0; } diff --git a/src/pipewire/context.h b/src/pipewire/context.h index 810127323..61c6662c4 100644 --- a/src/pipewire/context.h +++ b/src/pipewire/context.h @@ -5,13 +5,13 @@ #ifndef PIPEWIRE_CONTEXT_H #define PIPEWIRE_CONTEXT_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** \defgroup pw_context Context * * \brief The PipeWire context object manages all locally available @@ -144,6 +144,9 @@ void pw_context_release_loop(struct pw_context *context, struct pw_loop *loop); /** Get the work queue from the context: Since 0.3.26 */ struct pw_work_queue *pw_context_get_work_queue(struct pw_context *context); +/** Get the timer queue from the context: Since 1.6.0 */ +struct pw_timer_queue *pw_context_get_timer_queue(struct pw_context *context); + /** Get the memory pool from the context: Since 0.3.74 */ struct pw_mempool *pw_context_get_mempool(struct pw_context *context); diff --git a/src/pipewire/control.h b/src/pipewire/control.h index d5e73629b..daedcb8cf 100644 --- a/src/pipewire/control.h +++ b/src/pipewire/control.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_CONTROL_H #define PIPEWIRE_CONTROL_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \defgroup pw_control Control * * \brief A control can be used to control a port property. diff --git a/src/pipewire/core.h b/src/pipewire/core.h index be7beb416..cd4a5d551 100644 --- a/src/pipewire/core.h +++ b/src/pipewire/core.h @@ -5,10 +5,6 @@ #ifndef PIPEWIRE_CORE_H #define PIPEWIRE_CORE_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -16,6 +12,10 @@ extern "C" { #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_core Core * * \brief The core global object. diff --git a/src/pipewire/data-loop.h b/src/pipewire/data-loop.h index 166d3e08d..373bd9807 100644 --- a/src/pipewire/data-loop.h +++ b/src/pipewire/data-loop.h @@ -5,13 +5,13 @@ #ifndef PIPEWIRE_DATA_LOOP_H #define PIPEWIRE_DATA_LOOP_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** \defgroup pw_data_loop Data Loop * * \brief PipeWire rt-loop object @@ -79,8 +79,9 @@ bool pw_data_loop_in_thread(struct pw_data_loop *loop); struct spa_thread *pw_data_loop_get_thread(struct pw_data_loop *loop); /** invoke func in the context of the thread or in the caller thread when - * the loop is not running. May be called from the loop's thread, but otherwise - * can only be called by a single thread at a time. + * the loop is not running. May be called from the loop's thread, and + * can be called by multiple threads at the same time. + * * If called from the loop's thread, all callbacks previously queued with * pw_data_loop_invoke() will be run synchronously, which might cause * unexpected reentrancy problems. diff --git a/src/pipewire/device.h b/src/pipewire/device.h index 9154daee0..bff40530f 100644 --- a/src/pipewire/device.h +++ b/src/pipewire/device.h @@ -5,15 +5,15 @@ #ifndef PIPEWIRE_DEVICE_H #define PIPEWIRE_DEVICE_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_device Device * Device interface */ diff --git a/src/pipewire/extensions/client-node.h b/src/pipewire/extensions/client-node.h index 69f278e04..274441319 100644 --- a/src/pipewire/extensions/client-node.h +++ b/src/pipewire/extensions/client-node.h @@ -5,13 +5,13 @@ #ifndef PIPEWIRE_EXT_CLIENT_NODE_H #define PIPEWIRE_EXT_CLIENT_NODE_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** \defgroup pw_client_node Client Node * Client node interface */ diff --git a/src/pipewire/extensions/metadata.h b/src/pipewire/extensions/metadata.h index 58057c608..5a5f8d3c8 100644 --- a/src/pipewire/extensions/metadata.h +++ b/src/pipewire/extensions/metadata.h @@ -5,15 +5,15 @@ #ifndef PIPEWIRE_EXT_METADATA_H #define PIPEWIRE_EXT_METADATA_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_metadata Metadata * Metadata interface */ diff --git a/src/pipewire/extensions/profiler.h b/src/pipewire/extensions/profiler.h index d0e8908ed..c7a155d95 100644 --- a/src/pipewire/extensions/profiler.h +++ b/src/pipewire/extensions/profiler.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_EXT_PROFILER_H #define PIPEWIRE_EXT_PROFILER_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \defgroup pw_profiler Profiler * Profiler interface */ diff --git a/src/pipewire/extensions/protocol-native.h b/src/pipewire/extensions/protocol-native.h index ca4369200..7b88d8210 100644 --- a/src/pipewire/extensions/protocol-native.h +++ b/src/pipewire/extensions/protocol-native.h @@ -5,15 +5,15 @@ #ifndef PIPEWIRE_EXT_PROTOCOL_NATIVE_H #define PIPEWIRE_EXT_PROTOCOL_NATIVE_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_protocol_native Native Protocol * PipeWire native protocol interface */ diff --git a/src/pipewire/extensions/security-context.h b/src/pipewire/extensions/security-context.h index 5a3cd2c13..3da96ff45 100644 --- a/src/pipewire/extensions/security-context.h +++ b/src/pipewire/extensions/security-context.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_EXT_SECURITY_CONTEXT_H #define PIPEWIRE_EXT_SECURITY_CONTEXT_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \defgroup pw_security_context Security Context * Security Context interface */ diff --git a/src/pipewire/factory.h b/src/pipewire/factory.h index 9c6ab74fc..e337b1d2f 100644 --- a/src/pipewire/factory.h +++ b/src/pipewire/factory.h @@ -5,10 +5,6 @@ #ifndef PIPEWIRE_FACTORY_H #define PIPEWIRE_FACTORY_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -17,6 +13,10 @@ extern "C" { #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_factory Factory * Factory interface */ diff --git a/src/pipewire/filter.c b/src/pipewire/filter.c index 519c1e2f7..ea3e74cd2 100644 --- a/src/pipewire/filter.c +++ b/src/pipewire/filter.c @@ -26,7 +26,7 @@ PW_LOG_TOPIC_EXTERN(log_filter); #define PW_LOG_TOPIC_DEFAULT log_filter -#define MAX_BUFFERS 64 +#define MAX_BUFFERS 64u #define MASK_BUFFERS (MAX_BUFFERS-1) @@ -36,7 +36,7 @@ struct buffer { struct pw_buffer this; uint32_t id; #define BUFFER_FLAG_MAPPED (1 << 0) -#define BUFFER_FLAG_QUEUED (1 << 1) +#define BUFFER_FLAG_DEQUEUED (1 << 1) #define BUFFER_FLAG_ADDED (1 << 2) uint32_t flags; }; @@ -206,7 +206,7 @@ static void fix_datatype(struct spa_pod *param) if (spa_pod_get_int(&vals[0], (int32_t*)&dataType) < 0) return; - pw_log_debug("dataType: %u", dataType); + pw_log_debug("dataType: %" PRIu32, dataType); if (dataType & (1u << SPA_DATA_MemPtr)) { SPA_POD_VALUE(struct spa_pod_int, &vals[0]) = dataType | (1<flags, BUFFER_FLAG_QUEUED)) + if (buffer->id >= port->n_buffers) return -EINVAL; - SPA_FLAG_SET(buffer->flags, BUFFER_FLAG_QUEUED); - spa_ringbuffer_get_write_index(&queue->ring, &index); queue->ids[index & MASK_BUFFERS] = buffer->id; spa_ringbuffer_write_update(&queue->ring, index + 1); @@ -382,7 +380,6 @@ static inline struct buffer *pop_queue(struct port *port, struct queue *queue) spa_ringbuffer_read_update(&queue->ring, index + 1); buffer = &port->buffers[id]; - SPA_FLAG_CLEAR(buffer->flags, BUFFER_FLAG_QUEUED); return buffer; } @@ -888,8 +885,7 @@ static int impl_port_use_buffers(void *object, struct port *port; struct pw_filter *filter = &impl->this; uint32_t i, j, impl_flags; - int prot, res; - int size = 0; + int res, size = 0; pw_log_debug("%p: port:%d.%d buffers:%u disconnecting:%d", impl, direction, port_id, n_buffers, impl->disconnecting); @@ -903,7 +899,6 @@ static int impl_port_use_buffers(void *object, clear_buffers(port); impl_flags = port->flags; - prot = PROT_READ | (direction == SPA_DIRECTION_OUTPUT ? PROT_WRITE : 0); if (n_buffers > MAX_BUFFERS) return -ENOSPC; @@ -918,7 +913,12 @@ static int impl_port_use_buffers(void *object, if (SPA_FLAG_IS_SET(impl_flags, PW_FILTER_PORT_FLAG_MAP_BUFFERS)) { for (j = 0; j < buffers[i]->n_datas; j++) { struct spa_data *d = &buffers[i]->datas[j]; - if (SPA_FLAG_IS_SET(d->flags, SPA_DATA_FLAG_MAPPABLE)) { + if (d->data == NULL && SPA_FLAG_IS_SET(d->flags, SPA_DATA_FLAG_MAPPABLE)) { + int prot = 0; + if (SPA_FLAG_IS_SET(d->flags, SPA_DATA_FLAG_READABLE)) + prot |= PROT_READ; + if (SPA_FLAG_IS_SET(d->flags, SPA_DATA_FLAG_WRITABLE)) + prot |= PROT_WRITE; if ((res = map_data(impl, d, prot)) < 0) return res; SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); @@ -941,6 +941,7 @@ static int impl_port_use_buffers(void *object, pw_log_debug("%p: got buffer id:%d datas:%d mapped size %d", filter, i, buffers[i]->n_datas, size); } + port->n_buffers = n_buffers; for (i = 0; i < n_buffers; i++) { struct buffer *b = &port->buffers[i]; @@ -953,11 +954,9 @@ static int impl_port_use_buffers(void *object, } SPA_FLAG_SET(b->flags, BUFFER_FLAG_ADDED); + pw_filter_emit_add_buffer(filter, port->user_data, &b->this); } - - port->n_buffers = n_buffers; - return 0; } @@ -970,9 +969,7 @@ static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffe return -EINVAL; pw_log_trace("%p: recycle buffer %d", impl, buffer_id); - if (buffer_id < port->n_buffers) - push_queue(port, &port->queued, &port->buffers[buffer_id]); - + push_queue(port, &port->queued, &port->buffers[buffer_id]); return 0; } @@ -1449,7 +1446,7 @@ static void hook_removed(struct spa_hook *hook) { struct filter *impl = hook->priv; if (impl->data_loop) - pw_loop_invoke(impl->data_loop, do_remove_callbacks, 1, NULL, 0, true, impl); + pw_loop_locked(impl->data_loop, do_remove_callbacks, 1, NULL, 0, impl); else spa_zero(impl->rt_callbacks); hook->priv = NULL; @@ -1749,7 +1746,7 @@ static void add_audio_dsp_port_params(struct filter *impl, struct port *port) SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32))); spa_pod_builder_init(&b, buffer, sizeof(buffer)); - add_param(impl, port, SPA_PARAM_Buffers, PARAM_FLAG_LOCKED, + add_param(impl, port, SPA_PARAM_Buffers, 0, spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS), @@ -1855,8 +1852,10 @@ void *pw_filter_add_port(struct pw_filter *filter, add_control_dsp_port_params(impl, p, 1u << SPA_CONTROL_Midi); else if (spa_streq(str, "8 bit raw control")) add_control_dsp_port_params(impl, p, 0); - else if (spa_streq(str, "32 bit raw UMP")) + else if (spa_streq(str, "32 bit raw UMP")) { add_control_dsp_port_params(impl, p, 1u << SPA_CONTROL_UMP); + pw_properties_set(props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); + } } /* then override with user provided if any */ if (update_params(impl, p, SPA_ID_INVALID, params, n_params) < 0) @@ -1968,7 +1967,8 @@ int pw_filter_get_time(struct pw_filter *filter, struct pw_time *time) if (SPA_LIKELY(p != NULL)) { impl->time.now = p->clock.nsec; impl->time.rate = p->clock.rate; - if (SPA_UNLIKELY(impl->clock_id != p->clock.id)) { + if (SPA_UNLIKELY(impl->clock_id != p->clock.id || + SPA_FLAG_IS_SET(p->clock.flags, SPA_IO_CLOCK_FLAG_DISCONT))) { impl->base_pos = p->clock.position - impl->time.ticks; impl->clock_id = p->clock.id; } @@ -2011,6 +2011,7 @@ struct pw_buffer *pw_filter_dequeue_buffer(void *port_data) return NULL; } pw_log_trace_fp("%p: dequeue buffer %d", p->filter, b->id); + SPA_FLAG_SET(b->flags, BUFFER_FLAG_DEQUEUED); return &b->this; } @@ -2020,6 +2021,13 @@ int pw_filter_queue_buffer(void *port_data, struct pw_buffer *buffer) { struct port *p = SPA_CONTAINER_OF(port_data, struct port, user_data); struct buffer *b = SPA_CONTAINER_OF(buffer, struct buffer, this); + + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_DEQUEUED)) { + pw_log_warn("%p: tried to queue cleared buffer %d", p->filter, b->id); + return -EINVAL; + } + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_DEQUEUED); + pw_log_trace_fp("%p: queue buffer %d", p->filter, b->id); return push_queue(p, &p->queued, b); } @@ -2079,8 +2087,8 @@ SPA_EXPORT int pw_filter_flush(struct pw_filter *filter, bool drain) { struct filter *impl = SPA_CONTAINER_OF(filter, struct filter, this); - pw_loop_invoke(impl->data_loop, - drain ? do_drain : do_flush, 1, NULL, 0, true, impl); + pw_loop_locked(impl->data_loop, + drain ? do_drain : do_flush, 1, NULL, 0, impl); return 0; } diff --git a/src/pipewire/global.c b/src/pipewire/global.c index c9975f9b5..1973f88ca 100644 --- a/src/pipewire/global.c +++ b/src/pipewire/global.c @@ -237,8 +237,6 @@ SPA_EXPORT int pw_global_update_keys(struct pw_global *global, const struct spa_dict *dict, const char * const keys[]) { - if (global->registered) - return -EINVAL; return pw_properties_update_keys(global->properties, dict, keys); } diff --git a/src/pipewire/i18n.h b/src/pipewire/i18n.h index cfe981180..8ed238720 100644 --- a/src/pipewire/i18n.h +++ b/src/pipewire/i18n.h @@ -5,6 +5,8 @@ #ifndef PIPEWIRE_I18N_H #define PIPEWIRE_I18N_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -17,7 +19,6 @@ extern "C" { * \addtogroup pw_gettext * \{ */ -#include SPA_FORMAT_ARG_FUNC(1) const char *pw_gettext(const char *msgid); SPA_FORMAT_ARG_FUNC(1) const char *pw_ngettext(const char *msgid, const char *msgid_plural, unsigned long int n); diff --git a/src/pipewire/impl-client.c b/src/pipewire/impl-client.c index d46e07939..a13a40a5a 100644 --- a/src/pipewire/impl-client.c +++ b/src/pipewire/impl-client.c @@ -27,6 +27,13 @@ struct impl { unsigned int registered:1; }; +static const char * const global_keys[] = { + PW_KEY_ACCESS, + PW_KEY_CLIENT_ACCESS, + PW_KEY_APP_NAME, + NULL +}; + #define pw_client_resource(r,m,v,...) pw_resource_call(r,struct pw_client_events,m,v,__VA_ARGS__) #define pw_client_resource_info(r,...) pw_client_resource(r,info,0,__VA_ARGS__) #define pw_client_resource_permissions(r,...) pw_client_resource(r,permissions,0,__VA_ARGS__) @@ -132,7 +139,8 @@ static int client_error(void *object, uint32_t id, int res, const char *error) goto error_no_id; } - pw_log_debug("%p: sender %p: error for global %u", client, sender, id); + pw_log_debug("%p: sender %p: error for global %u: %s (%d)", + client, sender, id, error, res); pw_map_for_each(&client->objects, error_resource, &d); return 0; @@ -209,7 +217,6 @@ static int update_properties(struct pw_impl_client *client, const struct spa_dic } changed += pw_properties_set(client->properties, dict->items[i].key, dict->items[i].value); } - client->info.props = &client->properties->dict; pw_log_debug("%p: updated %d properties", client, changed); @@ -220,9 +227,11 @@ static int update_properties(struct pw_impl_client *client, const struct spa_dic pw_impl_client_emit_info_changed(client, &client->info); - if (client->global) + if (client->global) { + pw_global_update_keys(client->global, client->info.props, global_keys); spa_list_for_each(resource, &client->global->resource_list, link) pw_client_resource_info(resource, &client->info); + } client->info.change_mask = 0; @@ -238,13 +247,6 @@ static void update_busy(struct pw_impl_client *client) static int finish_register(struct pw_impl_client *client) { - static const char * const keys[] = { - PW_KEY_ACCESS, - PW_KEY_CLIENT_ACCESS, - PW_KEY_APP_NAME, - NULL - }; - struct impl *impl = SPA_CONTAINER_OF(client, struct impl, this); struct pw_impl_client *current; @@ -260,7 +262,7 @@ static int finish_register(struct pw_impl_client *client) update_busy(client); - pw_global_update_keys(client->global, client->info.props, keys); + pw_global_update_keys(client->global, client->info.props, global_keys); pw_global_register(client->global); #ifdef OLD_MEDIA_SESSION_WORKAROUND @@ -479,6 +481,8 @@ struct pw_impl_client *pw_context_create_client(struct pw_impl_core *core, pw_mempool_add_listener(this->pool, &impl->pool_listener, &pool_events, impl); this->properties = properties; + this->info.props = &this->properties->dict; + this->permission_func = client_permission_func; this->permission_data = impl; @@ -491,7 +495,6 @@ struct pw_impl_client *pw_context_create_client(struct pw_impl_core *core, pw_context_add_listener(this->context, &impl->context_listener, &context_events, impl); - this->info.props = &this->properties->dict; return this; @@ -557,7 +560,6 @@ int pw_impl_client_register(struct pw_impl_client *client, pw_properties_setf(client->properties, PW_KEY_OBJECT_ID, "%d", client->info.id); pw_properties_setf(client->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64, pw_global_get_serial(client->global)); - client->info.props = &client->properties->dict; pw_global_add_listener(client->global, &client->global_listener, &global_events, client); pw_global_update_keys(client->global, client->info.props, keys); diff --git a/src/pipewire/impl-client.h b/src/pipewire/impl-client.h index de9325cbd..98475c33f 100644 --- a/src/pipewire/impl-client.h +++ b/src/pipewire/impl-client.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_IMPL_CLIENT_H #define PIPEWIRE_IMPL_CLIENT_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \page page_client_impl Client Implementation * * \see \ref pw_impl_client diff --git a/src/pipewire/impl-core.c b/src/pipewire/impl-core.c index 1901b4b52..3a43a8e88 100644 --- a/src/pipewire/impl-core.c +++ b/src/pipewire/impl-core.c @@ -416,6 +416,7 @@ struct pw_impl_core *pw_context_create_core(struct pw_context *context, this->info.version = pw_get_library_version(); this->info.cookie = pw_rand32(); this->info.name = name; + this->info.props = &this->properties->dict; spa_hook_list_init(&this->listener_list); if (user_data_size > 0) @@ -552,7 +553,6 @@ int pw_impl_core_update_properties(struct pw_impl_core *core, const struct spa_d int changed; changed = pw_properties_update(core->properties, dict); - core->info.props = &core->properties->dict; pw_log_debug("%p: updated %d properties", core, changed); @@ -603,7 +603,6 @@ int pw_impl_core_register(struct pw_impl_core *core, pw_properties_setf(core->properties, PW_KEY_OBJECT_ID, "%d", core->info.id); pw_properties_setf(core->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64, pw_global_get_serial(core->global)); - core->info.props = &core->properties->dict; pw_global_update_keys(core->global, core->info.props, keys); diff --git a/src/pipewire/impl-device.c b/src/pipewire/impl-device.c index 71e2e266b..53b76b236 100644 --- a/src/pipewire/impl-device.c +++ b/src/pipewire/impl-device.c @@ -27,6 +27,19 @@ struct impl { unsigned int cache_params:1; }; +static const char * const global_keys[] = { + PW_KEY_OBJECT_PATH, + PW_KEY_MODULE_ID, + PW_KEY_FACTORY_ID, + PW_KEY_CLIENT_ID, + PW_KEY_DEVICE_API, + PW_KEY_DEVICE_DESCRIPTION, + PW_KEY_DEVICE_NAME, + PW_KEY_DEVICE_NICK, + PW_KEY_MEDIA_CLASS, + NULL +}; + #define pw_device_resource(r,m,v,...) pw_resource_call(r,struct pw_device_events,m,v,__VA_ARGS__) #define pw_device_resource_info(r,...) pw_device_resource(r,info,0,__VA_ARGS__) #define pw_device_resource_param(r,...) pw_device_resource(r,param,0,__VA_ARGS__) @@ -130,7 +143,6 @@ static int execute_match(void *data, const char *location, const char *action, struct pw_impl_device *this = match->device; if (spa_streq(action, "update-props")) { match->count += pw_properties_update_string(this->properties, val, len); - this->info.props = &this->properties->dict; } return 1; } @@ -184,7 +196,6 @@ struct pw_impl_device *pw_context_create_device(struct pw_context *context, this->context = context; this->properties = properties; - this->info.props = &properties->dict; this->info.params = this->params; spa_hook_list_init(&this->listener_list); @@ -581,19 +592,6 @@ SPA_EXPORT int pw_impl_device_register(struct pw_impl_device *device, struct pw_properties *properties) { - static const char * const keys[] = { - PW_KEY_OBJECT_PATH, - PW_KEY_MODULE_ID, - PW_KEY_FACTORY_ID, - PW_KEY_CLIENT_ID, - PW_KEY_DEVICE_API, - PW_KEY_DEVICE_DESCRIPTION, - PW_KEY_DEVICE_NAME, - PW_KEY_DEVICE_NICK, - PW_KEY_MEDIA_CLASS, - NULL - }; - struct pw_context *context = device->context; struct object_data *od; @@ -617,9 +615,8 @@ int pw_impl_device_register(struct pw_impl_device *device, pw_properties_setf(device->properties, PW_KEY_OBJECT_ID, "%d", device->info.id); pw_properties_setf(device->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64, pw_global_get_serial(device->global)); - device->info.props = &device->properties->dict; - pw_global_update_keys(device->global, device->info.props, keys); + pw_global_update_keys(device->global, device->info.props, global_keys); pw_impl_device_emit_initialized(device); @@ -668,10 +665,12 @@ static void emit_info_changed(struct pw_impl_device *device) pw_impl_device_emit_info_changed(device, &device->info); - if (device->global) + if (device->global) { + if (device->info.change_mask & PW_DEVICE_CHANGE_MASK_PROPS) + pw_global_update_keys(device->global, device->info.props, global_keys); spa_list_for_each(resource, &device->global->resource_list, link) pw_device_resource_info(resource, &device->info); - + } device->info.change_mask = 0; } @@ -688,7 +687,6 @@ static int update_properties(struct pw_impl_device *device, const struct spa_dic int changed; changed = pw_properties_update_ignore(device->properties, dict, filter ? ignored : NULL); - device->info.props = &device->properties->dict; pw_log_debug("%p: updated %d properties", device, changed); diff --git a/src/pipewire/impl-factory.c b/src/pipewire/impl-factory.c index 413fa3391..9a8e1c508 100644 --- a/src/pipewire/impl-factory.c +++ b/src/pipewire/impl-factory.c @@ -139,7 +139,6 @@ int pw_impl_factory_update_properties(struct pw_impl_factory *factory, const str int changed; changed = pw_properties_update(factory->properties, dict); - factory->info.props = &factory->properties->dict; pw_log_debug("%p: updated %d properties", factory, changed); @@ -192,7 +191,6 @@ int pw_impl_factory_register(struct pw_impl_factory *factory, pw_properties_set(factory->properties, PW_KEY_FACTORY_NAME, factory->info.name); pw_properties_setf(factory->properties, PW_KEY_FACTORY_TYPE_NAME, "%s", factory->info.type); pw_properties_setf(factory->properties, PW_KEY_FACTORY_TYPE_VERSION, "%d", factory->info.version); - factory->info.props = &factory->properties->dict; pw_global_update_keys(factory->global, factory->info.props, keys); diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c index 3df092ad8..3760cbe04 100644 --- a/src/pipewire/impl-link.c +++ b/src/pipewire/impl-link.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -24,6 +25,18 @@ PW_LOG_TOPIC_EXTERN(log_link); #define pw_link_resource_info(r,...) pw_resource_call(r,struct pw_link_events,info,0,__VA_ARGS__) +struct port_info { + uint32_t busy_id; + int pending_seq; + int result; + struct spa_hook port_listener; + struct spa_hook node_listener; + struct spa_hook global_listener; + struct pw_impl_port *port; + struct pw_impl_node *node; + struct pw_impl_port_mix *mix; +}; + /** \cond */ struct impl { struct pw_impl_link this; @@ -32,27 +45,13 @@ struct impl { struct pw_work_queue *work; - uint32_t output_busy_id; - uint32_t input_busy_id; - int output_pending_seq; - int input_pending_seq; - int output_result; - int input_result; + struct port_info input; + struct port_info output; struct spa_pod *format_filter; struct pw_properties *properties; - struct spa_hook input_port_listener; - struct spa_hook input_node_listener; - struct spa_hook input_global_listener; - struct spa_hook output_port_listener; - struct spa_hook output_node_listener; - struct spa_hook output_global_listener; - struct spa_io_buffers io[2]; - - struct pw_impl_node *inode, *onode; - bool async; }; /** \endcond */ @@ -73,35 +72,18 @@ static void info_changed(struct pw_impl_link *link) link->info.change_mask = 0; } -static inline int input_set_busy_id(struct pw_impl_link *link, uint32_t id, int pending_seq) +static inline int port_set_busy_id(struct pw_impl_link *link, struct port_info *info, uint32_t id, int pending_seq) { - struct impl *impl = SPA_CONTAINER_OF(link, struct impl, this); - int res = impl->input_result; - if (impl->input_busy_id != SPA_ID_INVALID) - link->input->busy_count--; + int res = info->result; + if (info->busy_id != SPA_ID_INVALID) + info->port->busy_count--; if (id != SPA_ID_INVALID) - link->input->busy_count++; - impl->input_busy_id = id; - impl->input_pending_seq = SPA_RESULT_ASYNC_SEQ(pending_seq); - impl->input_result = 0; - if (link->input->busy_count < 0) - pw_log_error("%s: invalid busy count:%d", link->name, link->input->busy_count); - return res; -} - -static inline int output_set_busy_id(struct pw_impl_link *link, uint32_t id, int pending_seq) -{ - struct impl *impl = SPA_CONTAINER_OF(link, struct impl, this); - int res = impl->output_result; - if (impl->output_busy_id != SPA_ID_INVALID) - link->output->busy_count--; - if (id != SPA_ID_INVALID) - link->output->busy_count++; - impl->output_busy_id = id; - impl->output_pending_seq = SPA_RESULT_ASYNC_SEQ(pending_seq); - impl->output_result = 0; - if (link->output->busy_count < 0) - pw_log_error("%s: invalid busy count:%d", link->name, link->output->busy_count); + info->port->busy_count++; + info->busy_id = id; + info->pending_seq = SPA_RESULT_ASYNC_SEQ(pending_seq); + info->result = 0; + if (info->port->busy_count < 0) + pw_log_error("%s: invalid busy count:%d", link->name, info->port->busy_count); return res; } @@ -122,8 +104,8 @@ static void link_update_state(struct pw_impl_link *link, enum pw_link_state stat pw_link_state_as_string(state), error); if (state == PW_LINK_STATE_ERROR) { - pw_log_error("(%s) %s -> error (%s) (%s-%s)", link->name, - pw_link_state_as_string(old), error, + pw_log_error("(%s) %s -> error %s (%d) (%s-%s)", link->name, + pw_link_state_as_string(old), error, res, pw_impl_port_state_as_string(link->output->state), pw_impl_port_state_as_string(link->input->state)); } else { @@ -160,30 +142,23 @@ static void link_update_state(struct pw_impl_link *link, enum pw_link_state stat link->prepared = false; link->preparing = false; - output_set_busy_id(link, SPA_ID_INVALID, SPA_ID_INVALID); - pw_work_queue_cancel(impl->work, &link->output_link, SPA_ID_INVALID); + port_set_busy_id(link, &impl->output, SPA_ID_INVALID, SPA_ID_INVALID); + pw_work_queue_cancel(impl->work, &impl->output, SPA_ID_INVALID); - input_set_busy_id(link, SPA_ID_INVALID, SPA_ID_INVALID); - pw_work_queue_cancel(impl->work, &link->input_link, SPA_ID_INVALID); + port_set_busy_id(link, &impl->input, SPA_ID_INVALID, SPA_ID_INVALID); + pw_work_queue_cancel(impl->work, &impl->input, SPA_ID_INVALID); } } static void complete_ready(void *obj, void *data, int res, uint32_t id) { - struct pw_impl_port *port; + struct port_info *info = obj; + struct pw_impl_port *port = info->port; struct pw_impl_link *this = data; - struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); - - if (obj == &this->input_link) - port = this->input; - else - port = this->output; if (id != SPA_ID_INVALID) { - if (id == impl->input_busy_id) - res = input_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID); - else if (id == impl->output_busy_id) - res = output_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID); + if (id == info->busy_id) + res = port_set_busy_id(this, info, SPA_ID_INVALID, SPA_ID_INVALID); else return; } @@ -199,36 +174,26 @@ static void complete_ready(void *obj, void *data, int res, uint32_t id) this->output->state >= PW_IMPL_PORT_STATE_READY) link_update_state(this, PW_LINK_STATE_ALLOCATING, 0, NULL); } else { - link_update_state(this, PW_LINK_STATE_ERROR, -EIO, strdup("Format negotiation failed")); + link_update_state(this, PW_LINK_STATE_ERROR, res, strdup("Format negotiation failed")); } } static void complete_paused(void *obj, void *data, int res, uint32_t id) { - struct pw_impl_port *port; + struct port_info *info = obj; + struct pw_impl_port *port = info->port; + struct pw_impl_port_mix *mix = info->mix; struct pw_impl_link *this = data; - struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); - struct pw_impl_port_mix *mix; - - if (obj == &this->input_link) { - port = this->input; - mix = &this->rt.in_mix; - } else { - port = this->output; - mix = &this->rt.out_mix; - } if (id != SPA_ID_INVALID) { - if (id == impl->input_busy_id) - res = input_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID); - else if (id == impl->output_busy_id) - res = output_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID); + if (id == info->busy_id) + res = port_set_busy_id(this, info, SPA_ID_INVALID, SPA_ID_INVALID); else return; } - pw_log_debug("%p: obj:%p port %p complete state:%d: %s", this, obj, port, - port->state, spa_strerror(res)); + pw_log_debug("%p: obj:%p port %p complete state:%d: %s (%d)", this, obj, port, + port->state, spa_strerror(res), res); if (SPA_RESULT_IS_OK(res)) { if (port->state < PW_IMPL_PORT_STATE_PAUSED) @@ -240,24 +205,198 @@ static void complete_paused(void *obj, void *data, int res, uint32_t id) link_update_state(this, PW_LINK_STATE_PAUSED, 0, NULL); } else { mix->have_buffers = false; - link_update_state(this, PW_LINK_STATE_ERROR, -EIO, strdup("Buffer allocation failed")); + link_update_state(this, PW_LINK_STATE_ERROR, res, strdup("Buffer allocation failed")); } } static void complete_sync(void *obj, void *data, int res, uint32_t id) { - struct pw_impl_port *port; + struct port_info *info = obj; + struct pw_impl_port *port = info->port; struct pw_impl_link *this = data; - if (obj == &this->input_link) - port = this->input; - else - port = this->output; - pw_log_debug("%p: obj:%p port %p complete state:%d: %s", this, obj, port, port->state, spa_strerror(res)); } +/* find a common format. info[0] has the higher priority. + * Either the format contains a valid common format or error is set. */ +static int link_find_format(struct pw_impl_link *this, + struct port_info *info[2], + uint32_t port_id[2], + struct spa_pod **format, + struct spa_pod_builder *builder, + char **error) +{ + int res; + uint32_t state[2]; + uint32_t idx[2] = { 0, 0 }; + struct spa_pod_builder fb = { 0 }; + uint8_t fbuf[4096]; + struct spa_pod *filter; + struct spa_node *node[2]; + const char *dir[2]; + + state[0] = info[0]->port->state; + state[1] = info[1]->port->state; + node[0] = info[0]->node->node; + node[1] = info[1]->node->node; + port_id[0] = info[0]->port->port_id; + port_id[1] = info[1]->port->port_id; + dir[0] = pw_direction_as_string(info[0]->port->direction); + dir[1] = pw_direction_as_string(info[1]->port->direction); + + pw_log_debug("%p: finding best format %d %d", this, state[0], state[1]); + + /* when a port is configured but the node is idle, we can reconfigure with a different format */ + if (state[1] > PW_IMPL_PORT_STATE_CONFIGURE && info[1]->node->info.state == PW_NODE_STATE_IDLE) + state[1] = PW_IMPL_PORT_STATE_CONFIGURE; + if (state[0] > PW_IMPL_PORT_STATE_CONFIGURE && info[0]->node->info.state == PW_NODE_STATE_IDLE) + state[0] = PW_IMPL_PORT_STATE_CONFIGURE; + + pw_log_debug("%p: states %d %d", this, state[0], state[1]); + + if (state[0] == PW_IMPL_PORT_STATE_CONFIGURE && state[1] > PW_IMPL_PORT_STATE_CONFIGURE) { + /* only port 0 needs format, take format from port 1 and filter */ + spa_pod_builder_init(&fb, fbuf, sizeof(fbuf)); + if ((res = spa_node_port_enum_params_sync(node[1], + info[1]->port->direction, port_id[1], + SPA_PARAM_Format, &idx[1], + NULL, &filter, &fb)) != 1) { + if (res < 0) + *error = spa_aprintf("error get %s format: %s", dir[1], + spa_strerror(res)); + else + *error = spa_aprintf("no %s formats", dir[1]); + goto error; + } + pw_log_debug("%p: Got %s format:", this, dir[1]); + pw_log_pod(SPA_LOG_LEVEL_DEBUG, filter); + + if ((res = spa_node_port_enum_params_sync(node[0], + info[0]->port->direction, port_id[0], + SPA_PARAM_EnumFormat, &idx[0], + filter, format, builder)) <= 0) { + if (res == -ENOENT || res == 0) { + pw_log_debug("%p: no %s format filter, using %s format: %s", + this, dir[0], dir[1], spa_strerror(res)); + + uint32_t offset = builder->state.offset; + res = spa_pod_builder_raw_padded(builder, filter, SPA_POD_SIZE(filter)); + if (res < 0) { + *error = spa_aprintf("failed to add pod"); + goto error; + } + + *format = spa_pod_builder_deref(builder, offset); + } else { + *error = spa_aprintf("error %s enum formats: %s", dir[0], + spa_strerror(res)); + goto error; + } + } + } else if (state[1] >= PW_IMPL_PORT_STATE_CONFIGURE && state[0] > PW_IMPL_PORT_STATE_CONFIGURE) { + /* only port 1 needs format, take and filter format from port 0 */ + spa_pod_builder_init(&fb, fbuf, sizeof(fbuf)); + if ((res = spa_node_port_enum_params_sync(node[0], + info[0]->port->direction, port_id[0], + SPA_PARAM_Format, &idx[0], + NULL, &filter, &fb)) != 1) { + if (res < 0) + *error = spa_aprintf("error get %s format: %s", dir[0], + spa_strerror(res)); + else + *error = spa_aprintf("no %s format", dir[0]); + goto error; + } + pw_log_debug("%p: Got %s format:", this, dir[0]); + pw_log_pod(SPA_LOG_LEVEL_DEBUG, filter); + + if ((res = spa_node_port_enum_params_sync(node[1], + info[1]->port->direction, port_id[1], + SPA_PARAM_EnumFormat, &idx[1], + filter, format, builder)) <= 0) { + if (res == -ENOENT || res == 0) { + pw_log_debug("%p: no %s format filter, using %s format: %s", + this, dir[1], dir[0], spa_strerror(res)); + + uint32_t offset = builder->state.offset; + res = spa_pod_builder_raw_padded(builder, filter, SPA_POD_SIZE(filter)); + if (res < 0) { + *error = spa_aprintf("failed to add pod"); + goto error; + } + + *format = spa_pod_builder_deref(builder, offset); + } else { + *error = spa_aprintf("error %s enum formats: %s", dir[1], + spa_strerror(res)); + goto error; + } + } + } else if (state[0] == PW_IMPL_PORT_STATE_CONFIGURE && state[1] == PW_IMPL_PORT_STATE_CONFIGURE) { + bool do_filter = true; + int count = 0; + again: + /* both ports need a format, we start with a format from port 0 and use that + * as a filter for port 1. Because the filter has higher priority, its + * defaults will be prefered. */ + pw_log_debug("%p: do enum %s %d", this, dir[0], idx[0]); + spa_pod_builder_init(&fb, fbuf, sizeof(fbuf)); + if ((res = spa_node_port_enum_params_sync(node[0], + info[0]->port->direction, port_id[0], + SPA_PARAM_EnumFormat, &idx[0], + NULL, &filter, &fb)) != 1) { + if (res == -ENOENT) { + pw_log_debug("%p: no %s filter", this, dir[0]); + filter = NULL; + } else { + if (res < 0) + *error = spa_aprintf("error %s enum formats: %s", dir[0], + spa_strerror(res)); + else if (do_filter && count > 0) { + do_filter = false; + idx[0] = 0; + goto again; + } else { + *error = spa_aprintf("no more %s formats", dir[0]); + } + goto error; + } + } + if (do_filter && filter) + if ((res = spa_pod_filter_make(filter)) > 0) + count += res; + + pw_log_debug("%p: enum %s %d with filter: %p", this, dir[1], idx[1], filter); + pw_log_pod(SPA_LOG_LEVEL_DEBUG, filter); + + if ((res = spa_node_port_enum_params_sync(node[1], + info[1]->port->direction, port_id[1], + SPA_PARAM_EnumFormat, &idx[1], + filter, format, builder)) != 1) { + if (res == 0 && filter != NULL) { + idx[1] = 0; + goto again; + } + *error = spa_aprintf("error %s enum formats: %s", dir[1], spa_strerror(res)); + goto error; + } + + pw_log_debug("%p: Got filtered:", this); + pw_log_pod(SPA_LOG_LEVEL_DEBUG, *format); + } else { + res = -EBADF; + *error = spa_aprintf("error bad node state"); + goto error; + } + return res; +error: + if (res == 0) + res = -EINVAL; + return res; +} + static int do_negotiate(struct pw_impl_link *this) { struct pw_context *context = this->context; @@ -265,65 +404,71 @@ static int do_negotiate(struct pw_impl_link *this) int res = -EIO, res2; struct spa_pod *format = NULL, *current; char *error = NULL; - bool changed = true; - struct pw_impl_port *input, *output; + bool changed = false; + struct port_info *info[2]; uint8_t buffer[4096]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); uint32_t index, busy_id; - uint32_t in_state, out_state; - struct spa_node *in_node, *out_node; - uint32_t in_port, out_port; + uint32_t state[2]; + struct spa_node *node[2]; + uint32_t port_id[2]; + const char *dir[2]; if (this->info.state >= PW_LINK_STATE_NEGOTIATING) return 0; - input = this->input; - output = this->output; + /* driver nodes have lower priority for selecting the format. + * Higher priority nodes go into info[0] */ + if (this->output->node->driver) { + info[0] = &impl->input; + info[1] = &impl->output; + } else { + info[0] = &impl->output; + info[1] = &impl->input; + } + state[0] = info[0]->port->state; + state[1] = info[1]->port->state; - in_state = input->state; - out_state = output->state; + dir[0] = pw_direction_as_string(info[0]->port->direction), + dir[1] = pw_direction_as_string(info[1]->port->direction), - pw_log_debug("%p: in_state:%d out_state:%d", this, in_state, out_state); + pw_log_info("%p: %s:%d -> %s:%d", this, dir[0], state[0], dir[1], state[1]); - if (in_state != PW_IMPL_PORT_STATE_CONFIGURE && out_state != PW_IMPL_PORT_STATE_CONFIGURE) + if (state[0] != PW_IMPL_PORT_STATE_CONFIGURE && state[1] != PW_IMPL_PORT_STATE_CONFIGURE) return 0; link_update_state(this, PW_LINK_STATE_NEGOTIATING, 0, NULL); - input = this->input; - output = this->output; #if 0 - in_node = input->mix; - in_port = this->rt.in_mix.port.port_id; - out_node = output->mix; - out_port = this->rt.out_mix.port.port_id; + node[0] = info[0]->port->mix; + port_id[0] = this->rt.in_mix.port.port_id; + node[1] = info[1]->port->mix; + port_id[1] = this->rt.out_mix.port.port_id; #else - in_node = input->node->node; - in_port = input->port_id; - out_node = output->node->node; - out_port = output->port_id; + node[0] = info[0]->node->node; + port_id[0] = info[0]->port->port_id; + node[1] = info[1]->node->node; + port_id[1] = info[1]->port->port_id; #endif /* find a common format for the ports */ - if ((res = pw_context_find_format(context, - output, SPA_ID_INVALID, - input, SPA_ID_INVALID, - NULL, 0, NULL, - &format, &b, &error)) < 0) { + if ((res = link_find_format(this, info, port_id, &format, &b, &error)) < 0) { format = NULL; goto error; } format = spa_pod_copy(format); + pw_log_pod(SPA_LOG_LEVEL_DEBUG, format); spa_pod_fixate(format); + pw_log_pod(SPA_LOG_LEVEL_DEBUG, format); spa_pod_builder_init(&b, buffer, sizeof(buffer)); - /* if output port had format and is idle, check if it changed. If so, renegotiate */ - if (out_state > PW_IMPL_PORT_STATE_CONFIGURE && output->node->info.state == PW_NODE_STATE_IDLE) { + /* if port 1 had format and is idle, check if it changed. If so, renegotiate */ + if (state[1] > PW_IMPL_PORT_STATE_CONFIGURE && info[1]->node->info.state == PW_NODE_STATE_IDLE) { index = 0; - res = spa_node_port_enum_params_sync(out_node, - output->direction, out_port, + res = spa_node_port_enum_params_sync(node[1], + info[1]->port->direction, port_id[1], SPA_PARAM_Format, &index, NULL, ¤t, &b); switch (res) { @@ -337,27 +482,29 @@ static int do_negotiate(struct pw_impl_link *this) res = -EBADF; SPA_FALLTHROUGH default: - error = spa_aprintf("error get output format: %s", spa_strerror(res)); + error = spa_aprintf("error get %s format: %s", + pw_direction_as_string(info[1]->port->direction), + spa_strerror(res)); goto error; } if (current == NULL || spa_pod_compare(current, format) != 0) { - pw_log_debug("%p: output format change, renegotiate", this); + pw_log_debug("%p: %s format change, renegotiate", this, + pw_direction_as_string(info[1]->port->direction)); if (current) pw_log_pod(SPA_LOG_LEVEL_DEBUG, current); pw_log_pod(SPA_LOG_LEVEL_DEBUG, format); - pw_impl_node_set_state(output->node, PW_NODE_STATE_SUSPENDED); - out_state = PW_IMPL_PORT_STATE_CONFIGURE; + pw_impl_node_set_state(info[1]->node, PW_NODE_STATE_SUSPENDED); + state[1] = PW_IMPL_PORT_STATE_CONFIGURE; } else { pw_log_debug("%p: format was already set", this); - changed = false; } } - /* if input port had format and is idle, check if it changed. If so, renegotiate */ - if (in_state > PW_IMPL_PORT_STATE_CONFIGURE && input->node->info.state == PW_NODE_STATE_IDLE) { + /* if port 0 had format and is idle, check if it changed. If so, renegotiate */ + if (state[0] > PW_IMPL_PORT_STATE_CONFIGURE && info[0]->node->info.state == PW_NODE_STATE_IDLE) { index = 0; - res = spa_node_port_enum_params_sync(in_node, - input->direction, in_port, + res = spa_node_port_enum_params_sync(node[0], + info[0]->port->direction, port_id[0], SPA_PARAM_Format, &index, NULL, ¤t, &b); switch (res) { @@ -371,70 +518,75 @@ static int do_negotiate(struct pw_impl_link *this) res = -EBADF; SPA_FALLTHROUGH default: - error = spa_aprintf("error get input format: %s", spa_strerror(res)); + error = spa_aprintf("error get %s format: %s", + pw_direction_as_string(info[0]->port->direction), + spa_strerror(res)); goto error; } if (current == NULL || spa_pod_compare(current, format) != 0) { - pw_log_debug("%p: input format change, renegotiate", this); + pw_log_debug("%p: %s format change, renegotiate", this, + pw_direction_as_string(info[0]->port->direction)); if (current) pw_log_pod(SPA_LOG_LEVEL_DEBUG, current); pw_log_pod(SPA_LOG_LEVEL_DEBUG, format); - pw_impl_node_set_state(input->node, PW_NODE_STATE_SUSPENDED); - in_state = PW_IMPL_PORT_STATE_CONFIGURE; + pw_impl_node_set_state(info[0]->node, PW_NODE_STATE_SUSPENDED); + state[0] = PW_IMPL_PORT_STATE_CONFIGURE; } else { pw_log_debug("%p: format was already set", this); - changed = false; } } - pw_log_pod(SPA_LOG_LEVEL_DEBUG, format); - SPA_POD_OBJECT_ID(format) = SPA_PARAM_Format; pw_log_debug("%p: doing set format %p fixated:%d", this, format, spa_pod_is_fixated(format)); + pw_log_pod(SPA_LOG_LEVEL_INFO, format); - if (out_state == PW_IMPL_PORT_STATE_CONFIGURE) { - pw_log_debug("%p: doing set format on output", this); - if ((res = pw_impl_port_set_param(output, + if (state[1] == PW_IMPL_PORT_STATE_CONFIGURE) { + pw_log_debug("%p: doing set format on %s", this, dir[1]); + if ((res = pw_impl_port_set_param(info[1]->port, SPA_PARAM_Format, 0, format)) < 0) { - error = spa_aprintf("error set output format: %d (%s)", res, spa_strerror(res)); - pw_log_error("tried to set output format:"); + error = spa_aprintf("error set %s format: %d (%s)", dir[1], + res, spa_strerror(res)); + pw_log_error("tried to set %s format:", dir[1]); pw_log_pod(SPA_LOG_LEVEL_ERROR, format); goto error; } + pw_log_debug("%s set format: %d", dir[1], res); if (SPA_RESULT_IS_ASYNC(res)) { - pw_log_info("output set format %d", res); - busy_id = pw_work_queue_add(impl->work, &this->output_link, - spa_node_sync(output->node->node, res), + busy_id = pw_work_queue_add(impl->work, info[1], + spa_node_sync(info[1]->node->node, res), complete_ready, this); - output_set_busy_id(this, busy_id, res); + port_set_busy_id(this, info[1], busy_id, res); } else { - complete_ready(&this->output_link, this, res, SPA_ID_INVALID); + complete_ready(info[1], this, res, SPA_ID_INVALID); } + changed = true; } - if (in_state == PW_IMPL_PORT_STATE_CONFIGURE) { - pw_log_debug("%p: doing set format on input", this); - if ((res2 = pw_impl_port_set_param(input, + if (state[0] == PW_IMPL_PORT_STATE_CONFIGURE) { + pw_log_debug("%p: doing set format on %s", this, dir[0]); + if ((res2 = pw_impl_port_set_param(info[0]->port, SPA_PARAM_Format, 0, format)) < 0) { - error = spa_aprintf("error set input format: %d (%s)", res2, spa_strerror(res2)); - pw_log_error("tried to set input format:"); + error = spa_aprintf("error set %s format: %d (%s)", dir[0], + res2, spa_strerror(res2)); + pw_log_error("tried to set %s format:", dir[0]); pw_log_pod(SPA_LOG_LEVEL_ERROR, format); goto error; } + pw_log_debug("%s set format: %d", dir[0], res2); if (SPA_RESULT_IS_ASYNC(res2)) { - pw_log_info("input set format %d", res2); - busy_id = pw_work_queue_add(impl->work, &this->input_link, - spa_node_sync(input->node->node, res2), + busy_id = pw_work_queue_add(impl->work, info[0], + spa_node_sync(info[0]->node->node, res2), complete_ready, this); - input_set_busy_id(this, busy_id, res2); + port_set_busy_id(this, info[0], busy_id, res2); if (res == 0) res = res2; } else { - complete_ready(&this->input_link, this, res2, SPA_ID_INVALID); + complete_ready(info[0], this, res2, SPA_ID_INVALID); } + changed = true; } free(this->info.format); @@ -447,21 +599,23 @@ static int do_negotiate(struct pw_impl_link *this) return res; error: - pw_context_debug_port_params(context, in_node, - input->direction, in_port, + pw_context_debug_port_params(context, node[0], + info[0]->port->direction, port_id[0], SPA_PARAM_EnumFormat, res, "input format (%s)", error); - pw_context_debug_port_params(context, out_node, - output->direction, out_port, + pw_context_debug_port_params(context, node[1], + info[1]->port->direction, port_id[1], SPA_PARAM_EnumFormat, res, "output format (%s)", error); link_update_state(this, PW_LINK_STATE_ERROR, res, error); free(format); return res; } -static int port_set_io(struct pw_impl_link *this, struct pw_impl_port *port, uint32_t id, - void *data, size_t size, struct pw_impl_port_mix *mix) +static int port_set_io(struct pw_impl_link *this, struct port_info *info, uint32_t id, + void *data, size_t size) { int res = 0; + struct pw_impl_port *port = info->port; + struct pw_impl_port_mix *mix = info->mix; pw_log_debug("%p: %s port %p %d.%d set io: %d %p %zd", this, pw_direction_as_string(port->direction), @@ -544,13 +698,11 @@ static int do_allocation(struct pw_impl_link *this) uint32_t in_port, out_port; flags = 0; - /* always shared buffers for the link */ - alloc_flags = PW_BUFFERS_FLAG_SHARED; /* always enable async mode */ - alloc_flags |= PW_BUFFERS_FLAG_ASYNC; + alloc_flags = PW_BUFFERS_FLAG_ASYNC; if (output->node->remote || input->node->remote) - alloc_flags |= PW_BUFFERS_FLAG_SHARED_MEM; + alloc_flags |= PW_BUFFERS_FLAG_SHARED; if (output->node->driver) alloc_flags |= PW_BUFFERS_FLAG_IN_PRIORITY; @@ -591,14 +743,14 @@ static int do_allocation(struct pw_impl_link *this) goto error_clear; } if (SPA_RESULT_IS_ASYNC(res)) { - busy_id = pw_work_queue_add(impl->work, &this->output_link, + busy_id = pw_work_queue_add(impl->work, &impl->output, spa_node_sync(output->node->node, res), complete_paused, this); - output_set_busy_id(this, busy_id, res); + port_set_busy_id(this, &impl->output, busy_id, res); if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) return 0; } else { - complete_paused(&this->output_link, this, res, SPA_ID_INVALID); + complete_paused(&impl->output, this, res, SPA_ID_INVALID); } } @@ -614,12 +766,12 @@ static int do_allocation(struct pw_impl_link *this) } if (SPA_RESULT_IS_ASYNC(res)) { - busy_id = pw_work_queue_add(impl->work, &this->input_link, + busy_id = pw_work_queue_add(impl->work, &impl->input, spa_node_sync(input->node->node, res), complete_paused, this); - input_set_busy_id(this, busy_id, res); + port_set_busy_id(this, &impl->input, busy_id, res); } else { - complete_paused(&this->input_link, this, res, SPA_ID_INVALID); + complete_paused(&impl->input, this, res, SPA_ID_INVALID); } return 0; @@ -635,15 +787,24 @@ int pw_impl_link_activate(struct pw_impl_link *this) struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); int res; uint32_t io_type, io_size; + bool reliable_driver; pw_log_debug("%p: activate activated:%d state:%s", this, impl->activated, pw_link_state_as_string(this->info.state)); if (this->destroyed || impl->activated || !this->prepared || - !impl->inode->runnable || !impl->onode->runnable) + !impl->input.node->runnable || !impl->output.node->runnable) return 0; - if (impl->async) { + /* check if the output node is a driver for the input node and if + * it has reliable scheduling. Because it is a driver, it will always be + * scheduled before the input node and there will not be any concurrent access + * to the io, so we don't need async IO, even when the input is async. This + * avoid the problem of out-of-order buffers after a stall. */ + reliable_driver = (impl->output.node == impl->input.node->driver_node) && + impl->output.node->reliable; + + if (this->async && !reliable_driver) { io_type = SPA_IO_AsyncBuffers; io_size = sizeof(struct spa_io_async_buffers); } else { @@ -651,11 +812,9 @@ int pw_impl_link_activate(struct pw_impl_link *this) io_size = sizeof(struct spa_io_buffers); } - if ((res = port_set_io(this, this->input, io_type, this->io, - io_size, &this->rt.in_mix)) < 0) + if ((res = port_set_io(this, &impl->input, io_type, this->io, io_size)) < 0) goto error; - if ((res = port_set_io(this, this->output, io_type, this->io, - io_size, &this->rt.out_mix)) < 0) + if ((res = port_set_io(this, &impl->output, io_type, this->io, io_size)) < 0) goto error_clean; impl->activated = true; @@ -665,7 +824,7 @@ int pw_impl_link_activate(struct pw_impl_link *this) return 0; error_clean: - port_set_io(this, this->input, io_type, NULL, 0, &this->rt.in_mix); + port_set_io(this, &impl->input, io_type, NULL, 0); error: pw_log_error("%p: can't activate link: %s", this, spa_strerror(res)); return res; @@ -720,13 +879,13 @@ static void check_states(void *obj, void *user_data, int res, uint32_t id) if (output->busy_count > 0) { pw_log_debug("%p: output port %p was busy %d", this, output, output->busy_count); res = spa_node_sync(output->node->node, 0); - pw_work_queue_add(impl->work, &this->output_link, res, complete_sync, this); + pw_work_queue_add(impl->work, &impl->output, res, complete_sync, this); goto exit; } else if (input->busy_count > 0) { pw_log_debug("%p: input port %p was busy %d", this, input, input->busy_count); res = spa_node_sync(input->node->node, 0); - pw_work_queue_add(impl->work, &this->input_link, res, complete_sync, this); + pw_work_queue_add(impl->work, &impl->input, res, complete_sync, this); goto exit; } @@ -746,61 +905,65 @@ exit: this, -EBUSY, (pw_work_func_t) check_states, this); } -static void input_remove(struct pw_impl_link *this, struct pw_impl_port *port) +static void input_remove(struct pw_impl_link *this) { struct impl *impl = (struct impl *) this; - struct pw_impl_port_mix *mix = &this->rt.in_mix; + struct port_info *info = &impl->input; + struct pw_impl_port_mix *mix = info->mix; + struct pw_impl_port *port = info->port; int res; pw_log_debug("%p: remove input port %p", this, port); - input_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID); + port_set_busy_id(this, info, SPA_ID_INVALID, SPA_ID_INVALID); - spa_hook_remove(&impl->input_port_listener); - spa_hook_remove(&impl->input_node_listener); - spa_hook_remove(&impl->input_global_listener); + spa_hook_remove(&info->port_listener); + spa_hook_remove(&info->node_listener); + spa_hook_remove(&info->global_listener); spa_list_remove(&this->input_link); - pw_impl_port_emit_link_removed(this->input, this); + pw_impl_port_emit_link_removed(port, this); - pw_impl_port_recalc_latency(this->input); - pw_impl_port_recalc_tag(this->input); + pw_impl_port_recalc_latency(port); + pw_impl_port_recalc_tag(port); - if ((res = port_set_io(this, this->input, SPA_IO_Buffers, NULL, 0, mix)) < 0) + if ((res = port_set_io(this, info, SPA_IO_Buffers, NULL, 0)) < 0) pw_log_warn("%p: port %p set_io error %s", this, port, spa_strerror(res)); if ((res = pw_impl_port_use_buffers(port, mix, 0, NULL, 0)) < 0) pw_log_warn("%p: port %p clear error %s", this, port, spa_strerror(res)); pw_impl_port_release_mix(port, mix); - pw_work_queue_cancel(impl->work, &this->input_link, SPA_ID_INVALID); + pw_work_queue_cancel(impl->work, info, SPA_ID_INVALID); this->input = NULL; } -static void output_remove(struct pw_impl_link *this, struct pw_impl_port *port) +static void output_remove(struct pw_impl_link *this) { struct impl *impl = (struct impl *) this; - struct pw_impl_port_mix *mix = &this->rt.out_mix; + struct port_info *info = &impl->output; + struct pw_impl_port_mix *mix = info->mix; + struct pw_impl_port *port = info->port; pw_log_debug("%p: remove output port %p", this, port); - output_set_busy_id(this, SPA_ID_INVALID, SPA_ID_INVALID); + port_set_busy_id(this, info, SPA_ID_INVALID, SPA_ID_INVALID); - spa_hook_remove(&impl->output_port_listener); - spa_hook_remove(&impl->output_node_listener); - spa_hook_remove(&impl->output_global_listener); + spa_hook_remove(&info->port_listener); + spa_hook_remove(&info->node_listener); + spa_hook_remove(&info->global_listener); spa_list_remove(&this->output_link); - pw_impl_port_emit_link_removed(this->output, this); + pw_impl_port_emit_link_removed(port, this); - pw_impl_port_recalc_latency(this->output); - pw_impl_port_recalc_tag(this->output); + pw_impl_port_recalc_latency(port); + pw_impl_port_recalc_tag(port); /* we don't clear output buffers when the link goes away. They will get * cleared when the node goes to suspend */ pw_impl_port_release_mix(port, mix); - pw_work_queue_cancel(impl->work, &this->output_link, SPA_ID_INVALID); + pw_work_queue_cancel(impl->work, info, SPA_ID_INVALID); this->output = NULL; } @@ -810,9 +973,9 @@ int pw_impl_link_prepare(struct pw_impl_link *this) pw_log_debug("%p: prepared:%d preparing:%d in_active:%d out_active:%d passive:%u", this, this->prepared, this->preparing, - impl->inode->active, impl->onode->active, this->passive); + impl->input.node->active, impl->output.node->active, this->passive); - if (!impl->inode->active || !impl->onode->active) + if (!impl->input.node->active || !impl->output.node->active) return 0; if (this->destroyed || this->preparing || this->prepared) @@ -835,10 +998,8 @@ int pw_impl_link_deactivate(struct pw_impl_link *this) if (!impl->activated) return 0; - port_set_io(this, this->output, SPA_IO_Buffers, NULL, 0, - &this->rt.out_mix); - port_set_io(this, this->input, SPA_IO_Buffers, NULL, 0, - &this->rt.in_mix); + port_set_io(this, &impl->output, SPA_IO_Buffers, NULL, 0); + port_set_io(this, &impl->input, SPA_IO_Buffers, NULL, 0); impl->activated = false; pw_log_info("(%s) deactivated", this->name); @@ -925,6 +1086,7 @@ static void port_param_changed(struct pw_impl_link *this, uint32_t id, if (inport) pw_impl_port_update_state(inport, target, 0, NULL); + pw_log_info("%p: format changed", this); this->preparing = this->prepared = false; link_update_state(this, PW_LINK_STATE_INIT, 0, NULL); pw_impl_link_prepare(this); @@ -1011,6 +1173,16 @@ static const struct pw_impl_port_events output_port_events = { static void node_result(struct impl *impl, void *obj, int seq, int res, uint32_t type, const void *result) { + struct port_info *info = obj; + struct pw_impl_port *port = info->port; + + pw_log_trace("%p: %s port %p result seq:%d %d res:%d type:%u", + impl, pw_direction_as_string(port->direction), + port, seq, SPA_RESULT_ASYNC_SEQ(seq), res, type); + + if (type == SPA_RESULT_TYPE_NODE_ERROR && info->pending_seq == seq) + info->result = res; + if (SPA_RESULT_IS_ASYNC(seq)) pw_work_queue_complete(impl->work, obj, SPA_RESULT_ASYNC_SEQ(seq), res); } @@ -1018,27 +1190,13 @@ static void node_result(struct impl *impl, void *obj, static void input_node_result(void *data, int seq, int res, uint32_t type, const void *result) { struct impl *impl = data; - struct pw_impl_port *port = impl->this.input; - pw_log_trace("%p: input port %p result seq:%d %d res:%d type:%u", - impl, port, seq, SPA_RESULT_ASYNC_SEQ(seq), res, type); - - if (type == SPA_RESULT_TYPE_NODE_ERROR && impl->input_pending_seq == seq) - impl->input_result = res; - - node_result(impl, &impl->this.input_link, seq, res, type, result); + node_result(impl, &impl->input, seq, res, type, result); } static void output_node_result(void *data, int seq, int res, uint32_t type, const void *result) { struct impl *impl = data; - struct pw_impl_port *port = impl->this.output; - pw_log_trace("%p: output port %p result seq:%d %d res:%d type:%u", - impl, port, seq, SPA_RESULT_ASYNC_SEQ(seq), res, type); - - if (type == SPA_RESULT_TYPE_NODE_ERROR && impl->output_pending_seq == seq) - impl->output_result = res; - - node_result(impl, &impl->this.output_link, seq, res, type, result); + node_result(impl, &impl->output, seq, res, type, result); } static void node_active_changed(void *data, bool active) @@ -1047,16 +1205,29 @@ static void node_active_changed(void *data, bool active) pw_impl_link_prepare(&impl->this); } +static void node_driver_changed(void *data, struct pw_impl_node *old, struct pw_impl_node *driver) +{ + struct impl *impl = data; + if (impl->this.async) { + /* for async links, input and output port latency depends on if the + * output node is directly driving the input node. */ + pw_impl_port_recalc_latency(impl->output.port); + pw_impl_port_recalc_latency(impl->input.port); + } +} + static const struct pw_impl_node_events input_node_events = { PW_VERSION_IMPL_NODE_EVENTS, .result = input_node_result, .active_changed = node_active_changed, + .driver_changed = node_driver_changed, }; static const struct pw_impl_node_events output_node_events = { PW_VERSION_IMPL_NODE_EVENTS, .result = output_node_result, .active_changed = node_active_changed, + .driver_changed = node_driver_changed, }; static bool pw_impl_node_can_reach(struct pw_impl_node *output, struct pw_impl_node *input, int hop) @@ -1313,8 +1484,8 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context, if (impl == NULL) goto error_no_mem; - impl->input_busy_id = SPA_ID_INVALID; - impl->output_busy_id = SPA_ID_INVALID; + impl->input.busy_id = SPA_ID_INVALID; + impl->output.busy_id = SPA_ID_INVALID; this = &impl->this; this->feedback = pw_impl_node_can_reach(input_node, output_node, 0); @@ -1329,6 +1500,7 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context, this->context = context; this->properties = properties; + this->info.props = &this->properties->dict; this->info.state = PW_LINK_STATE_INIT; this->output = output; @@ -1343,18 +1515,17 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context, if (this->passive && str == NULL) pw_properties_set(properties, PW_KEY_LINK_PASSIVE, "true"); - impl->async = (output_node->async || input_node->async) && + this->async = (output_node->async || input_node->async) && SPA_FLAG_IS_SET(output->flags, PW_IMPL_PORT_FLAG_ASYNC) && SPA_FLAG_IS_SET(input->flags, PW_IMPL_PORT_FLAG_ASYNC); - if (impl->async) + if (this->async) pw_properties_set(properties, PW_KEY_LINK_ASYNC, "true"); spa_hook_list_init(&this->listener_list); impl->format_filter = format_filter; this->info.format = NULL; - this->info.props = &this->properties->dict; this->rt.out_mix.peer_id = input->global->id; this->rt.in_mix.peer_id = output->global->id; @@ -1364,12 +1535,12 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context, if ((res = pw_impl_port_init_mix(input, &this->rt.in_mix)) < 0) goto error_input_mix; - pw_impl_port_add_listener(input, &impl->input_port_listener, &input_port_events, impl); - pw_impl_node_add_listener(input_node, &impl->input_node_listener, &input_node_events, impl); - pw_global_add_listener(input->global, &impl->input_global_listener, &input_global_events, impl); - pw_impl_port_add_listener(output, &impl->output_port_listener, &output_port_events, impl); - pw_impl_node_add_listener(output_node, &impl->output_node_listener, &output_node_events, impl); - pw_global_add_listener(output->global, &impl->output_global_listener, &output_global_events, impl); + pw_impl_port_add_listener(input, &impl->input.port_listener, &input_port_events, impl); + pw_impl_node_add_listener(input_node, &impl->input.node_listener, &input_node_events, impl); + pw_global_add_listener(input->global, &impl->input.global_listener, &input_global_events, impl); + pw_impl_port_add_listener(output, &impl->output.port_listener, &output_port_events, impl); + pw_impl_node_add_listener(output_node, &impl->output.node_listener, &output_node_events, impl); + pw_global_add_listener(output->global, &impl->output.global_listener, &output_global_events, impl); input_node->live = output_node->live; @@ -1381,14 +1552,12 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context, select_io(this); - if (this->feedback) { - impl->inode = output_node; - impl->onode = input_node; - } - else { - impl->onode = output_node; - impl->inode = input_node; - } + impl->input.port = input; + impl->output.port = output; + impl->output.node = output_node; + impl->input.node = input_node; + impl->input.mix = &this->rt.in_mix; + impl->output.mix = &this->rt.out_mix; pw_log_debug("%p: constructed out:%p:%d.%d -> in:%p:%d.%d", impl, output_node, output->port_id, this->rt.out_mix.port.port_id, @@ -1400,7 +1569,7 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context, pw_log_info("(%s) (%s) -> (%s) async:%d:%d:%d:%04x:%04x:%d", this->name, output_node->name, input_node->name, output_node->driving, output_node->async, input_node->async, - output->flags, input->flags, impl->async); + output->flags, input->flags, this->async); pw_impl_port_emit_link_added(output, this); pw_impl_port_emit_link_added(input, this); @@ -1412,8 +1581,12 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context, pw_impl_port_recalc_tag(output); pw_impl_port_recalc_tag(input); - if (impl->onode != impl->inode) - this->peer = pw_node_peer_ref(impl->onode, impl->inode); + if (impl->output.node != impl->input.node) { + if (this->feedback) + this->peer = pw_node_peer_ref(impl->input.node, impl->output.node); + else + this->peer = pw_node_peer_ref(impl->output.node, impl->input.node); + } return this; @@ -1516,7 +1689,6 @@ int pw_impl_link_register(struct pw_impl_link *link, pw_properties_setf(link->properties, PW_KEY_LINK_OUTPUT_PORT, "%u", link->info.output_port_id); pw_properties_setf(link->properties, PW_KEY_LINK_INPUT_NODE, "%u", link->info.input_node_id); pw_properties_setf(link->properties, PW_KEY_LINK_INPUT_PORT, "%u", link->info.input_port_id); - link->info.props = &link->properties->dict; pw_global_update_keys(link->global, link->info.props, keys); @@ -1556,8 +1728,8 @@ void pw_impl_link_destroy(struct pw_impl_link *link) try_unlink_controls(impl, link->output, link->input); - output_remove(link, link->output); - input_remove(link, link->input); + output_remove(link); + input_remove(link); if (link->global) { spa_hook_remove(&link->global_listener); diff --git a/src/pipewire/impl-module.c b/src/pipewire/impl-module.c index 10aa432fe..22c8e91fa 100644 --- a/src/pipewire/impl-module.c +++ b/src/pipewire/impl-module.c @@ -206,6 +206,7 @@ pw_context_load_module(struct pw_context *context, this = &impl->this; this->context = context; this->properties = properties; + this->info.props = &this->properties->dict; properties = NULL; spa_hook_list_init(&this->listener_list); @@ -234,7 +235,6 @@ pw_context_load_module(struct pw_context *context, pw_properties_setf(this->properties, PW_KEY_OBJECT_ID, "%d", this->info.id); pw_properties_setf(this->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64, pw_global_get_serial(this->global)); - this->info.props = &this->properties->dict; pw_global_update_keys(this->global, &this->properties->dict, keys); @@ -354,7 +354,6 @@ int pw_impl_module_update_properties(struct pw_impl_module *module, const struct int changed; changed = pw_properties_update(module->properties, dict); - module->info.props = &module->properties->dict; pw_log_debug("%p: updated %d properties", module, changed); diff --git a/src/pipewire/impl-module.h b/src/pipewire/impl-module.h index 6e0930333..37c6d51c9 100644 --- a/src/pipewire/impl-module.h +++ b/src/pipewire/impl-module.h @@ -7,14 +7,14 @@ #ifndef PIPEWIRE_IMPL_MODULE_H #define PIPEWIRE_IMPL_MODULE_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include +#ifdef __cplusplus +extern "C" { +#endif + #define PIPEWIRE_SYMBOL_MODULE_INIT "pipewire__module_init" #define PIPEWIRE_MODULE_PREFIX "libpipewire-" diff --git a/src/pipewire/impl-node.c b/src/pipewire/impl-node.c index 351f98b06..a8f65b06c 100644 --- a/src/pipewire/impl-node.c +++ b/src/pipewire/impl-node.c @@ -2,6 +2,8 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -11,8 +13,6 @@ #include #include -#include "config.h" - #include #include #include @@ -55,6 +55,27 @@ struct impl { char *sync_group; }; +static const char * const global_keys[] = { + PW_KEY_OBJECT_PATH, + PW_KEY_MODULE_ID, + PW_KEY_FACTORY_ID, + PW_KEY_CLIENT_ID, + PW_KEY_CLIENT_API, + PW_KEY_DEVICE_ID, + PW_KEY_PRIORITY_SESSION, + PW_KEY_PRIORITY_DRIVER, + PW_KEY_APP_NAME, + PW_KEY_NODE_DESCRIPTION, + PW_KEY_NODE_NAME, + PW_KEY_NODE_NICK, + PW_KEY_NODE_SESSION, + PW_KEY_MEDIA_CLASS, + PW_KEY_MEDIA_TYPE, + PW_KEY_MEDIA_CATEGORY, + PW_KEY_MEDIA_ROLE, + NULL +}; + #define pw_node_resource(r,m,v,...) pw_resource_call(r,struct pw_node_events,m,v,__VA_ARGS__) #define pw_node_resource_info(r,...) pw_node_resource(r,info,0,__VA_ARGS__) #define pw_node_resource_param(r,...) pw_node_resource(r,param,0,__VA_ARGS__) @@ -209,7 +230,7 @@ do_node_prepare(struct spa_loop *loop, bool async, uint32_t seq, static void add_node_to_graph(struct pw_impl_node *node) { - pw_loop_invoke(node->data_loop, do_node_prepare, 1, NULL, 0, true, node); + pw_loop_locked(node->data_loop, do_node_prepare, 1, NULL, 0, node); } /* called from the node data loop and undoes the changes done in do_node_prepare. */ @@ -225,17 +246,21 @@ do_node_unprepare(struct spa_loop *loop, bool async, uint32_t seq, pw_log_trace("%p: unprepare %d remote:%d exported:%d", this, this->rt.prepared, this->remote, this->exported); - /* We mark ourself as finished now, this will avoid going further into the process loop - * in case our fd was ready (removing ourselfs from the loop should avoid that as well). - * If we were supposed to be scheduled make sure we continue the graph for the peers we - * were supposed to trigger */ + if (!this->rt.prepared) + return 0; + + /* The remote client will INACTIVE itself and remove itself from the loop to avoid + * being scheduled. + * The server will mark remote nodes as FINISHED and trigger the peers. This will + * make sure the remote node will not trigger the peers anymore when it will + * stop (it only triggers peers when it has PENDING_TRIGGER (<= AWAKE)). We have + * to trigger the peers on the server because the client might simply be dead and + * not able to trigger anything. + */ old_state = SPA_ATOMIC_XCHG(this->rt.target.activation->status, PW_NODE_ACTIVATION_INACTIVE); if (PW_NODE_ACTIVATION_PENDING_TRIGGER(old_state)) trigger = get_time_ns(this->rt.target.system); - if (!this->rt.prepared) - return 0; - if (!this->remote) spa_loop_remove_source(loop, &this->source); @@ -248,7 +273,7 @@ do_node_unprepare(struct spa_loop *loop, bool async, uint32_t seq, static void remove_node_from_graph(struct pw_impl_node *node) { - pw_loop_invoke(node->data_loop, do_node_unprepare, 1, NULL, 0, true, node); + pw_loop_locked(node->data_loop, do_node_unprepare, 1, NULL, 0, node); } static void node_deactivate(struct pw_impl_node *this) @@ -357,6 +382,8 @@ static void emit_info_changed(struct pw_impl_node *node, bool flags_changed) if (node->global && node->info.change_mask != 0) { struct pw_resource *resource; + if (node->info.change_mask & PW_NODE_CHANGE_MASK_PROPS) + pw_global_update_keys(node->global, node->info.props, global_keys); spa_list_for_each(resource, &node->global->resource_list, link) pw_node_resource_info(resource, &node->info); } @@ -660,19 +687,20 @@ static int node_send_command(void *object, const struct spa_command *command) struct resource_data *data = object; struct pw_impl_node *node = data->node; uint32_t id = SPA_NODE_COMMAND_ID(command); + int res; pw_log_debug("%p: got command %d (%s)", node, id, spa_debug_type_find_name(spa_type_node_command_id, id)); switch (id) { case SPA_NODE_COMMAND_Suspend: - suspend_node(node); + res = suspend_node(node); break; default: - spa_node_send_command(node->node, command); + res = spa_node_send_command(node->node, command); break; } - return 0; + return res; } static const struct pw_node_methods node_methods = { @@ -809,8 +837,8 @@ int pw_impl_node_set_io(struct pw_impl_node *this, uint32_t id, void *data, size if (data != NULL && size < sizeof(struct spa_io_position)) return -EINVAL; pw_log_debug("%p: set position %p", this, data); - pw_loop_invoke(this->data_loop, - do_update_position, SPA_ID_INVALID, &data, sizeof(void*), true, this); + pw_loop_locked(this->data_loop, + do_update_position, SPA_ID_INVALID, &data, sizeof(void*), this); break; case SPA_IO_Clock: if (data != NULL && size < sizeof(struct spa_io_clock)) @@ -868,8 +896,8 @@ do_add_target(struct spa_loop *loop, SPA_EXPORT int pw_impl_node_add_target(struct pw_impl_node *node, struct pw_node_target *t) { - pw_loop_invoke(node->data_loop, - do_add_target, SPA_ID_INVALID, &node, sizeof(void *), true, t); + pw_loop_locked(node->data_loop, + do_add_target, SPA_ID_INVALID, &node, sizeof(void *), t); if (t->node) pw_impl_node_emit_peer_added(node, t->node); @@ -904,8 +932,8 @@ int pw_impl_node_remove_target(struct pw_impl_node *node, struct pw_node_target { /* we also update the target list for remote nodes so that the profiler * can inspect the nodes as well */ - pw_loop_invoke(node->data_loop, - do_remove_target, SPA_ID_INVALID, &node, sizeof(void *), true, t); + pw_loop_locked(node->data_loop, + do_remove_target, SPA_ID_INVALID, &node, sizeof(void *), t); if (t->node) pw_impl_node_emit_peer_removed(node, t->node); @@ -928,27 +956,6 @@ SPA_EXPORT int pw_impl_node_register(struct pw_impl_node *this, struct pw_properties *properties) { - static const char * const keys[] = { - PW_KEY_OBJECT_PATH, - PW_KEY_MODULE_ID, - PW_KEY_FACTORY_ID, - PW_KEY_CLIENT_ID, - PW_KEY_CLIENT_API, - PW_KEY_DEVICE_ID, - PW_KEY_PRIORITY_SESSION, - PW_KEY_PRIORITY_DRIVER, - PW_KEY_APP_NAME, - PW_KEY_NODE_DESCRIPTION, - PW_KEY_NODE_NAME, - PW_KEY_NODE_NICK, - PW_KEY_NODE_SESSION, - PW_KEY_MEDIA_CLASS, - PW_KEY_MEDIA_TYPE, - PW_KEY_MEDIA_CATEGORY, - PW_KEY_MEDIA_ROLE, - NULL - }; - struct pw_context *context = this->context; struct pw_impl_port *port; @@ -982,9 +989,8 @@ int pw_impl_node_register(struct pw_impl_node *this, pw_properties_setf(this->properties, PW_KEY_OBJECT_ID, "%d", this->global->id); pw_properties_setf(this->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64, pw_global_get_serial(this->global)); - this->info.props = &this->properties->dict; - pw_global_update_keys(this->global, &this->properties->dict, keys); + pw_global_update_keys(this->global, &this->properties->dict, global_keys); pw_impl_node_initialized(this); @@ -1107,7 +1113,6 @@ static int execute_match(void *data, const char *location, const char *action, struct pw_impl_node *this = match->node; if (spa_streq(action, "update-props")) { match->count += pw_properties_update_string(this->properties, val, len); - this->info.props = &this->properties->dict; } return 1; } @@ -1152,6 +1157,8 @@ static void check_properties(struct pw_impl_node *node) node->transport_sync = pw_properties_get_bool(node->properties, PW_KEY_NODE_TRANSPORT_SYNC, false); impl->cache_params = pw_properties_get_bool(node->properties, PW_KEY_NODE_CACHE_PARAMS, true); driver = pw_properties_get_bool(node->properties, PW_KEY_NODE_DRIVER, false); + node->exclusive = pw_properties_get_bool(node->properties, PW_KEY_NODE_EXCLUSIVE, false); + node->reliable = pw_properties_get_bool(node->properties, PW_KEY_NODE_RELIABLE, false); if (node->driver != driver) { pw_log_debug("%p: driver %d -> %d", node, node->driver, driver); @@ -1303,6 +1310,10 @@ static void check_properties(struct pw_impl_node *node) } } node->lock_rate = pw_properties_get_bool(node->properties, PW_KEY_NODE_LOCK_RATE, false); + /* the leaf node is one that only produces/consumes the data. We can deduce this from the + * absence of a link-group and the fact that it has no output/input ports. */ + node->leaf = node->link_groups == NULL && + (node->info.max_input_ports == 0 || node->info.max_output_ports == 0); value = pw_properties_get_uint32(node->properties, PW_KEY_NODE_FORCE_RATE, SPA_ID_INVALID); if (value == 0) @@ -1777,7 +1788,6 @@ static int update_properties(struct pw_impl_node *node, const struct spa_dict *d int changed; changed = pw_properties_update_ignore(node->properties, dict, filter ? ignored : NULL); - node->info.props = &node->properties->dict; pw_log_debug("%p: updated %d properties", node, changed); @@ -2358,8 +2368,8 @@ void pw_impl_node_add_rt_listener(struct pw_impl_node *node, void *data) { struct listener_data d = { .listener = listener, .events = events, .data = data }; - pw_loop_invoke(node->data_loop, - do_add_rt_listener, SPA_ID_INVALID, &d, sizeof(d), false, node); + pw_loop_locked(node->data_loop, + do_add_rt_listener, SPA_ID_INVALID, &d, sizeof(d), node); } static int do_remove_listener(struct spa_loop *loop, @@ -2374,8 +2384,8 @@ SPA_EXPORT void pw_impl_node_remove_rt_listener(struct pw_impl_node *node, struct spa_hook *listener) { - pw_loop_invoke(node->data_loop, - do_remove_listener, SPA_ID_INVALID, NULL, 0, true, listener); + pw_loop_locked(node->data_loop, + do_remove_listener, SPA_ID_INVALID, NULL, 0, listener); } /** Destroy a node @@ -2706,6 +2716,19 @@ error: return SPA_ID_INVALID; } +SPA_EXPORT +struct pw_impl_port *pw_impl_node_get_free_port(struct pw_impl_node *node, enum pw_direction direction) +{ + uint32_t port_id = pw_impl_node_get_free_port_id(node, direction); + if (port_id == SPA_ID_INVALID) + return NULL; + + spa_node_add_port(node->node, direction, port_id, NULL); + + return pw_impl_node_find_port(node, direction, port_id); +} + + static void on_state_complete(void *obj, void *data, int res, uint32_t seq) { struct pw_impl_node *node = obj; diff --git a/src/pipewire/impl-node.h b/src/pipewire/impl-node.h index 908b92f88..7f7edb37a 100644 --- a/src/pipewire/impl-node.h +++ b/src/pipewire/impl-node.h @@ -168,6 +168,8 @@ pw_impl_node_find_port(struct pw_impl_node *node, enum pw_direction direction, u /** Get a free unused port_id from the node */ uint32_t pw_impl_node_get_free_port_id(struct pw_impl_node *node, enum pw_direction direction); +/** Get a free unused port from the node */ +struct pw_impl_port *pw_impl_node_get_free_port(struct pw_impl_node *node, enum pw_direction direction); int pw_impl_node_initialized(struct pw_impl_node *node); diff --git a/src/pipewire/impl-port.c b/src/pipewire/impl-port.c index 4356604af..27c771446 100644 --- a/src/pipewire/impl-port.c +++ b/src/pipewire/impl-port.c @@ -39,9 +39,30 @@ struct impl { struct spa_list param_list; struct spa_list pending_list; + struct spa_hook node_listener; + unsigned int cache_params:1; }; +static const char * const global_keys[] = { + PW_KEY_OBJECT_PATH, + PW_KEY_FORMAT_DSP, + PW_KEY_NODE_ID, + PW_KEY_AUDIO_CHANNEL, + PW_KEY_PORT_ID, + PW_KEY_PORT_NAME, + PW_KEY_PORT_DIRECTION, + PW_KEY_PORT_MONITOR, + PW_KEY_PORT_PHYSICAL, + PW_KEY_PORT_TERMINAL, + PW_KEY_PORT_CONTROL, + PW_KEY_PORT_ALIAS, + PW_KEY_PORT_EXTRA, + PW_KEY_PORT_IGNORE_LATENCY, + PW_KEY_PORT_GROUP, + NULL +}; + #define pw_port_resource(r,m,v,...) pw_resource_call(r,struct pw_port_events,m,v,__VA_ARGS__) #define pw_port_resource_info(r,...) pw_port_resource(r,info,0,__VA_ARGS__) #define pw_port_resource_param(r,...) pw_port_resource(r,param,0,__VA_ARGS__) @@ -70,9 +91,12 @@ static void emit_info_changed(struct pw_impl_port *port) if (port->node) pw_impl_node_emit_port_info_changed(port->node, port, &port->info); - if (port->global) + if (port->global) { + if (port->info.change_mask & PW_PORT_CHANGE_MASK_PROPS) + pw_global_update_keys(port->global, port->info.props, global_keys); spa_list_for_each(resource, &port->global->resource_list, link) pw_port_resource_info(resource, &port->info); + } port->info.change_mask = 0; } @@ -241,8 +265,8 @@ static int port_set_io(void *object, case SPA_IO_Buffers: case SPA_IO_AsyncBuffers: if (data == NULL || size == 0) { - pw_loop_invoke(this->node->data_loop, - do_remove_mix, SPA_ID_INVALID, NULL, 0, true, mix); + pw_loop_locked(this->node->data_loop, + do_remove_mix, SPA_ID_INVALID, NULL, 0, mix); mix->io_data = mix->io[0] = mix->io[1] = NULL; } else if (data != NULL && size >= sizeof(struct spa_io_buffers)) { if (size >= sizeof(struct spa_io_async_buffers)) { @@ -253,8 +277,8 @@ static int port_set_io(void *object, } else { mix->io_data = mix->io[0] = mix->io[1] = data; } - pw_loop_invoke(this->node->data_loop, - do_add_mix, SPA_ID_INVALID, NULL, 0, false, mix); + pw_loop_locked(this->node->data_loop, + do_add_mix, SPA_ID_INVALID, NULL, 0, mix); } } return 0; @@ -279,6 +303,37 @@ static int tee_process(void *object) return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA; } +static int tee_process_reliable(void *object) +{ + struct impl *impl = object; + struct pw_impl_port *this = &impl->this; + struct pw_impl_port_mix *mix; + struct spa_io_buffers *io = &this->rt.io; + uint32_t cycle = this->node->rt.position->clock.cycle & 1; + + if (io->status == SPA_STATUS_HAVE_DATA) { + uint32_t buffer_id = io->buffer_id; + + pw_log_trace_fp("%p: tee input status:%d id:%d cycle:%d", this, io->status, buffer_id, cycle); + + spa_list_for_each(mix, &impl->rt.mix_list, rt.link) { + struct spa_io_buffers *mio = mix->io[cycle]; + + pw_log_trace_fp("%p: port %d %p->%p status:%d id:%d", this, + mix->port.port_id, io, mio, mio->status, mio->buffer_id); + + if (mio->status != SPA_STATUS_HAVE_DATA) { + io->buffer_id = mio->buffer_id; + io->status = SPA_STATUS_NEED_DATA; + mio->buffer_id = buffer_id; + mio->status = SPA_STATUS_HAVE_DATA; + break; + } + } + } + return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA; +} + static int tee_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) { struct impl *impl = object; @@ -298,6 +353,15 @@ static const struct spa_node_methods schedule_tee_node = { .process = tee_process, }; +static const struct spa_node_methods schedule_tee_node_reliable = { + SPA_VERSION_NODE_METHODS, + .add_listener = mix_add_listener, + .port_enum_params = mix_port_enum_params, + .port_set_io = port_set_io, + .port_reuse_buffer = tee_reuse_buffer, + .process = tee_process_reliable, +}; + static int schedule_mix_input(void *object) { struct impl *impl = object; @@ -348,6 +412,9 @@ int pw_impl_port_init_mix(struct pw_impl_port *port, struct pw_impl_port_mix *mi uint32_t port_id; int res = 0; + if (port->exclusive && port->n_mix != 0) + return -EBUSY; + port_id = pw_map_insert_new(&port->mix_port_map, mix); if (port_id == SPA_ID_INVALID) return -errno; @@ -427,6 +494,148 @@ int pw_impl_port_release_mix(struct pw_impl_port *port, struct pw_impl_port_mix return res; } +static int check_properties(struct pw_impl_port *port) +{ + struct impl *impl = SPA_CONTAINER_OF(port, struct impl, this); + struct pw_impl_node *node = port->node; + bool is_control, is_network, is_monitor, is_device, is_duplex, is_virtual, new_bool; + const char *media_class, *override_device_prefix, *channel_names; + const char *str, *prefix, *path, *desc, *nick, *name; + const struct pw_properties *nprops; + char position[256]; + int changed = 0; + + nprops = pw_impl_node_get_properties(node); + media_class = pw_properties_get(nprops, PW_KEY_MEDIA_CLASS); + is_network = pw_properties_get_bool(nprops, PW_KEY_NODE_NETWORK, false); + + is_control = PW_IMPL_PORT_IS_CONTROL(port); + is_monitor = pw_properties_get_bool(port->properties, PW_KEY_PORT_MONITOR, false); + + if (!is_monitor) { + if ((str = pw_properties_get(nprops, PW_KEY_NODE_TERMINAL)) != NULL) + changed += pw_properties_set(port->properties, PW_KEY_PORT_TERMINAL, str); + if ((str = pw_properties_get(nprops, PW_KEY_NODE_PHYSICAL)) != NULL) + changed += pw_properties_set(port->properties, PW_KEY_PORT_PHYSICAL, str); + } + + port->ignore_latency = pw_properties_get_bool(port->properties, PW_KEY_PORT_IGNORE_LATENCY, false); + new_bool = pw_properties_get_bool(port->properties, PW_KEY_PORT_EXCLUSIVE, node->exclusive); + if (new_bool != port->exclusive) { + pw_log_info("%p: exclusive %d->%d", port, port->exclusive, new_bool); + port->exclusive = new_bool; + } + + new_bool = pw_properties_get_bool(port->properties, PW_KEY_PORT_RELIABLE, node->reliable); + if (new_bool != port->reliable && port->direction == PW_DIRECTION_OUTPUT) { + pw_log_info("%p: reliable %d->%d", port, port->reliable, new_bool); + port->reliable = new_bool; + impl->mix_node.iface.cb.funcs = new_bool ? + &schedule_tee_node_reliable : + &schedule_tee_node; + } + + /* inherit passive state from parent node */ + port->passive = pw_properties_get_bool(port->properties, PW_KEY_PORT_PASSIVE, + port->direction == PW_DIRECTION_INPUT ? + node->in_passive : node->out_passive); + + if (media_class != NULL && + (strstr(media_class, "Sink") != NULL || + strstr(media_class, "Source") != NULL)) + is_device = true; + else + is_device = false; + + is_duplex = media_class != NULL && strstr(media_class, "Duplex") != NULL; + is_virtual = media_class != NULL && strstr(media_class, "Virtual") != NULL; + + override_device_prefix = pw_properties_get(nprops, PW_KEY_NODE_DEVICE_PORT_NAME_PREFIX); + + if (is_network) { + prefix = port->direction == PW_DIRECTION_INPUT ? + "send" : is_monitor ? "monitor" : "receive"; + } else if (is_duplex) { + prefix = port->direction == PW_DIRECTION_INPUT ? + "playback" : "capture"; + } else if (is_virtual) { + prefix = port->direction == PW_DIRECTION_INPUT ? + "input" : "capture"; + } else if (is_device) { + if (override_device_prefix != NULL) + prefix = is_monitor ? "monitor" : override_device_prefix; + else + prefix = port->direction == PW_DIRECTION_INPUT ? + "playback" : is_monitor ? "monitor" : "capture"; + } else { + prefix = port->direction == PW_DIRECTION_INPUT ? + "input" : is_monitor ? "monitor" : "output"; + } + + path = pw_properties_get(nprops, PW_KEY_OBJECT_PATH); + desc = pw_properties_get(nprops, PW_KEY_NODE_DESCRIPTION); + nick = pw_properties_get(nprops, PW_KEY_NODE_NICK); + name = pw_properties_get(nprops, PW_KEY_NODE_NAME); + + if (pw_properties_get(port->properties, PW_KEY_OBJECT_PATH) == NULL || + port->auto_path) { + if ((str = name) == NULL && (str = nick) == NULL && (str = desc) == NULL) + str = "node"; + + changed += pw_properties_setf(port->properties, PW_KEY_OBJECT_PATH, "%s:%s_%d", + path ? path : str, prefix, pw_impl_port_get_id(port)); + port->auto_path = true; + } + + str = pw_properties_get(port->properties, PW_KEY_AUDIO_CHANNEL); + if (str == NULL || spa_streq(str, "UNK")) + snprintf(position, sizeof(position), "%d", port->port_id + 1); + else if (str != NULL) + snprintf(position, sizeof(position), "%s", str); + + channel_names = pw_properties_get(nprops, PW_KEY_NODE_CHANNELNAMES); + if (channel_names != NULL) { + struct spa_json it[1]; + char v[256]; + uint32_t i; + + if (spa_json_begin_array_relax(&it[0], channel_names, strlen(channel_names)) > 0) { + for (i = 0; i < port->port_id + 1; i++) + if (spa_json_get_string(&it[0], v, sizeof(v)) <= 0) + break; + + if (i == port->port_id + 1 && strlen(v) > 0) + snprintf(position, sizeof(position), "%s", v); + } + } + + if (pw_properties_get(port->properties, PW_KEY_PORT_NAME) == NULL || + port->auto_name) { + if (is_control) + changed += pw_properties_setf(port->properties, PW_KEY_PORT_NAME, "%s", prefix); + else if (prefix == NULL || strlen(prefix) == 0) + changed += pw_properties_setf(port->properties, PW_KEY_PORT_NAME, "%s", position); + else + changed += pw_properties_setf(port->properties, PW_KEY_PORT_NAME, "%s_%s", prefix, position); + port->auto_name = true; + } + if (pw_properties_get(port->properties, PW_KEY_PORT_ALIAS) == NULL || + port->auto_alias) { + if ((str = nick) == NULL && (str = desc) == NULL && (str = name) == NULL) + str = "node"; + + if (is_control) + changed += pw_properties_setf(port->properties, PW_KEY_PORT_ALIAS, "%s:%s", + str, prefix); + else { + changed += pw_properties_setf(port->properties, PW_KEY_PORT_ALIAS, "%s:%s", + str, pw_properties_get(port->properties, PW_KEY_PORT_NAME)); + } + port->auto_alias = true; + } + return changed; +} + static int update_properties(struct pw_impl_port *port, const struct spa_dict *dict, bool filter) { static const char * const ignored[] = { @@ -437,14 +646,21 @@ static int update_properties(struct pw_impl_port *port, const struct spa_dict *d PW_KEY_PORT_ID, NULL }; - int changed; changed = pw_properties_update_ignore(port->properties, dict, filter ? ignored : NULL); - port->info.props = &port->properties->dict; + + pw_log_debug("%p: updated %d properties", port, changed); if (changed) { - pw_log_debug("%p: updated %d properties", port, changed); + /* check for explicit updates so we don't have to autogenerate */ + if (spa_dict_lookup(dict, PW_KEY_OBJECT_PATH) != NULL) + port->auto_path = false; + if (spa_dict_lookup(dict, PW_KEY_PORT_NAME) != NULL) + port->auto_name = false; + if (spa_dict_lookup(dict, PW_KEY_PORT_ALIAS) != NULL) + port->auto_alias = false; + check_properties(port); port->info.change_mask |= PW_PORT_CHANGE_MASK_PROPS; } return changed; @@ -752,6 +968,12 @@ struct pw_impl_port *pw_context_create_port( spa_list_init(&this->control_list[1]); spa_hook_list_init(&this->listener_list); + spa_hook_list_init(&impl->mix_hooks); + + pw_map_init(&this->mix_port_map, 64, 64); + + this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); + this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); if (this->direction == PW_DIRECTION_INPUT) mix_methods = &schedule_mix_node; @@ -762,15 +984,9 @@ struct pw_impl_port *pw_context_create_port( SPA_TYPE_INTERFACE_Node, SPA_VERSION_NODE, mix_methods, impl); - spa_hook_list_init(&impl->mix_hooks); pw_impl_port_set_mix(this, NULL, 0); - pw_map_init(&this->mix_port_map, 64, 64); - - this->latency[SPA_DIRECTION_INPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_INPUT); - this->latency[SPA_DIRECTION_OUTPUT] = SPA_LATENCY_INFO(SPA_DIRECTION_OUTPUT); - if (info) update_info(this, info); @@ -1115,25 +1331,6 @@ static const struct pw_global_events global_events = { int pw_impl_port_register(struct pw_impl_port *port, struct pw_properties *properties) { - static const char * const keys[] = { - PW_KEY_OBJECT_PATH, - PW_KEY_FORMAT_DSP, - PW_KEY_NODE_ID, - PW_KEY_AUDIO_CHANNEL, - PW_KEY_PORT_ID, - PW_KEY_PORT_NAME, - PW_KEY_PORT_DIRECTION, - PW_KEY_PORT_MONITOR, - PW_KEY_PORT_PHYSICAL, - PW_KEY_PORT_TERMINAL, - PW_KEY_PORT_CONTROL, - PW_KEY_PORT_ALIAS, - PW_KEY_PORT_EXTRA, - PW_KEY_PORT_IGNORE_LATENCY, - PW_KEY_PORT_GROUP, - NULL - }; - struct pw_impl_node *node = port->node; if (node == NULL || node->global == NULL) @@ -1156,27 +1353,40 @@ int pw_impl_port_register(struct pw_impl_port *port, pw_properties_setf(port->properties, PW_KEY_OBJECT_ID, "%d", port->info.id); pw_properties_setf(port->properties, PW_KEY_OBJECT_SERIAL, "%"PRIu64, pw_global_get_serial(port->global)); - port->info.props = &port->properties->dict; - pw_global_update_keys(port->global, &port->properties->dict, keys); + pw_global_update_keys(port->global, &port->properties->dict, global_keys); pw_impl_port_emit_initialized(port); return pw_global_register(port->global); } +static void node_info_changed(void *data, const struct pw_node_info *info) +{ + struct pw_impl_port *port = data; + if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) { + if (check_properties(port) > 0) { + port->info.change_mask |= PW_PORT_CHANGE_MASK_PROPS; + emit_info_changed(port); + } + } + +} +static const struct pw_impl_node_events node_events = { + PW_VERSION_IMPL_NODE_EVENTS, + .info_changed = node_info_changed, +}; + SPA_EXPORT int pw_impl_port_add(struct pw_impl_port *port, struct pw_impl_node *node) { + struct impl *impl = SPA_CONTAINER_OF(port, struct impl, this); uint32_t port_id = port->port_id; struct spa_list *ports; struct pw_map *portmap; struct pw_impl_port *find; - bool is_control, is_network, is_monitor, is_device, is_duplex, is_virtual; - const char *media_class, *override_device_prefix, *channel_names; - const char *str, *dir, *prefix, *path, *desc, *nick, *name; - const struct pw_properties *nprops; - char position[256]; + const char *dir; + bool is_control; int res; if (port->node != NULL) @@ -1198,136 +1408,28 @@ int pw_impl_port_add(struct pw_impl_port *port, struct pw_impl_node *node) return res; port->node = node; + pw_impl_node_add_listener(node, &impl->node_listener, &node_events, port); pw_impl_node_emit_port_init(node, port); check_params(port); - nprops = pw_impl_node_get_properties(node); - media_class = pw_properties_get(nprops, PW_KEY_MEDIA_CLASS); - is_network = pw_properties_get_bool(nprops, PW_KEY_NODE_NETWORK, false); - - is_monitor = pw_properties_get_bool(port->properties, PW_KEY_PORT_MONITOR, false); - - port->ignore_latency = pw_properties_get_bool(port->properties, PW_KEY_PORT_IGNORE_LATENCY, false); + check_properties(port); is_control = PW_IMPL_PORT_IS_CONTROL(port); if (is_control) { dir = port->direction == PW_DIRECTION_INPUT ? "control" : "notify"; - pw_properties_set(port->properties, PW_KEY_PORT_CONTROL, "true"); - } - else { - dir = port->direction == PW_DIRECTION_INPUT ? "in" : "out"; - } - pw_properties_set(port->properties, PW_KEY_PORT_DIRECTION, dir); - - /* inherit passive state from parent node */ - if (port->direction == PW_DIRECTION_INPUT) - port->passive = node->in_passive; - else - port->passive = node->out_passive; - /* override with specific port property if available */ - port->passive = pw_properties_get_bool(port->properties, PW_KEY_PORT_PASSIVE, - port->passive); - - if (media_class != NULL && - (strstr(media_class, "Sink") != NULL || - strstr(media_class, "Source") != NULL)) - is_device = true; - else - is_device = false; - - is_duplex = media_class != NULL && strstr(media_class, "Duplex") != NULL; - is_virtual = media_class != NULL && strstr(media_class, "Virtual") != NULL; - - override_device_prefix = pw_properties_get(nprops, PW_KEY_NODE_DEVICE_PORT_NAME_PREFIX); - - if (is_network) { - prefix = port->direction == PW_DIRECTION_INPUT ? - "send" : is_monitor ? "monitor" : "receive"; - } else if (is_duplex) { - prefix = port->direction == PW_DIRECTION_INPUT ? - "playback" : "capture"; - } else if (is_virtual) { - prefix = port->direction == PW_DIRECTION_INPUT ? - "input" : "capture"; - } else if (is_device) { - if (override_device_prefix != NULL) - prefix = is_monitor ? "monitor" : override_device_prefix; - else - prefix = port->direction == PW_DIRECTION_INPUT ? - "playback" : is_monitor ? "monitor" : "capture"; - } else { - prefix = port->direction == PW_DIRECTION_INPUT ? - "input" : is_monitor ? "monitor" : "output"; - } - - path = pw_properties_get(nprops, PW_KEY_OBJECT_PATH); - desc = pw_properties_get(nprops, PW_KEY_NODE_DESCRIPTION); - nick = pw_properties_get(nprops, PW_KEY_NODE_NICK); - name = pw_properties_get(nprops, PW_KEY_NODE_NAME); - - if (pw_properties_get(port->properties, PW_KEY_OBJECT_PATH) == NULL) { - if ((str = name) == NULL && (str = nick) == NULL && (str = desc) == NULL) - str = "node"; - - pw_properties_setf(port->properties, PW_KEY_OBJECT_PATH, "%s:%s_%d", - path ? path : str, prefix, pw_impl_port_get_id(port)); - } - - str = pw_properties_get(port->properties, PW_KEY_AUDIO_CHANNEL); - if (str == NULL || spa_streq(str, "UNK")) - snprintf(position, sizeof(position), "%d", port->port_id + 1); - else if (str != NULL) - snprintf(position, sizeof(position), "%s", str); - - channel_names = pw_properties_get(nprops, PW_KEY_NODE_CHANNELNAMES); - if (channel_names != NULL) { - struct spa_json it[1]; - char v[256]; - uint32_t i; - - if (spa_json_begin_array_relax(&it[0], channel_names, strlen(channel_names)) > 0) { - for (i = 0; i < port->port_id + 1; i++) - if (spa_json_get_string(&it[0], v, sizeof(v)) <= 0) - break; - - if (i == port->port_id + 1 && strlen(v) > 0) - snprintf(position, sizeof(position), "%s", v); - } - } - - if (pw_properties_get(port->properties, PW_KEY_PORT_NAME) == NULL) { - if (is_control) - pw_properties_setf(port->properties, PW_KEY_PORT_NAME, "%s", prefix); - else if (prefix == NULL || strlen(prefix) == 0) - pw_properties_setf(port->properties, PW_KEY_PORT_NAME, "%s", position); - else - pw_properties_setf(port->properties, PW_KEY_PORT_NAME, "%s_%s", prefix, position); - } - if (pw_properties_get(port->properties, PW_KEY_PORT_ALIAS) == NULL) { - if ((str = nick) == NULL && (str = desc) == NULL && (str = name) == NULL) - str = "node"; - - if (is_control) - pw_properties_setf(port->properties, PW_KEY_PORT_ALIAS, "%s:%s", - str, prefix); - else - pw_properties_setf(port->properties, PW_KEY_PORT_ALIAS, "%s:%s", - str, pw_properties_get(port->properties, PW_KEY_PORT_NAME)); - } - - port->info.props = &port->properties->dict; - - if (is_control) { pw_log_debug("%p: setting node control", port); + pw_properties_set(port->properties, PW_KEY_PORT_CONTROL, "true"); } else { + dir = port->direction == PW_DIRECTION_INPUT ? "in" : "out"; pw_log_debug("%p: setting mixer position io", port); spa_node_set_io(port->mix, SPA_IO_Position, node->rt.position, sizeof(struct spa_io_position)); } + pw_properties_set(port->properties, PW_KEY_PORT_DIRECTION, dir); pw_log_debug("%p: %d add to node %p", port, port_id, node); @@ -1380,6 +1482,7 @@ static int do_remove_port(struct spa_loop *loop, static void pw_impl_port_remove(struct pw_impl_port *port) { + struct impl *impl = SPA_CONTAINER_OF(port, struct impl, this); struct pw_impl_node *node = port->node; int res; @@ -1388,7 +1491,7 @@ static void pw_impl_port_remove(struct pw_impl_port *port) pw_log_debug("%p: remove", port); - pw_loop_invoke(node->data_loop, do_remove_port, SPA_ID_INVALID, NULL, 0, true, port); + pw_loop_locked(node->data_loop, do_remove_port, SPA_ID_INVALID, NULL, 0, port); if (SPA_FLAG_IS_SET(port->flags, PW_IMPL_PORT_FLAG_TO_REMOVE)) { if ((res = spa_node_remove_port(node->node, port->direction, port->port_id)) < 0) @@ -1409,6 +1512,7 @@ static void pw_impl_port_remove(struct pw_impl_port *port) spa_list_remove(&port->link); pw_impl_node_emit_port_removed(node, port); + spa_hook_remove(&impl->node_listener); port->node = NULL; } @@ -1651,7 +1755,7 @@ int pw_impl_port_for_each_link(struct pw_impl_port *port, int pw_impl_port_recalc_latency(struct pw_impl_port *port) { struct pw_impl_link *l; - struct spa_latency_info latency, *current; + struct spa_latency_info latency, *current, other_latency; struct pw_impl_port *other; struct spa_pod *param; struct spa_pod_builder b = { 0 }; @@ -1674,7 +1778,14 @@ int pw_impl_port_recalc_latency(struct pw_impl_port *port) port->info.id, other->info.id); continue; } - spa_latency_info_combine(&latency, &other->latency[other->direction]); + other_latency = other->latency[other->direction]; + if (l->async && other->node->driver_node != port->node) { + /* we add 1 cycle delay from async links */ + other_latency.min_quantum++; + other_latency.max_quantum++; + } + spa_latency_info_combine(&latency, &other_latency); + pw_log_debug("port %d: peer %d: latency %f-%f %d-%d %"PRIu64"-%"PRIu64, port->info.id, other->info.id, latency.min_quantum, latency.max_quantum, @@ -1690,7 +1801,15 @@ int pw_impl_port_recalc_latency(struct pw_impl_port *port) port->info.id, other->info.id); continue; } - spa_latency_info_combine(&latency, &other->latency[other->direction]); + other_latency = other->latency[other->direction]; + if (l->async && other->node != port->node->driver_node) { + /* we only add 1 cycle delay for async links that + * are not from our driver */ + other_latency.min_quantum++; + other_latency.max_quantum++; + } + spa_latency_info_combine(&latency, &other_latency); + pw_log_debug("port %d: peer %d: latency %f-%f %d-%d %"PRIu64"-%"PRIu64, port->info.id, other->info.id, latency.min_quantum, latency.max_quantum, @@ -1817,7 +1936,7 @@ int pw_impl_port_set_param(struct pw_impl_port *port, uint32_t id, uint32_t flag pw_log_debug("%p: %d set param %d %p", port, port->state, id, param); if (id == SPA_PARAM_Format) { - pw_loop_invoke(node->data_loop, do_remove_port, SPA_ID_INVALID, NULL, 0, true, port); + pw_loop_locked(node->data_loop, do_remove_port, SPA_ID_INVALID, NULL, 0, port); spa_node_port_set_io(node->node, port->direction, port->port_id, SPA_IO_Buffers, NULL, 0); @@ -1881,7 +2000,7 @@ int pw_impl_port_set_param(struct pw_impl_port *port, uint32_t id, uint32_t flag static int negotiate_mixer_buffers(struct pw_impl_port *port, uint32_t flags, struct spa_buffer **buffers, uint32_t n_buffers) { - int res; + int res, res2; struct pw_impl_node *node = port->node; if (SPA_FLAG_IS_SET(port->mix_flags, PW_IMPL_PORT_MIX_FLAG_MIX_ONLY)) @@ -1890,10 +2009,15 @@ static int negotiate_mixer_buffers(struct pw_impl_port *port, uint32_t flags, if (SPA_FLAG_IS_SET(port->mix_flags, PW_IMPL_PORT_MIX_FLAG_NEGOTIATE)) { int alloc_flags; - /* try dynamic data */ - alloc_flags = PW_BUFFERS_FLAG_DYNAMIC; + alloc_flags = 0; + if (SPA_FLAG_IS_SET(port->spa_flags, SPA_PORT_FLAG_DYNAMIC_DATA)) + alloc_flags |= PW_BUFFERS_FLAG_DYNAMIC; if (SPA_FLAG_IS_SET(node->spa_flags, SPA_NODE_FLAG_ASYNC)) alloc_flags |= PW_BUFFERS_FLAG_ASYNC; + if (SPA_FLAG_IS_SET(port->spa_flags, SPA_PORT_FLAG_CAN_ALLOC_BUFFERS)) { + alloc_flags |= PW_BUFFERS_FLAG_NO_MEM; + flags |= SPA_NODE_BUFFERS_FLAG_ALLOC; + } pw_log_debug("%p: %d.%d negotiate %d buffers on node: %p flags:%08x", port, port->direction, port->port_id, n_buffers, node->node, @@ -1903,7 +2027,7 @@ static int negotiate_mixer_buffers(struct pw_impl_port *port, uint32_t flags, port->direction, port->port_id, SPA_IO_Buffers, NULL, 0); - pw_loop_invoke(node->data_loop, do_remove_port, SPA_ID_INVALID, NULL, 0, true, port); + pw_loop_locked(node->data_loop, do_remove_port, SPA_ID_INVALID, NULL, 0, port); pw_buffers_clear(&port->mix_buffers); @@ -1930,9 +2054,16 @@ static int negotiate_mixer_buffers(struct pw_impl_port *port, uint32_t flags, flags, buffers, n_buffers); if (SPA_RESULT_IS_OK(res)) { - spa_node_port_use_buffers(port->mix, + res2 = spa_node_port_use_buffers(port->mix, pw_direction_reverse(port->direction), 0, 0, buffers, n_buffers); + if (res2 < 0) { + if (res2 != -ENOTSUP && n_buffers > 0) { + pw_log_warn("%p: mix use buffers failed: %d (%s)", + port, res2, spa_strerror(res2)); + return res2; + } + } } if (n_buffers > 0) { spa_node_port_set_io(node->node, @@ -1943,7 +2074,7 @@ static int negotiate_mixer_buffers(struct pw_impl_port *port, uint32_t flags, pw_direction_reverse(port->direction), 0, SPA_IO_Buffers, &port->rt.io, sizeof(port->rt.io)); - pw_loop_invoke(node->data_loop, do_add_port, SPA_ID_INVALID, NULL, 0, false, port); + pw_loop_locked(node->data_loop, do_add_port, SPA_ID_INVALID, NULL, 0, port); } return res; } diff --git a/src/pipewire/impl-port.h b/src/pipewire/impl-port.h index 34b8b7b9c..f68e850b8 100644 --- a/src/pipewire/impl-port.h +++ b/src/pipewire/impl-port.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_IMPL_PORT_H #define PIPEWIRE_IMPL_PORT_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \defgroup pw_impl_port Port Impl * * \brief A port can be used to link two nodes. diff --git a/src/pipewire/keys.h b/src/pipewire/keys.h index de4a9d408..13694bc29 100644 --- a/src/pipewire/keys.h +++ b/src/pipewire/keys.h @@ -5,11 +5,12 @@ #ifndef PIPEWIRE_KEYS_H #define PIPEWIRE_KEYS_H +#include + #ifdef __cplusplus extern "C" { #endif -#include /** * \defgroup pw_keys Key Names * @@ -228,6 +229,11 @@ extern "C" { * playback or disable the prefix * completely if an empty string * is provided */ +#define PW_KEY_NODE_PHYSICAL "node.physical" /**< ports from the node are physical */ +#define PW_KEY_NODE_TERMINAL "node.terminal" /**< ports from the node are terminal */ + +#define PW_KEY_NODE_RELIABLE "node.reliable" /**< node uses reliable transport 1.6.0 */ + /** Port keys */ #define PW_KEY_PORT_ID "port.id" /**< port id */ #define PW_KEY_PORT_NAME "port.name" /**< port name */ @@ -244,6 +250,8 @@ extern "C" { #define PW_KEY_PORT_PASSIVE "port.passive" /**< the ports wants passive links, since 0.3.67 */ #define PW_KEY_PORT_IGNORE_LATENCY "port.ignore-latency" /**< latency ignored by peers, since 0.3.71 */ #define PW_KEY_PORT_GROUP "port.group" /**< the port group of the port 1.2.0 */ +#define PW_KEY_PORT_EXCLUSIVE "port.exclusive" /**< link port only once 1.6.0 */ +#define PW_KEY_PORT_RELIABLE "port.reliable" /**< port uses reliable transport 1.6.0 */ /** link properties */ #define PW_KEY_LINK_ID "link.id" /**< a link id */ diff --git a/src/pipewire/link.h b/src/pipewire/link.h index 316fb0442..ac3133bc9 100644 --- a/src/pipewire/link.h +++ b/src/pipewire/link.h @@ -5,15 +5,15 @@ #ifndef PIPEWIRE_LINK_H #define PIPEWIRE_LINK_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_link Link * * A link is the connection between 2 nodes (\ref pw_node). Nodes are diff --git a/src/pipewire/log.c b/src/pipewire/log.c index c8410a6f5..7e7d6a66d 100644 --- a/src/pipewire/log.c +++ b/src/pipewire/log.c @@ -69,6 +69,7 @@ PW_LOG_TOPIC(log_proxy, "pw.proxy"); PW_LOG_TOPIC(log_resource, "pw.resource"); PW_LOG_TOPIC(log_stream, "pw.stream"); PW_LOG_TOPIC(log_thread_loop, "pw.thread-loop"); +PW_LOG_TOPIC(log_timer_queue, "pw.timer-queue"); PW_LOG_TOPIC(log_work_queue, "pw.work-queue"); PW_LOG_TOPIC(PW_LOG_TOPIC_DEFAULT, "default"); diff --git a/src/pipewire/loop.c b/src/pipewire/loop.c index 2e7fb47d5..2d38d4bac 100644 --- a/src/pipewire/loop.c +++ b/src/pipewire/loop.c @@ -178,15 +178,3 @@ int pw_loop_set_name(struct pw_loop *loop, const char *name) snprintf(impl->name, sizeof(impl->name), "%s", name); return 0; } - -SPA_EXPORT -int pw_loop_check(struct pw_loop *loop) -{ - struct impl *impl = SPA_CONTAINER_OF(loop, struct impl, this); - int res; - if (impl->cb && impl->cb->check) - res = impl->cb->check(impl->user_data, loop); - else - res = spa_loop_control_check(loop->control); - return res; -} diff --git a/src/pipewire/loop.h b/src/pipewire/loop.h index 9de1cbf3f..3fdf2818e 100644 --- a/src/pipewire/loop.h +++ b/src/pipewire/loop.h @@ -5,13 +5,13 @@ #ifndef PIPEWIRE_LOOP_H #define PIPEWIRE_LOOP_H +#include +#include + #ifdef __cplusplus extern "C" { #endif -#include -#include - /** \defgroup pw_loop Loop * * PipeWire loop object provides an implementation of @@ -64,6 +64,12 @@ PW_API_LOOP_IMPL int pw_loop_invoke(struct pw_loop *object, { return spa_loop_invoke(object->loop, func, seq, data, size, block, user_data); } +PW_API_LOOP_IMPL int pw_loop_locked(struct pw_loop *object, + spa_invoke_func_t func, uint32_t seq, const void *data, + size_t size, void *user_data) +{ + return spa_loop_locked(object->loop, func, seq, data, size, user_data); +} PW_API_LOOP_IMPL int pw_loop_get_fd(struct pw_loop *object) { @@ -88,6 +94,36 @@ PW_API_LOOP_IMPL int pw_loop_iterate(struct pw_loop *object, { return spa_loop_control_iterate_fast(object->control, timeout); } +PW_API_LOOP_IMPL int pw_loop_check(struct pw_loop *object) +{ + return spa_loop_control_check(object->control); +} +PW_API_LOOP_IMPL int pw_loop_lock(struct pw_loop *object) +{ + return spa_loop_control_lock(object->control); +} +PW_API_LOOP_IMPL int pw_loop_unlock(struct pw_loop *object) +{ + return spa_loop_control_unlock(object->control); +} +PW_API_LOOP_IMPL int pw_loop_get_time(struct pw_loop *object, struct timespec *abstime, int64_t timeout) +{ + return spa_loop_control_get_time(object->control, abstime, timeout); +} +PW_API_LOOP_IMPL int pw_loop_wait(struct pw_loop *object, const struct timespec *abstime) +{ + return spa_loop_control_wait(object->control, abstime); +} +PW_API_LOOP_IMPL int pw_loop_signal(struct pw_loop *object, bool wait_for_accept) +{ + return spa_loop_control_signal(object->control, wait_for_accept); +} +PW_API_LOOP_IMPL int pw_loop_accept(struct pw_loop *object) +{ + return spa_loop_control_accept(object->control); +} + + PW_API_LOOP_IMPL struct spa_source * pw_loop_add_io(struct pw_loop *object, int fd, uint32_t mask, @@ -141,7 +177,7 @@ pw_loop_add_signal(struct pw_loop *object, int signal_number, PW_API_LOOP_IMPL void pw_loop_destroy_source(struct pw_loop *object, struct spa_source *source) { - return spa_loop_utils_destroy_source(object->utils, source); + spa_loop_utils_destroy_source(object->utils, source); } /** diff --git a/src/pipewire/map.h b/src/pipewire/map.h index fb00ddf6c..3600fd0d7 100644 --- a/src/pipewire/map.h +++ b/src/pipewire/map.h @@ -5,16 +5,16 @@ #ifndef PIPEWIRE_MAP_H #define PIPEWIRE_MAP_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef PW_API_MAP #define PW_API_MAP static inline #endif diff --git a/src/pipewire/mem.c b/src/pipewire/mem.c index f5cc198f0..642a6b78e 100644 --- a/src/pipewire/mem.c +++ b/src/pipewire/mem.c @@ -167,7 +167,7 @@ struct pw_mempool *pw_mempool_new(struct pw_properties *props) impl->pagesize = sysconf(_SC_PAGESIZE); - pw_log_debug("%p: new", this); + pw_log_debug("%p: new pagesize:%" PRIu32 "", this, impl->pagesize); spa_hook_list_init(&impl->listener_list); pw_map_init(&impl->map, 64, 64); @@ -393,7 +393,6 @@ struct pw_memmap * pw_memblock_map(struct pw_memblock *block, struct mempool *p = SPA_CONTAINER_OF(block->pool, struct mempool, this); struct mapping *m; struct memmap *mm; - struct pw_map_range range; struct stat sb; if (b->this.fd == -1) { @@ -418,13 +417,15 @@ struct pw_memmap * pw_memblock_map(struct pw_memblock *block, return NULL; } - pw_map_range_init(&range, offset, size, p->pagesize); - m = memblock_find_mapping(b, flags, offset, size); - if (m == NULL) + if (m == NULL) { + struct pw_map_range range; + pw_map_range_init(&range, offset, size, p->pagesize); + m = memblock_map(b, flags, range.offset, range.size); - if (m == NULL) - return NULL; + if (m == NULL) + return NULL; + } mm = calloc(1, sizeof(struct memmap)); if (mm == NULL) { @@ -439,7 +440,7 @@ struct pw_memmap * pw_memblock_map(struct pw_memblock *block, mm->this.flags = flags; mm->this.offset = offset; mm->this.size = size; - mm->this.ptr = SPA_PTROFF(m->ptr, range.start, void); + mm->this.ptr = SPA_PTROFF(m->ptr, offset - m->offset, void); pw_log_debug("%p: map:%p block:%p fd:%d flags:%08x ptr:%p (%u %u) mapping:%p ref:%d", p, &mm->this, b, b->this.fd, b->this.flags, mm->this.ptr, offset, size, m, m->ref); diff --git a/src/pipewire/mem.h b/src/pipewire/mem.h index 525e58525..52abbd54d 100644 --- a/src/pipewire/mem.h +++ b/src/pipewire/mem.h @@ -7,6 +7,8 @@ #include +struct spa_hook; + #ifdef __cplusplus extern "C" { #endif diff --git a/src/pipewire/meson.build b/src/pipewire/meson.build index b19631a92..9769369a1 100644 --- a/src/pipewire/meson.build +++ b/src/pipewire/meson.build @@ -41,6 +41,7 @@ pipewire_headers = [ 'stream.h', 'thread.h', 'thread-loop.h', + 'timer-queue.h', 'type.h', 'utils.h', 'work-queue.h', @@ -78,6 +79,7 @@ pipewire_sources = [ 'stream.c', 'thread.c', 'thread-loop.c', + 'timer-queue.c', 'utils.c', 'work-queue.c', ] diff --git a/src/pipewire/module.h b/src/pipewire/module.h index f5531af89..cd5fa2b7b 100644 --- a/src/pipewire/module.h +++ b/src/pipewire/module.h @@ -5,15 +5,15 @@ #ifndef PIPEWIRE_MODULE_H #define PIPEWIRE_MODULE_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_module Module * Module interface */ diff --git a/src/pipewire/node.h b/src/pipewire/node.h index 1ee9d2123..6fcbeb312 100644 --- a/src/pipewire/node.h +++ b/src/pipewire/node.h @@ -5,10 +5,6 @@ #ifndef PIPEWIRE_NODE_H #define PIPEWIRE_NODE_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -19,6 +15,10 @@ extern "C" { #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_node Node * Node interface */ diff --git a/src/pipewire/permission.h b/src/pipewire/permission.h index 22eebdb26..55eeff2a3 100644 --- a/src/pipewire/permission.h +++ b/src/pipewire/permission.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_PERMISSION_H #define PIPEWIRE_PERMISSION_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \defgroup pw_permission Permission * * Permissions are kept for a client and describe what the client is diff --git a/src/pipewire/pipewire.h b/src/pipewire/pipewire.h index 0c495ed39..6f26a38b9 100644 --- a/src/pipewire/pipewire.h +++ b/src/pipewire/pipewire.h @@ -5,12 +5,9 @@ #ifndef PIPEWIRE_H #define PIPEWIRE_H -#ifdef __cplusplus -extern "C" { -#endif - #include +// IWYU pragma: begin_exports #include #include #include @@ -37,9 +34,15 @@ extern "C" { #include #include #include +#include #include #include #include +// IWYU pragma: end_exports + +#ifdef __cplusplus +extern "C" { +#endif /** \defgroup pw_pipewire Initialization * Initializing PipeWire and loading SPA modules. diff --git a/src/pipewire/port.h b/src/pipewire/port.h index ea4cb33b1..b711cda72 100644 --- a/src/pipewire/port.h +++ b/src/pipewire/port.h @@ -5,10 +5,6 @@ #ifndef PIPEWIRE_PORT_H #define PIPEWIRE_PORT_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include @@ -18,6 +14,10 @@ extern "C" { #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_port Port * Port interface */ diff --git a/src/pipewire/private.h b/src/pipewire/private.h index cddd3b820..36f85bd9d 100644 --- a/src/pipewire/private.h +++ b/src/pipewire/private.h @@ -7,10 +7,6 @@ /** \privatesection */ -#ifdef __cplusplus -extern "C" { -#endif - #include #include /* for pthread_t */ @@ -24,6 +20,10 @@ extern "C" { #include #include +#ifdef __cplusplus +extern "C" { +#endif + #if defined(__FreeBSD__) || defined(__MidnightBSD__) || defined(__GNU__) struct ucred { }; @@ -269,9 +269,6 @@ struct pw_impl_client { unsigned int destroyed:1; int refcount; - - /* v2 compatibility data */ - void *compat_v2; }; #define pw_global_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_global_events, m, v, ##__VA_ARGS__) @@ -343,18 +340,6 @@ pw_core_resource_errorf(struct pw_resource *resource, uint32_t id, int seq, va_end(args); } -struct pw_loop_callbacks { -#define PW_VERSION_LOOP_CALLBACKS 0 - uint32_t version; - - int (*check) (void *data, struct pw_loop *loop); -}; - -void -pw_loop_set_callbacks(struct pw_loop *loop, const struct pw_loop_callbacks *cb, void *data); - -int pw_loop_check(struct pw_loop *loop); - #define ensure_loop(loop,...) ({ \ int res = pw_loop_check(loop); \ if (res != 1) { \ @@ -420,6 +405,7 @@ struct pw_context { struct spa_thread_utils *thread_utils; struct pw_loop *main_loop; /**< main loop for control */ struct pw_work_queue *work_queue; /**< work queue */ + struct pw_timer_queue *timer_queue; /**< timer queue */ struct spa_support support[16]; /**< support for spa plugins */ uint32_t n_support; /**< number of support items */ @@ -797,6 +783,9 @@ struct pw_impl_node { unsigned int sync:1; /**< the sync-groups are active */ unsigned int async:1; /**< async processing, one cycle latency */ unsigned int lazy:1; /**< the graph is lazy scheduling */ + unsigned int exclusive:1; /**< ports can only be linked once */ + unsigned int leaf:1; /**< node only produces/consumes data */ + unsigned int reliable:1; /**< ports need reliable tee */ uint32_t transport; /**< latest transport request */ @@ -972,12 +961,17 @@ struct pw_impl_port { } rt; /**< data only accessed from the data thread */ unsigned int destroying:1; unsigned int passive:1; + unsigned int auto_path:1; /* path was automatically generated */ + unsigned int auto_name:1; /* name was automatically generated */ + unsigned int auto_alias:1; /* alias was automatically generated */ int busy_count; struct spa_latency_info latency[2]; /**< latencies */ unsigned int have_latency_param:1; unsigned int ignore_latency:1; unsigned int have_latency:1; + unsigned int exclusive:1; /**< port can only be linked once */ + unsigned int reliable:1; /**< port needs reliable tee */ unsigned int have_tag_param:1; struct spa_pod *tag[2]; /**< tags */ @@ -1036,6 +1030,7 @@ struct pw_impl_link { void *user_data; + unsigned int async:1; unsigned int registered:1; unsigned int feedback:1; unsigned int preparing:1; @@ -1265,19 +1260,6 @@ struct pw_control { void *user_data; }; -/** Find a good format between 2 ports */ -int pw_context_find_format(struct pw_context *context, - struct pw_impl_port *output, - uint32_t output_mix, - struct pw_impl_port *input, - uint32_t input_mix, - struct pw_properties *props, - uint32_t n_format_filters, - struct spa_pod **format_filters, - struct spa_pod **format, - struct spa_pod_builder *builder, - char **error); - int pw_context_debug_port_params(struct pw_context *context, struct spa_node *node, enum spa_direction direction, uint32_t port_id, uint32_t id, int err, const char *debug, ...); diff --git a/src/pipewire/properties.c b/src/pipewire/properties.c index de81088b2..ac0aac0d0 100644 --- a/src/pipewire/properties.c +++ b/src/pipewire/properties.c @@ -252,7 +252,8 @@ static int update_string(struct pw_properties *props, const char *str, size_t si continue; } /* item changed or added, apply changes later */ - if ((errno = -add_item(&changes, key, false, val, true) < 0)) { + if ((res = add_item(&changes, key, false, val, true)) < 0) { + errno = -res; it[0].state = SPA_JSON_ERROR_FLAG; break; } diff --git a/src/pipewire/properties.h b/src/pipewire/properties.h index 769198dc5..5dbcc283f 100644 --- a/src/pipewire/properties.h +++ b/src/pipewire/properties.h @@ -5,16 +5,16 @@ #ifndef PIPEWIRE_PROPERTIES_H #define PIPEWIRE_PROPERTIES_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef PW_API_PROPERTIES #define PW_API_PROPERTIES static inline #endif diff --git a/src/pipewire/protocol.h b/src/pipewire/protocol.h index 60a029033..bec5c4eb2 100644 --- a/src/pipewire/protocol.h +++ b/src/pipewire/protocol.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_PROTOCOL_H #define PIPEWIRE_PROTOCOL_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \defgroup pw_protocol Protocol * * \brief Manages protocols and their implementation diff --git a/src/pipewire/proxy.h b/src/pipewire/proxy.h index b46c41725..34bdfee06 100644 --- a/src/pipewire/proxy.h +++ b/src/pipewire/proxy.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_PROXY_H #define PIPEWIRE_PROXY_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \page page_proxy Proxy * * \see \ref pw_proxy diff --git a/src/pipewire/resource.h b/src/pipewire/resource.h index 86be9ee3d..41adab468 100644 --- a/src/pipewire/resource.h +++ b/src/pipewire/resource.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_RESOURCE_H #define PIPEWIRE_RESOURCE_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \defgroup pw_resource Resource * * \brief Client owned objects diff --git a/src/pipewire/settings.c b/src/pipewire/settings.c index 597e686a9..74d4cfcb2 100644 --- a/src/pipewire/settings.c +++ b/src/pipewire/settings.c @@ -245,24 +245,21 @@ void pw_settings_init(struct pw_context *this) static void expose_settings(struct pw_context *context, struct pw_impl_metadata *metadata) { struct settings *s = &context->settings; - uint32_t i, o; - char rates[MAX_RATES*16] = ""; + uint32_t i; + char rates[MAX_RATES*16]; + struct spa_strbuf b; pw_impl_metadata_set_propertyf(metadata, PW_ID_CORE, "log.level", "", "%d", s->log_level); pw_impl_metadata_set_propertyf(metadata, PW_ID_CORE, "clock.rate", "", "%d", s->clock_rate); - for (i = 0, o = 0; i < s->n_clock_rates; i++) { - int r = snprintf(rates+o, sizeof(rates)-o, "%s%d", i == 0 ? "" : ", ", + + spa_strbuf_init(&b, rates, sizeof(rates)); + for (i = 0; i < s->n_clock_rates; i++) + spa_strbuf_append(&b, "%s%d", i == 0 ? "" : ", ", s->clock_rates[i]); - if (r < 0 || o + r >= (int)sizeof(rates)) { - snprintf(rates, sizeof(rates), "%d", s->clock_rate); - break; - } - o += r; - } if (s->n_clock_rates == 0) - snprintf(rates, sizeof(rates), "%d", s->clock_rate); + spa_strbuf_append(&b, "%d", s->clock_rate); pw_impl_metadata_set_propertyf(metadata, PW_ID_CORE, "clock.allowed-rates", "", "[ %s ]", rates); diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c index 5e5aed3b7..9387ee58a 100644 --- a/src/pipewire/stream.c +++ b/src/pipewire/stream.c @@ -28,7 +28,8 @@ PW_LOG_TOPIC_EXTERN(log_stream); #define PW_LOG_TOPIC_DEFAULT log_stream -#define MAX_BUFFERS 64 +#define MAX_BUFFERS 64u +#define MAX_VALUES 256u #define MASK_BUFFERS (MAX_BUFFERS-1) @@ -38,7 +39,7 @@ struct buffer { struct pw_buffer this; uint32_t id; #define BUFFER_FLAG_MAPPED (1 << 0) -#define BUFFER_FLAG_QUEUED (1 << 1) +#define BUFFER_FLAG_DEQUEUED (1 << 1) #define BUFFER_FLAG_ADDED (1 << 2) uint32_t flags; struct spa_meta_busy *busy; @@ -72,7 +73,7 @@ struct control { struct pw_stream_control control; struct spa_pod *info; unsigned int emitted:1; - float values[64]; + float values[MAX_VALUES]; }; struct stream { @@ -96,6 +97,7 @@ struct stream { struct spa_io_buffers *io; struct spa_io_rate_match *rate_match; uint32_t rate_queued; + uint32_t have_requested; uint64_t rate_size; uint64_t port_change_mask_all; @@ -155,6 +157,8 @@ struct stream { int in_set_param; int in_emit_param_changed; int pending_drain; + + int in_trigger; }; static int get_param_index(uint32_t id) @@ -214,11 +218,11 @@ static void fix_datatype(struct spa_pod *param) if (spa_pod_get_int(&vals[0], (int32_t*)&dataType) < 0) return; - pw_log_debug("dataType: %u", dataType); + pw_log_debug("dataType: %" PRIu32, dataType); if (dataType & (1u << SPA_DATA_MemPtr)) { SPA_POD_VALUE(struct spa_pod_int, &vals[0]) = dataType | (1< %u", dataType, + pw_log_debug("Change dataType: %" PRIu32 " -> %" PRIu32, dataType, SPA_POD_VALUE(struct spa_pod_int, &vals[0])); } } @@ -227,7 +231,7 @@ static int add_param(struct stream *impl, uint32_t id, uint32_t flags, const struct spa_pod *param) { struct param *p; - int idx; + int idx, res; if (param != NULL && !spa_pod_is_object(param)) return -EINVAL; @@ -239,6 +243,23 @@ static int add_param(struct stream *impl, if (id == SPA_ID_INVALID) id = SPA_POD_OBJECT_ID(param); + switch (id) { + case SPA_PARAM_Latency: + { + struct spa_latency_info info; + if ((res = spa_latency_parse(param, &info)) < 0) + return res; + if (impl->this.node_id != SPA_ID_INVALID && + info.direction != impl->direction && + !SPA_FLAG_IS_SET(flags, PARAM_FLAG_LOCKED)) { + pw_log_warn("not adding locked Latency param %s %s", + pw_direction_as_string(info.direction), + pw_direction_as_string(impl->direction)); + return 0; + } + } + } + p = malloc(sizeof(struct param) + SPA_POD_SIZE(param)); if (p == NULL) return -errno; @@ -272,7 +293,7 @@ static int add_param(struct stream *impl, return 0; } -static void clear_params(struct stream *impl, uint32_t id) +static void clear_params(struct stream *impl, uint32_t id, uint32_t flags) { struct param *p, *t; bool found = false; @@ -280,7 +301,7 @@ static void clear_params(struct stream *impl, uint32_t id) spa_list_for_each_safe(p, t, &impl->param_list, link) { if (id == SPA_ID_INVALID || - (p->id == id && !(p->flags & PARAM_FLAG_LOCKED))) { + (p->id == id && (p->flags & PARAM_FLAG_LOCKED) == flags)) { found = true; spa_list_remove(&p->link); free(p); @@ -320,12 +341,12 @@ static int update_params(struct stream *impl, uint32_t id, uint32_t flags, int res = 0; if (id != SPA_ID_INVALID) { - clear_params(impl, id); + clear_params(impl, id, flags); } else { for (i = 0; i < n_params; i++) { if (params[i] == NULL || !spa_pod_is_object(params[i])) continue; - clear_params(impl, SPA_POD_OBJECT_ID(params[i])); + clear_params(impl, SPA_POD_OBJECT_ID(params[i]), flags); } } for (i = 0; i < n_params; i++) { @@ -335,16 +356,13 @@ static int update_params(struct stream *impl, uint32_t id, uint32_t flags, return res; } - static inline int queue_push(struct stream *stream, struct queue *queue, struct buffer *buffer) { uint32_t index; - if (SPA_FLAG_IS_SET(buffer->flags, BUFFER_FLAG_QUEUED) || - buffer->id >= stream->n_buffers) + if (buffer->id >= stream->n_buffers) return -EINVAL; - SPA_FLAG_SET(buffer->flags, BUFFER_FLAG_QUEUED); queue->incount += buffer->this.size; spa_ringbuffer_get_write_index(&queue->ring, &index); @@ -375,7 +393,6 @@ static inline struct buffer *queue_pop(struct stream *stream, struct queue *queu buffer = &stream->buffers[id]; queue->outcount += buffer->this.size; - SPA_FLAG_CLEAR(buffer->flags, BUFFER_FLAG_QUEUED); return buffer; } @@ -435,9 +452,10 @@ static inline uint32_t update_requested(struct stream *impl) buffer = &impl->buffers[id]; buffer->this.requested = impl->rate_size; - pw_log_trace_fp("%p: update buffer:%u req:%"PRIu64, impl, id, buffer->this.requested); + pw_log_trace_fp("%p: update buffer:%u req:%"PRIu64" %p", impl, id, buffer->this.requested, + impl->rate_match); - return buffer->this.requested > 0 ? 1 : 0; + return impl->have_requested; } static inline void call_process(struct stream *impl) @@ -446,7 +464,7 @@ static inline void call_process(struct stream *impl) if (impl->n_buffers == 0 || (impl->direction == SPA_DIRECTION_OUTPUT && update_requested(impl) <= 0)) return; - if (impl->rt_callbacks.funcs) + if (impl->rt_callbacks.funcs && !impl->disconnecting) spa_callbacks_call_fast(&impl->rt_callbacks, struct pw_stream_events, process, 0); } @@ -642,7 +660,8 @@ static inline void copy_position(struct stream *impl, int64_t queued) if (SPA_LIKELY(p != NULL)) { impl->time.now = p->clock.nsec; impl->time.rate = p->clock.rate; - if (SPA_UNLIKELY(impl->clock_id != p->clock.id)) { + if (SPA_UNLIKELY(impl->clock_id != p->clock.id || + SPA_FLAG_IS_SET(p->clock.flags, SPA_IO_CLOCK_FLAG_DISCONT))) { impl->base_pos = p->clock.position - impl->time.ticks; impl->clock_id = p->clock.id; } @@ -666,9 +685,11 @@ static inline void copy_position(struct stream *impl, int64_t queued) if (SPA_LIKELY(impl->rate_match != NULL)) { impl->rate_queued = impl->rate_match->delay; impl->rate_size = impl->rate_match->size; + impl->have_requested = impl->rate_size != 0; } else { impl->rate_queued = 0; impl->rate_size = impl->quantum; + impl->have_requested = 1; } SPA_SEQ_WRITE(impl->seq); } @@ -701,6 +722,7 @@ static int impl_send_command(void *object, const struct spa_command *command) impl->io->status = SPA_STATUS_NEED_DATA; } copy_position(impl, impl->queued.incount); + stream_set_state(stream, PW_STREAM_STATE_STREAMING, 0, NULL); } break; default: @@ -861,20 +883,20 @@ static void clear_buffers(struct pw_stream *stream) clear_queue(impl, &impl->queued); } -static int parse_latency(struct pw_stream *stream, const struct spa_pod *param) +static int parse_latency(struct pw_stream *stream, const struct spa_pod *param, uint32_t *flags) { - struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this); - struct spa_latency_info info; - int res; + struct stream *impl = SPA_CONTAINER_OF(stream, struct stream, this); + struct spa_latency_info info; + int res; - if (param == NULL) - return 0; - - if ((res = spa_latency_parse(param, &info)) < 0) + if (param == NULL) + info = SPA_LATENCY_INFO(SPA_DIRECTION_REVERSE(impl->direction)); + else if ((res = spa_latency_parse(param, &info)) < 0) return res; - pw_log_info("stream %p: set %s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, stream, + pw_log_info("stream %p: set %s/%s latency %f-%f %d-%d %"PRIu64"-%"PRIu64, stream, info.direction == SPA_DIRECTION_INPUT ? "input" : "output", + impl->direction == SPA_DIRECTION_INPUT ? "input" : "output", info.min_quantum, info.max_quantum, info.min_rate, info.max_rate, info.min_ns, info.max_ns); @@ -883,6 +905,7 @@ static int parse_latency(struct pw_stream *stream, const struct spa_pod *param) return 0; impl->latency = info; + *flags = PARAM_FLAG_LOCKED; return 0; } @@ -893,8 +916,10 @@ static int impl_port_set_param(void *object, { struct stream *impl = object; struct pw_stream *stream = &impl->this; - uint32_t user; + uint32_t user, fl = 0; int res; + const struct spa_pod *params[1]; + uint32_t n_params = 0; pw_log_debug("%p: port:%d.%d id:%d (%s) param:%p disconnecting:%d", impl, direction, port_id, id, @@ -907,7 +932,16 @@ static int impl_port_set_param(void *object, if (param) pw_log_pod(SPA_LOG_LEVEL_DEBUG, param); - if ((res = update_params(impl, id, 0, ¶m, param ? 1 : 0)) < 0) + params[0] = param; + n_params = param ? 1 : 0; + + switch (id) { + case SPA_PARAM_Latency: + parse_latency(stream, param, &fl); + break; + } + + if ((res = update_params(impl, id, fl, params, n_params)) < 0) return res; switch (id) { @@ -915,9 +949,6 @@ static int impl_port_set_param(void *object, clear_buffers(stream); user = impl->params[NODE_Format].user; break; - case SPA_PARAM_Latency: - parse_latency(stream, param); - break; default: break; } @@ -948,8 +979,7 @@ static int impl_port_use_buffers(void *object, struct stream *impl = object; struct pw_stream *stream = &impl->this; uint32_t i, j, impl_flags = impl->flags; - int prot, res; - int size = 0; + int res, size = 0; pw_log_debug("%p: port:%d.%d buffers:%u disconnecting:%d", impl, direction, port_id, n_buffers, impl->disconnecting); @@ -957,8 +987,6 @@ static int impl_port_use_buffers(void *object, if (impl->disconnecting && n_buffers > 0) return -EIO; - prot = PROT_READ | (direction == SPA_DIRECTION_OUTPUT ? PROT_WRITE : 0); - clear_buffers(stream); if (n_buffers > MAX_BUFFERS) @@ -974,7 +1002,12 @@ static int impl_port_use_buffers(void *object, if (SPA_FLAG_IS_SET(impl_flags, PW_STREAM_FLAG_MAP_BUFFERS)) { for (j = 0; j < buffers[i]->n_datas; j++) { struct spa_data *d = &buffers[i]->datas[j]; - if (SPA_FLAG_IS_SET(d->flags, SPA_DATA_FLAG_MAPPABLE)) { + if (d->data == NULL && SPA_FLAG_IS_SET(d->flags, SPA_DATA_FLAG_MAPPABLE)) { + int prot = 0; + if (SPA_FLAG_IS_SET(d->flags, SPA_DATA_FLAG_READABLE)) + prot |= PROT_READ; + if (SPA_FLAG_IS_SET(d->flags, SPA_DATA_FLAG_WRITABLE)) + prot |= PROT_WRITE; if ((res = map_data(impl, d, prot)) < 0) return res; SPA_FLAG_SET(b->flags, BUFFER_FLAG_MAPPED); @@ -1020,8 +1053,7 @@ static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffe { struct stream *d = object; pw_log_trace("%p: recycle buffer %d", d, buffer_id); - if (buffer_id < d->n_buffers) - queue_push(d, &d->queued, &d->buffers[buffer_id]); + queue_push(d, &d->queued, &d->buffers[buffer_id]); return 0; } @@ -1079,7 +1111,7 @@ static int impl_node_process_output(void *object) struct spa_io_buffers *io = impl->io; struct buffer *b; int res; - bool ask_more; + bool ask_more, driver_end; if (io == NULL) return -EIO; @@ -1089,6 +1121,8 @@ again: io->status, io->buffer_id); ask_more = false; + driver_end = stream->node->driving && !impl->in_trigger; + if ((res = io->status) != SPA_STATUS_HAVE_DATA) { /* recycle old buffer */ if ((b = get_buffer(stream, io->buffer_id)) != NULL) { @@ -1098,8 +1132,9 @@ again: ask_more = true; } - /* pop new buffer */ - if ((b = queue_pop(impl, &impl->queued)) != NULL) { + /* pop new buffer but only if we are not a driver and completing + * the cycle. */ + if (!driver_end && (b = queue_pop(impl, &impl->queued)) != NULL) { impl->drained = false; io->buffer_id = b->id; res = io->status = SPA_STATUS_HAVE_DATA; @@ -1117,6 +1152,9 @@ again: pw_log_trace_fp("%p: no more buffers %p", stream, io); ask_more = true; } + } else if (driver_end) { + /* if we are completing the cycle, don't say we have more data */ + res = SPA_STATUS_NEED_DATA; } copy_position(impl, impl->queued.outcount); @@ -1131,7 +1169,7 @@ again: pw_log_trace_fp("%p: res %d", stream, res); - if (stream->node->driving && impl->using_trigger && res != SPA_STATUS_HAVE_DATA) + if (driver_end && impl->using_trigger) call_trigger_done(impl); return res; @@ -1232,6 +1270,9 @@ static int node_event_param(void *object, int seq, return 0; c = calloc(1, sizeof(*c) + SPA_POD_SIZE(param)); + if (c == NULL) + return -errno; + c->info = SPA_PTROFF(c, sizeof(*c), struct spa_pod); memcpy(c->info, param, SPA_POD_SIZE(param)); c->control.n_values = 0; @@ -1248,12 +1289,12 @@ static int node_event_param(void *object, int seq, } pod = spa_pod_get_values(type, &n_vals, &choice); - if (n_vals == 0) { + if (n_vals < 1) { free(c); return -EINVAL; } - c->type = SPA_POD_TYPE(pod); + c->type = pod->type; if (spa_pod_is_float(pod)) vals = SPA_POD_BODY(pod); else if (spa_pod_is_double(pod)) { @@ -1280,19 +1321,12 @@ static int node_event_param(void *object, int seq, switch (choice) { case SPA_CHOICE_None: - if (n_vals < 1) { - free(c); - return -EINVAL; - } c->control.n_values = 1; c->control.max_values = 1; c->control.values[0] = c->control.def = c->control.min = c->control.max = vals[0]; break; case SPA_CHOICE_Range: - if (n_vals < 3) { - free(c); - return -EINVAL; - } + case SPA_CHOICE_Step: c->control.n_values = 1; c->control.max_values = 1; c->control.values[0] = vals[0]; @@ -1320,7 +1354,7 @@ static int node_event_param(void *object, int seq, double value_d; bool value_b; float *values; - uint32_t i, n_values; + uint32_t i, n_values, val_size, val_type; SPA_POD_OBJECT_FOREACH(obj, prop) { struct control *c; @@ -1351,9 +1385,11 @@ static int node_event_param(void *object, int seq, values = &value_f; break; case SPA_TYPE_Array: - if ((values = spa_pod_get_array(&prop->value, &n_values)) == NULL || - !spa_pod_is_float(SPA_POD_ARRAY_CHILD(&prop->value))) + if ((values = spa_pod_get_array_full(&prop->value, &n_values, &val_size, &val_type)) == NULL || + val_type != SPA_TYPE_Float || + val_size != sizeof(float)) continue; + n_values = SPA_MIN(n_values, MAX_VALUES); break; default: continue; @@ -1422,10 +1458,6 @@ static void node_state_changed(void *data, enum pw_node_state old, struct pw_stream *stream = data; switch (state) { - case PW_NODE_STATE_RUNNING: - if (stream->state == PW_STREAM_STATE_PAUSED) - stream_set_state(stream, PW_STREAM_STATE_STREAMING, 0, NULL); - break; case PW_NODE_STATE_ERROR: stream_set_state(stream, PW_STREAM_STATE_ERROR, -EIO, error); break; @@ -1709,7 +1741,7 @@ void pw_stream_destroy(struct pw_stream *stream) stream->core = NULL; } - clear_params(impl, SPA_ID_INVALID); + clear_params(impl, SPA_ID_INVALID, 0); pw_log_debug("%p: free", stream); free(stream->error); @@ -1746,7 +1778,7 @@ static void hook_removed(struct spa_hook *hook) { struct stream *impl = hook->priv; if (impl->data_loop) - pw_loop_invoke(impl->data_loop, do_remove_callbacks, 1, NULL, 0, true, impl); + pw_loop_locked(impl->data_loop, do_remove_callbacks, 1, NULL, 0, impl); else spa_zero(impl->rt_callbacks); hook->priv = NULL; @@ -1987,7 +2019,7 @@ pw_stream_connect(struct pw_stream *stream, impl->port_info.params = impl->port_params; impl->port_info.n_params = N_PORT_PARAMS; - clear_params(impl, SPA_ID_INVALID); + clear_params(impl, SPA_ID_INVALID, 0); for (i = 0; i < n_params; i++) add_param(impl, SPA_ID_INVALID, 0, params[i]); @@ -2053,7 +2085,7 @@ pw_stream_connect(struct pw_stream *stream, pw_properties_set(impl->port_props, PW_KEY_FORMAT_DSP, str); else if (impl->media_type == SPA_MEDIA_TYPE_application && impl->media_subtype == SPA_MEDIA_SUBTYPE_control) - pw_properties_set(impl->port_props, PW_KEY_FORMAT_DSP, "32 bit raw UMP"); + pw_properties_set(impl->port_props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); if (pw_properties_get(impl->port_props, PW_KEY_PORT_GROUP) == NULL) pw_properties_set(impl->port_props, PW_KEY_PORT_GROUP, "stream.0"); @@ -2344,6 +2376,18 @@ const struct pw_stream_control *pw_stream_get_control(struct pw_stream *stream, return NULL; } +static int +do_stop_drain(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct stream *impl = user_data; + pw_log_trace_fp("%p", impl); + if (impl->drained && impl->io != NULL) + impl->io->status = SPA_STATUS_NEED_DATA; + impl->draining = impl->drained = false; + return 0; +} + SPA_EXPORT int pw_stream_set_active(struct pw_stream *stream, bool active) { @@ -2357,12 +2401,8 @@ int pw_stream_set_active(struct pw_stream *stream, bool active) return -EIO; pw_impl_node_set_active(stream->node, active); + pw_loop_locked(impl->data_loop, do_stop_drain, 1, NULL, 0, impl); - if (!active || impl->drained) { - if (impl->drained && impl->io != NULL) - impl->io->status = SPA_STATUS_NEED_DATA; - impl->drained = impl->draining = false; - } return 0; } @@ -2387,6 +2427,7 @@ int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t uintptr_t seq1, seq2; uint32_t buffered, quantum, index, rate_size; int32_t avail_buffers; + struct spa_latency_info *latency = &impl->latency; do { seq1 = SPA_SEQ_READ(impl->seq); @@ -2402,9 +2443,9 @@ int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t else time->queued = (int64_t)(impl->queued.incount - time->queued); - time->delay += (int64_t)(((impl->latency.min_quantum + impl->latency.max_quantum) / 2.0f) * quantum); - time->delay += (impl->latency.min_rate + impl->latency.max_rate) / 2; - time->delay += ((impl->latency.min_ns + impl->latency.max_ns) / 2) * + time->delay += (int64_t)(((latency->min_quantum + latency->max_quantum) / 2.0f) * quantum); + time->delay += (latency->min_rate + latency->max_rate) / 2; + time->delay += ((latency->min_ns + latency->max_ns) / 2) * (int64_t)time->rate.denom / (int64_t)SPA_NSEC_PER_SEC; avail_buffers = spa_ringbuffer_get_read_index(&impl->dequeued.ring, &index); @@ -2449,7 +2490,9 @@ do_trigger_deprecated(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { struct stream *impl = user_data; + impl->in_trigger++; int res = impl->node_methods.process(impl); + impl->in_trigger--; return spa_node_call_ready(&impl->callbacks, res); } @@ -2460,6 +2503,16 @@ struct pw_buffer *pw_stream_dequeue_buffer(struct pw_stream *stream) struct buffer *b; int res; + /* For reliable output streams, only give buffers when both queue AND output IO are clear */ + if (impl->direction == SPA_DIRECTION_OUTPUT && stream->node->reliable) { + struct spa_io_buffers *io = impl->io; + + if (!queue_is_empty(impl, &impl->queued) || io->status == SPA_STATUS_HAVE_DATA) { + errno = EAGAIN; + return NULL; + } + } + if ((b = queue_pop(impl, &impl->dequeued)) == NULL) { res = -errno; pw_log_trace_fp("%p: no more buffers: %m", stream); @@ -2478,6 +2531,9 @@ struct pw_buffer *pw_stream_dequeue_buffer(struct pw_stream *stream) return NULL; } } + + SPA_FLAG_SET(b->flags, BUFFER_FLAG_DEQUEUED); + return &b->this; } @@ -2488,6 +2544,13 @@ int pw_stream_queue_buffer(struct pw_stream *stream, struct pw_buffer *buffer) struct buffer *b = SPA_CONTAINER_OF(buffer, struct buffer, this); int res; + if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_DEQUEUED)) { + pw_log_warn("%p: tried to queue cleared buffer %d", stream, b->id); + return -EINVAL; + } + + SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_DEQUEUED); + if (b->busy) SPA_ATOMIC_DEC(b->busy->count); @@ -2517,7 +2580,6 @@ static inline int queue_push_front(struct stream *stream, struct queue *queue, s index -= 1; queue->ids[index & MASK_BUFFERS] = buffer->id; queue->outcount -= buffer->this.size; - SPA_FLAG_SET(buffer->flags, BUFFER_FLAG_QUEUED); spa_ringbuffer_read_update(&queue->ring, index); return ret; @@ -2588,8 +2650,8 @@ int pw_stream_flush(struct pw_stream *stream, bool drain) if (stream->node == NULL) return -EIO; - pw_loop_invoke(impl->data_loop, - drain ? do_drain : do_flush, 1, NULL, 0, true, impl); + pw_loop_locked(impl->data_loop, + drain ? do_drain : do_flush, 1, NULL, 0, impl); if (!drain) spa_node_send_command(stream->node->node, @@ -2617,7 +2679,9 @@ do_trigger_driver(struct spa_loop *loop, int res; if (impl->direction == SPA_DIRECTION_OUTPUT) { call_process(impl); + impl->in_trigger++; res = impl->node_methods.process(impl); + impl->in_trigger--; } else { res = SPA_STATUS_NEED_DATA; } diff --git a/src/pipewire/stream.h b/src/pipewire/stream.h index 8e8ba0950..28b4eba1f 100644 --- a/src/pipewire/stream.h +++ b/src/pipewire/stream.h @@ -269,11 +269,11 @@ struct pw_buffer { * suggested amount of data to provide. For audio * streams this will be the amount of frames * required by the resampler. This field is 0 - * when no suggestion is provided. Since 0.3.49 */ + * when no suggestion is provided. Since 0.3.50 */ uint64_t time; /**< For capture streams, this field contains the * cycle time in nanoseconds when this buffer was * queued in the stream. It can be compared against - * the pw_time values or pw_stream_get_nsec() + * the \ref pw_time values or pw_stream_get_nsec() * Since 1.0.5 */ }; @@ -296,7 +296,7 @@ struct pw_stream_control { * stream. * * pw_time.ticks gives a monotonic increasing counter of the time in the graph - * driver. I can be used to generate a timetime to schedule samples as well + * driver. I can be used to generate a timeline to schedule samples as well * as detect discontinuities in the timeline caused by xruns. * * pw_time.delay is expressed as pw_time.rate, the time domain of the graph. This @@ -311,15 +311,20 @@ struct pw_stream_control { * * pw_time.delay contains the total delay that a signal will travel through the * graph. This includes the delay caused by filters in the graph as well as delays - * caused by the hardware. The delay is usually quite stable and should only change when - * the topology, quantum or samplerate of the graph changes. + * caused by the hardware and extra delay offsets added to this. The delay is usually + * quite stable and should only change when the topology, quantum or samplerate of + * the graph changes. * - * The delay requires the application to send the stream early relative to other synchronized - * streams in order to arrive at the edge of the graph in time. This is usually done by - * delaying the other streams with the given delay. + * The (positive) delay requires the application to send the stream early relative to other + * synchronized streams in order to arrive at the edge of the graph in time. This is usually + * done by delaying the other streams with the given delay. * - * Note that the delay can be negative. A negative delay means that this stream should be - * delayed with the (positive) delay relative to other streams. + * A delay offset is sometimes added (by the user) to improve synchronization of the streams + * when the reported latency is incorrect in some way. This means that with a large enough + * negative offset, the delay can become negative as well. A negative delay in this context + * means that the user would like this stream to be delayed with the (positive) delay amount + * in order to synchronize it with other streams. Streams are in general not expected to be + * able to delay themselves and it is acceptable to clamp negative delays to 0. * * pw_time.queued and pw_time.buffered is expressed in the time domain of the stream, * or the format that is used for the buffers of this stream. @@ -385,10 +390,11 @@ struct pw_time { * the playback device or the time a sample traveled * from the capture device. This delay includes the * delay introduced by all filters on the path between - * the stream and the device. The delay is normally - * constant in a graph and can change when the topology - * of the graph or the quantum changes. This delay does - * not include the delay caused by queued buffers. */ + * the stream and the device and extra delay offsets. The + * delay is normally constant in a graph and can change when + * the topology of the graph or the quantum changes. This delay + * does not include the delay caused by queued buffers. + * The delay can be negative, see \ref pw_time . */ uint64_t queued; /**< data queued in the stream, this is the sum * of the size fields in the pw_buffer that are * currently queued */ diff --git a/src/pipewire/thread-loop.c b/src/pipewire/thread-loop.c index 5f74f9487..d36482821 100644 --- a/src/pipewire/thread-loop.c +++ b/src/pipewire/thread-loop.c @@ -27,99 +27,16 @@ struct pw_thread_loop { struct spa_hook_list listener_list; - pthread_mutex_t lock; - pthread_cond_t cond; - pthread_cond_t accept_cond; - pthread_t thread; - int recurse; struct spa_hook hook; - struct spa_source *event; - - int n_waiting; - int n_waiting_for_accept; unsigned int created:1; unsigned int running:1; unsigned int start_signal:1; }; /** \endcond */ -static int do_lock(struct pw_thread_loop *this) -{ - int res; - if ((res = pthread_mutex_lock(&this->lock)) != 0) - pw_log_error("%p: thread:%p: %s", this, (void *) pthread_self(), strerror(res)); - else - this->recurse++; - return -res; -} - -static int do_unlock(struct pw_thread_loop *this) -{ - int res; - spa_return_val_if_fail(this->recurse > 0, -EIO); - this->recurse--; - if ((res = pthread_mutex_unlock(&this->lock)) != 0) { - pw_log_error("%p: thread:%p: %s", this, (void *) pthread_self(), strerror(res)); - this->recurse++; - } - return -res; -} - -static void impl_before(void *data) -{ - struct pw_thread_loop *this = data; - do_unlock(this); -} - -static void impl_after(void *data) -{ - struct pw_thread_loop *this = data; - do_lock(this); -} - -static const struct spa_loop_control_hooks impl_hooks = { - SPA_VERSION_LOOP_CONTROL_HOOKS, - .before = impl_before, - .after = impl_after, -}; - -static int impl_check(void *data, struct pw_loop *loop) -{ - struct pw_thread_loop *this = data; - int res; - - /* we are in the thread running the loop */ - if (spa_loop_control_check(this->loop->control) == 1) - return 1; - - /* if lock taken by something else, error */ - if ((res = pthread_mutex_trylock(&this->lock)) != 0) { - pw_log_debug("%p: thread:%p: %s", this, (void *) pthread_self(), strerror(res)); - return -res; - } - /* we could take the lock, check if we actually locked it somewhere */ - res = this->recurse > 0 ? 1 : -EPERM; - if (res < 0) - pw_log_debug("%p: thread:%p: recurse:%d", this, (void *) pthread_self(), this->recurse); - pthread_mutex_unlock(&this->lock); - return res; -} - -static const struct pw_loop_callbacks impl_callbacks = { - PW_VERSION_LOOP_CALLBACKS, - .check = impl_check, -}; - -static void do_stop(void *data, uint64_t count) -{ - struct pw_thread_loop *this = data; - pw_log_debug("stopping"); - this->running = false; -} - #define CHECK(expression,label) \ do { \ if ((errno = (expression)) != 0) { \ @@ -134,8 +51,6 @@ static struct pw_thread_loop *loop_new(struct pw_loop *loop, const struct spa_dict *props) { struct pw_thread_loop *this; - pthread_mutexattr_t attr; - pthread_condattr_t cattr; int res; this = calloc(1, sizeof(struct pw_thread_loop)); @@ -162,32 +77,8 @@ static struct pw_thread_loop *loop_new(struct pw_loop *loop, spa_hook_list_init(&this->listener_list); - CHECK(pthread_mutexattr_init(&attr), clean_this); - CHECK(pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE), clean_this); - CHECK(pthread_mutex_init(&this->lock, &attr), clean_this); - - CHECK(pthread_condattr_init(&cattr), clean_lock); - CHECK(pthread_condattr_setclock(&cattr, CLOCK_REALTIME), clean_lock); - - CHECK(pthread_cond_init(&this->cond, &cattr), clean_lock); - CHECK(pthread_cond_init(&this->accept_cond, &cattr), clean_cond); - - if ((this->event = pw_loop_add_event(this->loop, do_stop, this)) == NULL) { - res = -errno; - goto clean_acceptcond; - } - - pw_loop_set_callbacks(loop, &impl_callbacks, this); - pw_loop_add_hook(loop, &this->hook, &impl_hooks, this); - return this; -clean_acceptcond: - pthread_cond_destroy(&this->accept_cond); -clean_cond: - pthread_cond_destroy(&this->cond); -clean_lock: - pthread_mutex_destroy(&this->lock); clean_this: if (this->created && this->loop) pw_loop_destroy(this->loop); @@ -245,20 +136,13 @@ void pw_thread_loop_destroy(struct pw_thread_loop *loop) pw_thread_loop_stop(loop); - pw_loop_set_callbacks(loop->loop, NULL, NULL); spa_hook_remove(&loop->hook); spa_hook_list_clean(&loop->listener_list); - pw_loop_destroy_source(loop->loop, loop->event); - if (loop->created) pw_loop_destroy(loop->loop); - pthread_cond_destroy(&loop->accept_cond); - pthread_cond_destroy(&loop->cond); - pthread_mutex_destroy(&loop->lock); - free(loop); } @@ -283,7 +167,6 @@ static void *do_loop(void *user_data) struct pw_thread_loop *this = user_data; int res; - do_lock(this); pw_log_debug("%p: enter thread", this); pw_loop_enter(this->loop); @@ -300,7 +183,6 @@ static void *do_loop(void *user_data) } pw_log_debug("%p: leave thread", this); pw_loop_leave(this->loop); - do_unlock(this); return NULL; } @@ -339,6 +221,15 @@ error: return -err; } +static int do_stop(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + struct pw_thread_loop *this = user_data; + pw_log_debug("%p: stopping", this); + this->running = false; + return 0; +} + /** Quit the loop and stop its thread * * \param loop a \ref pw_thread_loop @@ -350,7 +241,7 @@ void pw_thread_loop_stop(struct pw_thread_loop *loop) pw_log_debug("%p stopping %d", loop, loop->running); if (loop->running) { pw_log_debug("%p signal", loop); - pw_loop_signal_event(loop->loop, loop->event); + pw_loop_invoke(loop->loop, do_stop, 1, NULL, 0, false, loop); pw_log_debug("%p join", loop); pthread_join(loop->thread, NULL); pw_log_debug("%p joined", loop); @@ -367,7 +258,7 @@ void pw_thread_loop_stop(struct pw_thread_loop *loop) SPA_EXPORT void pw_thread_loop_lock(struct pw_thread_loop *loop) { - do_lock(loop); + pw_loop_lock(loop->loop); pw_log_trace("%p", loop); } @@ -380,7 +271,7 @@ SPA_EXPORT void pw_thread_loop_unlock(struct pw_thread_loop *loop) { pw_log_trace("%p", loop); - do_unlock(loop); + pw_loop_unlock(loop->loop); } /** Signal the thread @@ -395,20 +286,8 @@ void pw_thread_loop_unlock(struct pw_thread_loop *loop) SPA_EXPORT void pw_thread_loop_signal(struct pw_thread_loop *loop, bool wait_for_accept) { - pw_log_trace("%p, waiting:%d accept:%d", - loop, loop->n_waiting, wait_for_accept); - if (loop->n_waiting > 0) - pthread_cond_broadcast(&loop->cond); - - if (wait_for_accept) { - loop->n_waiting_for_accept++; - - while (loop->n_waiting_for_accept > 0) { - int res; - if ((res = pthread_cond_wait(&loop->accept_cond, &loop->lock)) != 0) - pw_log_error("%p: thread:%p: %s", loop, (void *) pthread_self(), strerror(res)); - } - } + pw_log_trace("%p,accept:%d", loop, wait_for_accept); + pw_loop_signal(loop->loop, wait_for_accept); } /** Wait for the loop thread to call \ref pw_thread_loop_signal() @@ -419,17 +298,7 @@ void pw_thread_loop_signal(struct pw_thread_loop *loop, bool wait_for_accept) SPA_EXPORT void pw_thread_loop_wait(struct pw_thread_loop *loop) { - int res; - - pw_log_trace("%p, waiting:%d recurse:%d", loop, loop->n_waiting, loop->recurse); - spa_return_if_fail(loop->recurse > 0); - loop->n_waiting++; - loop->recurse--; - if ((res = pthread_cond_wait(&loop->cond, &loop->lock)) != 0) - pw_log_error("%p: thread:%p: %s", loop, (void *) pthread_self(), strerror(res)); - loop->recurse++; - loop->n_waiting--; - pw_log_trace("%p, waiting done %d", loop, loop->n_waiting); + pw_loop_wait(loop->loop, NULL); } /** Wait for the loop thread to call \ref pw_thread_loop_signal() @@ -464,16 +333,7 @@ int pw_thread_loop_timed_wait(struct pw_thread_loop *loop, int wait_max_sec) SPA_EXPORT int pw_thread_loop_get_time(struct pw_thread_loop *loop, struct timespec *abstime, int64_t timeout) { - if (clock_gettime(CLOCK_REALTIME, abstime) < 0) - return -errno; - - abstime->tv_sec += timeout / SPA_NSEC_PER_SEC; - abstime->tv_nsec += timeout % SPA_NSEC_PER_SEC; - if (abstime->tv_nsec >= SPA_NSEC_PER_SEC) { - abstime->tv_sec++; - abstime->tv_nsec -= SPA_NSEC_PER_SEC; - } - return 0; + return pw_loop_get_time(loop->loop, abstime, timeout); } /** Wait for the loop thread to call \ref pw_thread_loop_signal() @@ -487,14 +347,7 @@ int pw_thread_loop_get_time(struct pw_thread_loop *loop, struct timespec *abstim SPA_EXPORT int pw_thread_loop_timed_wait_full(struct pw_thread_loop *loop, const struct timespec *abstime) { - int ret; - spa_return_val_if_fail(loop->recurse > 0, -EIO); - loop->n_waiting++; - loop->recurse--; - ret = pthread_cond_timedwait(&loop->cond, &loop->lock, abstime); - loop->recurse++; - loop->n_waiting--; - return -ret; + return pw_loop_wait(loop->loop, abstime); } /** Signal the loop thread waiting for accept with \ref pw_thread_loop_signal() @@ -505,8 +358,7 @@ int pw_thread_loop_timed_wait_full(struct pw_thread_loop *loop, const struct tim SPA_EXPORT void pw_thread_loop_accept(struct pw_thread_loop *loop) { - loop->n_waiting_for_accept--; - pthread_cond_signal(&loop->accept_cond); + pw_loop_accept(loop->loop); } /** Check if we are inside the thread of the loop diff --git a/src/pipewire/thread-loop.h b/src/pipewire/thread-loop.h index 2734799df..d40e47b7a 100644 --- a/src/pipewire/thread-loop.h +++ b/src/pipewire/thread-loop.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_THREAD_LOOP_H #define PIPEWIRE_THREAD_LOOP_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \page page_thread_loop Thread Loop * * \see \ref pw_thread_loop @@ -126,29 +126,35 @@ void pw_thread_loop_lock(struct pw_thread_loop *loop); /** Unlock the loop */ void pw_thread_loop_unlock(struct pw_thread_loop *loop); -/** Release the lock and wait until some thread calls \ref pw_thread_loop_signal */ +/** Release the lock and wait until some thread calls \ref pw_thread_loop_signal. + * This must be called with the loop locked. */ void pw_thread_loop_wait(struct pw_thread_loop *loop); /** Release the lock and wait a maximum of 'wait_max_sec' seconds - * until some thread calls \ref pw_thread_loop_signal or time out */ + * until some thread calls \ref pw_thread_loop_signal or time out. + * This must be called with the loop locked. */ int pw_thread_loop_timed_wait(struct pw_thread_loop *loop, int wait_max_sec); /** Get a struct timespec suitable for \ref pw_thread_loop_timed_wait_full. + * This function may be called from any thread. * Since: 0.3.7 */ int pw_thread_loop_get_time(struct pw_thread_loop *loop, struct timespec *abstime, int64_t timeout); /** Release the lock and wait up to \a abstime until some thread calls * \ref pw_thread_loop_signal. Use \ref pw_thread_loop_get_time to make a timeout. + * This must be called with the loop locked. * Since: 0.3.7 */ int pw_thread_loop_timed_wait_full(struct pw_thread_loop *loop, const struct timespec *abstime); -/** Signal all threads waiting with \ref pw_thread_loop_wait */ +/** Signal all threads waiting with \ref pw_thread_loop_wait + * This must be called with the loop locked. */ void pw_thread_loop_signal(struct pw_thread_loop *loop, bool wait_for_accept); -/** Signal all threads executing \ref pw_thread_loop_signal with wait_for_accept */ +/** Signal all threads executing \ref pw_thread_loop_signal with wait_for_accept. + * This must be called with the loop locked. */ void pw_thread_loop_accept(struct pw_thread_loop *loop); -/** Check if inside the thread */ +/** Check if inside the thread. This can be called from any thread. */ bool pw_thread_loop_in_thread(struct pw_thread_loop *loop); /** diff --git a/src/pipewire/thread.c b/src/pipewire/thread.c index defa6a686..baca12212 100644 --- a/src/pipewire/thread.c +++ b/src/pipewire/thread.c @@ -125,16 +125,24 @@ static struct spa_thread *impl_create(void *object, static int impl_join(void *object, struct spa_thread *thread, void **retval) { pthread_t pt = (pthread_t)thread; - return pthread_join(pt, retval); + return -pthread_join(pt, retval); } static int impl_get_rt_range(void *object, const struct spa_dict *props, int *min, int *max) { - if (min) + if (min) { *min = sched_get_priority_min(SCHED_OTHER); - if (max) + if (*min < 0) + return -errno; + } + + if (max) { *max = sched_get_priority_max(SCHED_OTHER); + if (*max < 0) + return -errno; + } + return 0; } static int impl_acquire_rt(void *object, struct spa_thread *thread, int priority) diff --git a/src/pipewire/thread.h b/src/pipewire/thread.h index f53e62950..2f214988c 100644 --- a/src/pipewire/thread.h +++ b/src/pipewire/thread.h @@ -5,15 +5,15 @@ #ifndef PIPEWIRE_THREAD_H #define PIPEWIRE_THREAD_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** \defgroup pw_thread Thread * * \brief functions to manipulate threads diff --git a/src/pipewire/timer-queue.c b/src/pipewire/timer-queue.c new file mode 100644 index 000000000..cb7495dc0 --- /dev/null +++ b/src/pipewire/timer-queue.c @@ -0,0 +1,197 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include "timer-queue.h" + +PW_LOG_TOPIC_EXTERN(log_timer_queue); +#define PW_LOG_TOPIC_DEFAULT log_timer_queue + +struct pw_timer_queue { + struct pw_loop *loop; + struct spa_list entries; + struct timespec *next_timeout; + struct spa_source *timer; +}; + +static void rearm_timer(struct pw_timer_queue *queue) +{ + struct timespec *timeout = NULL; + struct pw_timer *timer; + + if (!spa_list_is_empty(&queue->entries)) { + timer = spa_list_first(&queue->entries, struct pw_timer, link); + timeout = &timer->timeout; + } + if (timeout != queue->next_timeout) { + if (timeout) + pw_log_debug("%p: arming with timeout %"PRIi64, queue, + (int64_t)SPA_TIMESPEC_TO_NSEC(timeout)); + else + pw_log_debug("%p: disarming (no entries)", queue); + + queue->next_timeout = timeout; + pw_loop_update_timer(queue->loop, queue->timer, + timeout, NULL, true); + } +} + +static void timer_timeout(void *user_data, uint64_t expirations) +{ + struct pw_timer_queue *queue = user_data; + struct pw_timer *timer; + + pw_log_debug("%p: timeout fired, expirations=%"PRIu64, queue, expirations); + + if (spa_list_is_empty(&queue->entries)) { + pw_log_debug("%p: no entries to process", queue); + return; + } + timer = spa_list_first(&queue->entries, struct pw_timer, link); + if (&timer->timeout != queue->next_timeout) { + /* this can happen when the timer expired but before we could + * dispatch the event, the timer got removed or a new one got + * added. The timer does not match the one we last scheduled + * and we need to wait for the rescheduled timer instead */ + pw_log_debug("%p: timer was rearmed", queue); + return; + } + queue->next_timeout = NULL; + + pw_log_debug("%p: processing timer %p", queue, timer); + timer->queue = NULL; + spa_list_remove(&timer->link); + + timer->callback(timer->data); + + rearm_timer(queue); +} + +SPA_EXPORT +struct pw_timer_queue *pw_timer_queue_new(struct pw_loop *loop) +{ + struct pw_timer_queue *queue; + int res; + + queue = calloc(1, sizeof(struct pw_timer_queue)); + if (queue == NULL) + return NULL; + + queue->loop = loop; + queue->timer = pw_loop_add_timer(loop, timer_timeout, queue); + if (queue->timer == NULL) { + res = -errno; + goto error_free; + } + + spa_list_init(&queue->entries); + pw_log_debug("%p: initialized", queue); + return queue; + +error_free: + free(queue); + errno = -res; + return NULL; +} + +SPA_EXPORT +void pw_timer_queue_destroy(struct pw_timer_queue *queue) +{ + struct pw_timer *timer; + int count = 0; + + pw_log_debug("%p: clearing", queue); + + if (queue->timer) + pw_loop_destroy_source(queue->loop, queue->timer); + + spa_list_consume(timer, &queue->entries, link) { + timer->queue = NULL; + spa_list_remove(&timer->link); + count++; + } + if (count > 0) + pw_log_debug("%p: cancelled %d entries", queue, count); + + free(queue); +} + +static int timespec_compare(const struct timespec *a, const struct timespec *b) +{ + if (a->tv_sec < b->tv_sec) + return -1; + if (a->tv_sec > b->tv_sec) + return 1; + if (a->tv_nsec < b->tv_nsec) + return -1; + if (a->tv_nsec > b->tv_nsec) + return 1; + return 0; +} + +SPA_EXPORT +int pw_timer_queue_add(struct pw_timer_queue *queue, struct pw_timer *timer, + struct timespec *abs_time, int64_t timeout_ns, + pw_timer_callback callback, void *data) +{ + struct timespec timeout; + struct pw_timer *iter; + + if (timer->queue != NULL) + return -EBUSY; + + if (abs_time == NULL) { + /* Use CLOCK_MONOTONIC to match the timerfd clock used by SPA loop */ + if (clock_gettime(CLOCK_MONOTONIC, &timeout) < 0) + return -errno; + } else { + timeout = *abs_time; + } + if (timeout_ns > 0) { + timeout.tv_sec += timeout_ns / SPA_NSEC_PER_SEC; + timeout.tv_nsec += timeout_ns % SPA_NSEC_PER_SEC; + if (timeout.tv_nsec >= SPA_NSEC_PER_SEC) { + timeout.tv_sec++; + timeout.tv_nsec -= SPA_NSEC_PER_SEC; + } + } + + timer->queue = queue; + timer->timeout = timeout; + timer->callback = callback; + timer->data = data; + + pw_log_debug("%p: adding timer %p with timeout %"PRIi64, + queue, timer, (int64_t)SPA_TIMESPEC_TO_NSEC(&timeout)); + + + /* Insert timer in sorted order (earliest timeout first) */ + spa_list_for_each(iter, &queue->entries, link) { + if (timespec_compare(&timer->timeout, &iter->timeout) < 0) + break; + } + spa_list_append(&iter->link, &timer->link); + + rearm_timer(queue); + return 0; +} + +SPA_EXPORT +int pw_timer_queue_cancel(struct pw_timer *timer) +{ + struct pw_timer_queue *queue = timer->queue; + + if (queue == NULL) + return 0; + + pw_log_debug("%p: cancelling timer %p", queue, timer); + + timer->queue = NULL; + spa_list_remove(&timer->link); + + rearm_timer(queue); + + return 0; +} diff --git a/src/pipewire/timer-queue.h b/src/pipewire/timer-queue.h new file mode 100644 index 000000000..81dc7b447 --- /dev/null +++ b/src/pipewire/timer-queue.h @@ -0,0 +1,51 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_TIMER_QUEUE_H +#define PIPEWIRE_TIMER_QUEUE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** \defgroup pw_timer_queue Timer Queue + * Processing of timer events. + */ + +/** + * \addtogroup pw_timer_queue + * \{ + */ +struct pw_timer_queue; + +#include + +typedef void (*pw_timer_callback) (void *data); + +struct pw_timer { + struct spa_list link; + struct pw_timer_queue *queue; + struct timespec timeout; + pw_timer_callback callback; + void *data; + uint32_t padding[16]; +}; + +struct pw_timer_queue *pw_timer_queue_new(struct pw_loop *loop); +void pw_timer_queue_destroy(struct pw_timer_queue *queue); + +int pw_timer_queue_add(struct pw_timer_queue *queue, struct pw_timer *timer, + struct timespec *abs_time, int64_t timeout_ns, + pw_timer_callback callback, void *data); +int pw_timer_queue_cancel(struct pw_timer *timer); + +/** + * \} + */ + +#ifdef __cplusplus +} +#endif + +#endif /* PIPEWIRE_TIMER_QUEUE_H */ diff --git a/src/pipewire/type.h b/src/pipewire/type.h index a8f982c59..e55cbaf33 100644 --- a/src/pipewire/type.h +++ b/src/pipewire/type.h @@ -5,12 +5,12 @@ #ifndef PIPEWIRE_TYPE_H #define PIPEWIRE_TYPE_H +#include + #ifdef __cplusplus extern "C" { #endif -#include - /** \defgroup pw_type Type info * Type information */ diff --git a/src/pipewire/utils.h b/src/pipewire/utils.h index 528f6764b..29e210a5a 100644 --- a/src/pipewire/utils.h +++ b/src/pipewire/utils.h @@ -5,10 +5,6 @@ #ifndef PIPEWIRE_UTILS_H #define PIPEWIRE_UTILS_H -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include @@ -17,14 +13,18 @@ extern "C" { #endif #include -#ifndef ENODATA -#define ENODATA 9919 -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef ENODATA +#define ENODATA 9919 +#endif + /** \defgroup pw_utils Utilities * * Various utility functions diff --git a/src/tests/test-endpoint.c b/src/tests/test-endpoint.c index c6956c2e2..412fa6703 100644 --- a/src/tests/test-endpoint.c +++ b/src/tests/test-endpoint.c @@ -199,10 +199,12 @@ endpoint_init(struct endpoint * self) spa_hook_list_init (&self->hooks); self->info.version = PW_VERSION_ENDPOINT_INFO; + self->info.id = SPA_ID_INVALID; self->info.change_mask = PW_ENDPOINT_CHANGE_MASK_ALL; self->info.name = "test-endpoint"; self->info.media_class = "Audio/Sink"; self->info.direction = PW_DIRECTION_OUTPUT; + self->info.flags = 0; self->info.n_streams = 0; self->info.session_id = SPA_ID_INVALID; @@ -434,12 +436,6 @@ static void test_endpoint(void) int main(int argc, char *argv[]) { - /* FIXME: This test has a leak and a use of uninitialized buffer - * that needs to be debugged and fixed (or excluded). Meanwhile - - * skip it from valgrind so we can at least use the others. */ - if (RUNNING_ON_VALGRIND) - return 77; - pw_init(&argc, &argv); alarm(5); /* watchdog; terminate after 5 seconds */ diff --git a/src/tools/dfffile.c b/src/tools/dfffile.c index c2e4db22e..c77b26adb 100644 --- a/src/tools/dfffile.c +++ b/src/tools/dfffile.c @@ -263,6 +263,11 @@ exit_free: return NULL; } +uint32_t dff_layout_stride(const struct dff_layout *layout) +{ + return layout->channels * SPA_ABS(layout->interleave); +} + static const uint8_t bitrev[256] = { 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, @@ -289,14 +294,10 @@ dff_file_read(struct dff_file *f, void *data, size_t samples, const struct dff_l int32_t step = SPA_ABS(layout->interleave); uint32_t channels = f->info.channels; bool rev = layout->lsb != f->info.lsb; - size_t total, offset, scale, pos; + size_t total, offset, pos; offset = f->offset; pos = offset % f->blocksize; - scale = SPA_CLAMP(f->info.rate / (44100u * 64u), 1u, 4u); - - samples *= step; - samples *= scale; for (total = 0; total < samples; total++) { uint32_t i; diff --git a/src/tools/dfffile.h b/src/tools/dfffile.h index de8153142..49384fd2c 100644 --- a/src/tools/dfffile.h +++ b/src/tools/dfffile.h @@ -24,6 +24,8 @@ struct dff_layout { bool lsb; }; +uint32_t dff_layout_stride(const struct dff_layout *layout); + struct dff_file * dff_file_open(const char *filename, const char *mode, struct dff_file_info *info); ssize_t dff_file_read(struct dff_file *f, void *data, size_t samples, const struct dff_layout *layout); diff --git a/src/tools/dsffile.c b/src/tools/dsffile.c index 4122944a4..757962e22 100644 --- a/src/tools/dsffile.c +++ b/src/tools/dsffile.c @@ -175,6 +175,11 @@ exit_free: return NULL; } +uint32_t dsf_layout_stride(const struct dsf_layout *layout) +{ + return layout->channels * SPA_ABS(layout->interleave); +} + static const uint8_t bitrev[256] = { 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, @@ -200,16 +205,12 @@ dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_l uint8_t *d = data; int step = SPA_ABS(layout->interleave); bool rev = layout->lsb != f->info.lsb; - size_t total, block, offset, pos, scale; + size_t total, block, offset, pos; size_t blocksize = f->info.blocksize * f->info.channels; block = f->offset / f->info.blocksize; offset = block * blocksize; pos = f->offset % f->info.blocksize; - scale = SPA_CLAMP(f->info.rate / (44100u * 64u), 1u, 4u); - - samples *= step; - samples *= scale; for (total = 0; total < samples; total++) { uint32_t i; diff --git a/src/tools/dsffile.h b/src/tools/dsffile.h index 303bf6c71..2970ca62b 100644 --- a/src/tools/dsffile.h +++ b/src/tools/dsffile.h @@ -24,6 +24,8 @@ struct dsf_layout { bool lsb; }; +uint32_t dsf_layout_stride(const struct dsf_layout *layout); + struct dsf_file * dsf_file_open(const char *filename, const char *mode, struct dsf_file_info *info); ssize_t dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_layout *layout); diff --git a/src/tools/meson.build b/src/tools/meson.build index 8fbc8e210..8147906fb 100644 --- a/src/tools/meson.build +++ b/src/tools/meson.build @@ -5,7 +5,7 @@ tools_sources = [ [ 'pw-dot', [ 'pw-dot.c' ] ], [ 'pw-dump', [ 'pw-dump.c' ] ], [ 'pw-profiler', [ 'pw-profiler.c' ] ], - [ 'pw-mididump', [ 'pw-mididump.c', 'midifile.c' ] ], + [ 'pw-mididump', [ 'pw-mididump.c', 'midifile.c', 'midievent.c', 'midiclip.c' ] ], [ 'pw-metadata', [ 'pw-metadata.c' ] ], [ 'pw-loopback', [ 'pw-loopback.c' ] ], [ 'pw-link', [ 'pw-link.c' ] ], @@ -48,6 +48,8 @@ if get_option('pw-cat').allowed() and sndfile_dep.found() pwcat_sources = [ 'pw-cat.c', 'midifile.c', + 'midiclip.c', + 'midievent.c', 'dfffile.c', 'dsffile.c', ] @@ -57,8 +59,11 @@ if get_option('pw-cat').allowed() and sndfile_dep.found() 'pw-record', 'pw-midiplay', 'pw-midirecord', + 'pw-midi2play', + 'pw-midi2record', 'pw-dsdplay', 'pw-encplay', + 'pw-sysex', ] pw_cat = executable('pw-cat', diff --git a/src/tools/midiclip.c b/src/tools/midiclip.c new file mode 100644 index 000000000..2d2449c9b --- /dev/null +++ b/src/tools/midiclip.c @@ -0,0 +1,329 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "midiclip.h" + +#define DEFAULT_BPM 120 +#define SEC_AS_10NS 100000000.0 +#define MINUTE_10NS 6000000000 /* in 10ns units */ +#define DEFAULT_TEMPO MINUTE_10NS/DEFAULT_BPM + +struct midi_clip { + int mode; + FILE *file; + bool close; + int64_t count; + + uint8_t data[16]; + + uint32_t next[4]; + int num; + + bool pass_all; + struct midi_clip_info info; + uint32_t tempo; + + int64_t tick; + int64_t tick_start; + double tick_sec; +}; + +static int read_header(struct midi_clip *mc) +{ + uint8_t data[8]; + + if (fread(data, sizeof(data), 1, mc->file) != 1 || + memcmp(data, "SMF2CLIP", 4) != 0) + return -EINVAL; + return 0; +} + +static inline int read_word(struct midi_clip *mc, uint32_t *val) +{ + uint32_t v; + if (fread(&v, 4, 1, mc->file) != 1) + return 0; + *val = ntohl(v); + return 1; +} + +static inline int read_ump(struct midi_clip *mc) +{ + int i, num; + mc->num = 0; + if (read_word(mc, &mc->next[0]) != 1) + return 0; + num = spa_ump_message_size(mc->next[0]>>28); + for (i = 1; i < num; i++) { + if (read_word(mc, &mc->next[i]) != 1) + return 0; + } + return mc->num = num; +} + +static int next_packet(struct midi_clip *mc) +{ + while (read_ump(mc) > 0) { + uint8_t type = mc->next[0] >> 28; + + switch (type) { + case 0x0: /* utility */ + switch ((mc->next[0] >> 20) & 0xf) { + case 0x3: /* DCTPQ */ + mc->info.division = (mc->next[0] & 0xffff); + break; + case 0x4: /* DC */ + mc->tick += (mc->next[0] & 0xfffff); + break; + } + break; + case 0x2: /* midi 1.0 */ + case 0x3: /* sysex 7bits */ + case 0x4: /* midi 2.0 */ + return mc->num; + case 0xd: /* flex data */ + if (((mc->next[0] >> 8) & 0xff) == 0 && + (mc->next[0] & 0xff) == 0) + mc->tempo = mc->next[1]; + break; + case 0xf: /* stream */ + break; + default: + break; + } + if (mc->pass_all) + return mc->num; + } + return 0; +} + +static int open_read(struct midi_clip *mc, const char *filename, struct midi_clip_info *info) +{ + int res; + + if (strcmp(filename, "-") != 0) { + if ((mc->file = fopen(filename, "r")) == NULL) { + res = -errno; + goto exit; + } + mc->close = true; + } else { + mc->file = stdin; + mc->close = false; + } + + if ((res = read_header(mc)) < 0) + goto exit_close; + + mc->tempo = DEFAULT_TEMPO; + mc->tick = 0; + mc->mode = 1; + + next_packet(mc); + *info = mc->info; + return 0; + +exit_close: + if (mc->close) + fclose(mc->file); +exit: + return res; +} + +static inline int write_n(FILE *file, const void *buf, int count) +{ + return fwrite(buf, 1, count, file) == (size_t)count ? count : -errno; +} + +static inline int write_be32(FILE *file, uint32_t val) +{ + uint32_t v = htonl(val); + return write_n(file, &v, 4); +} + +#define CHECK_RES(expr) if ((res = (expr)) < 0) return res + +static int write_headers(struct midi_clip *mc) +{ + int res; + CHECK_RES(write_n(mc->file, "SMF2CLIP", 8)); + + /* DC 0 */ + CHECK_RES(write_be32(mc->file, 0x00400000)); + /* DCTPQ division */ + CHECK_RES(write_be32(mc->file, 0x00300000 | mc->info.division)); + /* tempo */ + CHECK_RES(write_be32(mc->file, 0xd0100000)); + CHECK_RES(write_be32(mc->file, mc->tempo)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + /* start */ + CHECK_RES(write_be32(mc->file, 0xf0200000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + + return 0; +} + +static int open_write(struct midi_clip *mc, const char *filename, struct midi_clip_info *info) +{ + int res; + + if (info->format != 0) + return -EINVAL; + if (info->division == 0) + info->division = 96; + + if (strcmp(filename, "-") != 0) { + if ((mc->file = fopen(filename, "w")) == NULL) { + res = -errno; + goto exit; + } + mc->close = true; + } else { + mc->file = stdout; + mc->close = false; + } + mc->mode = 2; + mc->tempo = DEFAULT_TEMPO; + mc->info = *info; + + res = write_headers(mc); +exit: + return res; +} + +struct midi_clip * +midi_clip_open(const char *filename, const char *mode, struct midi_clip_info *info) +{ + int res; + struct midi_clip *mc; + + mc = calloc(1, sizeof(struct midi_clip)); + if (mc == NULL) + return NULL; + + if (spa_streq(mode, "r")) { + if ((res = open_read(mc, filename, info)) < 0) + goto exit_free; + } else if (spa_streq(mode, "w")) { + if ((res = open_write(mc, filename, info)) < 0) + goto exit_free; + } else { + res = -EINVAL; + goto exit_free; + } + return mc; + +exit_free: + free(mc); + errno = -res; + return NULL; +} + +int midi_clip_close(struct midi_clip *mc) +{ + int res; + + if (mc->mode == 2) { + CHECK_RES(write_be32(mc->file, 0xf0210000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + CHECK_RES(write_be32(mc->file, 0x00000000)); + } else if (mc->mode != 1) + return -EINVAL; + + if (mc->close) + fclose(mc->file); + free(mc); + return 0; +} + +int midi_clip_next_time(struct midi_clip *mc, double *sec) +{ + if (mc->num <= 0) + return 0; + + if (mc->info.division == 0) + *sec = 0.0; + else + *sec = mc->tick_sec + ((mc->tick - mc->tick_start) * (double)mc->tempo) / + (SEC_AS_10NS * mc->info.division); + return 1; +} + +int midi_clip_read_event(struct midi_clip *mc, struct midi_event *event) +{ + if (midi_clip_next_time(mc, &event->sec) != 1) + return 0; + event->track = 0; + event->type = MIDI_EVENT_TYPE_UMP; + event->data = mc->data; + event->size = mc->num * 4; + memcpy(mc->data, mc->next, event->size); + + next_packet(mc); + return 1; +} + +int midi_clip_write_event(struct midi_clip *mc, const struct midi_event *event) +{ + uint32_t tick; + void *data; + size_t size; + int res, i, ump_size; + int32_t diff; + uint32_t ump[4], *ump_data; + uint64_t state = 0; + + spa_return_val_if_fail(event != NULL, -EINVAL); + spa_return_val_if_fail(mc != NULL, -EINVAL); + spa_return_val_if_fail(event->track == 0, -EINVAL); + spa_return_val_if_fail(event->size > 1, -EINVAL); + + data = event->data; + size = event->size; + + tick = (uint32_t)(event->sec * (SEC_AS_10NS * mc->info.division) / (double)mc->tempo); + + diff = mc->count++ == 0 ? 0 : tick - mc->tick; + if (diff > 0 || mc->count == 1) + CHECK_RES(write_be32(mc->file, 0x00400000 | diff)); + mc->tick = tick; + + while (size > 0) { + switch (event->type) { + case MIDI_EVENT_TYPE_UMP: + ump_data = data; + ump_size = size; + size = 0; + break; + case MIDI_EVENT_TYPE_MIDI1: + ump_size = spa_ump_from_midi((uint8_t**)&data, &size, + ump, sizeof(ump), event->track, &state); + if (ump_size <= 0) + return ump_size; + ump_data = ump; + break; + default: + return -EINVAL; + } + for (i = 0; i < ump_size/4; i++) + CHECK_RES(write_be32(mc->file, ump_data[i])); + } + return 0; +} diff --git a/src/tools/midiclip.h b/src/tools/midiclip.h new file mode 100644 index 000000000..8c02a0b00 --- /dev/null +++ b/src/tools/midiclip.h @@ -0,0 +1,27 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include + +#include "midievent.h" + +struct midi_clip; + +struct midi_clip_info { + uint16_t format; + uint16_t division; +}; + +struct midi_clip * +midi_clip_open(const char *filename, const char *mode, struct midi_clip_info *info); + +int midi_clip_close(struct midi_clip *mc); + +int midi_clip_next_time(struct midi_clip *mc, double *sec); + +int midi_clip_read_event(struct midi_clip *mc, struct midi_event *event); + +int midi_clip_write_event(struct midi_clip *mc, const struct midi_event *event); diff --git a/src/tools/midievent.c b/src/tools/midievent.c new file mode 100644 index 000000000..e7f08660b --- /dev/null +++ b/src/tools/midievent.c @@ -0,0 +1,550 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "midievent.h" + +static const char * const event_names[] = { + "Text", "Copyright", "Sequence/Track Name", + "Instrument", "Lyric", "Marker", "Cue Point", + "Program Name", "Device (Port) Name" +}; + +static const char * const note_names[] = { + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" +}; + +static const char * const controller_names[128] = { + [0] = "Bank Select (coarse)", + [1] = "Modulation Wheel (coarse)", + [2] = "Breath controller (coarse)", + [4] = "Foot Pedal (coarse)", + [5] = "Portamento Time (coarse)", + [6] = "Data Entry (coarse)", + [7] = "Volume (coarse)", + [8] = "Balance (coarse)", + [10] = "Pan position (coarse)", + [11] = "Expression (coarse)", + [12] = "Effect Control 1 (coarse)", + [13] = "Effect Control 2 (coarse)", + [16] = "General Purpose Slider 1", + [17] = "General Purpose Slider 2", + [18] = "General Purpose Slider 3", + [19] = "General Purpose Slider 4", + [32] = "Bank Select (fine)", + [33] = "Modulation Wheel (fine)", + [34] = "Breath (fine)", + [36] = "Foot Pedal (fine)", + [37] = "Portamento Time (fine)", + [38] = "Data Entry (fine)", + [39] = "Volume (fine)", + [40] = "Balance (fine)", + [42] = "Pan position (fine)", + [43] = "Expression (fine)", + [44] = "Effect Control 1 (fine)", + [45] = "Effect Control 2 (fine)", + [64] = "Hold Pedal (on/off)", + [65] = "Portamento (on/off)", + [66] = "Sustenuto Pedal (on/off)", + [67] = "Soft Pedal (on/off)", + [68] = "Legato Pedal (on/off)", + [69] = "Hold 2 Pedal (on/off)", + [70] = "Sound Variation", + [71] = "Sound Timbre", + [72] = "Sound Release Time", + [73] = "Sound Attack Time", + [74] = "Sound Brightness", + [75] = "Sound Control 6", + [76] = "Sound Control 7", + [77] = "Sound Control 8", + [78] = "Sound Control 9", + [79] = "Sound Control 10", + [80] = "General Purpose Button 1 (on/off)", + [81] = "General Purpose Button 2 (on/off)", + [82] = "General Purpose Button 3 (on/off)", + [83] = "General Purpose Button 4 (on/off)", + [91] = "Effects Level", + [92] = "Tremulo Level", + [93] = "Chorus Level", + [94] = "Celeste Level", + [95] = "Phaser Level", + [96] = "Data Button increment", + [97] = "Data Button decrement", + [98] = "Non-registered Parameter (fine)", + [99] = "Non-registered Parameter (coarse)", + [100] = "Registered Parameter (fine)", + [101] = "Registered Parameter (coarse)", + [120] = "All Sound Off", + [121] = "All Controllers Off", + [122] = "Local Keyboard (on/off)", + [123] = "All Notes Off", + [124] = "Omni Mode Off", + [125] = "Omni Mode On", + [126] = "Mono Operation", + [127] = "Poly Operation", +}; + +static const char * const program_names[] = { + "Acoustic Grand", "Bright Acoustic", "Electric Grand", "Honky-Tonk", + "Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet", + "Celesta", "Glockenspiel", "Music Box", "Vibraphone", "Marimba", + "Xylophone", "Tubular Bells", "Dulcimer", "Drawbar Organ", "Percussive Organ", + "Rock Organ", "Church Organ", "Reed Organ", "Accoridan", "Harmonica", + "Tango Accordion", "Nylon String Guitar", "Steel String Guitar", + "Electric Jazz Guitar", "Electric Clean Guitar", "Electric Muted Guitar", + "Overdriven Guitar", "Distortion Guitar", "Guitar Harmonics", + "Acoustic Bass", "Electric Bass (fingered)", "Electric Bass (picked)", + "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2", + "Violin", "Viola", "Cello", "Contrabass", "Tremolo Strings", "Pizzicato Strings", + "Orchestral Strings", "Timpani", "String Ensemble 1", "String Ensemble 2", + "SynthStrings 1", "SynthStrings 2", "Choir Aahs", "Voice Oohs", "Synth Voice", + "Orchestra Hit", "Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn", + "Brass Section", "SynthBrass 1", "SynthBrass 2", "Soprano Sax", "Alto Sax", + "Tenor Sax", "Baritone Sax", "Oboe", "English Horn", "Bassoon", "Clarinet", + "Piccolo", "Flute", "Recorder", "Pan Flute", "Blown Bottle", "Skakuhachi", + "Whistle", "Ocarina", "Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)", + "Lead 4 (chiff)", "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", + "Lead 8 (bass+lead)", "Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)", + "Pad 4 (choir)", "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", + "Pad 8 (sweep)", "FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)", + "FX 4 (atmosphere)", "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)", + "FX 8 (sci-fi)", "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bagpipe", + "Fiddle", "Shanai", "Tinkle Bell", "Agogo", "Steel Drums", "Woodblock", + "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal", "Guitar Fret Noise", + "Breath Noise", "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter", + "Applause", "Gunshot" +}; + +static const char * const smpte_rates[] = { + "24 fps", + "25 fps", + "30 fps (drop frame)", + "30 fps (non drop frame)" +}; + +static const char * const major_keys[] = { + "Unknown major", "Fb", "Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F", + "C", "G", "D", "A", "E", "B", "F#", "C#", "G#", "Unknown major" +}; + +static const char * const minor_keys[] = { + "Unknown minor", "Dbm", "Abm", "Ebm", "Bbm", "Fm", "Cm", "Gm", "Dm", + "Am", "Em", "Bm", "F#m", "C#m", "G#m", "D#m", "A#m", "E#m", "Unknown minor" +}; + +static const char *controller_name(uint8_t ctrl) +{ + if (ctrl > 127 || + controller_names[ctrl] == NULL) + return "Unknown"; + return controller_names[ctrl]; +} + +static void dump_mem(FILE *out, const char *label, const uint8_t *data, uint32_t size) +{ + fprintf(out, "%s: ", label); + while (size--) + fprintf(out, "%02x ", *data++); +} + +static int dump_event_midi1(FILE *out, const struct midi_event *ev) +{ + fprintf(out, "track:%2d sec:%f ", ev->track, ev->sec); + + switch (ev->data[0]) { + case 0x80 ... 0x8f: + fprintf(out, "Note Off (channel %2d): note %3s%d, velocity %3d", + (ev->data[0] & 0x0f) + 1, + note_names[ev->data[1] % 12], ev->data[1] / 12 -1, + ev->data[2]); + break; + case 0x90 ... 0x9f: + fprintf(out, "Note On (channel %2d): note %3s%d, velocity %3d", + (ev->data[0] & 0x0f) + 1, + note_names[ev->data[1] % 12], ev->data[1] / 12 -1, + ev->data[2]); + break; + case 0xa0 ... 0xaf: + fprintf(out, "Aftertouch (channel %2d): note %3s%d, pressure %3d", + (ev->data[0] & 0x0f) + 1, + note_names[ev->data[1] % 12], ev->data[1] / 12 -1, + ev->data[2]); + break; + case 0xb0 ... 0xbf: + fprintf(out, "Controller (channel %2d): controller %3d (%s), value %3d", + (ev->data[0] & 0x0f) + 1, ev->data[1], + controller_name(ev->data[1]), ev->data[2]); + break; + case 0xc0 ... 0xcf: + fprintf(out, "Program (channel %2d): program %3d (%s)", + (ev->data[0] & 0x0f) + 1, ev->data[1], + program_names[ev->data[1]]); + break; + case 0xd0 ... 0xdf: + fprintf(out, "Channel Pressure (channel %2d): pressure %3d", + (ev->data[0] & 0x0f) + 1, ev->data[1]); + break; + case 0xe0 ... 0xef: + fprintf(out, "Pitch Bend (channel %2d): value %d", (ev->data[0] & 0x0f) + 1, + ((int)ev->data[2] << 7 | ev->data[1]) - 0x2000); + break; + case 0xf0: + case 0xf7: + dump_mem(out, "SysEx", ev->data, ev->size); + break; + case 0xf1: + fprintf(out, "MIDI Time Code Quarter Frame: type %d values %d", + ev->data[0] >> 4, ev->data[0] & 0xf); + break; + case 0xf2: + fprintf(out, "Song Position Pointer: value %d", + ((int)ev->data[1] << 7 | ev->data[0])); + break; + case 0xf3: + fprintf(out, "Song Select: value %d", (ev->data[0] & 0x7f)); + break; + case 0xf6: + fprintf(out, "Tune Request"); + break; + case 0xf8: + fprintf(out, "Timing Clock"); + break; + case 0xfa: + fprintf(out, "Start Sequence"); + break; + case 0xfb: + fprintf(out, "Continue Sequence"); + break; + case 0xfc: + fprintf(out, "Stop Sequence"); + break; + case 0xfe: + fprintf(out, "Active Sensing"); + break; + case 0xff: + { + uint8_t *meta = &ev->data[ev->meta.offset]; + fprintf(out, "Meta: "); + switch (ev->data[1]) { + case 0x00: + fprintf(out, "Sequence Number %3d %3d", meta[0], meta[1]); + break; + case 0x01 ... 0x09: + fprintf(out, "%s: %s", event_names[ev->data[1] - 1], meta); + break; + case 0x20: + fprintf(out, "Channel Prefix: %03d", meta[0]); + break; + case 0x21: + fprintf(out, "Midi Port: %03d", meta[0]); + break; + case 0x2f: + fprintf(out, "End Of Track"); + break; + case 0x51: + fprintf(out, "Tempo: %d microseconds per quarter note, %.2f BPM", + ev->meta.parsed.tempo.uspqn, + 60000000.0 / (double)ev->meta.parsed.tempo.uspqn); + break; + case 0x54: + fprintf(out, "SMPTE Offset: %s %02d:%02d:%02d:%02d.%03d", + smpte_rates[(meta[0] & 0x60) >> 5], + meta[0] & 0x1f, meta[1], meta[2], + meta[3], meta[4]); + break; + case 0x58: + fprintf(out, "Time Signature: %d/%d, %d clocks per click, %d notated 32nd notes per quarter note", + meta[0], (int)pow(2, meta[1]), meta[2], meta[3]); + break; + case 0x59: + { + int sf = meta[0]; + fprintf(out, "Key Signature: %d %s: %s", abs(sf), + sf > 0 ? "sharps" : "flats", + meta[1] == 0 ? + major_keys[SPA_CLAMP(sf + 9, 0, 18)] : + minor_keys[SPA_CLAMP(sf + 9, 0, 18)]); + break; + } + case 0x7f: + dump_mem(out, "Sequencer", ev->data, ev->size); + break; + default: + dump_mem(out, "Invalid", ev->data, ev->size); + } + break; + } + default: + dump_mem(out, "Unknown", ev->data, ev->size); + break; + } + return 0; +} + +static int dump_event_midi2_channel(FILE *out, const struct midi_event *ev) +{ + uint32_t *d = (uint32_t*)ev->data; + uint8_t status = d[0] >> 16; + + fprintf(out, "track:%2d sec:%f ", ev->track, ev->sec); + + switch (status) { + case 0x00 ... 0x0f: + case 0x10 ... 0x1f: + { + uint8_t note = (d[0] >> 8) & 0x7f; + uint8_t index = d[0] & 0xff; + fprintf(out, "%s Per-Note controller (channel %2d): note %3s%d, index %u, value %u", + (status & 0xf0) == 0x00 ? "Registered" : "Assignable", + (status & 0x0f) + 1, + note_names[note % 12], note / 12 -1, index, d[1]); + break; + } + case 0x20 ... 0x2f: + case 0x30 ... 0x3f: + { + uint16_t index = (d[0] & 0x7f) | ((d[0] & 0x7f00) >> 1); + fprintf(out, "%s controller (channel %2d): index %u, value %u", + (status & 0xf0) == 0x20 ? "Registered" : "Assignable", + (status & 0x0f) + 1, index, d[1]); + break; + } + case 0x40 ... 0x4f: + case 0x50 ... 0x5f: + { + uint16_t index = (d[0] & 0x7f) | ((d[0] & 0x7f00) >> 1); + fprintf(out, "Relative %s controller (channel %2d): index %u, value %u", + (status & 0xf0) == 0x20 ? "Registered" : "Assignable", + (status & 0x0f) + 1, index, d[1]); + break; + } + case 0x60 ... 0x6f: + { + uint8_t note = (d[0] >> 8) & 0x7f; + fprintf(out, "Per-Note Pitch Bend (channel %2d): note %3s%d, pitch %u", + (status & 0x0f) + 1, + note_names[note % 12], note / 12 -1, d[1]); + break; + } + case 0x80 ... 0x8f: + { + uint8_t note = (d[0] >> 8) & 0x7f; + uint8_t attr_type = d[0] & 0xff; + uint16_t velocity = (d[1] >> 16) & 0xffff; + uint16_t attr_data = (d[1]) & 0xffff; + fprintf(out, "Note Off (channel %2d): note %3s%d, velocity %5d, attr (%u)%u", + (status & 0x0f) + 1, + note_names[note % 12], note / 12 -1, + velocity, attr_type, attr_data); + break; + } + case 0x90 ... 0x9f: + { + uint8_t note = (d[0] >> 8) & 0x7f; + uint8_t attr_type = d[0] & 0xff; + uint16_t velocity = (d[1] >> 16) & 0xffff; + uint16_t attr_data = (d[1]) & 0xffff; + fprintf(out, "Note On (channel %2d): note %3s%d, velocity %5d, attr (%u)%u", + (status & 0x0f) + 1, + note_names[note % 12], note / 12 -1, + velocity, attr_type, attr_data); + break; + } + case 0xa0 ... 0xaf: + { + uint8_t note = (d[0] >> 8) & 0x7f; + fprintf(out, "Aftertouch (channel %2d): note %3s%d, pressure %u", + (status & 0x0f) + 1, + note_names[note % 12], note / 12 -1, d[1]); + break; + } + case 0xb0 ... 0xbf: + { + uint8_t index = (d[0] >> 8) & 0x7f; + fprintf(out, "Controller (channel %2d): controller %3d (%s), value %u", + (status & 0x0f) + 1, index, + controller_name(index), d[1]); + break; + } + case 0xc0 ... 0xcf: + { + uint8_t flags = (d[0] & 0xff); + uint8_t program = (d[1] >> 24) & 0x7f; + uint16_t bank = (d[1] & 0x7f) | ((d[1] & 0x7f00) >> 1); + fprintf(out, "Program (channel %2d): flags %u program %3d (%s), bank %u", + (status & 0x0f) + 1, flags, program, + program_names[program], bank); + break; + } + case 0xd0 ... 0xdf: + fprintf(out, "Channel Pressure (channel %2d): pressure %u", + (status & 0x0f) + 1, d[1]); + break; + case 0xe0 ... 0xef: + fprintf(out, "Pitch Bend (channel %2d): value %u", + (status & 0x0f) + 1, d[1]); + break; + case 0xf0 ... 0xff: + { + uint8_t note = (d[0] >> 8) & 0x7f; + uint8_t flags = d[0] & 0xff; + fprintf(out, "Per-Note management (channel %2d): note %3s%d, flags %u", + (status & 0x0f) + 1, + note_names[note % 12], note / 12 -1, flags); + break; + } + default: + dump_mem(out, "Unknown", ev->data, ev->size); + break; + } + + return 0; +} + +static int dump_event_ump(FILE *out, const struct midi_event *ev) +{ + uint32_t *d = (uint32_t*)ev->data; + uint8_t group = (d[0] >> 24) & 0xf; + uint8_t mt = (d[0] >> 28) & 0xf; + int res = 0; + + fprintf(out, "group:%2d ", group); + + switch (mt) { + case 0x0: + switch ((d[0] >> 20) & 0xf) { + case 0x1: + fprintf(out, "JR clock: value %d", d[0] & 0xffff); + break; + case 0x2: + fprintf(out, "JR timestamp: value %d", d[0] & 0xffff); + break; + case 0x3: + fprintf(out, "DCTPQ: value %d", d[0] & 0xffff); + break; + case 0x4: + fprintf(out, "DC: value %d", d[0] & 0xfffff); + break; + default: + dump_mem(out, "Utility unkown", ev->data, ev->size); + } + break; + case 0x1: + { + uint8_t b[3] = { (d[0] >> 16) & 0x7f, (d[0] >> 8) & 0x7f, d[0] & 0x7f }; + switch (b[0]) { + case 0xf1: + fprintf(out, "MIDI Time Code Quarter Frame: type %d values %d", + b[1] >> 4, b[1] & 0xf); + break; + case 0xf2: + fprintf(out, "Song Position Pointer: value %d", + ((int)b[2] << 7 | b[1])); + break; + case 0xf3: + fprintf(out, "Song Select: value %d", b[1]); + break; + case 0xf6: + fprintf(out, "Tune Request"); + break; + case 0xf8: + fprintf(out, "Timing Clock"); + break; + case 0xfa: + fprintf(out, "Start Sequence"); + break; + case 0xfb: + fprintf(out, "Continue Sequence"); + break; + case 0xfc: + fprintf(out, "Stop Sequence"); + break; + case 0xfe: + fprintf(out, "Active Sensing"); + break; + case 0xff: + fprintf(out, "System Reset"); + break; + default: + dump_mem(out, "SysRT", ev->data, ev->size); + break; + } + break; + } + case 0x2: + { + struct midi_event ev1; + uint8_t b[3] = { d[0] >> 16, d[0] >> 8, d[0] }; + + ev1 = *ev; + if (b[0] >= 0xc0 && b[0] <= 0xdf) + ev1.size = 2; + else + ev1.size = 3; + ev1.data = b; + dump_event_midi1(out, &ev1); + break; + } + case 0x3: + { + uint8_t status = (d[0] >> 20) & 0xf; + uint8_t bytes = SPA_CLAMP((d[0] >> 16) & 0xf, 0u, 6u); + uint8_t b[6] = { d[0] >> 8, d[0], d[1] >> 24, d[1] >> 16, d[1] >> 8, d[1] }; + switch (status) { + case 0x0: + dump_mem(out, "SysEx7 (Complete) ", b, bytes); + break; + case 0x1: + dump_mem(out, "SysEx7 (Start) ", b, bytes); + break; + case 0x2: + dump_mem(out, "SysEx7 (Continue) ", b, bytes); + break; + case 0x3: + dump_mem(out, "SysEx7 (End) ", b, bytes); + break; + default: + dump_mem(out, "SysEx7 (invalid)", ev->data, ev->size); + break; + } + break; + } + case 0x4: + res = dump_event_midi2_channel(out, ev); + break; + case 0x5: + dump_mem(out, "Data128", ev->data, ev->size); + break; + default: + dump_mem(out, "Reserved", ev->data, ev->size); + break; + } + return res; +} + +int midi_event_dump(FILE *out, const struct midi_event *ev) +{ + int res; + switch (ev->type) { + case MIDI_EVENT_TYPE_MIDI1: + res = dump_event_midi1(out, ev); + break; + case MIDI_EVENT_TYPE_UMP: + res = dump_event_ump(out, ev); + break; + default: + return -EINVAL; + } + fprintf(out, "\n"); + return res; +} diff --git a/src/tools/midievent.h b/src/tools/midievent.h new file mode 100644 index 000000000..c2e1d978d --- /dev/null +++ b/src/tools/midievent.h @@ -0,0 +1,33 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2020 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef MIDI_EVENT_H +#define MIDI_EVENT_H + +#include + +#include + +struct midi_event { +#define MIDI_EVENT_TYPE_MIDI1 0 +#define MIDI_EVENT_TYPE_UMP 1 + uint32_t type; + uint32_t track; + double sec; + uint8_t *data; + uint32_t size; + struct { + uint32_t offset; + uint32_t size; + union { + struct { + uint32_t uspqn; /* microseconds per quarter note */ + } tempo; + } parsed; + } meta; +}; + +int midi_event_dump(FILE *out, const struct midi_event *event); + +#endif /* MIDI_EVENT_H */ diff --git a/src/tools/midifile.c b/src/tools/midifile.c index 80c5bbb1c..974cec6e5 100644 --- a/src/tools/midifile.c +++ b/src/tools/midifile.c @@ -58,6 +58,17 @@ static inline uint32_t parse_be32(const uint8_t *in) return (in[0] << 24) | (in[1] << 16) | (in[2] << 8) | in[3]; } +static inline int mf_seek(struct midi_file *mf, long offs) +{ + int res; + if (mf->pos == offs) + return 0; + if ((res = fseek(mf->file, offs, SEEK_SET)) != 0) + return -errno; + mf->pos = offs; + return 0; +} + static inline int mf_read(struct midi_file *mf, void *data, size_t size) { if (fread(data, size, 1, mf->file) != 1) @@ -178,10 +189,8 @@ static int open_read(struct midi_file *mf, const char *filename, struct midi_fil tr->id = i; if (i + 1 < mf->info.ntracks && - fseek(mf->file, tr->start + tr->size, SEEK_SET) != 0) { - res = -errno; + (res = mf_seek(mf, tr->start + tr->size)) < 0) goto exit_close; - } } mf->mode = 1; *info = mf->info; @@ -218,7 +227,7 @@ static int write_headers(struct midi_file *mf) struct midi_track *tr = &mf->tracks[0]; int res; - fseek(mf->file, 0, SEEK_SET); + mf_seek(mf, 0); mf->length = 6; CHECK_RES(write_n(mf->file, "MThd", 4)); @@ -302,7 +311,7 @@ int midi_file_close(struct midi_file *mf) CHECK_RES(write_n(mf->file, buf, 4)); mf->tracks[0].size += 4; CHECK_RES(write_headers(mf)); - } else + } else if (mf->mode != 1) return -EINVAL; if (mf->close) @@ -351,7 +360,6 @@ int midi_file_read_event(struct midi_file *mf, struct midi_event *event) uint32_t size; uint8_t status, meta; int res, running; - long offs; event->data = NULL; @@ -360,11 +368,8 @@ int midi_file_read_event(struct midi_file *mf, struct midi_event *event) tr = &mf->tracks[event->track]; - offs = tr->pos; - if (offs != mf->pos) { - if (fseek(mf->file, offs, SEEK_SET) != 0) - return -errno; - } + if ((res = mf_seek(mf, tr->pos)) < 0) + return res; mf_read(mf, &status, 1); @@ -491,524 +496,45 @@ int midi_file_write_event(struct midi_file *mf, const struct midi_event *event) { struct midi_track *tr; uint32_t tick; - void *data; + void *data, *ev_data; size_t size; - int res; + int res, ev_size; uint8_t ev[32]; + uint64_t state = 0; spa_return_val_if_fail(event != NULL, -EINVAL); spa_return_val_if_fail(mf != NULL, -EINVAL); spa_return_val_if_fail(event->track == 0, -EINVAL); spa_return_val_if_fail(event->size > 1, -EINVAL); - switch (event->type) { - case MIDI_EVENT_TYPE_MIDI1: - data = event->data; - size = event->size; - break; - case MIDI_EVENT_TYPE_UMP: - data = ev; - size = spa_ump_to_midi((uint32_t*)event->data, event->size, ev, sizeof(ev)); - if (size == 0) - return 0; - break; - default: - return -EINVAL; - } + data = event->data; + size = event->size; tr = &mf->tracks[event->track]; - tick = (uint32_t)(event->sec * (1000000.0 * mf->info.division) / (double)mf->tempo); - CHECK_RES(write_varlen(mf, tr, tick - tr->tick)); - tr->tick = tick; - - CHECK_RES(write_n(mf->file, data, size)); - tr->size += size; - - return 0; -} - -static const char * const event_names[] = { - "Text", "Copyright", "Sequence/Track Name", - "Instrument", "Lyric", "Marker", "Cue Point", - "Program Name", "Device (Port) Name" -}; - -static const char * const note_names[] = { - "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" -}; - -static const char * const controller_names[128] = { - [0] = "Bank Select (coarse)", - [1] = "Modulation Wheel (coarse)", - [2] = "Breath controller (coarse)", - [4] = "Foot Pedal (coarse)", - [5] = "Portamento Time (coarse)", - [6] = "Data Entry (coarse)", - [7] = "Volume (coarse)", - [8] = "Balance (coarse)", - [10] = "Pan position (coarse)", - [11] = "Expression (coarse)", - [12] = "Effect Control 1 (coarse)", - [13] = "Effect Control 2 (coarse)", - [16] = "General Purpose Slider 1", - [17] = "General Purpose Slider 2", - [18] = "General Purpose Slider 3", - [19] = "General Purpose Slider 4", - [32] = "Bank Select (fine)", - [33] = "Modulation Wheel (fine)", - [34] = "Breath (fine)", - [36] = "Foot Pedal (fine)", - [37] = "Portamento Time (fine)", - [38] = "Data Entry (fine)", - [39] = "Volume (fine)", - [40] = "Balance (fine)", - [42] = "Pan position (fine)", - [43] = "Expression (fine)", - [44] = "Effect Control 1 (fine)", - [45] = "Effect Control 2 (fine)", - [64] = "Hold Pedal (on/off)", - [65] = "Portamento (on/off)", - [66] = "Sustenuto Pedal (on/off)", - [67] = "Soft Pedal (on/off)", - [68] = "Legato Pedal (on/off)", - [69] = "Hold 2 Pedal (on/off)", - [70] = "Sound Variation", - [71] = "Sound Timbre", - [72] = "Sound Release Time", - [73] = "Sound Attack Time", - [74] = "Sound Brightness", - [75] = "Sound Control 6", - [76] = "Sound Control 7", - [77] = "Sound Control 8", - [78] = "Sound Control 9", - [79] = "Sound Control 10", - [80] = "General Purpose Button 1 (on/off)", - [81] = "General Purpose Button 2 (on/off)", - [82] = "General Purpose Button 3 (on/off)", - [83] = "General Purpose Button 4 (on/off)", - [91] = "Effects Level", - [92] = "Tremulo Level", - [93] = "Chorus Level", - [94] = "Celeste Level", - [95] = "Phaser Level", - [96] = "Data Button increment", - [97] = "Data Button decrement", - [98] = "Non-registered Parameter (fine)", - [99] = "Non-registered Parameter (coarse)", - [100] = "Registered Parameter (fine)", - [101] = "Registered Parameter (coarse)", - [120] = "All Sound Off", - [121] = "All Controllers Off", - [122] = "Local Keyboard (on/off)", - [123] = "All Notes Off", - [124] = "Omni Mode Off", - [125] = "Omni Mode On", - [126] = "Mono Operation", - [127] = "Poly Operation", -}; - -static const char * const program_names[] = { - "Acoustic Grand", "Bright Acoustic", "Electric Grand", "Honky-Tonk", - "Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet", - "Celesta", "Glockenspiel", "Music Box", "Vibraphone", "Marimba", - "Xylophone", "Tubular Bells", "Dulcimer", "Drawbar Organ", "Percussive Organ", - "Rock Organ", "Church Organ", "Reed Organ", "Accoridan", "Harmonica", - "Tango Accordion", "Nylon String Guitar", "Steel String Guitar", - "Electric Jazz Guitar", "Electric Clean Guitar", "Electric Muted Guitar", - "Overdriven Guitar", "Distortion Guitar", "Guitar Harmonics", - "Acoustic Bass", "Electric Bass (fingered)", "Electric Bass (picked)", - "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2", - "Violin", "Viola", "Cello", "Contrabass", "Tremolo Strings", "Pizzicato Strings", - "Orchestral Strings", "Timpani", "String Ensemble 1", "String Ensemble 2", - "SynthStrings 1", "SynthStrings 2", "Choir Aahs", "Voice Oohs", "Synth Voice", - "Orchestra Hit", "Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn", - "Brass Section", "SynthBrass 1", "SynthBrass 2", "Soprano Sax", "Alto Sax", - "Tenor Sax", "Baritone Sax", "Oboe", "English Horn", "Bassoon", "Clarinet", - "Piccolo", "Flute", "Recorder", "Pan Flute", "Blown Bottle", "Skakuhachi", - "Whistle", "Ocarina", "Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)", - "Lead 4 (chiff)", "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", - "Lead 8 (bass+lead)", "Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)", - "Pad 4 (choir)", "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", - "Pad 8 (sweep)", "FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)", - "FX 4 (atmosphere)", "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)", - "FX 8 (sci-fi)", "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bagpipe", - "Fiddle", "Shanai", "Tinkle Bell", "Agogo", "Steel Drums", "Woodblock", - "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal", "Guitar Fret Noise", - "Breath Noise", "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter", - "Applause", "Gunshot" -}; - -static const char * const smpte_rates[] = { - "24 fps", - "25 fps", - "30 fps (drop frame)", - "30 fps (non drop frame)" -}; - -static const char * const major_keys[] = { - "Unknown major", "Fb", "Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F", - "C", "G", "D", "A", "E", "B", "F#", "C#", "G#", "Unknown major" -}; - -static const char * const minor_keys[] = { - "Unknown minor", "Dbm", "Abm", "Ebm", "Bbm", "Fm", "Cm", "Gm", "Dm", - "Am", "Em", "Bm", "F#m", "C#m", "G#m", "D#m", "A#m", "E#m", "Unknown minor" -}; - -static const char *controller_name(uint8_t ctrl) -{ - if (ctrl > 127 || - controller_names[ctrl] == NULL) - return "Unknown"; - return controller_names[ctrl]; -} - -static void dump_mem(FILE *out, const char *label, uint8_t *data, uint32_t size) -{ - fprintf(out, "%s: ", label); - while (size--) - fprintf(out, "%02x ", *data++); -} - -static int dump_event_midi1(FILE *out, const struct midi_event *ev) -{ - fprintf(out, "track:%2d sec:%f ", ev->track, ev->sec); - - switch (ev->data[0]) { - case 0x80 ... 0x8f: - fprintf(out, "Note Off (channel %2d): note %3s%d, velocity %3d", - (ev->data[0] & 0x0f) + 1, - note_names[ev->data[1] % 12], ev->data[1] / 12 -1, - ev->data[2]); - break; - case 0x90 ... 0x9f: - fprintf(out, "Note On (channel %2d): note %3s%d, velocity %3d", - (ev->data[0] & 0x0f) + 1, - note_names[ev->data[1] % 12], ev->data[1] / 12 -1, - ev->data[2]); - break; - case 0xa0 ... 0xaf: - fprintf(out, "Aftertouch (channel %2d): note %3s%d, pressure %3d", - (ev->data[0] & 0x0f) + 1, - note_names[ev->data[1] % 12], ev->data[1] / 12 -1, - ev->data[2]); - break; - case 0xb0 ... 0xbf: - fprintf(out, "Controller (channel %2d): controller %3d (%s), value %3d", - (ev->data[0] & 0x0f) + 1, ev->data[1], - controller_name(ev->data[1]), ev->data[2]); - break; - case 0xc0 ... 0xcf: - fprintf(out, "Program (channel %2d): program %3d (%s)", - (ev->data[0] & 0x0f) + 1, ev->data[1], - program_names[ev->data[1]]); - break; - case 0xd0 ... 0xdf: - fprintf(out, "Channel Pressure (channel %2d): pressure %3d", - (ev->data[0] & 0x0f) + 1, ev->data[1]); - break; - case 0xe0 ... 0xef: - fprintf(out, "Pitch Bend (channel %2d): value %d", (ev->data[0] & 0x0f) + 1, - ((int)ev->data[2] << 7 | ev->data[1]) - 0x2000); - break; - case 0xf0: - case 0xf7: - dump_mem(out, "SysEx", ev->data, ev->size); - break; - case 0xf1: - fprintf(out, "MIDI Time Code Quarter Frame: type %d values %d", - ev->data[0] >> 4, ev->data[0] & 0xf); - break; - case 0xf2: - fprintf(out, "Song Position Pointer: value %d", - ((int)ev->data[1] << 7 | ev->data[0])); - break; - case 0xf3: - fprintf(out, "Song Select: value %d", (ev->data[0] & 0x7f)); - break; - case 0xf6: - fprintf(out, "Tune Request"); - break; - case 0xf8: - fprintf(out, "Timing Clock"); - break; - case 0xfa: - fprintf(out, "Start Sequence"); - break; - case 0xfb: - fprintf(out, "Continue Sequence"); - break; - case 0xfc: - fprintf(out, "Stop Sequence"); - break; - case 0xfe: - fprintf(out, "Active Sensing"); - break; - case 0xff: - { - uint8_t *meta = &ev->data[ev->meta.offset]; - fprintf(out, "Meta: "); - switch (ev->data[1]) { - case 0x00: - fprintf(out, "Sequence Number %3d %3d", meta[0], meta[1]); + while (size > 0) { + switch (event->type) { + case MIDI_EVENT_TYPE_MIDI1: + ev_data = data; + ev_size = size; + size = 0; break; - case 0x01 ... 0x09: - fprintf(out, "%s: %s", event_names[ev->data[1] - 1], meta); - break; - case 0x20: - fprintf(out, "Channel Prefix: %03d", meta[0]); - break; - case 0x21: - fprintf(out, "Midi Port: %03d", meta[0]); - break; - case 0x2f: - fprintf(out, "End Of Track"); - break; - case 0x51: - fprintf(out, "Tempo: %d microseconds per quarter note, %.2f BPM", - ev->meta.parsed.tempo.uspqn, - 60000000.0 / (double)ev->meta.parsed.tempo.uspqn); - break; - case 0x54: - fprintf(out, "SMPTE Offset: %s %02d:%02d:%02d:%02d.%03d", - smpte_rates[(meta[0] & 0x60) >> 5], - meta[0] & 0x1f, meta[1], meta[2], - meta[3], meta[4]); - break; - case 0x58: - fprintf(out, "Time Signature: %d/%d, %d clocks per click, %d notated 32nd notes per quarter note", - meta[0], (int)pow(2, meta[1]), meta[2], meta[3]); - break; - case 0x59: - { - int sf = meta[0]; - fprintf(out, "Key Signature: %d %s: %s", abs(sf), - sf > 0 ? "sharps" : "flats", - meta[1] == 0 ? - major_keys[SPA_CLAMP(sf + 9, 0, 18)] : - minor_keys[SPA_CLAMP(sf + 9, 0, 18)]); - break; - } - case 0x7f: - dump_mem(out, "Sequencer", ev->data, ev->size); + case MIDI_EVENT_TYPE_UMP: + ev_size = spa_ump_to_midi((const uint32_t**)&data, &size, ev, sizeof(ev), &state); + if (ev_size <= 0) + return ev_size; + ev_data = ev; break; default: - dump_mem(out, "Invalid", ev->data, ev->size); + return -EINVAL; } - break; - } - default: - dump_mem(out, "Unknown", ev->data, ev->size); - break; + + CHECK_RES(write_varlen(mf, tr, tick - tr->tick)); + tr->tick = tick; + + CHECK_RES(write_n(mf->file, ev_data, ev_size)); + tr->size += ev_size; } return 0; } - -static int dump_event_midi2_channel(FILE *out, const struct midi_event *ev) -{ - uint32_t *d = (uint32_t*)ev->data; - uint8_t status = d[0] >> 16; - - fprintf(out, "track:%2d sec:%f ", ev->track, ev->sec); - - switch (status) { - case 0x00 ... 0x0f: - case 0x10 ... 0x1f: - { - uint8_t note = (d[0] >> 8) & 0x7f; - uint8_t index = d[0] & 0xff; - fprintf(out, "%s Per-Note controller (channel %2d): note %3s%d, index %u, value %u", - (status & 0xf0) == 0x00 ? "Registered" : "Assignable", - (status & 0x0f) + 1, - note_names[note % 12], note / 12 -1, index, d[1]); - break; - } - case 0x20 ... 0x2f: - case 0x30 ... 0x3f: - { - uint16_t index = (d[0] & 0x7f) | ((d[0] & 0x7f00) >> 1); - fprintf(out, "%s controller (channel %2d): index %u, value %u", - (status & 0xf0) == 0x20 ? "Registered" : "Assignable", - (status & 0x0f) + 1, index, d[1]); - break; - } - case 0x40 ... 0x4f: - case 0x50 ... 0x5f: - { - uint16_t index = (d[0] & 0x7f) | ((d[0] & 0x7f00) >> 1); - fprintf(out, "Relative %s controller (channel %2d): index %u, value %u", - (status & 0xf0) == 0x20 ? "Registered" : "Assignable", - (status & 0x0f) + 1, index, d[1]); - break; - } - case 0x60 ... 0x6f: - { - uint8_t note = (d[0] >> 8) & 0x7f; - fprintf(out, "Per-Note Pitch Bend (channel %2d): note %3s%d, pitch %u", - (status & 0x0f) + 1, - note_names[note % 12], note / 12 -1, d[1]); - break; - } - case 0x80 ... 0x8f: - { - uint8_t note = (d[0] >> 8) & 0x7f; - uint8_t attr_type = d[0] & 0xff; - uint16_t velocity = (d[1] >> 16) & 0xffff; - uint16_t attr_data = (d[1]) & 0xffff; - fprintf(out, "Note Off (channel %2d): note %3s%d, velocity %5d, attr (%u)%u", - (status & 0x0f) + 1, - note_names[note % 12], note / 12 -1, - velocity, attr_type, attr_data); - break; - } - case 0x90 ... 0x9f: - { - uint8_t note = (d[0] >> 8) & 0x7f; - uint8_t attr_type = d[0] & 0xff; - uint16_t velocity = (d[1] >> 16) & 0xffff; - uint16_t attr_data = (d[1]) & 0xffff; - fprintf(out, "Note On (channel %2d): note %3s%d, velocity %5d, attr (%u)%u", - (status & 0x0f) + 1, - note_names[note % 12], note / 12 -1, - velocity, attr_type, attr_data); - break; - } - case 0xa0 ... 0xaf: - { - uint8_t note = (d[0] >> 8) & 0x7f; - fprintf(out, "Aftertouch (channel %2d): note %3s%d, pressure %u", - (status & 0x0f) + 1, - note_names[note % 12], note / 12 -1, d[1]); - break; - } - case 0xb0 ... 0xbf: - { - uint8_t index = (d[0] >> 8) & 0x7f; - fprintf(out, "Controller (channel %2d): controller %3d (%s), value %u", - (status & 0x0f) + 1, index, - controller_name(index), d[1]); - break; - } - case 0xc0 ... 0xcf: - { - uint8_t flags = (d[0] & 0xff); - uint8_t program = (d[1] >> 24) & 0x7f; - uint16_t bank = (d[1] & 0x7f) | ((d[1] & 0x7f00) >> 1); - fprintf(out, "Program (channel %2d): flags %u program %3d (%s), bank %u", - (status & 0x0f) + 1, flags, program, - program_names[program], bank); - break; - } - case 0xd0 ... 0xdf: - fprintf(out, "Channel Pressure (channel %2d): pressure %u", - (status & 0x0f) + 1, d[1]); - break; - case 0xe0 ... 0xef: - fprintf(out, "Pitch Bend (channel %2d): value %u", - (status & 0x0f) + 1, d[1]); - break; - case 0xf0 ... 0xff: - { - uint8_t note = (d[0] >> 8) & 0x7f; - uint8_t flags = d[0] & 0xff; - fprintf(out, "Per-Note management (channel %2d): note %3s%d, flags %u", - (status & 0x0f) + 1, - note_names[note % 12], note / 12 -1, flags); - break; - } - default: - dump_mem(out, "Unknown", ev->data, ev->size); - break; - } - - return 0; -} - -static int dump_event_ump(FILE *out, const struct midi_event *ev) -{ - uint32_t *d = (uint32_t*)ev->data; - uint8_t group = (d[0] >> 24) & 0xf; - uint8_t mt = (d[0] >> 28) & 0xf; - int res = 0; - - fprintf(out, "group:%2d ", group); - - switch (mt) { - case 0x0: - dump_mem(out, "Utility", ev->data, ev->size); - break; - case 0x1: - dump_mem(out, "SysRT", ev->data, ev->size); - break; - case 0x2: - { - struct midi_event ev1; - uint8_t msg[4]; - - ev1 = *ev; - msg[0] = (d[0] >> 16); - msg[1] = (d[0] >> 8); - msg[2] = (d[0]); - if (msg[0] >= 0xc0 && msg[0] <= 0xdf) - ev1.size = 2; - else - ev1.size = 3; - ev1.data = msg; - dump_event_midi1(out, &ev1); - break; - } - case 0x3: - { - uint8_t status = (d[0] >> 20) & 0xf; - uint8_t bytes = SPA_CLAMP((d[0] >> 16) & 0xf, 0u, 6u); - uint8_t b[6] = { d[0] >> 8, d[0], d[1] >> 24, d[1] >> 16, d[1] >> 8, d[1] }; - switch (status) { - case 0x0: - dump_mem(out, "SysEx7 (Complete) ", b, bytes); - break; - case 0x1: - dump_mem(out, "SysEx7 (Start) ", b, bytes); - break; - case 0x2: - dump_mem(out, "SysEx7 (Continue) ", b, bytes); - break; - case 0x3: - dump_mem(out, "SysEx7 (End) ", b, bytes); - break; - default: - dump_mem(out, "SysEx7 (invalid)", ev->data, ev->size); - break; - } - break; - } - case 0x4: - res = dump_event_midi2_channel(out, ev); - break; - case 0x5: - dump_mem(out, "Data128", ev->data, ev->size); - break; - default: - dump_mem(out, "Reserved", ev->data, ev->size); - break; - } - return res; -} - -int midi_file_dump_event(FILE *out, const struct midi_event *ev) -{ - int res; - switch (ev->type) { - case MIDI_EVENT_TYPE_MIDI1: - res = dump_event_midi1(out, ev); - break; - case MIDI_EVENT_TYPE_UMP: - res = dump_event_ump(out, ev); - break; - default: - return -EINVAL; - } - fprintf(out, "\n"); - return res; -} diff --git a/src/tools/midifile.h b/src/tools/midifile.h index 6b3c23b2a..375877311 100644 --- a/src/tools/midifile.h +++ b/src/tools/midifile.h @@ -6,26 +6,9 @@ #include -struct midi_file; +#include "midievent.h" -struct midi_event { -#define MIDI_EVENT_TYPE_MIDI1 0 -#define MIDI_EVENT_TYPE_UMP 1 - uint32_t type; - uint32_t track; - double sec; - uint8_t *data; - uint32_t size; - struct { - uint32_t offset; - uint32_t size; - union { - struct { - uint32_t uspqn; /* microseconds per quarter note */ - } tempo; - } parsed; - } meta; -}; +struct midi_file; struct midi_file_info { uint16_t format; @@ -43,5 +26,3 @@ int midi_file_next_time(struct midi_file *mf, double *sec); int midi_file_read_event(struct midi_file *mf, struct midi_event *event); int midi_file_write_event(struct midi_file *mf, const struct midi_event *event); - -int midi_file_dump_event(FILE *out, const struct midi_event *event); diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index f485a08f1..632a513bf 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -3,6 +3,8 @@ /* @author Pantelis Antoniou */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include #include #include @@ -34,15 +36,23 @@ #include #include -#include "config.h" - #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION #include #include #include + +#ifndef AV_PROFILE_DTS_HD_MA +#define AV_PROFILE_DTS_HD_MA FF_PROFILE_DTS_HD_MA +#endif + +#ifndef AV_PROFILE_DTS_HD_HRA +#define AV_PROFILE_DTS_HD_HRA FF_PROFILE_DTS_HD_HRA +#endif + #endif #include "midifile.h" +#include "midiclip.h" #include "dfffile.h" #include "dsffile.h" @@ -60,6 +70,8 @@ #define DEFAULT_VOLUME 1.0 #define DEFAULT_QUALITY 4 +#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS + enum mode { mode_none, mode_playback, @@ -81,7 +93,7 @@ typedef int (*fill_fn)(struct data *d, void *dest, unsigned int n_frames, bool * struct channelmap { uint32_t n_channels; - uint32_t channels[SPA_AUDIO_MAX_CHANNELS]; + uint32_t channels[MAX_CHANNELS]; }; struct data { @@ -103,6 +115,8 @@ struct data { #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION #define TYPE_ENCODED 3 #endif +#define TYPE_SYSEX 4 +#define TYPE_MIDI2 5 int data_type; bool raw; const char *remote_name; @@ -141,7 +155,15 @@ struct data { struct { struct midi_file *file; struct midi_file_info info; +#define MIDI_FORCE_NONE 0 +#define MIDI_FORCE_UMP 1 +#define MIDI_FORCE_MIDI1 2 + int force_type; } midi; + struct { + struct midi_clip *file; + struct midi_clip_info info; + } clip; struct { struct dsf_file *file; struct dsf_file_info info; @@ -162,6 +184,12 @@ struct data { int64_t accumulated_excess_playtime; } encoded; #endif + struct { + FILE *file; + } sysex; + + uint64_t sample_limit; /* 0 means unlimited */ + uint64_t samples_processed; }; #define STR_FMTS "(ulaw|alaw|u8|s8|s16|s32|f32|f64)" @@ -249,8 +277,10 @@ static int encoded_playback_fill(struct data *d, void *dest, unsigned int n_fram { AVPacket *packet = d->encoded.packet; int ret; + uint8_t *dest_ptr = (uint8_t *)dest; struct pw_time time; int64_t quantum_duration; + int64_t accumulated_duration; int64_t excess_playtime; int64_t cycle_length; int64_t av_time_base_num, av_time_base_denom; @@ -298,32 +328,42 @@ static int encoded_playback_fill(struct data *d, void *dest, unsigned int n_fram return 0; } - /* Keep reading packets until we get one from the stream we are - * interested in. This is relevant when playing data that contains - * several multiplexed streams. */ - while (true) { - if ((ret = av_read_frame(d->encoded.format_context, packet) < 0)) - break; + accumulated_duration = 0; - if (packet->stream_index == d->encoded.stream_index) + /* Continue filling the buffer until at least a quantum's worth of data + * has been written. Encoded frames may have a playtime that is _shorter_ + * than a quantum. In that case, multiple frames must be written into the + * buffer to cover a quantum length. */ + while (true) { + /* Keep reading packets until we get one from the stream we are + * interested in. This is relevant when playing data that contains + * several multiplexed streams. */ + while (true) { + if ((ret = av_read_frame(d->encoded.format_context, packet) < 0)) + break; + + if (packet->stream_index == d->encoded.stream_index) + break; + } + + memcpy(dest_ptr, packet->data, packet->size); + + accumulated_duration += packet->duration; + dest_ptr += packet->size; + + if (accumulated_duration >= quantum_duration) + { + excess_playtime = accumulated_duration - quantum_duration; + d->encoded.accumulated_excess_playtime += excess_playtime; break; + } } - memcpy(dest, packet->data, packet->size); - - if (packet->duration > quantum_duration) - excess_playtime = packet->duration - quantum_duration; - else - excess_playtime = 0; - d->encoded.accumulated_excess_playtime += excess_playtime; - - return packet->size; + return (dest_ptr - ((uint8_t*)dest)); } static int av_codec_params_to_audio_info(struct data *data, AVCodecParameters *codec_params, struct spa_audio_info *info) { - int32_t profile; - switch (codec_params->codec_id) { case AV_CODEC_ID_VORBIS: info->media_subtype = SPA_MEDIA_SUBTYPE_vorbis; @@ -348,32 +388,50 @@ static int av_codec_params_to_audio_info(struct data *data, AVCodecParameters *c case AV_CODEC_ID_WMAVOICE: case AV_CODEC_ID_WMALOSSLESS: info->media_subtype = SPA_MEDIA_SUBTYPE_wma; + info->info.wma.rate = data->rate; + info->info.wma.channels = data->channels; + info->info.wma.bitrate = data->bitrate; + info->info.wma.block_align = codec_params->block_align; + /* The codec tag is not decided by FFmpeg; instead, it is + * directly taken from the container. FFmpeg currently has + * no named constants for the WMA versions, so numeric + * constants have to be used here instead. */ switch (codec_params->codec_tag) { - /* TODO see if these hex constants can be replaced by named constants from FFmpeg */ + /* This originates from Microsoft's mmreg.h , where + * 0x160 is specified as meaning WMA v1 (which is commonly + * referred to as WMA 7). */ + case 0x160: + info->info.wma.profile = SPA_AUDIO_WMA_PROFILE_WMA7; + break; + /* This originates from Microsoft's mmreg.h , where + * 0x161 is specified as meaning WMA v2 (which is commonly + * referred to as WMA 8 or 9). */ case 0x161: - profile = SPA_AUDIO_WMA_PROFILE_WMA9; + info->info.wma.profile = SPA_AUDIO_WMA_PROFILE_WMA9; break; + /* This originates from Microsoft's mmreg.h , where + * 0x162 is specified as meaning WMA v3 (which is commonly + * referred to as WMA 9 Pro). */ case 0x162: - profile = SPA_AUDIO_WMA_PROFILE_WMA9_PRO; + info->info.wma.profile = SPA_AUDIO_WMA_PROFILE_WMA9_PRO; break; + /* This originates from Microsoft's mmreg.h , where + * 0x163 is specified as meaning WMA 9 lossless. */ case 0x163: - profile = SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS; + info->info.wma.profile = SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS; break; + /* WMA 10 is not mentioned in mmreg.h - these were empirically + * determined instead. */ case 0x166: - profile = SPA_AUDIO_WMA_PROFILE_WMA10; + info->info.wma.profile = SPA_AUDIO_WMA_PROFILE_WMA10; break; case 0x167: - profile = SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS; + info->info.wma.profile = SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS; break; default: fprintf(stderr, "error: invalid WMA profile\n"); return -EINVAL; } - info->info.wma.rate = data->rate; - info->info.wma.channels = data->channels; - info->info.wma.bitrate = data->bitrate; - info->info.wma.block_align = codec_params->block_align; - info->info.wma.profile = profile; break; case AV_CODEC_ID_FLAC: info->media_subtype = SPA_MEDIA_SUBTYPE_flac; @@ -408,6 +466,41 @@ static int av_codec_params_to_audio_info(struct data *data, AVCodecParameters *c info->info.amr.channels = data->channels; info->info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_WB; break; + case AV_CODEC_ID_AC3: + info->media_subtype = SPA_MEDIA_SUBTYPE_ac3; + info->info.ac3.rate = data->rate; + info->info.ac3.channels = data->channels; + break; + case AV_CODEC_ID_EAC3: + info->media_subtype = SPA_MEDIA_SUBTYPE_eac3; + info->info.eac3.rate = data->rate; + info->info.eac3.channels = data->channels; + break; + case AV_CODEC_ID_TRUEHD: + info->media_subtype = SPA_MEDIA_SUBTYPE_truehd; + info->info.truehd.rate = data->rate; + info->info.truehd.channels = data->channels; + break; + case AV_CODEC_ID_DTS: + info->media_subtype = SPA_MEDIA_SUBTYPE_dts; + info->info.dts.rate = data->rate; + info->info.dts.channels = data->channels; + switch (codec_params->profile) { + case AV_PROFILE_DTS_HD_MA: + info->info.dts.ext_type = SPA_AUDIO_DTS_EXT_HD_MA; + break; + case AV_PROFILE_DTS_HD_HRA: + info->info.dts.ext_type = SPA_AUDIO_DTS_EXT_HD_HRA; + break; + default: + info->info.dts.ext_type = SPA_AUDIO_DTS_EXT_NONE; + break; + } + break; + case AV_CODEC_ID_MPEGH_3D_AUDIO: + info->media_subtype = SPA_MEDIA_SUBTYPE_mpegh; + info->info.mpegh.rate = data->rate; + break; default: fprintf(stderr, "Unsupported encoded media subtype\n"); return -EINVAL; @@ -611,7 +704,8 @@ static int parse_channelmap(const char *channel_map, struct channelmap *map) } } - spa_audio_parse_position(channel_map, strlen(channel_map), map->channels, &map->n_channels); + spa_audio_parse_position_n(channel_map, strlen(channel_map), + map->channels, SPA_N_ELEMENTS(map->channels), &map->n_channels); return 0; } @@ -653,10 +747,11 @@ static int channelmap_default(struct channelmap *map, int n_channels) static void channelmap_print(struct channelmap *map) { uint32_t i; - + char pos[8]; for (i = 0; i < map->n_channels; i++) { - const char *name = spa_type_audio_channel_to_short_name(map->channels[i]); - fprintf(stderr, "%s%s", name, i + 1 < map->n_channels ? "," : ""); + fprintf(stderr, "%s%s", i ? "," : "", + spa_type_audio_channel_make_short_name(map->channels[i], + pos, sizeof(pos), "UNK")); } } @@ -787,7 +882,7 @@ on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) data->dff.layout.channels = info.info.dsd.channels; data->dff.layout.lsb = info.info.dsd.bitorder == SPA_PARAM_BITORDER_lsb; - data->stride = data->dsf.layout.channels * SPA_ABS(data->dsf.layout.interleave); + data->stride = dsf_layout_stride(&data->dsf.layout); if (data->verbose) { fprintf(stderr, "DSD: channels:%d bitorder:%s interleave:%d stride:%d\n", @@ -831,8 +926,20 @@ static void on_process(void *userdata) * fill callback actually returns number of bytes, not frames, since * this is encoded data. However, the calculations below still work * out because the stride is set to 1 in setup_encodedfile(). */ + if (data->sample_limit > 0) { + uint64_t samples_left = data->sample_limit - data->samples_processed; + if (samples_left == 0) { + pw_main_loop_quit(data->loop); + return; + } + n_frames = SPA_MIN(n_frames, (int)samples_left); + } + n_fill_frames = data->fill(data, p, n_frames, &null_frame); + if (data->sample_limit > 0 && n_fill_frames > 0) + data->samples_processed += n_fill_frames; + if (null_frame) { /* A null frame is not to be confused with the drain scenario. * In this case, we want to continue streaming, but in this @@ -864,8 +971,20 @@ static void on_process(void *userdata) n_frames = size / data->stride; + if (data->sample_limit > 0) { + uint64_t samples_left = data->sample_limit - data->samples_processed; + if (samples_left == 0) { + pw_main_loop_quit(data->loop); + return; + } + n_frames = SPA_MIN(n_frames, (int)samples_left); + } + n_fill_frames = data->fill(data, p, n_frames, &null_frame); + if (data->sample_limit > 0 && n_fill_frames > 0) + data->samples_processed += n_fill_frames; + have_data = true; } @@ -942,6 +1061,7 @@ static const struct option long_options[] = { { "midi", no_argument, NULL, 'm' }, { "dsd", no_argument, NULL, 'd' }, { "encoded", no_argument, NULL, 'o' }, + { "sysex", no_argument, NULL, 's' }, { "remote", required_argument, NULL, 'R' }, @@ -959,6 +1079,9 @@ static const struct option long_options[] = { { "volume", required_argument, NULL, OPT_VOLUME }, { "quality", required_argument, NULL, 'q' }, { "raw", no_argument, NULL, 'a' }, + { "force-midi", required_argument, NULL, 'M' }, + { "sample-count", required_argument, NULL, 'n' }, + { "midi-clip", no_argument, NULL, 'c' }, { NULL, 0, NULL, 0 } }; @@ -1004,6 +1127,8 @@ static void show_usage(const char *name, bool is_error) " --volume Stream volume 0-1.0 (default %.3f)\n" " -q --quality Resampler quality (0 - 15) (default %d)\n" " -a, --raw RAW mode\n" + " -M, --force-midi Force midi format, one of \"midi\" or \"ump\", (default ump)\n" + " -n, --sample-count COUNT Stop after COUNT samples\n" "\n"), DEFAULT_RATE, DEFAULT_CHANNELS, @@ -1020,6 +1145,8 @@ static void show_usage(const char *name, bool is_error) #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION " -o, --encoded Encoded mode\n" #endif + " -s, --sysex SysEx mode\n" + " -c, --midi-clip MIDI clip mode\n" "\n"), fp); } } @@ -1044,6 +1171,8 @@ static int midi_play(struct data *d, void *src, unsigned int n_frames, bool *nul while (1) { uint32_t frame; struct midi_event ev; + uint64_t state = 0; + size_t size; res = midi_file_next_time(d->midi.file, &ev.sec); if (res <= 0) { @@ -1063,32 +1192,48 @@ static int midi_play(struct data *d, void *src, unsigned int n_frames, bool *nul midi_file_read_event(d->midi.file, &ev); if (d->verbose) - midi_file_dump_event(stderr, &ev); + midi_event_dump(stderr, &ev); + + size = ev.size; if (ev.type == MIDI_EVENT_TYPE_MIDI1) { - size_t size; - uint8_t *data; - uint64_t state = 0; - if (ev.data[0] == 0xff) + if (size < 1 || ev.data[0] == 0xff) continue; - data = ev.data; - size = ev.size; + if (d->midi.force_type == MIDI_FORCE_UMP) { + uint8_t *data = ev.data; + while (size > 0) { + uint32_t ump[4]; + int ump_size = spa_ump_from_midi(&data, &size, + ump, sizeof(ump), 0, &state); + if (ump_size <= 0) + break; - while (size > 0) { - uint32_t ump[4]; - int ump_size = spa_ump_from_midi(&data, &size, - ump, sizeof(ump), 0, &state); - if (ump_size <= 0) - break; - - spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&b, ump, ump_size); + spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&b, ump, ump_size); + } + } else { + spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, ev.data, ev.size); } } else if (ev.type == MIDI_EVENT_TYPE_UMP) { - spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&b, ev.data, ev.size); + if (d->midi.force_type == MIDI_FORCE_MIDI1) { + const uint32_t *data = (const uint32_t*)ev.data; + while (size > 0) { + uint8_t ev[16]; + int ev_size = spa_ump_to_midi(&data, &size, + ev, sizeof(ev), &state); + if (ev_size <= 0) + break; + + spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, ev, ev_size); + } + } else { + spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&b, ev.data, ev.size); + } } else continue; @@ -1102,32 +1247,41 @@ static int midi_play(struct data *d, void *src, unsigned int n_frames, bool *nul static int midi_record(struct data *d, void *src, unsigned int n_frames, bool *null_frame) { - struct spa_pod *pod; - struct spa_pod_control *c; - uint32_t frame; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + const void *seq_body, *c_body; + struct spa_pod_control c; + uint32_t offset; - frame = d->clock_time; + offset = d->clock_time; d->clock_time += d->position->clock.duration; - if ((pod = spa_pod_from_data(src, n_frames, 0, n_frames)) == NULL) - return 0; - if (!spa_pod_is_sequence(pod)) + spa_pod_parser_init_from_data(&parser, src, n_frames, 0, n_frames); + + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) return 0; - SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) { + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { struct midi_event ev; - if (c->type != SPA_CONTROL_UMP) + switch (c.type) { + case SPA_CONTROL_UMP: + ev.type = MIDI_EVENT_TYPE_UMP; + break; + case SPA_CONTROL_Midi: + ev.type = MIDI_EVENT_TYPE_MIDI1; + break; + default: continue; - + } ev.track = 0; - ev.sec = (frame + c->offset) / (float) d->position->clock.rate.denom; - ev.data = SPA_POD_BODY(&c->value), - ev.size = SPA_POD_BODY_SIZE(&c->value); - ev.type = MIDI_EVENT_TYPE_UMP; + ev.sec = (offset + c.offset) / (float) d->position->clock.rate.denom; + ev.data = (uint8_t*)c_body; + ev.size = c.value.size; if (d->verbose) - midi_file_dump_event(stderr, &ev); + midi_event_dump(stderr, &ev); midi_file_write_event(d->midi.file, &ev); } @@ -1163,6 +1317,184 @@ static int setup_midifile(struct data *data) return 0; } +static int clip_play(struct data *d, void *src, unsigned int n_frames, bool *null_frame) +{ + int res; + struct spa_pod_builder b; + struct spa_pod_frame f; + uint32_t first_frame, last_frame; + bool have_data = false; + + spa_zero(b); + spa_pod_builder_init(&b, src, n_frames); + + spa_pod_builder_push_sequence(&b, &f, 0); + + first_frame = d->clock_time; + last_frame = first_frame + d->position->clock.duration; + d->clock_time = last_frame; + + while (1) { + uint32_t frame; + struct midi_event ev; + uint64_t state = 0; + size_t size; + + res = midi_clip_next_time(d->clip.file, &ev.sec); + if (res <= 0) { + if (have_data) + break; + return res; + } + + frame = (uint32_t)(ev.sec * d->position->clock.rate.denom); + if (frame < first_frame) + frame = 0; + else if (frame < last_frame) + frame -= first_frame; + else + break; + + midi_clip_read_event(d->clip.file, &ev); + + if (d->verbose) + midi_event_dump(stderr, &ev); + + size = ev.size; + + if (d->midi.force_type == MIDI_FORCE_MIDI1) { + const uint32_t *data = (const uint32_t*)ev.data; + while (size > 0) { + uint8_t ev[16]; + int ev_size = spa_ump_to_midi(&data, &size, + ev, sizeof(ev), &state); + if (ev_size <= 0) + break; + + spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, ev, ev_size); + } + } else { + spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP); + spa_pod_builder_bytes(&b, ev.data, ev.size); + } + have_data = true; + } + spa_pod_builder_pop(&b, &f); + + return b.state.offset; +} + +static int clip_record(struct data *d, void *src, unsigned int n_frames, bool *null_frame) +{ + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + const void *seq_body, *c_body; + struct spa_pod_control c; + uint32_t offset; + + offset = d->clock_time; + d->clock_time += d->position->clock.duration; + + spa_pod_parser_init_from_data(&parser, src, n_frames, 0, n_frames); + + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) + return 0; + + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { + struct midi_event ev; + + switch (c.type) { + case SPA_CONTROL_UMP: + ev.type = MIDI_EVENT_TYPE_UMP; + break; + case SPA_CONTROL_Midi: + ev.type = MIDI_EVENT_TYPE_MIDI1; + break; + default: + continue; + } + ev.track = 0; + ev.sec = (offset + c.offset) / (float) d->position->clock.rate.denom; + ev.data = (uint8_t*)c_body; + ev.size = c.value.size; + + if (d->verbose) + midi_event_dump(stderr, &ev); + + midi_clip_write_event(d->clip.file, &ev); + } + return 0; +} + +static int setup_midiclip(struct data *data) +{ + if (data->mode == mode_record) { + spa_zero(data->clip.info); + data->clip.info.format = 0; + } + + data->clip.file = midi_clip_open(data->filename, + data->mode == mode_playback ? "r" : "w", + &data->clip.info); + if (data->clip.file == NULL) { + fprintf(stderr, "midiclip: can't read midi file '%s': %m\n", data->filename); + return -errno; + } + + if (data->verbose) + fprintf(stderr, "midifile: opened file \"%s\" format %08x div:%d\n", + data->filename, + data->clip.info.format, data->clip.info.division); + + data->fill = data->mode == mode_playback ? clip_play : clip_record; + data->stride = 1; + + return 0; +} + +static int sysex_play(struct data *d, void *dst, unsigned int n_frames, bool *null_frame) +{ + struct spa_pod_builder b; + struct spa_pod_frame f; + size_t size, to_read = n_frames - 64; + uint8_t bytes[to_read]; + + spa_zero(b); + spa_pod_builder_init(&b, dst, n_frames); + + spa_pod_builder_push_sequence(&b, &f, 0); + spa_pod_builder_control(&b, 0, SPA_CONTROL_Midi); + + size = fread(bytes, 1, to_read, d->sysex.file); + + spa_pod_builder_bytes(&b, bytes, size); + spa_pod_builder_pop(&b, &f); + + return b.state.offset; +} + +static int setup_sysex(struct data *data) +{ + if (data->mode == mode_record) + return -ENOTSUP; + + data->sysex.file = fopen(data->filename, "r"); + if (data->sysex.file == NULL) { + fprintf(stderr, "sysex: can't read file '%s': %m\n", data->filename); + return -errno; + } + + if (data->verbose) + fprintf(stderr, "sysex: opened file \"%s\"\n", data->filename); + + data->fill = sysex_play; + data->stride = 1; + + return 0; +} + struct dsd_layout_info { uint32_t type; struct spa_audio_layout_info info; @@ -1343,7 +1675,7 @@ static void format_from_filename(SF_INFO *info, const char *filename) } } if (format == -1) - format = SF_FORMAT_WAV; + format = spa_streq(filename, "-") ? SF_FORMAT_AU : SF_FORMAT_WAV; if (format == SF_FORMAT_WAV && info->channels > 2) format = SF_FORMAT_WAVEX; @@ -1436,6 +1768,21 @@ static int setup_encodedfile(struct data *data) } #endif +static const char *endianness_to_name(int format) +{ + switch (format & SF_FORMAT_ENDMASK) { + case SF_ENDIAN_FILE: + return "Default Endian"; + case SF_ENDIAN_LITTLE: + return "Little Endian"; + case SF_ENDIAN_BIG: + return "Big Endian"; + case SF_ENDIAN_CPU: + return "CPU Endian"; + } + return "unknown"; +} + static int setup_sndfile(struct data *data) { const struct format_info *fi = NULL; @@ -1470,12 +1817,29 @@ static int setup_sndfile(struct data *data) if (!data->file) { fprintf(stderr, "sndfile: failed to open audio file \"%s\": %s\n", data->filename, sf_strerror(NULL)); + if (data->verbose) { + char loginfo[4096]; + if (sf_command(NULL, SFC_GET_LOG_INFO, &loginfo, sizeof(loginfo)) > 0) + fprintf(stderr, "%s\n", loginfo); + } return -EIO; } - if (data->verbose) - fprintf(stderr, "sndfile: opened file \"%s\" format %08x channels:%d rate:%d\n", - data->filename, info.format, info.channels, info.samplerate); + if (data->verbose) { + SF_FORMAT_INFO ti, sti; + spa_zero(ti); + ti.format = info.format & SF_FORMAT_TYPEMASK; + if (sf_command(NULL, SFC_GET_FORMAT_INFO, &ti, sizeof(ti)) != 0) + ti.name = "unknown"; + spa_zero(sti); + sti.format = info.format & SF_FORMAT_SUBMASK; + if (sf_command(NULL, SFC_GET_FORMAT_INFO, &sti, sizeof(sti)) != 0) + sti.name = "unknown"; + + fprintf(stderr, "sndfile: opened file \"%s\" format \"%s %s %s\" channels:%d rate:%d\n", + data->filename, endianness_to_name(info.format), + ti.name, sti.name, info.channels, info.samplerate); + } if (data->channels > 0 && info.channels != (int)data->channels) { fprintf(stderr, "sndfile: given channels (%u) don't match file channels (%d)\n", data->channels, info.channels); @@ -1511,7 +1875,6 @@ static int setup_sndfile(struct data *data) /* try native format first, else decode to float */ if ((fi = format_info_by_sf_format(info.format)) == NULL) fi = format_info_by_sf_format(SF_FORMAT_FLOAT); - } if (fi == NULL) return -EIO; @@ -1646,6 +2009,15 @@ int main(int argc, char *argv[]) } else if (spa_streq(prog, "pw-midirecord")) { data.mode = mode_record; data.data_type = TYPE_MIDI; + } else if (spa_streq(prog, "pw-midi2play")) { + data.mode = mode_playback; + data.data_type = TYPE_MIDI2; + } else if (spa_streq(prog, "pw-midi2record")) { + data.mode = mode_record; + data.data_type = TYPE_MIDI2; + } else if (spa_streq(prog, "pw-sysex")) { + data.mode = mode_playback; + data.data_type = TYPE_SYSEX; } else if (spa_streq(prog, "pw-dsdplay")) { data.mode = mode_playback; data.data_type = TYPE_DSD; @@ -1660,6 +2032,7 @@ int main(int argc, char *argv[]) /* negative means no volume adjustment */ data.volume = -1.0; data.quality = -1; + data.midi.force_type = MIDI_FORCE_UMP; data.props = pw_properties_new( PW_KEY_APP_NAME, prog, PW_KEY_NODE_NAME, prog, @@ -1671,9 +2044,9 @@ int main(int argc, char *argv[]) } #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION - while ((c = getopt_long(argc, argv, "hvprmdoR:q:P:a", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hvprmdosR:q:P:aM:n:c", long_options, NULL)) != -1) { #else - while ((c = getopt_long(argc, argv, "hvprmdR:q:P:a", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hvprmdsR:q:P:aM:n:c", long_options, NULL)) != -1) { #endif switch (c) { @@ -1716,6 +2089,9 @@ int main(int argc, char *argv[]) data.data_type = TYPE_ENCODED; break; #endif + case 's': + data.data_type = TYPE_SYSEX; + break; case 'R': data.remote_name = optarg; @@ -1729,6 +2105,17 @@ int main(int argc, char *argv[]) data.raw = true; break; + case 'M': + if (spa_streq(optarg, "midi")) + data.midi.force_type = MIDI_FORCE_MIDI1; + else if (spa_streq(optarg, "ump")) + data.midi.force_type = MIDI_FORCE_UMP; + else { + fprintf(stderr, "error: bad force-midi %s\n", optarg); + goto error_usage; + } + break; + case OPT_MEDIA_TYPE: data.media_type = optarg; break; @@ -1792,6 +2179,12 @@ int main(int argc, char *argv[]) if (!spa_atof(optarg, &data.volume)) data.volume = (float)atof(optarg); break; + case 'n': + data.sample_limit = strtoull(optarg, NULL, 10); + break; + case 'c': + data.data_type = TYPE_MIDI2; + break; default: goto error_usage; } @@ -1805,6 +2198,8 @@ int main(int argc, char *argv[]) if (!data.media_type) { switch (data.data_type) { case TYPE_MIDI: + case TYPE_MIDI2: + case TYPE_SYSEX: data.media_type = DEFAULT_MIDI_MEDIA_TYPE; break; default: @@ -1884,6 +2279,9 @@ int main(int argc, char *argv[]) case TYPE_MIDI: ret = setup_midifile(&data); break; + case TYPE_MIDI2: + ret = setup_midiclip(&data); + break; case TYPE_DSD: ret = setup_dsdfile(&data); break; @@ -1892,6 +2290,9 @@ int main(int argc, char *argv[]) ret = setup_encodedfile(&data); break; #endif + case TYPE_SYSEX: + ret = setup_sysex(&data); + break; default: ret = -ENOTSUP; break; @@ -1947,19 +2348,35 @@ int main(int argc, char *argv[]) .rate = data.rate, .channels = data.channels); - if (data.channelmap.n_channels) - memcpy(info.position, data.channelmap.channels, data.channels * sizeof(int)); - + if (data.channels > MAX_CHANNELS) { + fprintf(stderr, "error: too many channels %d > %d\n", + data.channels, MAX_CHANNELS); + goto error_bad_file; + } + if (data.channelmap.n_channels) { + if (data.channels > MAX_CHANNELS) { + fprintf(stderr, "error: too many channels in channelmap %d > %d\n", + data.channelmap.n_channels, MAX_CHANNELS); + goto error_bad_file; + } + uint32_t i; + for (i = 0; i < data.channelmap.n_channels; i++) + info.position[i] = data.channelmap.channels[i]; + for (; i < data.channels; i++) + info.position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; + } params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); break; } case TYPE_MIDI: + case TYPE_MIDI2: + case TYPE_SYSEX: params[n_params++] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); - pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "32 bit raw UMP"); + pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); break; case TYPE_DSD: { @@ -2075,6 +2492,8 @@ error_no_main_loop: sf_close(data.file); if (data.midi.file) midi_file_close(data.midi.file); + if (data.clip.file) + midi_clip_close(data.clip.file); if (data.dsf.file) dsf_file_close(data.dsf.file); if (data.dff.file) diff --git a/src/tools/pw-cli.c b/src/tools/pw-cli.c index 467a4be63..88356cb8f 100644 --- a/src/tools/pw-cli.c +++ b/src/tools/pw-cli.c @@ -80,6 +80,16 @@ struct global { struct pw_properties *properties; }; +struct var { + int type; +#define TYPE_UNKNOWN 0 +#define TYPE_REMOTE 1 +#define TYPE_PROXY 2 +#define TYPE_MODULE 3 + uint32_t id; + void *object; +}; + struct remote_data { struct spa_list link; struct data *data; @@ -107,6 +117,7 @@ struct proxy_data { const struct class *class; struct spa_hook proxy_listener; struct spa_hook object_listener; + uint32_t id; }; struct command { @@ -116,6 +127,70 @@ struct command { bool (*func) (struct data *data, const char *cmd, char *args, char **error); }; + +static uint32_t add_var(struct data *data, void *object, int type) +{ + struct var *var; + if ((var = calloc(1, sizeof(*var))) == NULL) + return SPA_ID_INVALID; + var->type = type; + var->object = object; + var->id = pw_map_insert_new(&data->vars, var); + return var->id; +} +static void *find_var(struct data *data, uint32_t id, int type) +{ + struct var *var; + var = pw_map_lookup(&data->vars, id); + if (var == NULL || var->type != type) + return NULL; + return var->object; +} + +static void remove_var(struct data *data, uint32_t id, int type) +{ + struct var *var; + var = pw_map_lookup(&data->vars, id); + if (var == NULL || var->type != type) + return; + pw_map_remove(&data->vars, var->id); + free(var); +} + +static int list_var(void *item_data, void *data) +{ + struct var *var = item_data; + switch (var->type) { + case TYPE_MODULE: + printf("%d = @module:%d\n", var->id, pw_global_get_id(pw_impl_module_get_global(var->object))); + break; + case TYPE_PROXY: + printf("%d = @proxy:%d\n", var->id, pw_proxy_get_id(var->object)); + break; + case TYPE_REMOTE: + { + struct remote_data *rd = var->object; + printf("%d = @remote:%p\n", var->id, rd->core); + break; + } + } + return 0; +} + +static void print_var(struct data *data, uint32_t id) +{ + struct var *var; + var = pw_map_lookup(&data->vars, id); + if (var == NULL) + return; + list_var(var, data); +} + +static void list_vars(struct data *data) +{ + pw_map_for_each(&data->vars, list_var, data); +} + static struct spa_dict * global_props(struct global *global); static struct global * obj_global(struct remote_data *rd, uint32_t id); static int children_of(struct remote_data *rd, uint32_t parent_id, @@ -170,6 +245,7 @@ static bool do_not_implemented(struct data *data, const char *cmd, char *args, c #endif static bool do_help(struct data *data, const char *cmd, char *args, char **error); +static bool do_list_vars(struct data *data, const char *cmd, char *args, char **error); static bool do_load_module(struct data *data, const char *cmd, char *args, char **error); static bool do_unload_module(struct data *data, const char *cmd, char *args, char **error); static bool do_list_objects(struct data *data, const char *cmd, char *args, char **error); @@ -194,6 +270,7 @@ static bool do_quit(struct data *data, const char *cmd, char *args, char **error static const struct command command_list[] = { { "help", "h", "Show this help", do_help }, + { "list-vars", "lv", "List all variables", do_list_vars }, { "load-module", "lm", "Load a module. []", do_load_module }, { "unload-module", "um", "Unload a module. ", do_unload_module }, { "connect", "con", "Connect to a remote. []", do_connect }, @@ -238,6 +315,13 @@ static bool do_help(struct data *data, const char *cmd, char *args, char **error return true; } +static bool do_list_vars(struct data *data, const char *cmd, char *args, char **error) +{ + printf("Known variables:\n"); + list_vars(data); + return true; +} + static bool do_load_module(struct data *data, const char *cmd, char *args, char **error) { struct pw_impl_module *module; @@ -257,9 +341,9 @@ static bool do_load_module(struct data *data, const char *cmd, char *args, char return false; } - id = pw_map_insert_new(&data->vars, module); + id = add_var(data, module, TYPE_MODULE); if (data->interactive) - printf("%d = @module:%d\n", id, pw_global_get_id(pw_impl_module_get_global(module))); + print_var(data, id); return true; } @@ -277,12 +361,12 @@ static bool do_unload_module(struct data *data, const char *cmd, char *args, cha return false; } idx = atoi(a[0]); - module = pw_map_lookup(&data->vars, idx); + module = find_var(data, idx, TYPE_MODULE); if (module == NULL) { *error = spa_aprintf("%s: unknown module '%s'", cmd, a[0]); return false; } - pw_map_remove(&data->vars, idx); + remove_var(data, idx, TYPE_MODULE); pw_impl_module_destroy(module); return true; } @@ -472,8 +556,8 @@ static void on_core_error(void *_data, uint32_t id, int seq, int res, const char struct remote_data *rd = _data; struct data *data = rd->data; - pw_log_error("remote %p: error id:%u seq:%d res:%d (%s): %s", rd, - id, seq, res, spa_strerror(res), message); + fprintf(stderr, "remote %" PRIu32 ": error id:%" PRIu32 " seq:%d res:%d (%s): %s\n", + rd->id, id, seq, res, spa_strerror(res), message); if (id == PW_ID_CORE && res == -EPIPE) program_quit(data); @@ -496,7 +580,7 @@ static void on_core_destroy(void *_data) spa_hook_remove(&rd->core_listener); spa_hook_remove(&rd->proxy_core_listener); - pw_map_remove(&data->vars, rd->id); + remove_var(data, rd->id, TYPE_REMOTE); pw_map_for_each(&rd->globals, destroy_global, rd); pw_map_clear(&rd->globals); @@ -539,11 +623,11 @@ static bool do_connect(struct data *data, const char *cmd, char *args, char **er rd->core = core; rd->data = data; pw_map_init(&rd->globals, 64, 16); - rd->id = pw_map_insert_new(&data->vars, rd); + rd->id = add_var(data, rd, TYPE_REMOTE); spa_list_append(&data->remotes, &rd->link); if (rd->data->interactive) - printf("%d = @remote:%p\n", rd->id, rd->core); + print_var(data, rd->id); data->current = rd; @@ -572,7 +656,7 @@ static bool do_disconnect(struct data *data, const char *cmd, char *args, char * n = pw_split_ip(args, WHITESPACE, 1, a); if (n >= 1) { idx = atoi(a[0]); - rd = pw_map_lookup(&data->vars, idx); + rd = find_var(data, idx, TYPE_REMOTE); if (rd == NULL) goto no_remote; @@ -614,7 +698,7 @@ static bool do_switch_remote(struct data *data, const char *cmd, char *args, cha if (n == 1) idx = atoi(a[0]); - rd = pw_map_lookup(&data->vars, idx); + rd = find_var(data, idx, TYPE_REMOTE); if (rd == NULL) goto no_remote; @@ -1454,7 +1538,6 @@ static bool do_create_device(struct data *data, const char *cmd, char *args, cha struct remote_data *rd = data->current; char *a[2]; int n; - uint32_t id; struct pw_proxy *proxy; struct pw_properties *props = NULL; struct proxy_data *pd; @@ -1484,9 +1567,9 @@ static bool do_create_device(struct data *data, const char *cmd, char *args, cha pw_proxy_add_object_listener(proxy, &pd->object_listener, &device_events, pd); pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd); - id = pw_map_insert_new(&data->vars, proxy); + pd->id = add_var(data, proxy, TYPE_PROXY); if (rd->data->interactive) - printf("%d = @proxy:%d\n", id, pw_proxy_get_id(proxy)); + print_var(data, pd->id); return true; } @@ -1496,7 +1579,6 @@ static bool do_create_node(struct data *data, const char *cmd, char *args, char struct remote_data *rd = data->current; char *a[2]; int n; - uint32_t id; struct pw_proxy *proxy; struct pw_properties *props = NULL; struct proxy_data *pd; @@ -1526,9 +1608,9 @@ static bool do_create_node(struct data *data, const char *cmd, char *args, char pw_proxy_add_object_listener(proxy, &pd->object_listener, &node_events, pd); pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd); - id = pw_map_insert_new(&data->vars, proxy); + pd->id = add_var(data, proxy, TYPE_PROXY); if (rd->data->interactive) - printf("%d = @proxy:%d\n", id, pw_proxy_get_id(proxy)); + printf("%d = @proxy:%d\n", pd->id, pw_proxy_get_id(proxy)); return true; } @@ -1559,7 +1641,7 @@ static struct global * obj_global_port(struct remote_data *rd, struct global *global, const char *port_direction, const char *port_id) { struct global *global_port_found = NULL; - uint32_t *ports = NULL; + spa_autofree uint32_t *ports = NULL; int port_count; port_count = children_of(rd, global->id, PW_TYPE_INTERFACE_Port, &ports); @@ -1582,14 +1664,12 @@ obj_global_port(struct remote_data *rd, struct global *global, const char *port_ } } - free(ports); return global_port_found; } static void create_link_with_properties(struct data *data, const struct pw_properties *props) { struct remote_data *rd = data->current; - uint32_t id; struct pw_proxy *proxy; struct proxy_data *pd; @@ -1607,9 +1687,9 @@ static void create_link_with_properties(struct data *data, const struct pw_prope pw_proxy_add_object_listener(proxy, &pd->object_listener, &link_events, pd); pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd); - id = pw_map_insert_new(&data->vars, proxy); + pd->id = add_var(data, proxy, TYPE_PROXY); if (rd->data->interactive) - printf("%d = @proxy:%d\n", id, pw_proxy_get_id((struct pw_proxy*)proxy)); + printf("%d = @proxy:%d\n", pd->id, pw_proxy_get_id((struct pw_proxy*)proxy)); } static bool do_create_link(struct data *data, const char *cmd, char *args, char **error) @@ -1713,7 +1793,7 @@ static bool do_export_node(struct data *data, const char *cmd, char *args, char } if (n == 2) { idx = atoi(a[1]); - rd = pw_map_lookup(&data->vars, idx); + rd = find_var(data, idx, TYPE_REMOTE); if (rd == NULL) goto no_remote; } @@ -1730,7 +1810,7 @@ static bool do_export_node(struct data *data, const char *cmd, char *args, char node = pw_global_get_object(global); proxy = pw_core_export(rd->core, PW_TYPE_INTERFACE_Node, NULL, node, 0); - id = pw_map_insert_new(&data->vars, proxy); + id = add_var(data, proxy, TYPE_PROXY); if (rd->data->interactive) printf("%d = @proxy:%d\n", id, pw_proxy_get_id((struct pw_proxy*)proxy)); diff --git a/src/tools/pw-dump.c b/src/tools/pw-dump.c index 179e0c673..e72e7d257 100644 --- a/src/tools/pw-dump.c +++ b/src/tools/pw-dump.c @@ -31,6 +31,7 @@ #define INDENT 2 static bool colors = false; +static bool raw = false; #define NORMAL (colors ? SPA_ANSI_RESET : "") #define LITERAL (colors ? SPA_ANSI_BRIGHT_MAGENTA : "") @@ -56,12 +57,16 @@ struct data { FILE *out; int level; + int indent; #define STATE_KEY (1<<0) #define STATE_COMMA (1<<1) #define STATE_FIRST (1<<2) #define STATE_MASK 0xffff0000 #define STATE_SIMPLE (1<<16) uint32_t state; + const char *comma_char; + const char *keysep_char; + bool simple_string; unsigned int monitor:1; }; @@ -214,14 +219,26 @@ static void object_destroy(struct object *o) static void put_key(struct data *d, const char *key); +#define REJECT "\"\\'=:,{}[]()#" + +static bool is_simple_string(const char *val) +{ + int i; + for (i = 0; val[i]; i++) { + if (val[i] < 0x20 || strchr(REJECT, val[i]) != NULL) + return false; + } + return true; +} + static SPA_PRINTF_FUNC(3,4) void put_fmt(struct data *d, const char *key, const char *fmt, ...) { va_list va; if (key) put_key(d, key); fprintf(d->out, "%s%s%*s", - d->state & STATE_COMMA ? "," : "", - d->state & (STATE_MASK | STATE_KEY) ? " " : d->state & STATE_FIRST ? "" : "\n", + d->state & STATE_COMMA ? d->comma_char : "", + d->state & (STATE_MASK | STATE_KEY) ? " " : (d->state & STATE_FIRST) || raw ? "" : "\n", d->state & (STATE_MASK | STATE_KEY) ? 0 : d->level, ""); va_start(va, fmt); vfprintf(d->out, fmt, va); @@ -232,22 +249,26 @@ static SPA_PRINTF_FUNC(3,4) void put_fmt(struct data *d, const char *key, const static void put_key(struct data *d, const char *key) { int size = (strlen(key) + 1) * 4; - char *str = alloca(size); - spa_json_encode_string(str, size, key); - put_fmt(d, NULL, "%s%s%s:", KEY, str, NORMAL); + if (d->simple_string && is_simple_string(key)) { + put_fmt(d, NULL, "%s%s%s%s", KEY, key, NORMAL, d->keysep_char); + } else { + char *str = alloca(size); + spa_json_encode_string(str, size, key); + put_fmt(d, NULL, "%s%s%s%s", KEY, str, NORMAL, d->keysep_char); + } d->state = (d->state & STATE_MASK) + STATE_KEY; } static void put_begin(struct data *d, const char *key, const char *type, uint32_t flags) { put_fmt(d, key, "%s", type); - d->level += INDENT; + d->level += d->indent; d->state = (d->state & STATE_MASK) + (flags & STATE_SIMPLE); } static void put_end(struct data *d, const char *type, uint32_t flags) { - d->level -= INDENT; + d->level -= d->indent; d->state = d->state & STATE_MASK; put_fmt(d, NULL, "%s", type); d->state = (d->state & STATE_MASK) + STATE_COMMA - (flags & STATE_SIMPLE); @@ -260,9 +281,13 @@ static void put_encoded_string(struct data *d, const char *key, const char *val) static void put_string(struct data *d, const char *key, const char *val) { int size = (strlen(val) + 1) * 4; - char *str = alloca(size); - spa_json_encode_string(str, size, val); - put_encoded_string(d, key, str); + if (d->simple_string && is_simple_string(val)) { + put_encoded_string(d, key, val); + } else { + char *str = alloca(size); + spa_json_encode_string(str, size, val); + put_encoded_string(d, key, str); + } } static void put_literal(struct data *d, const char *key, const char *val) @@ -316,12 +341,16 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type put_key(d, key); switch (type) { case SPA_TYPE_Bool: + if (size < sizeof(int32_t)) + break; put_value(d, NULL, *(int32_t*)body ? "true" : "false"); break; case SPA_TYPE_Id: { const char *str; char fallback[32]; + if (size < sizeof(uint32_t)) + break; uint32_t id = *(uint32_t*)body; str = spa_debug_type_find_short_name(info, *(uint32_t*)body); if (str == NULL) { @@ -332,24 +361,38 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type break; } case SPA_TYPE_Int: + if (size < sizeof(int32_t)) + break; put_int(d, NULL, *(int32_t*)body); break; case SPA_TYPE_Fd: case SPA_TYPE_Long: + if (size < sizeof(int64_t)) + break; put_int(d, NULL, *(int64_t*)body); break; case SPA_TYPE_Float: + if (size < sizeof(float)) + break; put_double(d, NULL, *(float*)body); break; case SPA_TYPE_Double: + if (size < sizeof(double)) + break; put_double(d, NULL, *(double*)body); break; case SPA_TYPE_String: + if (size < 1 || ((const char *)body)[size - 1]) + break; put_string(d, NULL, (const char*)body); break; case SPA_TYPE_Rectangle: { - struct spa_rectangle *r = (struct spa_rectangle *)body; + struct spa_rectangle *r; + + if (size < sizeof(*r)) + break; + r = (struct spa_rectangle *)body; put_begin(d, NULL, "{", STATE_SIMPLE); put_int(d, "width", r->width); put_int(d, "height", r->height); @@ -358,7 +401,11 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type } case SPA_TYPE_Fraction: { - struct spa_fraction *f = (struct spa_fraction *)body; + struct spa_fraction *f; + + if (size < sizeof(*f)) + break; + f = (struct spa_fraction *)body; put_begin(d, NULL, "{", STATE_SIMPLE); put_int(d, "num", f->num); put_int(d, "denom", f->denom); @@ -367,8 +414,12 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type } case SPA_TYPE_Array: { - struct spa_pod_array_body *b = (struct spa_pod_array_body *)body; + struct spa_pod_array_body *b; void *p; + + if (size < sizeof(*b)) + break; + b = (struct spa_pod_array_body *)body; info = info && info->values ? info->values: info; put_begin(d, NULL, "[", STATE_SIMPLE); SPA_POD_ARRAY_BODY_FOREACH(b, size, p) @@ -486,10 +537,8 @@ static void put_pod(struct data *d, const char *key, const struct spa_pod *pod) if (pod == NULL) { put_value(d, key, NULL); } else { - put_pod_value(d, key, SPA_TYPE_ROOT, - SPA_POD_TYPE(pod), - SPA_POD_BODY(pod), - SPA_POD_BODY_SIZE(pod)); + put_pod_value(d, key, SPA_TYPE_ROOT, pod->type, + SPA_POD_BODY(pod), pod->size); } } @@ -1512,7 +1561,10 @@ static void show_help(struct data *data, const char *name, bool error) " -r, --remote Remote daemon name\n" " -m, --monitor monitor changes\n" " -N, --no-colors disable color output\n" - " -C, --color[=WHEN] whether to enable color support. WHEN is `never`, `always`, or `auto`\n", + " -C, --color[=WHEN] whether to enable color support. WHEN is `never`, `always`, or `auto`\n" + " -R, --raw force raw output\n" + " -i, --indent indentation amount (default 2)\n" + " -s, --spa SPA JSON output\n", name); } @@ -1529,6 +1581,9 @@ int main(int argc, char *argv[]) { "monitor", no_argument, NULL, 'm' }, { "no-colors", no_argument, NULL, 'N' }, { "color", optional_argument, NULL, 'C' }, + { "raw", no_argument, NULL, 'R' }, + { "indent", required_argument, NULL, 'i' }, + { "spa", no_argument, NULL, 's' }, { NULL, 0, NULL, 0} }; int c; @@ -1541,7 +1596,11 @@ int main(int argc, char *argv[]) colors = true; setlinebuf(data.out); - while ((c = getopt_long(argc, argv, "hVr:mNC", long_options, NULL)) != -1) { + data.comma_char = ","; + data.keysep_char = ":"; + data.indent = INDENT; + + while ((c = getopt_long(argc, argv, "hVr:mNC::Ri:s", long_options, NULL)) != -1) { switch (c) { case 'h' : show_help(&data, argv[0], false); @@ -1563,6 +1622,9 @@ int main(int argc, char *argv[]) case 'N' : colors = false; break; + case 'R' : + raw = true; + break; case 'C' : if (optarg == NULL || !strcmp(optarg, "auto")) break; /* nothing to do, tty detection was done @@ -1577,11 +1639,21 @@ int main(int argc, char *argv[]) return -1; } break; + case 'i' : + data.indent = atoi(optarg); + break; + case 's' : + data.comma_char = ""; + data.keysep_char = " ="; + data.simple_string = true; + break; default: show_help(&data, argv[0], true); return -1; } } + if (raw) + data.indent = 0; if (optind < argc) data.pattern = argv[optind++]; diff --git a/src/tools/pw-link.c b/src/tools/pw-link.c index 3d87cbaa9..1853783ca 100644 --- a/src/tools/pw-link.c +++ b/src/tools/pw-link.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -38,11 +39,24 @@ union object_data { struct object { struct spa_list link; + struct data *d; uint32_t id; enum object_type type; struct pw_properties *props; union object_data data; +#define STATE_NONE 0 +#define STATE_NEW 1 +#define STATE_CHANGED 2 +#define STATE_DELETE 3 + int state; + + struct pw_proxy *proxy; + struct spa_hook proxy_listener; + struct spa_hook object_listener; + + struct spa_latency_info latency[2]; + bool latency_changed[2]; }; struct target_link { @@ -65,6 +79,7 @@ enum list_target { LIST_INPUT = 1 << 1, LIST_PORTS = LIST_OUTPUT | LIST_INPUT, LIST_LINKS = 1 << 2, + LIST_LATENCY = 1 << 3, }; struct data { @@ -98,12 +113,20 @@ struct data { bool monitoring; bool list_inputs; bool list_outputs; - const char *prefix; regex_t out_port_regex, *out_regex; regex_t in_port_regex, *in_regex; }; +static void destroy_object(struct object *obj) +{ + spa_list_remove(&obj->link); + pw_properties_free(obj->props); + if (obj->proxy) + pw_proxy_destroy(obj->proxy); + free(obj); +} + static void link_event(struct target_link *tl, enum pw_link_state state, int result) { /* Ignore non definitive states (negotiating, allocating, etc). */ @@ -183,6 +206,23 @@ static void core_sync(struct data *data) data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync); } +static const char *state_name(struct data *d, struct object *o) +{ + if (!d->opt_monitor) + return ""; + switch (o->state) { + case STATE_NONE: + return " "; + case STATE_NEW: + return "+"; + case STATE_CHANGED: + return "*"; + case STATE_DELETE: + return "-"; + } + return " "; +} + static struct object *find_object(struct data *data, enum object_type type, uint32_t id) { struct object *o; @@ -274,37 +314,58 @@ static char *port_alias(char *buffer, int size, struct object *n, struct object return buffer; } -static void print_port(struct data *data, const char *prefix, struct object *n, - struct object *p, bool verbose) +static void print_port(struct data *data, const char *prefix, const char *state, + struct object *n, struct object *p, bool verbose) { char buffer[1024], id[64] = ""; const char *prefix2 = ""; + if (state == NULL) + state = state_name(data, p); + if (data->opt_id) { snprintf(id, sizeof(id), "%4d ", p->id); prefix2 = " "; } - printf("%s%s%s%s\n", data->prefix, prefix, + printf("%s%s%s%s\n", state, prefix, id, port_name(buffer, sizeof(buffer), n, p)); if (verbose) { port_path(buffer, sizeof(buffer), n, p); if (buffer[0] != '\0') - printf("%s %s%s%s\n", data->prefix, prefix2, prefix, buffer); + printf("%s %s%s%s\n", state, prefix2, prefix, buffer); port_alias(buffer, sizeof(buffer), n, p); if (buffer[0] != '\0') - printf("%s %s%s%s\n", data->prefix, prefix2, prefix, buffer); + printf("%s %s%s%s\n", state, prefix2, prefix, buffer); } } -static void print_port_id(struct data *data, const char *prefix, uint32_t peer) +static void print_port_id(struct data *data, const char *prefix, uint32_t peer, struct object *l) { struct object *n, *p; if ((p = find_object(data, OBJECT_PORT, peer)) == NULL) return; if ((n = find_object(data, OBJECT_NODE, p->data.port.node)) == NULL) return; - print_port(data, prefix, n, p, false); + print_port(data, prefix, state_name(data, l), n, p, false); +} + +static void print_port_latency(struct data *data, const char *prefix, + struct object *p, enum spa_direction direction) +{ + const char *state; + struct spa_latency_info *info = &p->latency[direction]; + + if (p->state == STATE_NONE || p->state == STATE_CHANGED) + state = p->latency_changed[direction] ? "*" : "="; + else + state = state_name(data, p); + + printf("%s%s %s latency: { quantum=[ %f %f ], rate=[ %d %d ], ns=[ %"PRIi64" %"PRIi64" ] }\n", + state, prefix, direction == SPA_DIRECTION_INPUT ? "input ": "output", + info->min_quantum, info->max_quantum, + info->min_rate, info->max_rate, info->min_ns, info->max_ns); + p->latency_changed[direction] = false; } static void do_list_port_links(struct data *data, struct object *node, struct object *port) @@ -339,10 +400,10 @@ static void do_list_port_links(struct data *data, struct object *node, struct ob continue; if (first) { - print_port(data, "", node, port, data->opt_verbose); + print_port(data, "", NULL, node, port, data->opt_verbose); first = false; } - print_port_id(data, prefix, peer); + print_port_id(data, prefix, peer, o); } } @@ -389,6 +450,8 @@ static void do_list_ports(struct data *data, struct object *node, spa_list_for_each(o, &data->objects, link) { if (o->type != OBJECT_PORT) continue; + if (o->state == STATE_NONE) + continue; if (o->data.port.node != node->id) continue; if (o->data.port.direction != direction) @@ -398,7 +461,11 @@ static void do_list_ports(struct data *data, struct object *node, continue; if (data->opt_list & LIST_PORTS) - print_port(data, "", node, o, data->opt_verbose); + print_port(data, "", NULL, node, o, data->opt_verbose); + if (data->opt_list & LIST_LATENCY) { + print_port_latency(data, "", o, SPA_DIRECTION_INPUT); + print_port_latency(data, "", o, SPA_DIRECTION_OUTPUT); + } if (data->opt_list & LIST_LINKS) do_list_port_links(data, node, o); } @@ -406,7 +473,7 @@ static void do_list_ports(struct data *data, struct object *node, static void do_list(struct data *data) { - struct object *n; + struct object *n, *t; spa_list_for_each(n, &data->objects, link) { if (n->type != OBJECT_NODE) @@ -415,6 +482,13 @@ static void do_list(struct data *data) do_list_ports(data, n, PW_DIRECTION_OUTPUT, data->out_regex); if (data->list_inputs) do_list_ports(data, n, PW_DIRECTION_INPUT, data->in_regex); + + } + spa_list_for_each_safe(n, t, &data->objects, link) { + if (n->state == STATE_DELETE) + destroy_object(n); + else + n->state = STATE_NONE; } } @@ -598,64 +672,49 @@ static int do_unlink_ports(struct data *data) return 0; } -static int do_monitor_port(struct data *data, struct object *port) +static void +removed_proxy (void *data) { - regex_t *regex = NULL; - bool do_print = false; - struct object *node; - - if (port->data.port.direction == PW_DIRECTION_OUTPUT && data->list_outputs) { - regex = data->out_regex; - do_print = true; - } - if (port->data.port.direction == PW_DIRECTION_INPUT && data->list_inputs) { - regex = data->in_regex; - do_print = true; - } - if (!do_print) - return 0; - - if ((node = find_object(data, OBJECT_NODE, port->data.port.node)) == NULL) - return -ENOENT; - - if (regex && !port_regex(data, node, port, regex)) - return 0; - - print_port(data, "", node, port, data->opt_verbose); - return 0; + struct object *obj = data; + pw_proxy_destroy(obj->proxy); } -static int do_monitor_link(struct data *data, struct object *link) +static void +destroy_proxy (void *data) { - char buffer1[1024], buffer2[1024], id[64] = ""; - struct object *n1, *n2, *p1, *p2; - - if (!(data->opt_list & LIST_LINKS)) - return 0; - - if ((p1 = find_object(data, OBJECT_PORT, link->data.link.output_port)) == NULL) - return -ENOENT; - if ((n1 = find_object(data, OBJECT_NODE, p1->data.port.node)) == NULL) - return -ENOENT; - if (data->out_regex && !port_regex(data, n1, p1, data->out_regex)) - return 0; - - if ((p2 = find_object(data, OBJECT_PORT, link->data.link.input_port)) == NULL) - return -ENOENT; - if ((n2 = find_object(data, OBJECT_NODE, p2->data.port.node)) == NULL) - return -ENOENT; - if (data->in_regex && !port_regex(data, n2, p2, data->in_regex)) - return 0; - - if (data->opt_id) - snprintf(id, sizeof(id), "%4d ", link->id); - - printf("%s%s%s -> %s\n", data->prefix, id, - port_name(buffer1, sizeof(buffer1), n1, p1), - port_name(buffer2, sizeof(buffer2), n2, p2)); - return 0; + struct object *obj = data; + spa_hook_remove(&obj->proxy_listener); + spa_hook_remove(&obj->object_listener); + obj->proxy = NULL; } +static const struct pw_proxy_events proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = removed_proxy, + .destroy = destroy_proxy, +}; + +static void port_event_param(void *_data, int seq, uint32_t id, + uint32_t index, uint32_t next, const struct spa_pod *param) +{ + struct object *obj = _data; + struct spa_latency_info info; + + if (id != SPA_PARAM_Latency || + spa_latency_parse(param, &info) < 0) + return; + obj->latency[info.direction] = info; + if (obj->state == STATE_NONE) + obj->state = STATE_CHANGED; + obj->latency_changed[info.direction] = true; + core_sync(obj->d); +} + +static const struct pw_port_events port_events = { + PW_VERSION_PORT_EVENTS, + .param = port_event_param +}; + static void registry_event_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) @@ -663,7 +722,7 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, struct data *d = data; enum object_type t; union object_data extra = {0}; - struct object *obj; + struct object *obj, *p; const char *str; if (props == NULL) @@ -698,6 +757,12 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, if ((str = spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT)) == NULL) return; extra.link.input_port = atoi(str); + if ((p = find_object(d, OBJECT_PORT, extra.link.output_port)) != NULL) + if (p->state == STATE_NONE) + p->state = STATE_CHANGED; + if ((p = find_object(d, OBJECT_PORT, extra.link.input_port)) != NULL) + if (p->state == STATE_NONE) + p->state = STATE_CHANGED; } else return; @@ -706,57 +771,42 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, obj->id = id; obj->props = pw_properties_new_dict(props); obj->data = extra; + obj->state = STATE_NEW; + obj->d = d; spa_list_append(&d->objects, &obj->link); - if (d->monitoring) { - d->prefix = "+ "; - switch (obj->type) { - case OBJECT_ANY: - spa_assert_not_reached(); - case OBJECT_NODE: - break; - case OBJECT_PORT: - do_monitor_port(d, obj); - break; - case OBJECT_LINK: - do_monitor_link(d, obj); - break; - } + switch (obj->type) { + case OBJECT_PORT: + { + uint32_t subs[] = { SPA_PARAM_Latency }; + obj->proxy = pw_registry_bind(d->registry, id, type, PW_VERSION_PORT, 0); + pw_proxy_add_object_listener(obj->proxy, &obj->object_listener, &port_events, obj); + pw_proxy_add_listener(obj->proxy, &obj->proxy_listener, &proxy_events, obj); + pw_port_subscribe_params((struct pw_port*)obj->proxy, subs, SPA_N_ELEMENTS(subs)); + break; } -} - -static void destroy_object(struct object *obj) -{ - spa_list_remove(&obj->link); - pw_properties_free(obj->props); - free(obj); + default: + break; + } + core_sync(d); } static void registry_event_global_remove(void *data, uint32_t id) { struct data *d = data; - struct object *obj; + struct object *obj, *p; if ((obj = find_object(d, OBJECT_ANY, id)) == NULL) return; - if (d->monitoring) { - d->prefix = "- "; - switch (obj->type) { - case OBJECT_ANY: - spa_assert_not_reached(); - case OBJECT_NODE: - break; - case OBJECT_PORT: - do_monitor_port(d, obj); - break; - case OBJECT_LINK: - do_monitor_link(d, obj); - break; - } + if (obj->type == OBJECT_LINK) { + if ((p = find_object(d, OBJECT_PORT, obj->data.link.output_port)) != NULL) + p->state = STATE_CHANGED; + if ((p = find_object(d, OBJECT_PORT, obj->data.link.input_port)) != NULL) + p->state = STATE_CHANGED; } - - destroy_object(obj); + obj->state = STATE_DELETE; + core_sync(d); } static const struct pw_registry_events registry_events = { @@ -806,6 +856,7 @@ static const struct pw_core_events core_events = { static void do_quit(void *userdata, int signal_number) { struct data *data = userdata; + data->opt_monitor = false; pw_main_loop_quit(data->loop); } @@ -820,6 +871,7 @@ static void show_help(struct data *data, const char *name, bool error) " -o, --output List output ports\n" " -i, --input List input ports\n" " -l, --links List links\n" + " -t, --latency List port latencies\n" " -m, --monitor Monitor links and ports\n" " -I, --id List IDs\n" " -v, --verbose Verbose port properties\n" @@ -896,6 +948,7 @@ static int run(int argc, char *argv[]) { "props", required_argument, NULL, 'p' }, { "wait", no_argument, NULL, 'w' }, { "disconnect", no_argument, NULL, 'd' }, + { "latency", no_argument, NULL, 't' }, { NULL, 0, NULL, 0} }; @@ -905,7 +958,7 @@ static int run(int argc, char *argv[]) return -1; } - while ((c = getopt_long(argc, argv, "hVr:oilmIvLPp:wd", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hVr:oilmIvLPp:wdt", long_options, NULL)) != -1) { switch (c) { case 'h': show_help(&data, argv[0], false); @@ -933,6 +986,10 @@ static int run(int argc, char *argv[]) data.opt_mode = MODE_LIST; data.opt_list |= LIST_LINKS; break; + case 't': + data.opt_mode = MODE_LIST; + data.opt_list |= LIST_LATENCY; + break; case 'm': data.opt_monitor = true; break; @@ -1033,8 +1090,6 @@ static int run(int argc, char *argv[]) &data.registry_listener, ®istry_events, &data); - data.prefix = data.opt_monitor ? "= " : ""; - core_sync(&data); pw_main_loop_run(data.loop); @@ -1071,6 +1126,7 @@ static int run(int argc, char *argv[]) } if (data.nb_links > 0) { + core_sync(&data); pw_main_loop_run(data.loop); struct target_link *tl; @@ -1085,10 +1141,10 @@ static int run(int argc, char *argv[]) break; } - if (data.opt_monitor) { - data.monitoring = true; + while (data.opt_monitor) { pw_main_loop_run(data.loop); - data.monitoring = false; + if (data.opt_monitor) + do_list(&data); } return 0; diff --git a/src/tools/pw-loopback.c b/src/tools/pw-loopback.c index 65a613c15..1d76eb284 100644 --- a/src/tools/pw-loopback.c +++ b/src/tools/pw-loopback.c @@ -103,6 +103,7 @@ int main(int argc, char *argv[]) { "group", required_argument, NULL, 'g' }, { "name", required_argument, NULL, 'n' }, { "channels", required_argument, NULL, 'c' }, + { "channel-map", required_argument, NULL, 'm' }, { "latency", required_argument, NULL, 'l' }, { "delay", required_argument, NULL, 'd' }, { "capture", required_argument, NULL, 'C' }, diff --git a/src/tools/pw-mididump.c b/src/tools/pw-mididump.c index a29bb725d..277784bb1 100644 --- a/src/tools/pw-mididump.c +++ b/src/tools/pw-mididump.c @@ -19,6 +19,7 @@ #include #include "midifile.h" +#include "midiclip.h" struct data; @@ -32,8 +33,32 @@ struct data { struct pw_filter *filter; struct port *in_port; int64_t clock_time; + bool opt_midi1; }; + +static int dump_clip(const char *filename) +{ + struct midi_clip *file; + struct midi_clip_info info; + struct midi_event ev; + + file = midi_clip_open(filename, "r", &info); + if (file == NULL) { + fprintf(stderr, "error opening %s: %m\n", filename); + return -1; + } + + printf("opened %s format:%u division:%u\n", filename, info.format, info.division); + + while (midi_clip_read_event(file, &ev) == 1) + midi_event_dump(stdout, &ev); + + midi_clip_close(file); + + return 0; +} + static int dump_file(const char *filename) { struct midi_file *file; @@ -42,15 +67,14 @@ static int dump_file(const char *filename) file = midi_file_open(filename, "r", &info); if (file == NULL) { - fprintf(stderr, "error opening %s: %m\n", filename); - return -1; + return dump_clip(filename); } printf("opened %s format:%u ntracks:%u division:%u\n", filename, info.format, info.ntracks, info.division); - while (midi_file_read_event(file, &ev) == 1) { - midi_file_dump_event(stdout, &ev); - } + while (midi_file_read_event(file, &ev) == 1) + midi_event_dump(stdout, &ev); + midi_file_close(file); return 0; @@ -62,11 +86,14 @@ static void on_process(void *_data, struct spa_io_position *position) struct pw_buffer *b; struct spa_buffer *buf; struct spa_data *d; - struct spa_pod *pod; - struct spa_pod_control *c; - uint64_t frame; + struct spa_pod_parser parser; + struct spa_pod_frame frame; + struct spa_pod_sequence seq; + const void *seq_body, *c_body; + struct spa_pod_control c; + uint64_t offset; - frame = data->clock_time; + offset = data->clock_time; data->clock_time += position->clock.duration; b = pw_filter_dequeue_buffer(data->in_port); @@ -79,25 +106,31 @@ static void on_process(void *_data, struct spa_io_position *position) if (d->data == NULL) goto done; - if ((pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size)) == NULL) - goto done; - if (!spa_pod_is_sequence(pod)) + spa_pod_parser_init_from_data(&parser, d->data, d->maxsize, d->chunk->offset, d->chunk->size); + + if (spa_pod_parser_push_sequence_body(&parser, &frame, &seq, &seq_body) < 0) goto done; - SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) { + while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { struct midi_event ev; - if (c->type != SPA_CONTROL_UMP) + switch (c.type) { + case SPA_CONTROL_UMP: + ev.type = MIDI_EVENT_TYPE_UMP; + break; + case SPA_CONTROL_Midi: + ev.type = MIDI_EVENT_TYPE_MIDI1; + break; + default: continue; - + } ev.track = 0; - ev.sec = (frame + c->offset) / (float) position->clock.rate.denom; - ev.data = SPA_POD_BODY(&c->value), - ev.size = SPA_POD_BODY_SIZE(&c->value); - ev.type = MIDI_EVENT_TYPE_UMP; + ev.sec = (offset + c.offset) / (float) position->clock.rate.denom; + ev.data = (uint8_t*)c_body; + ev.size = c.value.size; - fprintf(stdout, "%4d: ", c->offset); - midi_file_dump_event(stdout, &ev); + fprintf(stdout, "%4d: ", c.offset); + midi_event_dump(stdout, &ev); } done: @@ -141,7 +174,7 @@ static int dump_filter(struct data *data) PW_FILTER_PORT_FLAG_MAP_BUFFERS, sizeof(struct port), pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit raw UMP", + PW_KEY_FORMAT_DSP, data->opt_midi1 ? "8 bit raw midi" : "32 bit raw UMP", PW_KEY_PORT_NAME, "input", NULL), NULL, 0); @@ -164,7 +197,8 @@ static void show_help(const char *name, bool error) fprintf(error ? stderr : stdout, "%s [options] [FILE]\n" " -h, --help Show this help\n" " --version Show version\n" - " -r, --remote Remote daemon name\n", + " -r, --remote Remote daemon name\n" + " -M, --force-midi Force midi format, one of \"midi\" or \"ump\",(default ump)\n", name); } @@ -176,6 +210,7 @@ int main(int argc, char *argv[]) { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, { "remote", required_argument, NULL, 'r' }, + { "force-midi", required_argument, NULL, 'M' }, { NULL, 0, NULL, 0} }; @@ -184,7 +219,7 @@ int main(int argc, char *argv[]) setlinebuf(stdout); - while ((c = getopt_long(argc, argv, "hVr:", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hVr:M:", long_options, NULL)) != -1) { switch (c) { case 'h': show_help(argv[0], false); @@ -200,6 +235,19 @@ int main(int argc, char *argv[]) case 'r': data.opt_remote = optarg; break; + + case 'M': + if (spa_streq(optarg, "midi")) + data.opt_midi1 = true; + else if (spa_streq(optarg, "ump")) + data.opt_midi1 = false; + else { + fprintf(stderr, "error: bad force-midi %s\n", optarg); + show_help(argv[0], true); + return 0; + } + break; + default: show_help(argv[0], true); return -1; diff --git a/src/tools/pw-profiler.c b/src/tools/pw-profiler.c index a73cc7ec7..2115a0899 100644 --- a/src/tools/pw-profiler.c +++ b/src/tools/pw-profiler.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -307,24 +308,24 @@ static int process_follower_clock(struct data *d, const struct spa_pod *pod, str static void dump_point(struct data *d, struct point *point) { int i; - int64_t d1, d2; - int64_t delay, period_usecs; + double d1, d2; + double delay, period_usecs; -#define CLOCK_AS_USEC(cl,val) (int64_t)(val * (float)SPA_USEC_PER_SEC / (cl)->rate.denom) -#define CLOCK_AS_SUSEC(cl,val) (int64_t)(val * (float)SPA_USEC_PER_SEC / ((cl)->rate.denom * (cl)->rate_diff)) +#define CLOCK_AS_USEC(cl,val) (double)(val * (double)SPA_USEC_PER_SEC / (cl)->rate.denom) +#define CLOCK_AS_SUSEC(cl,val) (double)(val * (double)SPA_USEC_PER_SEC / ((cl)->rate.denom * (cl)->rate_diff)) delay = CLOCK_AS_USEC(&point->clock, point->clock.delay); period_usecs = CLOCK_AS_SUSEC(&point->clock, point->clock.duration); - d1 = (point->driver.signal - point->driver.prev_signal) / 1000; - d2 = (point->driver.finish - point->driver.signal) / 1000; + d1 = (point->driver.signal - point->driver.prev_signal) / 1000.0; + d2 = (point->driver.finish - point->driver.signal) / 1000.0; if (d1 > period_usecs * 1.3 || d2 > period_usecs * 1.3) - d1 = d2 = (int64_t)(period_usecs * 1.4); + d1 = d2 = (double)(period_usecs * 1.4); /* 4 columns for the driver */ - fprintf(d->output, "%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t", + fprintf(d->output, "%.3f\t%.3f\t%.3f\t%.3f\t", d1 > 0 ? d1 : 0, d2 > 0 ? d2 : 0, delay, period_usecs); for (i = 0; i < MAX_FOLLOWERS; i++) { @@ -332,11 +333,11 @@ static void dump_point(struct data *d, struct point *point) if (point->follower[i].status == 0) { fprintf(d->output, " \t \t \t \t \t \t \t \t"); } else { - int64_t d4 = (point->follower[i].signal - point->driver.signal) / 1000; - int64_t d5 = (point->follower[i].awake - point->driver.signal) / 1000; - int64_t d6 = (point->follower[i].finish - point->driver.signal) / 1000; + double d4 = (point->follower[i].signal - point->driver.signal) / 1000.0; + double d5 = (point->follower[i].awake - point->driver.signal) / 1000.0; + double d6 = (point->follower[i].finish - point->driver.signal) / 1000.0; - fprintf(d->output, "%u\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%d\t0\t", + fprintf(d->output, "%u\t%.3f\t%.3f\t%.3f\t%.3f\t%.3f\t%d\t0\t", d->followers[i].id, d4 > 0 ? d4 : 0, d5 > 0 ? d5 : 0, diff --git a/src/tools/pw-top.c b/src/tools/pw-top.c index fc4da9239..6b936a9bf 100644 --- a/src/tools/pw-top.c +++ b/src/tools/pw-top.c @@ -44,6 +44,7 @@ struct measurement { int64_t finish; struct spa_fraction latency; uint32_t xrun_count; + bool async; }; struct node { @@ -53,7 +54,9 @@ struct node { char name[MAX_NAME+1]; enum pw_node_state state; struct measurement measurement; + uint32_t measurement_base; struct driver info; + uint32_t info_base; struct node *driver; uint32_t generation; char format[MAX_FORMAT+1]; @@ -417,7 +420,8 @@ static int process_follower_block(struct data *d, const struct spa_pod *pod, str SPA_POD_Long(&m.finish), SPA_POD_Int(&m.status), SPA_POD_Fraction(&m.latency), - SPA_POD_OPT_Int(&m.xrun_count))) < 0) + SPA_POD_OPT_Int(&m.xrun_count), + SPA_POD_OPT_Bool(&m.async))) < 0) return res; if ((n = find_node(d, id)) == NULL) @@ -486,8 +490,9 @@ static const char *state_as_string(enum pw_node_state state, uint32_t transport) return "!"; } -static void print_node(struct data *d, struct driver *i, struct node *n, int y) +static void print_node(struct data *d, struct node *dr, struct node *n, int y) { + struct driver *i = &dr->info; char buf1[64]; char buf2[64]; char buf3[64]; @@ -534,9 +539,10 @@ static void print_node(struct data *d, struct driver *i, struct node *n, int y) print_perc(buf3, active, 64, waiting, quantum), print_perc(buf4, active, 64, busy, quantum), n->measurement.xrun_count == XRUN_INVALID ? - i->xrun_count : n->measurement.xrun_count, + i->xrun_count - dr->info_base : + n->measurement.xrun_count - n->measurement_base, active ? n->format : "", - n->driver == n ? "" : " + ", + n->driver == n ? "" : n->measurement.async ? " = " : " + ", n->name); } @@ -558,7 +564,7 @@ static void do_refresh(struct data *d, bool force_refresh) return; if (!d->batch_mode) { - wclear(d->win); + werase(d->win); wattron(d->win, A_REVERSE); wprintw(d->win, "%-*.*s", COLS, COLS, HEADER); wattroff(d->win, A_REVERSE); @@ -570,7 +576,7 @@ static void do_refresh(struct data *d, bool force_refresh) if (n->driver != n) continue; - print_node(d, &n->info, n, y++); + print_node(d, n, n, y++); if(!d->batch_mode && y > LINES) break; @@ -581,7 +587,7 @@ static void do_refresh(struct data *d, bool force_refresh) if (f->driver != n || f == n) continue; - print_node(d, &n->info, f, y++); + print_node(d, n, f, y++); if(!d->batch_mode && y > LINES) break; @@ -612,6 +618,16 @@ static void do_timeout(void *data, uint64_t expirations) do_refresh(d, true); } +static void reset_xruns(struct data *d) +{ + struct node *n; + spa_list_for_each(n, &d->node_list, link) { + n->info_base = n->info.xrun_count; + n->measurement_base = n->measurement.xrun_count; + } + do_refresh(d, true); +} + static void profiler_profile(void *data, const struct spa_pod *pod) { struct data *d = data; @@ -788,6 +804,9 @@ static void do_handle_io(void *data, int fd, uint32_t mask) case 'q': pw_main_loop_quit(d->loop); break; + case 'c': + reset_xruns(d); + break; default: do_refresh(d, !d->batch_mode); break; diff --git a/src/tools/reserve.c b/src/tools/reserve.c index c28cec1a2..9f85c945f 100644 --- a/src/tools/reserve.c +++ b/src/tools/reserve.c @@ -395,6 +395,7 @@ rd_device_new(DBusConnection *connection, const char *device_name, const char *a error_free: free(d->service_name); free(d->object_path); + free(d->application_name); free(d); errno = -res; return NULL; diff --git a/test/meson.build b/test/meson.build index 43a59a254..5e38db383 100644 --- a/test/meson.build +++ b/test/meson.build @@ -9,7 +9,6 @@ pwtest_deps = [ pipewire_dep, mathlib, dl_lib, - cap_lib, epoll_shim_dep ] @@ -53,6 +52,7 @@ test('test-pw-utils', 'test-properties.c', 'test-array.c', 'test-map.c', + 'test-mempool.c', 'test-utils.c', include_directories: pwtest_inc, dependencies: [ spa_dep ], @@ -112,6 +112,7 @@ test('test-spa', executable('test-spa', 'test-spa-buffer.c', 'test-spa-control.c', + 'test-spa-format.c', 'test-spa-json.c', 'test-spa-utils.c', 'test-spa-log.c', @@ -135,10 +136,15 @@ endif summary({'pactl': pactl.found()}, bool_yn: true, section: 'Functional test programs') if default_sm == 'media-session' or default_sm == 'wireplumber' + test_functional_c_args = [] + if get_option('b_sanitize').contains('address') + test_functional_c_args += ['-DHAVE_ASAN'] + endif test('test-functional', executable('test-functional', 'test-functional.c', include_directories: pwtest_inc, + c_args: [test_functional_c_args], dependencies: [ spa_dep ], link_with: pwtest_lib) ) diff --git a/test/pwtest.c b/test/pwtest.c index 5726fc744..89a385f53 100644 --- a/test/pwtest.c +++ b/test/pwtest.c @@ -19,9 +19,6 @@ #ifdef HAVE_PIDFD_OPEN #include #endif -#ifdef HAVE_LIBCAP -#include -#endif #include #include #include @@ -33,6 +30,7 @@ #include #include "spa/utils/ansi.h" +#include "spa/utils/cleanup.h" #include "spa/utils/string.h" #include "spa/utils/defs.h" #include "spa/utils/list.h" @@ -788,6 +786,7 @@ static void set_test_env(struct pwtest_context *ctx, struct pwtest_test *t) replace_env(t, "ACP_PROFILES_DIR", SOURCE_ROOT "/spa/plugins/alsa/mixer/profile-sets"); replace_env(t, "PIPEWIRE_LOG_SYSTEMD", "false"); replace_env(t, "PWTEST_DATA_DIR", SOURCE_ROOT "/test/data"); + replace_env(t, "LD_LIBRARY_PATH", BUILD_ROOT "/src/pipewire:" BUILD_ROOT "pipewire-jack/src"); } static void close_pipes(int fds[_FD_LAST]) @@ -1297,39 +1296,20 @@ static void list_tests(struct pwtest_context *ctx) static bool is_debugger_attached(void) { - bool rc = false; -#ifdef HAVE_LIBCAP - int status; - int pid = fork(); + spa_autofree char *line = NULL; + size_t length = 0; - if (pid == -1) - return 0; + spa_autoptr(FILE) f = fopen("/proc/self/status", "re"); + if (!f) + return false; - if (pid == 0) { - int ppid = getppid(); - cap_t caps = cap_get_pid(ppid); - cap_flag_value_t cap_val; - - if (cap_get_flag(caps, CAP_SYS_PTRACE, CAP_EFFECTIVE, &cap_val) == -1 || - cap_val != CAP_SET) - _exit(false); - - if (ptrace(PTRACE_ATTACH, ppid, NULL, 0) == 0) { - waitpid(ppid, NULL, 0); - ptrace(PTRACE_CONT, ppid, NULL, 0); - ptrace(PTRACE_DETACH, ppid, NULL, 0); - rc = false; - } else { - rc = true; - } - _exit(rc); - } else { - waitpid(pid, &status, 0); - rc = WEXITSTATUS(status); + while (getline(&line, &length, f) >= 0) { + unsigned int tracer_pid; + if (sscanf(line, "TracerPid: %u", &tracer_pid) == 1) + return tracer_pid > 0; } -#endif - return !!rc; + return false; } static void usage(FILE *fp, const char *progname) diff --git a/test/pwtest.h b/test/pwtest.h index 8234624ce..dd9f3d7db 100644 --- a/test/pwtest.h +++ b/test/pwtest.h @@ -2,14 +2,10 @@ /* SPDX-FileCopyrightText: Copyright © 2021 Red Hat, Inc. */ /* SPDX-License-Identifier: MIT */ -#include "config.h" - #ifndef PWTEST_H #define PWTEST_H -#ifdef __cplusplus -extern "C" { -#endif +#include "config.h" #include #include @@ -20,6 +16,10 @@ extern "C" { #include #include "spa/support/plugin.h" +#ifdef __cplusplus +extern "C" { +#endif + /** * \defgroup pwtest Test Suite * \brief `pwtest` is a test runner framework for PipeWire. diff --git a/test/test-functional.c b/test/test-functional.c index 3cdcb6161..29277ffb0 100644 --- a/test/test-functional.c +++ b/test/test-functional.c @@ -2,17 +2,22 @@ /* SPDX-FileCopyrightText: Copyright © 2019 Wim Taymans */ /* SPDX-License-Identifier: MIT */ -#include - #include "config.h" +#include + #include "pwtest.h" PWTEST(openal_info_test) { -#ifdef OPENAL_INFO_PATH + /* openal-info tries to load libpipewire, which would need + * LD_PRELOAD=/lib64/libasan.so.XX to work when ASan is enabled. Don't try to + * figure out the right preload, but just disable the test in that case. + */ +#if defined(OPENAL_INFO_PATH) && !defined(HAVE_ASAN) int status = pwtest_spawn(OPENAL_INFO_PATH, (char *[]){ "openal-info", NULL }); pwtest_int_eq(WEXITSTATUS(status), 0); + pwtest_int_eq(WIFSIGNALED(status), 0); return PWTEST_PASS; #else return PWTEST_SKIP; @@ -24,6 +29,7 @@ PWTEST(pactl_test) #ifdef PACTL_PATH int status = pwtest_spawn(PACTL_PATH, (char *[]){ "pactl", "info", NULL }); pwtest_int_eq(WEXITSTATUS(status), 0); + pwtest_int_eq(WIFSIGNALED(status), 0); return PWTEST_PASS; #else return PWTEST_SKIP; diff --git a/test/test-logger.c b/test/test-logger.c index 81132bffa..2c5a5bd9a 100644 --- a/test/test-logger.c +++ b/test/test-logger.c @@ -355,7 +355,7 @@ PWTEST(logger_debug_env_invalid) fsync(STDERR_FILENO); lseek(fd, SEEK_SET, 0); - while ((rc = read(fd, buf, sizeof(buf) - 1) > 0)) { + while ((rc = read(fd, buf, sizeof(buf) - 1)) > 0) { if (strstr(buf, "Ignoring invalid format in log level")) { error_message_found = true; break; @@ -496,6 +496,9 @@ PWTEST(logger_journal) pwtest_ptr_notnull(iface); rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY|SD_JOURNAL_CURRENT_USER); + if (rc == -ENOSYS) + return PWTEST_SKIP; + pwtest_neg_errno_ok(rc); sd_journal_seek_head(journal); @@ -565,6 +568,9 @@ PWTEST(logger_journal_chain) pwtest_ptr_notnull(iface); rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY); + if (rc == -ENOSYS) + return PWTEST_SKIP; + pwtest_neg_errno_ok(rc); sd_journal_seek_head(journal); if (sd_journal_next(journal) == 0) { /* No entries? We don't have a journal */ diff --git a/test/test-mempool.c b/test/test-mempool.c new file mode 100644 index 000000000..36f69c571 --- /dev/null +++ b/test/test-mempool.c @@ -0,0 +1,49 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2025 PipeWire authors */ +/* SPDX-License-Identifier: MIT */ + +#include + +#include +#include + +#include "pwtest.h" + +PWTEST(mempool_issue4884) +{ + /* + * See https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/4884. This + * test checks if the offset is correctly applied when a mapping is reused. + */ + + long page_size = sysconf(_SC_PAGESIZE); + pwtest_errno_ok(page_size); + pwtest_int_ge(page_size, 8); + + struct pw_mempool *p = pw_mempool_new(NULL); + pwtest_ptr_notnull(p); + + struct pw_memblock *b = pw_mempool_alloc(p, PW_MEMBLOCK_FLAG_READWRITE, SPA_DATA_MemFd, 2 * page_size); + pwtest_ptr_notnull(b); + + struct pw_memmap *m1 = pw_mempool_map_id(p, b->id, PW_MEMMAP_FLAG_READWRITE, page_size / 2, page_size, NULL); + pwtest_ptr_notnull(m1); + pwtest_ptr_eq(m1->block, b); + + struct pw_memmap *m2 = pw_mempool_map_id(p, b->id, PW_MEMMAP_FLAG_READWRITE, 3 * page_size / 2, page_size / 2, NULL); + pwtest_ptr_notnull(m2); + pwtest_ptr_eq(m2->block, b); + + pwtest_int_eq(SPA_PTRDIFF(m2->ptr, m1->ptr), page_size); + + pw_mempool_destroy(p); + + return PWTEST_PASS; +} + +PWTEST_SUITE(pw_mempool) +{ + pwtest_add(mempool_issue4884, PWTEST_NOARG); + + return PWTEST_PASS; +} diff --git a/test/test-spa-control.c b/test/test-spa-control.c index d493b2934..97589efd7 100644 --- a/test/test-spa-control.c +++ b/test/test-spa-control.c @@ -123,6 +123,7 @@ static int do_ump_to_midi_test(char *ump, char *midi) size_t m_size, u_size, m_offs = 0; uint8_t *m_data = alloca(strlen(midi) / 2); uint32_t *u_data = alloca(strlen(ump) / 2); + uint64_t state = 0; u_size = parse_ump(ump, u_data, sizeof(u_data)); m_size = parse_midi(midi, m_data, sizeof(m_data)); @@ -133,8 +134,9 @@ static int do_ump_to_midi_test(char *ump, char *midi) while (u_size > 0) { uint8_t midi[32]; fprintf(stdout, "%zd %08x\n", u_size, *u_data); - int midi_size = spa_ump_to_midi(u_data, u_size, - midi, sizeof(midi)); + + int midi_size = spa_ump_to_midi((const uint32_t**)&u_data, &u_size, + midi, sizeof(midi), &state); if (midi_size <= 0) return midi_size; @@ -145,8 +147,6 @@ static int do_ump_to_midi_test(char *ump, char *midi) fprintf(stdout, "%08x %08x\n", m_data[m_offs], midi[i]); spa_assert(m_data[m_offs++] == midi[i]); } - u_size -= spa_ump_message_size(*u_data >> 28) * 4; - u_data += spa_ump_message_size(*u_data >> 28); } return 0; } @@ -160,6 +160,11 @@ PWTEST(control_ump_to_midi) spa_assert(do_ump_to_midi_test("30160102 03040506 30260708 09101112 30311300 00000000", "f0 01 02 03 04 05 06 07 08 09 10 11 12 13 f7") >= 0); + + spa_assert(do_ump_to_midi_test("40cf0000 11000000", "cf 11") >= 0); + + spa_assert(do_ump_to_midi_test("40cf0001 11002233", "bf 00 22 bf 20 33 cf 11") >= 0); + return PWTEST_PASS; } diff --git a/test/test-spa-format.c b/test/test-spa-format.c new file mode 100644 index 000000000..7cbec7690 --- /dev/null +++ b/test/test-spa-format.c @@ -0,0 +1,129 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2025 Pauli Virtanen */ +/* SPDX-License-Identifier: MIT */ + +#include +#include + +#include "pwtest.h" + +PWTEST(audio_format_sizes) +{ + union { + uint8_t buf[1024]; + struct spa_audio_info align; + } data; + struct spa_audio_info info; + size_t i; + + memset(&info, 0xf3, sizeof(info)); + info.media_type = SPA_MEDIA_TYPE_audio; + info.media_subtype = SPA_MEDIA_SUBTYPE_raw; + info.info.raw.channels = 5; + info.info.raw.format = SPA_AUDIO_FORMAT_F32P; + info.info.raw.rate = 12345; + info.info.raw.flags = 0; + info.info.raw.position[0] = 1; + info.info.raw.position[1] = 2; + info.info.raw.position[2] = 3; + info.info.raw.position[3] = 4; + info.info.raw.position[4] = 5; + + for (i = 0; i < sizeof(data.buf); ++i) { + struct spa_pod *pod; + uint8_t buf[4096]; + struct spa_pod_builder b; + + spa_pod_builder_init(&b, buf, sizeof(buf)); + memcpy(data.buf, &info, sizeof(info)); + + pod = spa_format_audio_ext_build(&b, 123, (void *)data.buf, i); + if (i < offsetof(struct spa_audio_info, info.raw) + + offsetof(struct spa_audio_info_raw, position)) + pwtest_bool_true(!pod); + else + pwtest_bool_true(pod); + } + + for (i = 0; i < sizeof(data.buf); ++i) { + struct spa_pod *pod; + uint8_t buf[4096]; + struct spa_pod_builder b; + int ret; + + spa_pod_builder_init(&b, buf, sizeof(buf)); + pod = spa_format_audio_ext_build(&b, 123, &info, sizeof(info)); + pwtest_bool_true(pod); + + memset(data.buf, 0xf3, sizeof(data.buf)); + + ret = spa_format_audio_ext_parse(pod, (void *)data.buf, i); + if (i < offsetof(struct spa_audio_info, info.raw) + + offsetof(struct spa_audio_info_raw, position) + + info.info.raw.channels*sizeof(uint32_t)) { + for (size_t j = i; j < sizeof(data.buf); ++j) + pwtest_int_eq(data.buf[j], 0xf3); + pwtest_int_lt(ret, 0); + } else { + pwtest_int_ge(ret, 0); + pwtest_bool_true(memcmp(data.buf, &info, SPA_MIN(i, sizeof(info))) == 0); + } + } + + memset(&info, 0xf3, sizeof(info)); + info.media_type = SPA_MEDIA_TYPE_audio; + info.media_subtype = SPA_MEDIA_SUBTYPE_aac; + info.info.aac.rate = 12345; + info.info.aac.channels = 6; + info.info.aac.bitrate = 54321; + info.info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_MP4LATM; + + for (i = 0; i < sizeof(data.buf); ++i) { + struct spa_pod *pod; + uint8_t buf[4096]; + struct spa_pod_builder b; + + spa_pod_builder_init(&b, buf, sizeof(buf)); + memcpy(data.buf, &info, sizeof(info)); + + pod = spa_format_audio_ext_build(&b, 123, (void *)data.buf, i); + if (i < offsetof(struct spa_audio_info, info.raw) + + sizeof(struct spa_audio_info_aac)) + pwtest_bool_true(!pod); + else + pwtest_bool_true(pod); + } + + for (i = 0; i < sizeof(data.buf); ++i) { + struct spa_pod *pod; + uint8_t buf[4096]; + struct spa_pod_builder b; + int ret; + + spa_pod_builder_init(&b, buf, sizeof(buf)); + pod = spa_format_audio_ext_build(&b, 123, &info, sizeof(info)); + pwtest_bool_true(pod); + + memset(data.buf, 0xf3, sizeof(data.buf)); + + ret = spa_format_audio_ext_parse(pod, (void *)data.buf, i); + if (i < offsetof(struct spa_audio_info, info.raw) + + sizeof(struct spa_audio_info_aac)) { + for (size_t j = i; j < sizeof(data.buf); ++j) + pwtest_int_eq(data.buf[j], 0xf3); + pwtest_int_lt(ret, 0); + } else { + pwtest_int_ge(ret, 0); + pwtest_bool_true(memcmp(data.buf, &info, SPA_MIN(i, sizeof(info))) == 0); + } + } + + return PWTEST_PASS; +} + +PWTEST_SUITE(spa_format) +{ + pwtest_add(audio_format_sizes, PWTEST_NOARG); + + return PWTEST_PASS; +} diff --git a/test/test-spa-json.c b/test/test-spa-json.c index b07acc00d..0c3c46f59 100644 --- a/test/test-spa-json.c +++ b/test/test-spa-json.c @@ -609,7 +609,7 @@ static void test_array(const char *str, const char * const vals[]) spa_json_init(&it[0], str, strlen(str)); if (spa_json_enter_array(&it[0], &it[1]) <= 0) - spa_json_init(&it[1], str, strlen(str)); + spa_json_init_relax(&it[1], '[', str, strlen(str)); for (i = 0; vals[i]; i++) { pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0); pwtest_str_eq(val, vals[i]); @@ -624,6 +624,7 @@ PWTEST(json_array) test_array("[FL FR]", (const char *[]){ "FL", "FR", NULL }); test_array("FL FR", (const char *[]){ "FL", "FR", NULL }); test_array("[ FL FR ]", (const char *[]){ "FL", "FR", NULL }); + test_array("FL FR FC", (const char *[]){ "FL", "FR", "FC", NULL }); return PWTEST_PASS; } @@ -882,8 +883,15 @@ static int validate_strict_json(struct spa_json *it, int depth, FILE *f) fprintf(f, "%d", v); } else if (spa_json_is_float(value, len)) { float v; - if (spa_json_parse_float(value, len, &v) > 0) - fprintf(f, "%G", v); + char float_str[64]; + if (spa_json_parse_float(value, len, &v) > 0) { + int i, l; + l = spa_scnprintf(float_str, sizeof(float_str), "%G", v); + for (i = 0; i < l; i++) + if (float_str[i] == ',') + float_str[i] = '.'; + fprintf(f, "%s", float_str); + } } else { /* bare value: error here, as we want to test * int/float/etc parsing */ diff --git a/test/test-spa-pod.c b/test/test-spa-pod.c index e84b5a18e..964844f75 100644 --- a/test/test-spa-pod.c +++ b/test/test-spa-pod.c @@ -286,7 +286,7 @@ PWTEST(pod_init) spa_assert_se(SPA_POD_SIZE(&pod) == 14); spa_assert_se(SPA_POD_TYPE(&pod) == SPA_TYPE_String); spa_assert_se(SPA_POD_BODY_SIZE(&pod) == 6); - spa_assert_se(!spa_pod_is_string(&pod.pod.pod)); + spa_assert_se(spa_pod_is_string(&pod.pod.pod)); spa_assert_se(spa_pod_copy_string(&pod.pod.pod, sizeof(val), val) < 0); } { @@ -524,6 +524,8 @@ PWTEST(pod_build) spa_assert_se(memcmp(&val.F, &SPA_FRACTION(25,1), sizeof(struct spa_fraction)) == 0); spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod)); + spa_assert_se(array->type == SPA_TYPE_Array); + spa_assert_se(array->size >= sizeof(struct spa_pod_array_body)); spa_assert_se(spa_pod_is_array(pod)); spa_assert_se(SPA_POD_ARRAY_VALUE_TYPE(pod) == SPA_TYPE_Int); spa_assert_se(SPA_POD_ARRAY_VALUE_SIZE(pod) == sizeof(int32_t)); @@ -542,6 +544,8 @@ PWTEST(pod_build) } spa_assert_se((pod = spa_pod_next(pod)) != NULL && spa_pod_is_inside(head, len, pod)); + spa_assert_se(array->type == SPA_TYPE_Array); + spa_assert_se(array->size >= sizeof(struct spa_pod_array_body)); spa_assert_se(spa_pod_is_array(pod)); spa_assert_se(SPA_POD_ARRAY_VALUE_TYPE(pod) == SPA_TYPE_Long); spa_assert_se(SPA_POD_ARRAY_VALUE_SIZE(pod) == sizeof(int64_t)); @@ -691,6 +695,10 @@ PWTEST(pod_empty) array = spa_pod_builder_pop(&b, &f); spa_assert_se(array != NULL); spa_debug_mem(0, array, 16); + spa_assert_se(array->type == SPA_TYPE_Array); + spa_assert_se(array->size == sizeof(struct spa_pod_array_body)); + spa_assert_se(SPA_POD_ARRAY_VALUE_TYPE(array) == SPA_TYPE_Id); + spa_assert_se(SPA_POD_ARRAY_VALUE_SIZE(array) == 4); spa_assert_se(spa_pod_is_array(array)); a2 = spa_pod_get_array(array, &n_vals); spa_assert_se(a2 != NULL); @@ -700,6 +708,8 @@ PWTEST(pod_empty) spa_assert_se(spa_pod_builder_push_array(&b, &f) == 0); array = spa_pod_builder_pop(&b, &f); spa_assert_se(array != NULL); + spa_assert_se(array->type == SPA_TYPE_Array); + spa_assert_se(array->size >= sizeof(struct spa_pod_array_body)); spa_assert_se(spa_pod_is_array(array)); a2 = spa_pod_get_array(array, &n_vals); spa_assert_se(a2 != NULL); @@ -710,6 +720,8 @@ PWTEST(pod_empty) spa_assert_se(spa_pod_builder_none(&b) == 0); array = spa_pod_builder_pop(&b, &f); spa_assert_se(array != NULL); + spa_assert_se(array->type == SPA_TYPE_Array); + spa_assert_se(array->size >= sizeof(struct spa_pod_array_body)); spa_assert_se(spa_pod_is_array(array)); a2 = spa_pod_get_array(array, &n_vals); spa_assert_se(a2 != NULL); @@ -718,6 +730,8 @@ PWTEST(pod_empty) spa_pod_builder_init(&b, buffer, sizeof(buffer)); spa_assert_se(spa_pod_builder_array(&b, 4, SPA_TYPE_Id, 0, NULL) == 0); array = (struct spa_pod*)buffer; + spa_assert_se(array->type == SPA_TYPE_Array); + spa_assert_se(array->size >= sizeof(struct spa_pod_array_body)); spa_assert_se(spa_pod_is_array(array)); a2 = spa_pod_get_array(array, &n_vals); spa_assert_se(a2 != NULL); @@ -730,6 +744,10 @@ PWTEST(pod_empty) choice = spa_pod_builder_pop(&b, &f); spa_assert_se(choice != NULL); spa_debug_mem(0, choice, 32); + spa_assert_se(choice->type == SPA_TYPE_Choice); + spa_assert_se(choice->size == sizeof(struct spa_pod_choice_body)); + spa_assert_se(SPA_POD_CHOICE_TYPE(choice) == SPA_CHOICE_None); + spa_assert_se(SPA_POD_CHOICE_CHILD(choice)->size == 4); spa_assert_se(spa_pod_is_choice(choice)); ch2 = spa_pod_get_values(choice, &n_vals, &ch); spa_assert_se(ch2 != NULL); @@ -739,6 +757,8 @@ PWTEST(pod_empty) spa_assert_se(spa_pod_builder_push_choice(&b, &f, 0, 0) == 0); choice = spa_pod_builder_pop(&b, &f); spa_assert_se(choice != NULL); + spa_assert_se(SPA_POD_CHOICE_TYPE(choice) == SPA_CHOICE_None); + spa_assert_se(SPA_POD_CHOICE_CHILD(choice)->size == 0); spa_assert_se(spa_pod_is_choice(choice)); ch2 = spa_pod_get_values(choice, &n_vals, &ch); spa_assert_se(ch2 != NULL); @@ -749,6 +769,8 @@ PWTEST(pod_empty) spa_assert_se(spa_pod_builder_none(&b) == 0); choice = spa_pod_builder_pop(&b, &f); spa_assert_se(choice != NULL); + spa_assert_se(SPA_POD_CHOICE_TYPE(choice) == SPA_CHOICE_None); + spa_assert_se(SPA_POD_CHOICE_CHILD(choice)->size == 0); spa_assert_se(spa_pod_is_choice(choice)); ch2 = spa_pod_get_values(choice, &n_vals, &ch); spa_assert_se(ch2 != NULL); @@ -1248,7 +1270,7 @@ PWTEST(pod_parser) spa_assert_se(spa_pod_parser_push_object(&p, &f, SPA_TYPE_OBJECT_Format, NULL) == -EPROTO); spa_assert_se(p.state.offset == 0); spa_assert_se(spa_pod_parser_push_object(&p, &f, SPA_TYPE_OBJECT_Props, NULL) == 0); - spa_assert_se(p.state.offset == 392); + spa_assert_se(p.state.offset == sizeof(struct spa_pod_object)); spa_assert_se(spa_pod_parser_frame(&p, &f) == val.P); spa_zero(val); diff --git a/test/test-spa-utils.c b/test/test-spa-utils.c index 06b8f8d17..43c2ffa2c 100644 --- a/test/test-spa-utils.c +++ b/test/test-spa-utils.c @@ -853,9 +853,6 @@ PWTEST(utils_snprintf_abort_neg_size) size_t size = pwtest_get_iteration(current_test); char dest[8]; - if (RUNNING_ON_VALGRIND) - return PWTEST_SKIP; - spa_scnprintf(dest, size, "1234"); /* expected to abort() */ return PWTEST_FAIL;