diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 448bfa06e..2918ce62e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -410,13 +410,15 @@ build_on_fedora_html_docs: -Dsndfile=enabled -Dsession-managers=[] before_script: - - git fetch origin 1.0 1.2 1.4 master + - git fetch origin 1.0 1.2 1.4 1.6 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 1.6 origin/1.6 + - git clone -b 1.6 . branch-1.6 - git branch -f master origin/master - git clone -b master . branch-master - !reference [.build, before_script] @@ -433,6 +435,10 @@ build_on_fedora_html_docs: - meson setup builddir $MESON_OPTIONS - meson compile -C builddir doc/pipewire-docs - cd .. + - cd branch-1.6 + - 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 @@ -658,7 +664,7 @@ doccheck: - cat pipewire_module_pages - | for page in $(cat pipewire_module_pages); do - git grep -q -e "\\\subpage $page" || (echo "\\page $page is missing \\subpage entry in doc/pipewire-modules.dox" && false) + git grep -q -e "\\\subpage $page" || (echo "\\page $page is missing \\subpage entry in doc/dox/modules.dox" && false) done check_missing_headers: @@ -682,12 +688,13 @@ pages: - job: build_on_fedora_html_docs artifacts: true script: - - mkdir public public/1.0 public/1.2 public/1.4 public/devel + - mkdir public public/1.0 public/1.2 public/1.4 public/1.6 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-1.6/builddir/doc/html/* public/1.6/ - cp -R branch-master/builddir/doc/html/* public/devel/ - - (cd public && ln -s 1.4/* .) + - (cd public && ln -s 1.6/* .) artifacts: paths: - public diff --git a/doc/DoxygenLayout.xml b/doc/DoxygenLayout.xml index 300c8400e..10da55da5 100644 --- a/doc/DoxygenLayout.xml +++ b/doc/DoxygenLayout.xml @@ -44,6 +44,7 @@ + diff --git a/doc/dox/config/pipewire-client.conf.5.md b/doc/dox/config/pipewire-client.conf.5.md index db43839a0..321538a79 100644 --- a/doc/dox/config/pipewire-client.conf.5.md +++ b/doc/dox/config/pipewire-client.conf.5.md @@ -80,6 +80,9 @@ stream.properties = { #channelmix.fc-cutoff = 12000.0 #channelmix.rear-delay = 12.0 #channelmix.stereo-widen = 0.0 + #channelmix.center-level = 0.707106781 + #channelmix.surround-level = 0.707106781 + #channelmix.lfe-level = 0.5 #channelmix.hilbert-taps = 0 #dither.noise = 0 #dither.method = none # rectangular, triangular, triangular-hf, wannamaker3, shaped5 diff --git a/doc/dox/config/pipewire-props.7.md b/doc/dox/config/pipewire-props.7.md index e561979d5..44b16f505 100644 --- a/doc/dox/config/pipewire-props.7.md +++ b/doc/dox/config/pipewire-props.7.md @@ -450,11 +450,25 @@ 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). +This can be used to configure the port.passive property for all ports of this node. + +Possible values are: + + * "out": output ports are passive, They will not make the peers active and active peers will + not make this node active. + * "in": input ports are passive, They will not make the peers active and active peers will + not make this node active. + * "true": A combination in "in" and "out", both input and output ports are passive. + * "out-follow": output ports will not make the peer active but when the peer is activated via + some other way, this node will also become active. + * "in-follow": input ports will not make the peer active but when the peer is activated via + some other way, this node will also become active. + * "follow": A combination of "in-follow" and "out-follow". This is used for filter nodes that sit in front of sinks/sources and need to suspend together with the sink/source. \endparblock + @PAR@ node-prop node.link-group = ID Add the node to a certain link group. Nodes from the same link group are not automatically linked to each other by the session manager. And example is a coupled stream where you don't want the output to link to the input streams, making a useless loop. @@ -769,6 +783,15 @@ more to the center speaker and leaves the ambient sound in the stereo channels. This is only active when up-mix is enabled and a Front Center channel is mixed. \endparblock +@PAR@ node-prop channelmix.center-level = 0.707106781 +The level of the center channel when up/downmixing. + +@PAR@ node-prop channelmix.surround-level = 0.707106781 +The level of the surround channels when up/downmixing. + +@PAR@ node-prop channelmix.lfe-level = 0.5 +The level of the LFE channel when up/downmixing. + @PAR@ node-prop channelmix.hilbert-taps = 0 \parblock This option will apply a 90 degree phase shift to the rear channels to improve specialization. @@ -1375,9 +1398,9 @@ Default: as per QoS preset. @PAR@ device-prop bluez5.bap.force-target-latency = "balanced" # string BAP QoS target latency profile forced for QoS configuration selection. -If not set or set to "balanced", both low-latency and high-reliabilty QoS configuration table are used. +If not set or set to "balanced", both low-latency and high-reliability QoS configuration table are used. This property is experimental. -Available: low-latency, high-reliabilty, balanced +Available: low-latency, high-reliability, balanced ## Node properties @@ -1420,6 +1443,11 @@ them. Below are some port properties may interesting for users: \copydoc PW_KEY_PORT_ALIAS \endparblock +@PAR@ port-prop port.passive # string +\parblock +\copydoc PW_KEY_PORT_PASSIVE +\endparblock + \see pw_keys in the API documentation for a full list. # LINK PROPERTIES @IDX@ props diff --git a/doc/dox/config/pipewire-pulse.conf.5.md b/doc/dox/config/pipewire-pulse.conf.5.md index ad1b213c7..9cc8d4c48 100644 --- a/doc/dox/config/pipewire-pulse.conf.5.md +++ b/doc/dox/config/pipewire-pulse.conf.5.md @@ -93,6 +93,9 @@ stream.properties = { #channelmix.fc-cutoff = 12000.0 #channelmix.rear-delay = 12.0 #channelmix.stereo-widen = 0.0 + #channelmix.center-level = 0.707106781 + #channelmix.surround-level = 0.707106781 + #channelmix.lfe-level = 0.5 #channelmix.hilbert-taps = 0 #dither.noise = 0 #dither.method = none # rectangular, triangular, triangular-hf, wannamaker3, shaped5 diff --git a/doc/dox/internals/dma-buf.dox b/doc/dox/internals/dma-buf.dox index 5042f285c..ec02b9da7 100644 --- a/doc/dox/internals/dma-buf.dox +++ b/doc/dox/internals/dma-buf.dox @@ -349,10 +349,10 @@ rectangles. For example params[n_params++] = spa_pod_builder_pop(&b, &f); ``` -After having received the first \ref SPA_PARAM_PeerCapability param, if it contained the \ref -PW_CAPABILITY_DEVICE_ID set to `true`, the full set of formats can be sent using \ref -pw_stream_update_params following by activating the stream using -`pw_stream_set_active(stream, true)`. +After having received the first \ref SPA_PARAM_PeerCapability param, if it contained the +\ref PW_CAPABILITY_DEVICE_ID_NEGOTIATION set to a supported API version number, the full +set of formats can be sent using \ref pw_stream_update_params following by activating the +stream usina supported API version numberstream_set_active(stream, true)`. Note that the first \ref SPA_PARAM_Format received may be the result of the initial format negotian with bare minimum parameters, and will be superseded by the result of the format diff --git a/doc/dox/internals/index.dox b/doc/dox/internals/index.dox index 89d2e9da3..357e2f126 100644 --- a/doc/dox/internals/index.dox +++ b/doc/dox/internals/index.dox @@ -10,6 +10,7 @@ - \subpage page_objects_design - \subpage page_library - \subpage page_dma_buf +- \subpage page_running - \subpage page_scheduling - \subpage page_driver - \subpage page_latency diff --git a/doc/dox/internals/latency.dox b/doc/dox/internals/latency.dox index 0ff2cbe95..efc2b4c8b 100644 --- a/doc/dox/internals/latency.dox +++ b/doc/dox/internals/latency.dox @@ -103,7 +103,7 @@ 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. +node and will be scheduled differently, see \ref page_scheduling . 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. diff --git a/doc/dox/internals/midi.dox b/doc/dox/internals/midi.dox index 4c86c516b..e89b24578 100644 --- a/doc/dox/internals/midi.dox +++ b/doc/dox/internals/midi.dox @@ -62,6 +62,13 @@ As of 1.4, SPA_CONTROL_UMP (Universal Midi Packet) is the prefered format for the MIDI 1.0 and 2.0 messages in the \ref spa_pod_sequence. Conversion to SPA_CONTROL_Midi is performed for legacy applications. +As of 1.7 the prefered format is Midi1 again because most devices and +applications are still Midi1 and conversions between Midi1 and UMP are not +completely transparent in ALSA and PipeWire. UMP in the ALSA sequencer +and consumers must be enabled explicitly. UMP in producers is supported +still and will be converted to Midi1 by all consumers that did not explicitly +enable UMP support. + ## The PipeWire Daemon Nothing special is implemented for MIDI. Negotiation of formats @@ -104,13 +111,14 @@ filtering out the \ref SPA_CONTROL_Midi, \ref SPA_CONTROL_OSC and \ref SPA_CONTROL_UMP types. On output ports, the JACK event stream is 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 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 or the JackPortIsMIDI2 flag, in which case -the UMP messages are simply passed on. +Normally, all MIDI and UMP input messages are converted to MIDI1 jack +events unless the JACK port was created with an explcit "32 bit raw UMP" +format or with the JackPortIsMIDI2 flag, in which case the messages are +converted to UMP or passed on directly. + +For output ports, the JACK events are assumed to be +MIDI1 unless the port has the "32 bit raw UMP" format or the JackPortIsMIDI2 +flag, in which case the control messages are assumed to be UMP. 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/running.dox b/doc/dox/internals/running.dox new file mode 100644 index 000000000..e7ffe06db --- /dev/null +++ b/doc/dox/internals/running.dox @@ -0,0 +1,393 @@ +/** \page page_running Node running + +This document tries to explain how the Nodes in a PipeWire graph become +runnable so that they can be scheduled later. + +It also describes how nodes are grouped together and scheduled together. + +# Runnable Nodes + +A runnable node is a node that will participate in the dataflow in the +PipeWire graph. + +Not all nodes should participate by default. For example, filters or device +nodes that are not linked to any runnable nodes should not do useless +processing. + +Nodes form one or more groups depending on properties and how they are +linked. For each group, one driver is selected to run the group. Inside the +group, all runnable nodes are scheduled by the group driver. If there are no +runnable nodes in a group, the driver is not started. + +PipeWire provides mechanisms to precisely describe how and when nodes should +be scheduled and grouped using: + + - port properties to control how port links control runnable state + - node properties to control processing + - grouping of nodes and internal links between nodes + +# Port passive modes + +A Port has 4 passive modes, this depends on the value of the `port.passive` property: + + - `false`, the port will make the peer active and an active peer will make this port + active. + - `true`, the port will not make the peer active and an active peer will not make this + port active. + - `follow`, the port will not make the peer active but an active peer will make this + port active. + - `follow-suspend`, the port will only make another follow-suspend peer active but any + active peer will make this port active. + +The combination of these 4 modes on the output and input ports of a link results in a +wide range of use cases. + +# Node passive modes + +A Node can have 10 passive modes, `node.passive` can be set to a comma separated list +of the following values: + + - `false`, both input and output ports have `port.passive = false` + - `in`, input ports have `port.passive = true` + - `out`, output ports have `port.passive = true` + - `true`, both input and output ports have `port.passive = true`. This is the same + as `in,out`. + - `in-follow`, input ports have `port.passive = follow` + - `out-follow`, output ports have `port.passive = follow` + - `follow`, input and output ports have `port.passive = follow`. This is the same + as `in-follow,out-follow`. + - `in-follow-suspend`, input ports have `port.passive = follow-suspend` + - `out-follow-suspend`, output ports have `port.passive = follow-suspend` + - `follow-suspend`, input and output ports have `port.passive = follow-suspend`. + This is the same as `in-follow-suspend,out-follow-suspend`. + +Nodes by default have the `false` mode but nodes with the `media.class` property +containing `Sink`, `Source` or `Duplex` receive the `follow-suspend` mode by default. + +Unless explicitly configured, ports inherit the mode from their parent node. + +# Updating the node runnable state + +We iterate all nodes A in the graph and look at its peers B. + +Based on the port passive modes of the port links we can decide if the nodes are +runnable or not. A link will always make both nodes runnable or none. + +The following table decides the runnability of the 2 nodes based on the port.passive +mode of the link between the 2 ports: + +``` + B-false B-true B-follow B-follow-suspend + +A-false X X X X +A-true +A-follow +A-follow-suspend X + Table 1 +``` + + +When a node is made runnable, the port passive mode will then decide if the peer ports +should become active as well with the following table. + +``` + B-false B-true B-follow B-follow-suspend + +A-false X X X +A-true X X X +A-follow X X X +A-follow-suspend X X X + Table 2 +``` + +So when A is runnable, all peers are activated except those with `port.passive=true`. + +When A is runnable, all the nodes that share the same group or link-group will also +be made runnable. + +# Use cases + +Let's check some cases that we want to solve with these node and port properties. + +## Device nodes + +``` + +--------+ +--------+ + | ALSA | | ALSA | + | Source | | Sink | + | FL FL | + | FR FR | + +--------+ +--------+ +``` + +Unlinked device nodes are supposed to stay suspended when nothing is linked to +them. + +``` + +----------+ +--------+ + | playback | | ALSA | + | | | Sink | + | FL ------ FL | + | FR ------ FR | + +----------+ +--------+ +``` + +An (active) player node linked to a device node should make both nodes runnable. + +Device nodes have the `port.passive = follow-suspend` property by default. The +playback node has the `port.passive = false` by default. + +If we look at the playback node as A and the sink as B, both nodes will be made +runnable according to Table 1. + +The two runnable nodes form a group and will be scheduled together. One of the +nodes of a group with the `node.driver = true` property is selected as the +driver. In the above case, that will be the ALSA Sink. + +Likewise, a capture node linked to an ALSA Source should make both nodes runnable. + +``` + +--------+ +---------+ + | ALSA | | capture | + | Source | | | + | FL ------ FL | + | FR ------ FR | + +--------+ +---------+ +``` + +The ALSA Source is now the driver. + +Also, linking 2 device nodes together should make them runnable: + +``` + +--------+ +--------+ + | ALSA | | ALSA | + | Source | | Sink | + | FL ----------------------- FL | + | FR ----------------------- FR | + +--------+ +--------+ +``` + +This is the case because in Table 1, the two `port.passive = follow-suspend` ports +from the Source and Sink activate each other. + +## Filter nodes + +When there is a filter in front of the ALSA Sink, it should not make the filter and +sink runnable. + +``` + +--------+ +--------+ + | filter | | ALSA | + | | | Sink | + FL FL ------ FL | + FR FR ------ FR | + +--------+ +--------+ +``` + +The links between the filter and ALSA Sink are `port.passive = true` and don't make +the nodes runnable. + +The filter needs to be made runnable via some other means to also make the ALSA +Sink runnable, for example by linking a playback node: + +``` + +----------+ +--------+ +--------+ + | playback | | filter | | ALSA | + | | | | | Sink | + | FL ------ FL FL ------ FL | + | FR ------ FR FR ------ FR | + +----------+ +--------+ +--------+ +``` + +The input port of the filter is `port.passive = follow-suspend' and so it can be +activated by the playback node. + +Likewise, if the ALSA Sink is runnable, it should not automatically make the +filter runnable. For example: + +``` + +--------+ +--------+ + | filter | | ALSA | + | | | Sink | + FL FL ---+-- FL | + FR FR ---|+- FR | + +--------+ || +--------+ + || + +----------+ || + | playback | || + | | || + | FL ---+| + | FR ----+ + +----------+ +``` + +Here the playback node makes the ALSA Sink runnable but the filter +stays not-runnable because the output port is `port.passive = true`. + +## Device node monitor + +Consider the case where we have an ALSA Sink and a monitor stream +connected to the sink monitor ports. + +``` + +-------+ +--------++ + | ALSA | | monitor | + | Sink | | | + FL FL ------ FL | + FR FR ------ FR | + +-------+ +---------+ +``` + +We would like to keep the monitor stream and the ALSA sink suspended +unless something else activates the ALSA Sink: + + +``` + +----------+ +-------+ +---------+ + | playback | | ALSA | | monitor | + | | | Sink | | | + | FL ------ FL FL ------ FL | + | FR ------ FR FR ------ FR | + +----------+ +-------+ +---------+ +``` + +We can do this by making the monitor stream input ports `port.passive = follow` +and leave the ALSA Sink monitor output ports as `port.passive = follow-suspend`. + +According to Table 1, both nodes will not activate each other but when ALSA Sink +becomes runnable because of playback, according to Table 2, the monitor will +become runnable as well. + +Note how we need the distinction between `follow` and `follow-suspend` for this +use case. + +## Node groups + +Normally when an application makes a capture and playback node, both nodes will +be scheduled in different groups, consider: + + +``` + +--------+ +---------+ + | ALSA | | capture | + | Source | | | + | FL ------ FL | + | FR ------ FR | + +--------+ +---------+ + + +----------+ +--------+ + | playback | | ALSA | + | | | Sink | + | FL ------ FL | + | FR ------ FR | + +----------+ +--------+ +``` + +Here we see 2 groups with the ALSA Source and ALSA Sink respectively as the +drivers. Depending on the clocks of the nodes, the capture and playback will not +be in sync. They will each run in their own time domain depending on the rate of +the drivers. + +When we place a node.group property with the same value on the capture and playback +nodes, they will be grouped together and this whole graph becomes one single group. + +Because there are 2 potential drivers in the group, the one with the highest +`priority.driver` property is selected as the driver in the group. The other nodes +in the group (including the other driver) become followers in the group. + +When a node becomes runnable, all other nodes with the same node.group property +become runnable as well. + +## Node link groups + +When we have a filter that is constructed from two nodes, an input and an output +node, we could use the `node.group` property to make sure they are both scheduled +and made runnable together. + +``` + +--------+ +-------+ +--------+ +-------+ + | ALSA | | input | | output | | ALSA | + | Source | | | | | | Sink | + | FL ------ FL -- processing-- FL ------ FL | + | FR ------ FR | | FR ------ FR | + +--------+ +-------+ +--------+ +-------+ +``` + +This would work fine but it does not describe that there is an implicit internal +link between the input and output node. This information is important for the +session manager to avoid linking the output node to the input node and make a +loop. + +The `node.link-group` property can be used to both group the nodes together and +descibe that they are internally linked together. + +When a node becomes runnable, all other nodes with the same node.link-group property +become runnable as well. + +For the 2 node filters, like loopback and filter-chain, the same `port.passive` +property rules apply as for the filter nodes. Note that for the virtual devices, +the Source/Sink nodes will be `follow-suspend` by default and the other node should +be set to `node.passive = true` to make the ports passive. + +## Want driver + +When there is no driver node in the group, nothing should be scheduled. This can +happen when a playback node is linked to a capture node: + +``` + +--------+ +---------+ + | player | | capture | + | | | | + | FL ----------- FL | + | FR ----------- FR | + +--------+ +---------+ +``` + +None of these nodes is a driver so there is no driver in the group and nothing +will be scheduled. + +When one of the nodes has `node.want-driver = true` they are grouped and +scheduled with a random driver node. This is often the driver node with the +highest priority (usually the Dummy-Driver) or otherwise a driver that is already +scheduling some other nodes. + +## Always process nodes + +A simple node, unlinked to anything should normally not run. + +``` + +--------+ + | player | + | | + | FL + | FR + +--------+ +``` + +When the `node.always-process = true` property is set, the node will however be +made runnable even if unlinked. This is done by adding the node to a random driver. + +`node.always-process = true` implies the `node.want-driver = true` property. + +## Sync groups + +In some cases, you only want to group nodes together depending on some condition. + +For example, when the JACK transport is activated, all nodes in the graph should share +the same driver node, regardless of the grouping or linking of the nodes. + +This is done by setting the same node.sync-group property on all nodes (by default all +nodes have `node.sync-group = group.sync.0`). When a node sets `node.sync = true` all +the other nodes with the same `node.sync-group` property are grouped together. + +This can be used to implement the JACK transport. When the transport is started, the +`node.sync=true` property is set and all nodes join one group with a shared driver +and timing information. + + + + +*/ + + diff --git a/doc/dox/internals/scheduling.dox b/doc/dox/internals/scheduling.dox index 38b05596b..c74124480 100644 --- a/doc/dox/internals/scheduling.dox +++ b/doc/dox/internals/scheduling.dox @@ -23,6 +23,9 @@ node is scheduled to run. This document describes the processing that happens in the data processing thread after the main thread has configured it. +Before scheduling of the node happens, the scheduler will collect a list of +nodes that are runnable, see \ref page_running + # Nodes Nodes are objects with 0 or more input and output ports. diff --git a/doc/dox/modules.dox b/doc/dox/modules.dox index 4e9358197..85b0a1418 100644 --- a/doc/dox/modules.dox +++ b/doc/dox/modules.dox @@ -81,6 +81,7 @@ List of known modules: - \subpage page_module_raop_discover - \subpage page_module_roc_sink - \subpage page_module_roc_source +- \subpage page_module_scheduler_v1 - \subpage page_module_rtp_sap - \subpage page_module_rtp_sink - \subpage page_module_rtp_source @@ -90,6 +91,8 @@ List of known modules: - \subpage page_module_spa_node_factory - \subpage page_module_spa_device - \subpage page_module_spa_device_factory +- \subpage page_module_sendspin_recv +- \subpage page_module_sendspin_send - \subpage page_module_session_manager - \subpage page_module_snapcast_discover - \subpage page_module_vban_recv diff --git a/doc/dox/overview.dox b/doc/dox/overview.dox index 1490cd444..9e2530163 100644 --- a/doc/dox/overview.dox +++ b/doc/dox/overview.dox @@ -77,7 +77,7 @@ Certain properties are, by convention, expected for specific object types. Each object type has a list of methods that it needs to implement. -The session manager is responsible for defining the list of permissions each client has. Each permission entry is an object ID and four flags. The four flags are: +The session manager is responsible for defining the list of permissions each client has. Each permission entry is an object ID and five flags. The five flags are: - Read: the object can be seen and events can be received; - Write: the object can be modified, usually through methods (which requires the execute flag) @@ -109,7 +109,7 @@ Modules in PipeWire can only be loaded in their own process. A client, for examp Nodes are the core data processing entities in PipeWire. They may produce data (capture devices, signal generators, ...), consume data (playback devices, network endpoints, ...) or both (filters). -Notes have a method `process`, which eats up data from input ports and provides data for each output port. +Nodes have a method `process`, which eats up data from input ports and provides data for each output port. #### Ports diff --git a/doc/dox/programs/pw-cat.1.md b/doc/dox/programs/pw-cat.1.md index b681e54a1..8ec02c711 100644 --- a/doc/dox/programs/pw-cat.1.md +++ b/doc/dox/programs/pw-cat.1.md @@ -124,6 +124,9 @@ Set a node target (default auto). The value can be: - \: The object.serial or the node.name of a target node \endparblock +\par -C | \--monitor +In recording mode, record from monitor ports. + \par \--latency=VALUE\[*units*\] \parblock Set the node latency (default 100ms) diff --git a/doc/dox/programs/pw-top.1.md b/doc/dox/programs/pw-top.1.md index 1207e3205..353015d1a 100644 --- a/doc/dox/programs/pw-top.1.md +++ b/doc/dox/programs/pw-top.1.md @@ -188,6 +188,11 @@ Quit Clear the ERR counters. This does *not* clear the counters globally, it will only reset the counters in this instance of *pw-top*. +\par [f|F] +Cycle through filter presets. If any nodes are filtered from view, +the current preset will be indicated in the header bar. Only nodes +with the indicated state or higher, will be printed. + # OPTIONS \par -h | \--help @@ -199,6 +204,9 @@ Run in non-interactive batch mode, similar to top\'s batch mode. \par -n | \--iterations=NUMBER Exit after NUMBER of batch iterations. Only used in batch mode. +\par -f | \--filter=NUMBER +Start with filter preset NUMBER selected. + \par -r | \--remote=NAME The name the *remote* instance to monitor. If left unspecified, a connection is made to the default PipeWire instance. diff --git a/doc/meson.build b/doc/meson.build index f4aa4ba6a..d014d227d 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -67,6 +67,7 @@ extra_docs = [ 'dox/internals/session-manager.dox', 'dox/internals/objects.dox', 'dox/internals/audio.dox', + 'dox/internals/running.dox', 'dox/internals/scheduling.dox', 'dox/internals/driver.dox', 'dox/internals/protocol.dox', diff --git a/meson.build b/meson.build index c954a644c..37badca51 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('pipewire', ['c' ], - version : '1.6.0', + version : '1.7.0', license : [ 'MIT', 'LGPL-2.1-or-later', 'GPL-2.0-only' ], meson_version : '>= 0.61.1', default_options : [ 'warning_level=3', @@ -116,6 +116,7 @@ cc_flags = common_flags + [ '-Werror=old-style-definition', '-Werror=missing-parameter-type', '-Werror=strict-prototypes', + '-Werror=discarded-qualifiers', ] add_project_arguments(cc.get_supported_arguments(cc_flags), language: 'c') add_project_arguments(cc_native.get_supported_arguments(cc_flags), @@ -367,7 +368,7 @@ cdata.set('HAVE_OPUS', opus_dep.found()) summary({'readline (for pw-cli)': readline_dep.found()}, bool_yn: true, section: 'Misc dependencies') cdata.set('HAVE_READLINE', readline_dep.found()) ncurses_dep = dependency('ncursesw', required : false) -sndfile_dep = dependency('sndfile', version : '>= 1.0.20', required : get_option('sndfile')) +sndfile_dep = dependency('sndfile', version : '>= 1.1.0', required : get_option('sndfile')) summary({'sndfile': sndfile_dep.found()}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump/filter-chain') cdata.set('HAVE_SNDFILE', sndfile_dep.found()) pulseaudio_dep = dependency('libpulse', required : get_option('libpulse')) @@ -411,7 +412,7 @@ gst_deps_def = { 'gio-unix-2.0': {}, 'gstreamer-1.0': {'version': '>= 1.10.0'}, 'gstreamer-base-1.0': {}, - 'gstreamer-video-1.0': {}, + 'gstreamer-video-1.0': {'version': '>= 1.22.0'}, 'gstreamer-audio-1.0': {}, 'gstreamer-allocators-1.0': {}, } diff --git a/pipewire-jack/src/meson.build b/pipewire-jack/src/meson.build index 0630d96a8..639405bb9 100644 --- a/pipewire-jack/src/meson.build +++ b/pipewire-jack/src/meson.build @@ -55,7 +55,7 @@ pipewire_jackserver = shared_library('jackserver', pipewire_jackserver_sources, soversion : soversion, version : libjackversion, - c_args : pipewire_jack_c_args, + c_args : pipewire_jack_c_args + '-DLIBJACKSERVER', include_directories : [configinc, jack_inc], dependencies : [pipewire_dep, mathlib], install : true, diff --git a/pipewire-jack/src/pipewire-jack.c b/pipewire-jack/src/pipewire-jack.c index 1bef74283..5ae097ba8 100644 --- a/pipewire-jack/src/pipewire-jack.c +++ b/pipewire-jack/src/pipewire-jack.c @@ -86,7 +86,7 @@ PW_LOG_TOPIC_STATIC(jack_log_topic, "jack"); #define OTHER_CONNECT_FAIL -1 #define OTHER_CONNECT_IGNORE 0 -#define NOTIFY_BUFFER_SIZE (1u<<13) +#define NOTIFY_BUFFER_SIZE (1u<<16) #define NOTIFY_BUFFER_MASK (NOTIFY_BUFFER_SIZE-1) struct notify { @@ -104,8 +104,8 @@ struct notify { #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; + struct object *object; const char *msg; }; @@ -492,6 +492,8 @@ struct client { jack_position_t jack_position; jack_transport_state_t jack_state; struct frame_times jack_times; + + struct object dummy_port; }; #define return_val_if_fail(expr, val) \ @@ -1446,8 +1448,9 @@ static size_t convert_from_event(void *midi, void *buffer, size_t size, uint32_t switch (type) { case TYPE_ID_MIDI: + event_type = SPA_CONTROL_Midi; + break; case TYPE_ID_OSC: - /* we handle MIDI as OSC, check below */ event_type = SPA_CONTROL_OSC; break; case TYPE_ID_UMP: @@ -1464,27 +1467,15 @@ static size_t convert_from_event(void *midi, void *buffer, size_t size, uint32_t for (i = 0; i < count; i++) { jack_midi_event_t ev; jack_midi_event_get(&ev, midi, i); + uint32_t ev_type; - if (type != TYPE_ID_MIDI || is_osc(&ev)) { - /* no midi port or it's OSC */ - spa_pod_builder_control(&b, ev.time, event_type); - spa_pod_builder_bytes(&b, ev.buffer, ev.size); - } else { - /* midi port and it's not OSC, convert to UMP */ - uint8_t *data = ev.buffer; - size_t size = ev.size; - uint64_t state = 0; + if (type == TYPE_ID_MIDI && is_osc(&ev)) + ev_type = SPA_CONTROL_OSC; + else + ev_type = event_type; - 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, ev.time, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&b, ump, ump_size); - } - } + spa_pod_builder_control(&b, ev.time, ev_type); + spa_pod_builder_bytes(&b, ev.buffer, ev.size); } spa_pod_builder_pop(&b, &f); return b.state.offset; @@ -2208,7 +2199,7 @@ on_rtsocket_condition(void *data, int fd, uint32_t mask) } } else if (SPA_LIKELY(mask & SPA_IO_IN)) { uint32_t buffer_frames; - int status = 0; + int status = -EBUSY; buffer_frames = cycle_run(c); @@ -4468,6 +4459,11 @@ jack_client_t * jack_client_open (const char *client_name, 0, NULL, &client->info); client->info.change_mask = 0; + client->dummy_port.type = INTERFACE_Port; + snprintf(client->dummy_port.port.name, sizeof(client->dummy_port.port.name), "%s:dummy", client_name); + snprintf(client->dummy_port.port.alias1, sizeof(client->dummy_port.port.alias1), "%s:dummy", client_name); + snprintf(client->dummy_port.port.alias2, sizeof(client->dummy_port.port.alias2), "%s:dummy", client_name); + client->show_monitor = pw_properties_get_bool(client->props, "jack.show-monitor", true); client->show_midi = pw_properties_get_bool(client->props, "jack.show-midi", true); client->merge_monitor = pw_properties_get_bool(client->props, "jack.merge-monitor", true); @@ -4868,7 +4864,7 @@ int jack_activate (jack_client_t *client) freeze_callbacks(c); /* reemit buffer_frames */ - c->buffer_frames = 0; + c->buffer_frames = (uint32_t)-1; pw_data_loop_start(c->loop); c->active = true; @@ -4880,9 +4876,21 @@ int jack_activate (jack_client_t *client) c->activation->pending_sync = true; spa_list_for_each(o, &c->context.objects, link) { +#if !defined(LIBJACKSERVER) if (o->type != INTERFACE_Port || o->port.port == NULL || o->port.port->client != c || !o->port.port->valid) continue; +#else + /* emits all foreign active ports, skips own (already announced via jack_port_register) */ + if (o->type != INTERFACE_Port || o->removed) + continue; + /* own ports are handled by jack_port_register */ + if (o->port.port != NULL && o->port.port->client == c) + continue; + /* only announce ports whose node is active */ + if (o->port.node != NULL && !node_is_active(c, o->port.node)) + continue; +#endif o->registered = 0; queue_notify(c, NOTIFY_TYPE_PORTREGISTRATION, o, 1, NULL); } @@ -5318,7 +5326,7 @@ int jack_set_freewheel(jack_client_t* client, int onoff) pw_thread_loop_lock(c->context.loop); str = pw_properties_get(c->props, PW_KEY_NODE_GROUP); if (str != NULL) { - char *p = strstr(str, ",pipewire.freewheel"); + const char *p = strstr(str, ",pipewire.freewheel"); if (p == NULL) p = strstr(str, "pipewire.freewheel"); if (p == NULL && onoff) @@ -5437,7 +5445,7 @@ SPA_EXPORT jack_nframes_t jack_get_buffer_size (jack_client_t *client) { struct client *c = (struct client *) client; - jack_nframes_t res = -1; + uint32_t res = -1; return_val_if_fail(c != NULL, 0); @@ -5454,7 +5462,7 @@ jack_nframes_t jack_get_buffer_size (jack_client_t *client) } c->buffer_frames = res; pw_log_debug("buffer_frames: %u", res); - return res; + return (jack_nframes_t)res; } SPA_EXPORT @@ -5951,9 +5959,7 @@ static const char *port_name(struct object *o) { const char *name; struct client *c = o->client; - if (c == NULL) - return NULL; - if (c->default_as_system && is_port_default(c, o)) + if (c != NULL && c->default_as_system && is_port_default(c, o)) name = o->port.system; else name = o->port.name; @@ -6007,7 +6013,16 @@ jack_port_type_id_t jack_port_type_id (const jack_port_t *port) return_val_if_fail(o != NULL, 0); if (o->type != INTERFACE_Port) return TYPE_ID_OTHER; - return o->port.type_id; + + /* map internal type IDs to jack1/jack2 compatible public values */ + switch (o->port.type_id) { + case TYPE_ID_AUDIO: return 0; + case TYPE_ID_MIDI: + case TYPE_ID_OSC: + case TYPE_ID_UMP: return 1; /* all MIDI variants map to 1 */ + case TYPE_ID_VIDEO: return 3; /* video maps to 3 */ + default: return o->port.type_id; + } } SPA_EXPORT @@ -6999,13 +7014,11 @@ jack_port_t * jack_port_by_id (jack_client_t *client, pthread_mutex_lock(&c->context.lock); res = find_by_serial(c, port_id); - if (res && res->type != INTERFACE_Port) - res = NULL; - pw_log_debug("%p: port %d -> %p", c, port_id, res); pthread_mutex_unlock(&c->context.lock); + if (res == NULL || res->type != INTERFACE_Port) + res = &c->dummy_port; - if (res == NULL) - pw_log_info("%p: port %d not found", c, port_id); + pw_log_debug("%p: port %d -> %p", c, port_id, res); return object_to_port(res); } diff --git a/pipewire-v4l2/src/pipewire-v4l2.c b/pipewire-v4l2/src/pipewire-v4l2.c index 8fc07151a..7a5e5c057 100644 --- a/pipewire-v4l2/src/pipewire-v4l2.c +++ b/pipewire-v4l2/src/pipewire-v4l2.c @@ -2570,7 +2570,10 @@ static void *v4l2_mmap(void *addr, size_t length, int prot, buf = &file->buffers[id]; data = &buf->buf->buffer->datas[0]; - pw_map_range_init(&range, data->mapoffset, data->maxsize, 1024); + if (pw_map_range_init(&range, data->mapoffset, data->maxsize, 1024) < 0) { + res = MAP_FAILED; + goto error_unlock; + } if (!SPA_FLAG_IS_SET(data->flags, SPA_DATA_FLAG_READABLE)) prot &= ~PROT_READ; diff --git a/po/kk.po b/po/kk.po index 6a00211f3..507c967e0 100644 --- a/po/kk.po +++ b/po/kk.po @@ -1,15 +1,14 @@ # Kazakh translation of pipewire. # Copyright (C) 2020 The pipewire authors. # This file is distributed under the same license as the pipewire package. -# Baurzhan Muftakhidinov , 2020. +# Baurzhan Muftakhidinov , 2020-2026. # msgid "" msgstr "" "Project-Id-Version: \n" -"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/" -"issues/new\n" -"POT-Creation-Date: 2021-04-18 16:54+0800\n" -"PO-Revision-Date: 2020-06-30 08:04+0500\n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues\n" +"POT-Creation-Date: 2026-03-09 12:19+0000\n" +"PO-Revision-Date: 2026-03-17 00:04+0500\n" "Last-Translator: Baurzhan Muftakhidinov \n" "Language-Team: \n" "Language: kk\n" @@ -17,96 +16,199 @@ 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.3.1\n" +"X-Generator: Poedit 3.9\n" -#: src/daemon/pipewire.c:43 +#: 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 [опциялар]\n" +" -h, --help Осы көмекті көрсету\n" +" -v, --verbose Ақпараттылығын бір деңгейге арттыру\n" +" --version Нұсқасын көрсету\n" +" -c, --config Конфигурацияны жүктеу (Бастапқы %s)\n" +" -P --properties Контекст қасиеттерін орнату\n" + +#: src/daemon/pipewire.desktop.in:3 +msgid "PipeWire Media System" +msgstr "PipeWire медиа жүйесі" #: src/daemon/pipewire.desktop.in:4 -msgid "PipeWire Media System" -msgstr "" - -#: src/daemon/pipewire.desktop.in:5 msgid "Start the PipeWire Media System" -msgstr "" +msgstr "PipeWire медиа жүйесін іске қосу" -#: src/examples/media-session/alsa-monitor.c:526 -#: spa/plugins/alsa/acp/compat.c:187 -msgid "Built-in Audio" -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/examples/media-session/alsa-monitor.c:530 -#: spa/plugins/alsa/acp/compat.c:192 -msgid "Modem" -msgstr "Модем" +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "Жалған шығыс" -#: src/examples/media-session/alsa-monitor.c:539 +#: src/modules/module-pulse-tunnel.c:761 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "%s@%s үшін туннель" + +#: src/modules/module-zeroconf-discover.c:290 msgid "Unknown device" -msgstr "" +msgstr "Белгісіз құрылғы" -#: src/tools/pw-cat.c:991 +#: src/modules/module-zeroconf-discover.c:302 +#, c-format +msgid "%s on %s@%s" +msgstr "%s, %s@%s ішінде" + +#: src/modules/module-zeroconf-discover.c:306 +#, c-format +msgid "%s on %s" +msgstr "%s, %s ішінде" + +#: src/tools/pw-cat.c:269 +#, c-format +msgid "Supported formats:\n" +msgstr "Қолдау көрсетілетін пішімдер:\n" + +#: src/tools/pw-cat.c:754 +#, c-format +msgid "Supported channel layouts:\n" +msgstr "Қолдау көрсетілетін арна жаймалары:\n" + +#: src/tools/pw-cat.c:764 +#, c-format +msgid "Supported channel layout aliases:\n" +msgstr "Қолдау көрсетілетін арна жаймаларының алиастары:\n" + +#: src/tools/pw-cat.c:766 +#, c-format +msgid " %s -> %s\n" +msgstr " %s -> %s\n" + +#: src/tools/pw-cat.c:771 +#, c-format +msgid "Supported channel names:\n" +msgstr "Қолдау көрсетілетін арна атаулары:\n" + +#: src/tools/pw-cat.c:1182 #, c-format msgid "" -"%s [options] \n" +"%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 +#: src/tools/pw-cat.c:1189 #, 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 (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" -" --list-targets List available targets for --target\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 Түйін мақсатының сериялық нөмірін немесе атын орнату (бастапқы " +"%s)\n" +" 0 байланыстырмауды білдіреді\n" +" --latency Түйін кідірісін орнату (бастапқы %s)\n" +" Xюнит (юнит = с, мс, мкс, нс)\n" +" немесе тікелей үлгілер (256)\n" +" жиілік бастапқы файлдың жиілігі болып табылады\n" +" -P --properties Түйін қасиеттерін орнату\n" +"\n" -#: src/tools/pw-cat.c:1016 +#: src/tools/pw-cat.c:1207 #, 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 (default %u)\n" +" --channels Number of channels (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" +" a channel layout: \"Stereo\", \"5.1\",... or\n" +" comma separated list of channel names: eg. \"FL,FR\"\n" +" --list-layouts List supported channel layouts\n" +" --list-channel-names List supported channel maps\n" +" --format Sample format (default %s)\n" +" --list-formats List supported sample formats\n" +" --container Container format\n" +" --list-containers List supported containers and extensions\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" +" -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" +" --channels Арналар саны (бастапқы %u)\n" +" --channel-map Арналар картасы\n" +" арна жаймасы: «Stereo», «5.1»,... немесе\n" +" үтірмен ажыратылған арна атауларының тізімі: мысалы, " +"«FL,FR»\n" +" --list-layouts Қолдау көрсетілетін арна жаймаларын тізімдеу\n" +" --list-channel-names Қолдау көрсетілетін арналар картасын тізімдеу\n" +" --format Дискреттеу пішімі (бастапқы %s)\n" +" --list-formats Қолдау көрсетілетін дискреттеу пішімдерін тізімдеу\n" +" --container Контейнер пішімі\n" +" --list-containers Қолдау көрсетілетін контейнерлер мен кеңейтулерді тізімдеу\n" +" --volume Ағын дыбыс деңгейі 0-1.0 (бастапқы %.3f)\n" +" -q --quality Қайта дискреттеу сапасы (0 - 15) (бастапқы %d)\n" +" -a, --raw RAW режимі\n" +" -M, --force-midi Midi пішімін мәжбүрлеу, «midi» немесе «ump» біреуі, (бастапқы " +"ump)\n" +" -n, --sample-count COUNT COUNT үлгісінен кейін тоқтату\n" +"\n" -#: src/tools/pw-cat.c:1033 +#: src/tools/pw-cat.c:1232 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" +" -r, --record Жазу режимі\n" +" -m, --midi Midi режимі\n" +" -d, --dsd DSD режимі\n" +" -o, --encoded Шифрленген режим\n" +" -s, --sysex SysEx режимі\n" +" -c, --midi-clip MIDI clip режимі\n" +"\n" -#: src/tools/pw-cli.c:2932 +#: src/tools/pw-cat.c:1837 +#, c-format +msgid "Supported containers and extensions:\n" +msgstr "Қолдау көрсетілетін контейнерлер мен кеңейтулер:\n" + +#: src/tools/pw-cli.c:2386 #, c-format msgid "" "%s [options] [command]\n" @@ -114,465 +216,506 @@ msgid "" " --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 Қызмет ретінде іске қосу (Бастапқы false)\n" +" -r, --remote Қашықтағы қызмет атауы\n" +" -m, --monitor Белсенділікті бақылау\n" +"\n" -#: spa/plugins/alsa/acp/acp.c:290 +#: spa/plugins/alsa/acp/acp.c:361 msgid "Pro Audio" -msgstr "" +msgstr "Кәсіби аудио" -#: spa/plugins/alsa/acp/acp.c:411 spa/plugins/alsa/acp/alsa-mixer.c:4704 -#: spa/plugins/bluez5/bluez5-device.c:1000 +#: spa/plugins/alsa/acp/acp.c:535 spa/plugins/alsa/acp/alsa-mixer.c:4699 +#: spa/plugins/bluez5/bluez5-device.c:2021 msgid "Off" msgstr "Сөнд." -#: spa/plugins/alsa/acp/channelmap.h:466 -msgid "(invalid)" -msgstr "(жарамсыз)" +#: spa/plugins/alsa/acp/acp.c:618 +#, c-format +msgid "%s [ALSA UCM error]" +msgstr "%s [ALSA UCM қатесі]" -#: spa/plugins/alsa/acp/alsa-mixer.c:2709 +#: spa/plugins/alsa/acp/alsa-mixer.c:2721 msgid "Input" msgstr "Кіріс" -#: spa/plugins/alsa/acp/alsa-mixer.c:2710 +#: spa/plugins/alsa/acp/alsa-mixer.c:2722 msgid "Docking Station Input" msgstr "Док-станция кірісі" -#: spa/plugins/alsa/acp/alsa-mixer.c:2711 +#: spa/plugins/alsa/acp/alsa-mixer.c:2723 msgid "Docking Station Microphone" msgstr "Док-станция микрофоны" -#: spa/plugins/alsa/acp/alsa-mixer.c:2712 +#: spa/plugins/alsa/acp/alsa-mixer.c:2724 msgid "Docking Station Line In" msgstr "Док-станцияның сызықтық кірісі" -#: spa/plugins/alsa/acp/alsa-mixer.c:2713 -#: spa/plugins/alsa/acp/alsa-mixer.c:2804 +#: spa/plugins/alsa/acp/alsa-mixer.c:2725 spa/plugins/alsa/acp/alsa-mixer.c:2816 msgid "Line In" msgstr "Сызықтық кіріс" -#: spa/plugins/alsa/acp/alsa-mixer.c:2714 -#: spa/plugins/alsa/acp/alsa-mixer.c:2798 -#: spa/plugins/bluez5/bluez5-device.c:1145 +#: spa/plugins/alsa/acp/alsa-mixer.c:2726 spa/plugins/alsa/acp/alsa-mixer.c:2810 +#: spa/plugins/bluez5/bluez5-device.c:2422 msgid "Microphone" msgstr "Микрофон" -#: spa/plugins/alsa/acp/alsa-mixer.c:2715 -#: spa/plugins/alsa/acp/alsa-mixer.c:2799 +#: spa/plugins/alsa/acp/alsa-mixer.c:2727 spa/plugins/alsa/acp/alsa-mixer.c:2811 msgid "Front Microphone" msgstr "Алдыңғы микрофон" -#: spa/plugins/alsa/acp/alsa-mixer.c:2716 -#: spa/plugins/alsa/acp/alsa-mixer.c:2800 +#: spa/plugins/alsa/acp/alsa-mixer.c:2728 spa/plugins/alsa/acp/alsa-mixer.c:2812 msgid "Rear Microphone" msgstr "Артқы микрофон" -#: spa/plugins/alsa/acp/alsa-mixer.c:2717 +#: spa/plugins/alsa/acp/alsa-mixer.c:2729 msgid "External Microphone" msgstr "Сыртқы микрофон" -#: spa/plugins/alsa/acp/alsa-mixer.c:2718 -#: spa/plugins/alsa/acp/alsa-mixer.c:2802 +#: spa/plugins/alsa/acp/alsa-mixer.c:2730 spa/plugins/alsa/acp/alsa-mixer.c:2814 msgid "Internal Microphone" msgstr "Ішкі микрофон" -#: spa/plugins/alsa/acp/alsa-mixer.c:2719 -#: spa/plugins/alsa/acp/alsa-mixer.c:2805 +#: spa/plugins/alsa/acp/alsa-mixer.c:2731 spa/plugins/alsa/acp/alsa-mixer.c:2817 msgid "Radio" msgstr "Радио" -#: spa/plugins/alsa/acp/alsa-mixer.c:2720 -#: spa/plugins/alsa/acp/alsa-mixer.c:2806 +#: spa/plugins/alsa/acp/alsa-mixer.c:2732 spa/plugins/alsa/acp/alsa-mixer.c:2818 msgid "Video" msgstr "Видео" -#: spa/plugins/alsa/acp/alsa-mixer.c:2721 +#: spa/plugins/alsa/acp/alsa-mixer.c:2733 msgid "Automatic Gain Control" msgstr "Күшейтуді автореттеу" -#: spa/plugins/alsa/acp/alsa-mixer.c:2722 +#: spa/plugins/alsa/acp/alsa-mixer.c:2734 msgid "No Automatic Gain Control" msgstr "Күшейтуді автореттеу жоқ" -#: spa/plugins/alsa/acp/alsa-mixer.c:2723 +#: spa/plugins/alsa/acp/alsa-mixer.c:2735 msgid "Boost" msgstr "Күшейту" -#: spa/plugins/alsa/acp/alsa-mixer.c:2724 +#: spa/plugins/alsa/acp/alsa-mixer.c:2736 msgid "No Boost" msgstr "Күшейту жоқ" -#: spa/plugins/alsa/acp/alsa-mixer.c:2725 +#: spa/plugins/alsa/acp/alsa-mixer.c:2737 msgid "Amplifier" msgstr "Күшейткіш" -#: spa/plugins/alsa/acp/alsa-mixer.c:2726 +#: spa/plugins/alsa/acp/alsa-mixer.c:2738 msgid "No Amplifier" msgstr "Күшейткіш жоқ" -#: spa/plugins/alsa/acp/alsa-mixer.c:2727 +#: spa/plugins/alsa/acp/alsa-mixer.c:2739 msgid "Bass Boost" msgstr "Бас күшейту" -#: spa/plugins/alsa/acp/alsa-mixer.c:2728 +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 msgid "No Bass Boost" msgstr "Бас күшейту жоқ" -#: spa/plugins/alsa/acp/alsa-mixer.c:2729 -#: spa/plugins/bluez5/bluez5-device.c:1150 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 spa/plugins/bluez5/bluez5-device.c:2428 msgid "Speaker" msgstr "Динамик" -#: spa/plugins/alsa/acp/alsa-mixer.c:2730 -#: spa/plugins/alsa/acp/alsa-mixer.c:2808 +#. 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:2434 spa/plugins/bluez5/bluez5-device.c:2501 msgid "Headphones" msgstr "Құлаққаптар" -#: spa/plugins/alsa/acp/alsa-mixer.c:2797 +#: spa/plugins/alsa/acp/alsa-mixer.c:2809 msgid "Analog Input" msgstr "Аналогтық кіріс" -#: spa/plugins/alsa/acp/alsa-mixer.c:2801 +#: spa/plugins/alsa/acp/alsa-mixer.c:2813 msgid "Dock Microphone" msgstr "Док-станция микрофоны" -#: spa/plugins/alsa/acp/alsa-mixer.c:2803 +#: spa/plugins/alsa/acp/alsa-mixer.c:2815 msgid "Headset Microphone" msgstr "Гарнитура микрофоны" -#: spa/plugins/alsa/acp/alsa-mixer.c:2807 +#: spa/plugins/alsa/acp/alsa-mixer.c:2819 msgid "Analog Output" msgstr "Аналогтық шығыс" -#: spa/plugins/alsa/acp/alsa-mixer.c:2809 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:2821 msgid "Headphones 2" -msgstr "Құлаққаптар" +msgstr "Құлаққап 2" -#: spa/plugins/alsa/acp/alsa-mixer.c:2810 +#: spa/plugins/alsa/acp/alsa-mixer.c:2822 msgid "Headphones Mono Output" msgstr "Құлаққаптардың моно шығысы" -#: spa/plugins/alsa/acp/alsa-mixer.c:2811 +#: spa/plugins/alsa/acp/alsa-mixer.c:2823 msgid "Line Out" msgstr "Сызықтық шығыс" -#: spa/plugins/alsa/acp/alsa-mixer.c:2812 +#: spa/plugins/alsa/acp/alsa-mixer.c:2824 msgid "Analog Mono Output" msgstr "Аналогтық моно шығысы" -#: spa/plugins/alsa/acp/alsa-mixer.c:2813 +#: spa/plugins/alsa/acp/alsa-mixer.c:2825 msgid "Speakers" msgstr "Динамиктер" -#: spa/plugins/alsa/acp/alsa-mixer.c:2814 +#: spa/plugins/alsa/acp/alsa-mixer.c:2826 msgid "HDMI / DisplayPort" msgstr "HDMI / DisplayPort" -#: spa/plugins/alsa/acp/alsa-mixer.c:2815 +#: spa/plugins/alsa/acp/alsa-mixer.c:2827 msgid "Digital Output (S/PDIF)" msgstr "Цифрлық шығыс (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2816 +#: spa/plugins/alsa/acp/alsa-mixer.c:2828 msgid "Digital Input (S/PDIF)" msgstr "Цифрлық кіріс (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2817 +#: spa/plugins/alsa/acp/alsa-mixer.c:2829 msgid "Multichannel Input" msgstr "Көпарналы кіріс" -#: spa/plugins/alsa/acp/alsa-mixer.c:2818 +#: spa/plugins/alsa/acp/alsa-mixer.c:2830 msgid "Multichannel Output" msgstr "Көпарналы шығыс" -#: spa/plugins/alsa/acp/alsa-mixer.c:2819 +#: spa/plugins/alsa/acp/alsa-mixer.c:2831 msgid "Game Output" msgstr "Ойын шығысы" -#: spa/plugins/alsa/acp/alsa-mixer.c:2820 -#: spa/plugins/alsa/acp/alsa-mixer.c:2821 +#: spa/plugins/alsa/acp/alsa-mixer.c:2832 spa/plugins/alsa/acp/alsa-mixer.c:2833 msgid "Chat Output" msgstr "Чат шығысы" -#: spa/plugins/alsa/acp/alsa-mixer.c:2822 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:2834 msgid "Chat Input" -msgstr "Чат шығысы" +msgstr "Чат кірісі" -#: spa/plugins/alsa/acp/alsa-mixer.c:2823 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:2835 msgid "Virtual Surround 7.1" -msgstr "Виртуалды көлемді аудиоқабылдағыш" +msgstr "Виртуалды көлемді дыбыс 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4527 +#: spa/plugins/alsa/acp/alsa-mixer.c:4522 msgid "Analog Mono" msgstr "Аналогтық моно" -#: spa/plugins/alsa/acp/alsa-mixer.c:4528 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:4523 msgid "Analog Mono (Left)" -msgstr "Аналогтық моно" +msgstr "Аналогты моно (Сол жақ)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4529 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:4524 msgid "Analog Mono (Right)" -msgstr "Аналогтық моно" +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:4530 -#: spa/plugins/alsa/acp/alsa-mixer.c:4538 -#: spa/plugins/alsa/acp/alsa-mixer.c:4539 +#: 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 "Аналогтық стерео" -#: spa/plugins/alsa/acp/alsa-mixer.c:4531 +#: spa/plugins/alsa/acp/alsa-mixer.c:4526 msgid "Mono" msgstr "Моно" -#: spa/plugins/alsa/acp/alsa-mixer.c:4532 +#: spa/plugins/alsa/acp/alsa-mixer.c:4527 msgid "Stereo" msgstr "Стерео" -#: spa/plugins/alsa/acp/alsa-mixer.c:4540 -#: spa/plugins/alsa/acp/alsa-mixer.c:4698 -#: spa/plugins/bluez5/bluez5-device.c:1135 +#: spa/plugins/alsa/acp/alsa-mixer.c:4535 spa/plugins/alsa/acp/alsa-mixer.c:4693 +#: spa/plugins/bluez5/bluez5-device.c:2410 msgid "Headset" msgstr "Гарнитура" -#: spa/plugins/alsa/acp/alsa-mixer.c:4541 -#: spa/plugins/alsa/acp/alsa-mixer.c:4699 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:4536 spa/plugins/alsa/acp/alsa-mixer.c:4694 msgid "Speakerphone" msgstr "Динамик" -#: spa/plugins/alsa/acp/alsa-mixer.c:4542 -#: spa/plugins/alsa/acp/alsa-mixer.c:4543 +#: spa/plugins/alsa/acp/alsa-mixer.c:4537 spa/plugins/alsa/acp/alsa-mixer.c:4538 msgid "Multichannel" msgstr "Көпарналы" -#: spa/plugins/alsa/acp/alsa-mixer.c:4544 +#: spa/plugins/alsa/acp/alsa-mixer.c:4539 msgid "Analog Surround 2.1" msgstr "Аналогтық көлемді 2.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4545 +#: spa/plugins/alsa/acp/alsa-mixer.c:4540 msgid "Analog Surround 3.0" msgstr "Аналогтық көлемді 3.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4546 +#: spa/plugins/alsa/acp/alsa-mixer.c:4541 msgid "Analog Surround 3.1" msgstr "Аналогтық көлемді 3.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4547 +#: spa/plugins/alsa/acp/alsa-mixer.c:4542 msgid "Analog Surround 4.0" msgstr "Аналогтық көлемді 4.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4548 +#: spa/plugins/alsa/acp/alsa-mixer.c:4543 msgid "Analog Surround 4.1" msgstr "Аналогтық көлемді 4.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4549 +#: spa/plugins/alsa/acp/alsa-mixer.c:4544 msgid "Analog Surround 5.0" msgstr "Аналогтық көлемді 5.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4550 +#: spa/plugins/alsa/acp/alsa-mixer.c:4545 msgid "Analog Surround 5.1" msgstr "Аналогтық көлемді 5.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4551 +#: spa/plugins/alsa/acp/alsa-mixer.c:4546 msgid "Analog Surround 6.0" msgstr "Аналогтық көлемді 6.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4552 +#: spa/plugins/alsa/acp/alsa-mixer.c:4547 msgid "Analog Surround 6.1" msgstr "Аналогтық көлемді 6.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4553 +#: spa/plugins/alsa/acp/alsa-mixer.c:4548 msgid "Analog Surround 7.0" msgstr "Аналогтық көлемді 7.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4554 +#: spa/plugins/alsa/acp/alsa-mixer.c:4549 msgid "Analog Surround 7.1" msgstr "Аналогтық көлемді 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4555 +#: spa/plugins/alsa/acp/alsa-mixer.c:4550 msgid "Digital Stereo (IEC958)" msgstr "Цифрлық стерео (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4556 +#: spa/plugins/alsa/acp/alsa-mixer.c:4551 msgid "Digital Surround 4.0 (IEC958/AC3)" msgstr "Цифрлық көлемді 4.0 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4557 +#: spa/plugins/alsa/acp/alsa-mixer.c:4552 msgid "Digital Surround 5.1 (IEC958/AC3)" msgstr "Цифрлық көлемді 5.1 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4558 +#: spa/plugins/alsa/acp/alsa-mixer.c:4553 msgid "Digital Surround 5.1 (IEC958/DTS)" msgstr "Цифрлық көлемді 5.1 (IEC958/DTS)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4559 +#: spa/plugins/alsa/acp/alsa-mixer.c:4554 msgid "Digital Stereo (HDMI)" msgstr "Цифрлық стерео (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4560 +#: spa/plugins/alsa/acp/alsa-mixer.c:4555 msgid "Digital Surround 5.1 (HDMI)" msgstr "Цифрлық көлемді 5.1 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4561 +#: spa/plugins/alsa/acp/alsa-mixer.c:4556 msgid "Chat" -msgstr "" +msgstr "Чат" -#: spa/plugins/alsa/acp/alsa-mixer.c:4562 +#: spa/plugins/alsa/acp/alsa-mixer.c:4557 msgid "Game" -msgstr "" +msgstr "Ойын" -#: spa/plugins/alsa/acp/alsa-mixer.c:4696 +#: spa/plugins/alsa/acp/alsa-mixer.c:4691 msgid "Analog Mono Duplex" msgstr "Аналогтық моно дуплекс" -#: spa/plugins/alsa/acp/alsa-mixer.c:4697 +#: spa/plugins/alsa/acp/alsa-mixer.c:4692 msgid "Analog Stereo Duplex" msgstr "Аналогтық стерео дуплекс" -#: spa/plugins/alsa/acp/alsa-mixer.c:4700 +#: spa/plugins/alsa/acp/alsa-mixer.c:4695 msgid "Digital Stereo Duplex (IEC958)" msgstr "Цифрлық стерео дуплекс (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4701 +#: spa/plugins/alsa/acp/alsa-mixer.c:4696 msgid "Multichannel Duplex" msgstr "Көпарналы дуплекс" -#: spa/plugins/alsa/acp/alsa-mixer.c:4702 +#: spa/plugins/alsa/acp/alsa-mixer.c:4697 msgid "Stereo Duplex" msgstr "Стерео дуплекс" -#: spa/plugins/alsa/acp/alsa-mixer.c:4703 +#: spa/plugins/alsa/acp/alsa-mixer.c:4698 msgid "Mono Chat + 7.1 Surround" -msgstr "" +msgstr "Моно чат + 7.1 көлемді дыбыс" -#: spa/plugins/alsa/acp/alsa-mixer.c:4806 +#: spa/plugins/alsa/acp/alsa-mixer.c:4799 #, c-format msgid "%s Output" msgstr "%s шығысы" -#: spa/plugins/alsa/acp/alsa-mixer.c:4813 +#: spa/plugins/alsa/acp/alsa-mixer.c:4807 #, c-format msgid "%s Input" msgstr "%s кірісі" -#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269 +#: 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() өте үлкен мән қайтарды: %lu байт (%lu мс).\n" +"Бұл ALSA драйверіндегі («%s») қате болуы әбден мүмкін. Бұл мәселе туралы ALSA әзірлеушілеріне хабарлаңыз." msgstr[1] "" +"snd_pcm_avail() өте үлкен мән қайтарды: %lu байт (%lu мс).\n" +"Бұл ALSA драйверіндегі («%s») қате болуы әбден мүмкін. Бұл мәселе туралы ALSA әзірлеушілеріне хабарлаңыз." -#: spa/plugins/alsa/acp/alsa-util.c:1241 +#: 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() өте үлкен мән қайтарды: %li байт (%s%lu мс).\n" +"Бұл ALSA драйверіндегі («%s») қате болуы әбден мүмкін. Бұл мәселе туралы ALSA әзірлеушілеріне хабарлаңыз." msgstr[1] "" +"snd_pcm_delay() өте үлкен мән қайтарды: %li байт (%s%lu мс).\n" +"Бұл ALSA драйверіндегі («%s») қате болуы әбден мүмкін. Бұл мәселе туралы ALSA әзірлеушілеріне хабарлаңыз." -#: spa/plugins/alsa/acp/alsa-util.c:1288 +#: 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_delay() оғаш мәндер қайтарды: кідіріс %lu мәні қолжетімді %lu мәнінен аз.\n" +"Бұл ALSA драйверіндегі («%s») қате болуы әбден мүмкін. Бұл мәселе туралы ALSA әзірлеушілеріне хабарлаңыз." -#: spa/plugins/alsa/acp/alsa-util.c:1331 +#: 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() өте үлкен мән қайтарды: %lu байт (%lu мс).\n" +"Бұл ALSA драйверіндегі («%s») қате болуы әбден мүмкін. Бұл мәселе туралы ALSA әзірлеушілеріне хабарлаңыз." msgstr[1] "" +"snd_pcm_mmap_begin() өте үлкен мән қайтарды: %lu байт (%lu мс).\n" +"Бұл ALSA драйверіндегі («%s») қате болуы әбден мүмкін. Бұл мәселе туралы ALSA әзірлеушілеріне хабарлаңыз." -#: spa/plugins/bluez5/bluez5-device.c:1010 +#: spa/plugins/alsa/acp/channelmap.h:460 +msgid "(invalid)" +msgstr "(жарамсыз)" + +#: spa/plugins/alsa/acp/compat.c:194 +msgid "Built-in Audio" +msgstr "Құрамындағы аудио" + +#: spa/plugins/alsa/acp/compat.c:199 +msgid "Modem" +msgstr "Модем" + +#: spa/plugins/bluez5/bluez5-device.c:2032 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" -msgstr "" +msgstr "Аудио шлюзі (A2DP бастапқы көзі және HSP/HFP AG)" -#: spa/plugins/bluez5/bluez5-device.c:1033 +#: spa/plugins/bluez5/bluez5-device.c:2061 +msgid "Audio Streaming for Hearing Aids (ASHA Sink)" +msgstr "Есту аппараттарына арналған аудио ағыны (ASHA қабылдағышы)" + +#: spa/plugins/bluez5/bluez5-device.c:2104 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" -msgstr "" +msgstr "Жоғары сапалы ойнату (A2DP қабылдағышы, кодек %s)" -#: spa/plugins/bluez5/bluez5-device.c:1035 +#: spa/plugins/bluez5/bluez5-device.c:2107 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" -msgstr "" +msgstr "Жоғары сапалы дуплекс (A2DP бастапқы көзі/қабылдағышы, кодек %s)" -#: spa/plugins/bluez5/bluez5-device.c:1041 +#: spa/plugins/bluez5/bluez5-device.c:2115 msgid "High Fidelity Playback (A2DP Sink)" -msgstr "" +msgstr "Жоғары сапалы ойнату (A2DP қабылдағышы)" -#: spa/plugins/bluez5/bluez5-device.c:1043 +#: spa/plugins/bluez5/bluez5-device.c:2117 msgid "High Fidelity Duplex (A2DP Source/Sink)" -msgstr "" +msgstr "Жоғары сапалы дуплекс (A2DP бастапқы көзі/қабылдағышы)" -#: spa/plugins/bluez5/bluez5-device.c:1070 +#: spa/plugins/bluez5/bluez5-device.c:2194 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "Жоғары сапалы ойнату (BAP қабылдағышы, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:2199 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "Жоғары сапалы кіріс (BAP бастапқы көзі, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:2203 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "Жоғары сапалы дуплекс (BAP бастапқы көзі/қабылдағышы, кодек %s)" + +#: spa/plugins/bluez5/bluez5-device.c:2212 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "Жоғары сапалы ойнату (BAP қабылдағышы)" + +#: spa/plugins/bluez5/bluez5-device.c:2216 +msgid "High Fidelity Input (BAP Source)" +msgstr "Жоғары сапалы кіріс (BAP бастапқы көзі)" + +#: spa/plugins/bluez5/bluez5-device.c:2219 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "Жоғары сапалы дуплекс (BAP бастапқы көзі/қабылдағышы)" + +#: spa/plugins/bluez5/bluez5-device.c:2259 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" -msgstr "" +msgstr "Гарнитура (HSP/HFP, кодек %s)" -#: spa/plugins/bluez5/bluez5-device.c:1074 -msgid "Headset Head Unit (HSP/HFP)" -msgstr "" - -#: spa/plugins/bluez5/bluez5-device.c:1140 +#: spa/plugins/bluez5/bluez5-device.c:2411 spa/plugins/bluez5/bluez5-device.c:2416 +#: spa/plugins/bluez5/bluez5-device.c:2423 spa/plugins/bluez5/bluez5-device.c:2429 +#: spa/plugins/bluez5/bluez5-device.c:2435 spa/plugins/bluez5/bluez5-device.c:2441 +#: spa/plugins/bluez5/bluez5-device.c:2447 spa/plugins/bluez5/bluez5-device.c:2453 +#: spa/plugins/bluez5/bluez5-device.c:2459 msgid "Handsfree" msgstr "Хендс-фри" -#: spa/plugins/bluez5/bluez5-device.c:1155 -msgid "Headphone" -msgstr "Құлаққап" +#: spa/plugins/bluez5/bluez5-device.c:2417 +msgid "Handsfree (HFP)" +msgstr "Гарнитура (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:1160 +#: spa/plugins/bluez5/bluez5-device.c:2440 msgid "Portable" msgstr "Портативті динамик" -#: spa/plugins/bluez5/bluez5-device.c:1165 +#: spa/plugins/bluez5/bluez5-device.c:2446 msgid "Car" msgstr "Автомобильдік динамик" -#: spa/plugins/bluez5/bluez5-device.c:1170 +#: spa/plugins/bluez5/bluez5-device.c:2452 msgid "HiFi" msgstr "HiFi" -#: spa/plugins/bluez5/bluez5-device.c:1175 +#: spa/plugins/bluez5/bluez5-device.c:2458 msgid "Phone" msgstr "Телефон" -#: spa/plugins/bluez5/bluez5-device.c:1181 +#: spa/plugins/bluez5/bluez5-device.c:2465 msgid "Bluetooth" msgstr "Bluetooth" + +#: spa/plugins/bluez5/bluez5-device.c:2466 +msgid "Bluetooth Handsfree" +msgstr "Bluetooth гарнитурасы" + +#~ msgid "Headphone" +#~ msgstr "Құлаққап" diff --git a/po/sv.po b/po/sv.po index 0bc2796a4..1474f5084 100644 --- a/po/sv.po +++ b/po/sv.po @@ -1,9 +1,9 @@ # Swedish translation for pipewire. -# Copyright © 2008-2025 Free Software Foundation, Inc. +# Copyright © 2008-2026 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, 2025. +# Anders Jonsson , 2021, 2022, 2023, 2024, 2025, 2026. # # 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: 2025-04-16 15:31+0000\n" -"PO-Revision-Date: 2025-04-22 10:43+0200\n" +"POT-Creation-Date: 2026-02-09 12:55+0000\n" +"PO-Revision-Date: 2026-02-22 21:48+0100\n" "Last-Translator: Anders Jonsson \n" "Language-Team: Swedish \n" "Language: sv\n" @@ -28,7 +28,7 @@ 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 3.5\n" +"X-Generator: Poedit 3.8\n" #: src/daemon/pipewire.c:29 #, c-format @@ -47,11 +47,11 @@ msgstr "" " -c, --config Läs in konfig (standard %s)\n" " -P --properties Ställ in kontextegenskaper\n" -#: src/daemon/pipewire.desktop.in:4 +#: src/daemon/pipewire.desktop.in:3 msgid "PipeWire Media System" msgstr "PipeWire mediasystem" -#: src/daemon/pipewire.desktop.in:5 +#: src/daemon/pipewire.desktop.in:4 msgid "Start the PipeWire Media System" msgstr "Starta mediasystemet PipeWire" @@ -65,26 +65,51 @@ msgstr "Tunnel till %s%s%s" msgid "Dummy Output" msgstr "Attrapputgång" -#: src/modules/module-pulse-tunnel.c:760 +#: src/modules/module-pulse-tunnel.c:761 #, c-format msgid "Tunnel for %s@%s" msgstr "Tunnel för %s@%s" -#: src/modules/module-zeroconf-discover.c:320 +#: src/modules/module-zeroconf-discover.c:326 msgid "Unknown device" msgstr "Okänd enhet" -#: src/modules/module-zeroconf-discover.c:332 +#: src/modules/module-zeroconf-discover.c:338 #, c-format msgid "%s on %s@%s" msgstr "%s på %s@%s" -#: src/modules/module-zeroconf-discover.c:336 +#: src/modules/module-zeroconf-discover.c:342 #, c-format msgid "%s on %s" msgstr "%s på %s" -#: src/tools/pw-cat.c:973 +#: src/tools/pw-cat.c:264 +#, c-format +msgid "Supported formats:\n" +msgstr "Format som stöds:\n" + +#: src/tools/pw-cat.c:749 +#, c-format +msgid "Supported channel layouts:\n" +msgstr "Kanallayouter som stöds:\n" + +#: src/tools/pw-cat.c:759 +#, c-format +msgid "Supported channel layout aliases:\n" +msgstr "Kanallayoutalias som stöds:\n" + +#: src/tools/pw-cat.c:761 +#, c-format +msgid " %s -> %s\n" +msgstr " %s -> %s\n" + +#: src/tools/pw-cat.c:766 +#, c-format +msgid "Supported channel names:\n" +msgstr "Kanalnamn som stöds:\n" + +#: src/tools/pw-cat.c:1177 #, c-format msgid "" "%s [options] [|-]\n" @@ -99,7 +124,7 @@ msgstr "" " -v, --verbose Aktivera utförliga operationer\n" "\n" -#: src/tools/pw-cat.c:980 +#: src/tools/pw-cat.c:1184 #, c-format msgid "" " -R, --remote Remote daemon name\n" @@ -131,50 +156,64 @@ msgstr "" " -P --properties Sätt nodegenskaper\n" "\n" -#: src/tools/pw-cat.c:998 +#: src/tools/pw-cat.c:1202 #, 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 (default %u)\n" +" --channels Number of channels (default %u)\n" " --channel-map Channel map\n" -" one of: \"stereo\", " -"\"surround-51\",... or\n" +" a channel layout: \"Stereo\", " +"\"5.1\",... or\n" " comma separated list of channel " "names: eg. \"FL,FR\"\n" -" --format Sample format %s (req. for rec) " -"(default %s)\n" +" --list-layouts List supported channel layouts\n" +" --list-channel-names List supported channel maps\n" +" --format Sample format (default %s)\n" +" --list-formats List supported sample formats\n" +" --container Container format\n" +" --list-containers List supported containers and " +"extensions\n" " --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 Samplingsfrekvens (krävs för insp.) " -"(standard %u)\n" -" --channels Antal kanaler (krävs för insp.) " -"(standard %u)\n" +" --rate Samplingsfrekvens (standard %u)\n" +" --channels Antal kanaler (standard %u)\n" " --channel-map Kanalmappning\n" -" en av: \"stereo\", " -"\"surround-51\",... eller\n" +" en kanallayout: \"Stereo\", " +"\"5.1\",... eller\n" " kommaseparerad lista av " "kanalnamn: t.ex. \"FL,FR\"\n" -" --format Samplingsformat %s (krävs för insp.) " -"(standard %s)\n" +" --list-layouts Lista kanallayouter som stöds\n" +" --list-channel-names Lista kanalmappningar som stöds\n" +" --format Samplingsformat (standard %s)\n" +" --list-formats Lista samplingsformat som stöds\n" +" --container Behållarformat\n" +" --list-containers Lista behållare och tillägg som " +"stöds\n" " --volume Strömvolym 0-1.0 (standard %.3f)\n" " -q --quality Omsamplarkvalitet (0 - 15) (standard " "%d)\n" " -a, --raw RAW-läge\n" +" -M, --force-midi Tvinga midi-format, en av \"midi\" " +"eller \"ump\", (standard ump)\n" +" -n, --sample-count ANTAL Stoppa efter ANTAL samplar\n" "\n" -#: src/tools/pw-cat.c:1016 +#: src/tools/pw-cat.c:1227 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 Uppspelningsläge\n" @@ -182,9 +221,16 @@ msgstr "" " -m, --midi Midiläge\n" " -d, --dsd DSD-läge\n" " -o, --encoded Kodat läge\n" +" -s, --sysex SysEx-läge\n" +" -c, --midi-clip MIDI-klippläge\n" "\n" -#: src/tools/pw-cli.c:2306 +#: src/tools/pw-cat.c:1827 +#, c-format +msgid "Supported containers and extensions:\n" +msgstr "Behållare och tillägg som stöds:\n" + +#: src/tools/pw-cli.c:2386 #, c-format msgid "" "%s [options] [command]\n" @@ -203,200 +249,203 @@ msgstr "" " -m, --monitor Övervaka aktivitet\n" "\n" -#: spa/plugins/alsa/acp/acp.c:350 +#: spa/plugins/alsa/acp/acp.c:361 msgid "Pro Audio" msgstr "Professionellt ljud" -#: spa/plugins/alsa/acp/acp.c:511 spa/plugins/alsa/acp/alsa-mixer.c:4635 -#: spa/plugins/bluez5/bluez5-device.c:1802 +#: spa/plugins/alsa/acp/acp.c:537 spa/plugins/alsa/acp/alsa-mixer.c:4699 +#: spa/plugins/bluez5/bluez5-device.c:2021 msgid "Off" msgstr "Av" -#: spa/plugins/alsa/acp/acp.c:593 +#: spa/plugins/alsa/acp/acp.c:620 #, c-format msgid "%s [ALSA UCM error]" msgstr "%s [ALSA UCM-fel]" -#: spa/plugins/alsa/acp/alsa-mixer.c:2652 +#: spa/plugins/alsa/acp/alsa-mixer.c:2721 msgid "Input" msgstr "Ingång" -#: spa/plugins/alsa/acp/alsa-mixer.c:2653 +#: spa/plugins/alsa/acp/alsa-mixer.c:2722 msgid "Docking Station Input" msgstr "Ingång för dockningsstation" -#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +#: spa/plugins/alsa/acp/alsa-mixer.c:2723 msgid "Docking Station Microphone" msgstr "Mikrofon för dockningsstation" -#: spa/plugins/alsa/acp/alsa-mixer.c:2655 +#: spa/plugins/alsa/acp/alsa-mixer.c:2724 msgid "Docking Station Line In" msgstr "Linje in för dockningsstation" -#: 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 "Linje in" -#: spa/plugins/alsa/acp/alsa-mixer.c:2657 -#: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:2146 +#: spa/plugins/alsa/acp/alsa-mixer.c:2726 +#: spa/plugins/alsa/acp/alsa-mixer.c:2810 +#: spa/plugins/bluez5/bluez5-device.c:2422 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 "Frontmikrofon" -#: 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 "Bakre mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +#: spa/plugins/alsa/acp/alsa-mixer.c:2729 msgid "External Microphone" msgstr "Extern 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 "Intern 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 "Radio" -#: 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 "Automatisk förstärkningskontroll" -#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +#: spa/plugins/alsa/acp/alsa-mixer.c:2734 msgid "No Automatic Gain Control" msgstr "Ingen automatisk förstärkningskontroll" -#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +#: spa/plugins/alsa/acp/alsa-mixer.c:2735 msgid "Boost" msgstr "Ökning" -#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +#: spa/plugins/alsa/acp/alsa-mixer.c:2736 msgid "No Boost" msgstr "Ingen ökning" -#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +#: spa/plugins/alsa/acp/alsa-mixer.c:2737 msgid "Amplifier" msgstr "Förstärkare" -#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +#: spa/plugins/alsa/acp/alsa-mixer.c:2738 msgid "No Amplifier" msgstr "Ingen förstärkare" -#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +#: spa/plugins/alsa/acp/alsa-mixer.c:2739 msgid "Bass Boost" msgstr "Basökning" -#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 msgid "No Bass Boost" msgstr "Ingen basökning" -#: spa/plugins/alsa/acp/alsa-mixer.c:2672 -#: spa/plugins/bluez5/bluez5-device.c:2152 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:2428 msgid "Speaker" msgstr "Högtalare" -#: 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:2434 +#: spa/plugins/bluez5/bluez5-device.c:2501 msgid "Headphones" msgstr "Hörlurar" -#: spa/plugins/alsa/acp/alsa-mixer.c:2740 +#: spa/plugins/alsa/acp/alsa-mixer.c:2809 msgid "Analog Input" msgstr "Analog ingång" -#: spa/plugins/alsa/acp/alsa-mixer.c:2744 +#: spa/plugins/alsa/acp/alsa-mixer.c:2813 msgid "Dock Microphone" msgstr "Dockmikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +#: spa/plugins/alsa/acp/alsa-mixer.c:2815 msgid "Headset Microphone" msgstr "Headset-mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +#: spa/plugins/alsa/acp/alsa-mixer.c:2819 msgid "Analog Output" msgstr "Analog utgång" -#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +#: spa/plugins/alsa/acp/alsa-mixer.c:2821 msgid "Headphones 2" msgstr "Hörlurar 2" -#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +#: spa/plugins/alsa/acp/alsa-mixer.c:2822 msgid "Headphones Mono Output" msgstr "Monoutgång för hörlurar" -#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +#: spa/plugins/alsa/acp/alsa-mixer.c:2823 msgid "Line Out" msgstr "Linje ut" -#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +#: spa/plugins/alsa/acp/alsa-mixer.c:2824 msgid "Analog Mono Output" msgstr "Analog monoutgång" -#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +#: spa/plugins/alsa/acp/alsa-mixer.c:2825 msgid "Speakers" msgstr "Högtalare" -#: 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 "Digital utgång (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2759 +#: spa/plugins/alsa/acp/alsa-mixer.c:2828 msgid "Digital Input (S/PDIF)" msgstr "Digital ingång (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2760 +#: spa/plugins/alsa/acp/alsa-mixer.c:2829 msgid "Multichannel Input" msgstr "Multikanalingång" -#: spa/plugins/alsa/acp/alsa-mixer.c:2761 +#: spa/plugins/alsa/acp/alsa-mixer.c:2830 msgid "Multichannel Output" msgstr "Multikanalutgång" -#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +#: spa/plugins/alsa/acp/alsa-mixer.c:2831 msgid "Game Output" msgstr "Spelutgång" -#: 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 "Chatt-utgång" -#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +#: spa/plugins/alsa/acp/alsa-mixer.c:2834 msgid "Chat Input" msgstr "Chatt-ingång" -#: spa/plugins/alsa/acp/alsa-mixer.c:2766 +#: spa/plugins/alsa/acp/alsa-mixer.c:2835 msgid "Virtual Surround 7.1" msgstr "Virtual surround 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +#: spa/plugins/alsa/acp/alsa-mixer.c:4522 msgid "Analog Mono" msgstr "Analog mono" -#: spa/plugins/alsa/acp/alsa-mixer.c:4459 +#: spa/plugins/alsa/acp/alsa-mixer.c:4523 msgid "Analog Mono (Left)" msgstr "Analog mono (vänster)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +#: spa/plugins/alsa/acp/alsa-mixer.c:4524 msgid "Analog Mono (Right)" msgstr "Analog mono (höger)" @@ -405,142 +454,142 @@ 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:4461 -#: spa/plugins/alsa/acp/alsa-mixer.c:4469 -#: spa/plugins/alsa/acp/alsa-mixer.c:4470 +#: 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:4462 +#: spa/plugins/alsa/acp/alsa-mixer.c:4526 msgid "Mono" msgstr "Mono" -#: spa/plugins/alsa/acp/alsa-mixer.c:4463 +#: spa/plugins/alsa/acp/alsa-mixer.c:4527 msgid "Stereo" msgstr "Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4471 -#: spa/plugins/alsa/acp/alsa-mixer.c:4629 -#: spa/plugins/bluez5/bluez5-device.c:2134 +#: spa/plugins/alsa/acp/alsa-mixer.c:4535 +#: spa/plugins/alsa/acp/alsa-mixer.c:4693 +#: spa/plugins/bluez5/bluez5-device.c:2410 msgid "Headset" msgstr "Headset" -#: spa/plugins/alsa/acp/alsa-mixer.c:4472 -#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +#: spa/plugins/alsa/acp/alsa-mixer.c:4536 +#: spa/plugins/alsa/acp/alsa-mixer.c:4694 msgid "Speakerphone" msgstr "Högtalartelefon" -#: spa/plugins/alsa/acp/alsa-mixer.c:4473 -#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +#: spa/plugins/alsa/acp/alsa-mixer.c:4537 +#: spa/plugins/alsa/acp/alsa-mixer.c:4538 msgid "Multichannel" msgstr "Multikanal" -#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +#: spa/plugins/alsa/acp/alsa-mixer.c:4539 msgid "Analog Surround 2.1" msgstr "Analog surround 2.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +#: spa/plugins/alsa/acp/alsa-mixer.c:4540 msgid "Analog Surround 3.0" msgstr "Analog surround 3.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +#: spa/plugins/alsa/acp/alsa-mixer.c:4541 msgid "Analog Surround 3.1" msgstr "Analog surround 3.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +#: spa/plugins/alsa/acp/alsa-mixer.c:4542 msgid "Analog Surround 4.0" msgstr "Analog surround 4.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +#: spa/plugins/alsa/acp/alsa-mixer.c:4543 msgid "Analog Surround 4.1" msgstr "Analog surround 4.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +#: spa/plugins/alsa/acp/alsa-mixer.c:4544 msgid "Analog Surround 5.0" msgstr "Analog surround 5.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +#: spa/plugins/alsa/acp/alsa-mixer.c:4545 msgid "Analog Surround 5.1" msgstr "Analog surround 5.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +#: spa/plugins/alsa/acp/alsa-mixer.c:4546 msgid "Analog Surround 6.0" msgstr "Analog surround 6.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +#: spa/plugins/alsa/acp/alsa-mixer.c:4547 msgid "Analog Surround 6.1" msgstr "Analog surround 6.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +#: spa/plugins/alsa/acp/alsa-mixer.c:4548 msgid "Analog Surround 7.0" msgstr "Analog surround 7.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +#: spa/plugins/alsa/acp/alsa-mixer.c:4549 msgid "Analog Surround 7.1" msgstr "Analog surround 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +#: spa/plugins/alsa/acp/alsa-mixer.c:4550 msgid "Digital Stereo (IEC958)" msgstr "Digital stereo (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +#: spa/plugins/alsa/acp/alsa-mixer.c:4551 msgid "Digital Surround 4.0 (IEC958/AC3)" msgstr "Digital surround 4.0 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +#: spa/plugins/alsa/acp/alsa-mixer.c:4552 msgid "Digital Surround 5.1 (IEC958/AC3)" msgstr "Digital surround 5.1 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +#: spa/plugins/alsa/acp/alsa-mixer.c:4553 msgid "Digital Surround 5.1 (IEC958/DTS)" msgstr "Digital surround 5.1 (IEC958/DTS)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +#: spa/plugins/alsa/acp/alsa-mixer.c:4554 msgid "Digital Stereo (HDMI)" msgstr "Digital stereo (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +#: spa/plugins/alsa/acp/alsa-mixer.c:4555 msgid "Digital Surround 5.1 (HDMI)" msgstr "Digital surround 5.1 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4492 +#: spa/plugins/alsa/acp/alsa-mixer.c:4556 msgid "Chat" msgstr "Chatt" -#: spa/plugins/alsa/acp/alsa-mixer.c:4493 +#: spa/plugins/alsa/acp/alsa-mixer.c:4557 msgid "Game" msgstr "Spel" -#: spa/plugins/alsa/acp/alsa-mixer.c:4627 +#: spa/plugins/alsa/acp/alsa-mixer.c:4691 msgid "Analog Mono Duplex" msgstr "Analog mono duplex" -#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +#: spa/plugins/alsa/acp/alsa-mixer.c:4692 msgid "Analog Stereo Duplex" msgstr "Analog stereo duplex" -#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +#: spa/plugins/alsa/acp/alsa-mixer.c:4695 msgid "Digital Stereo Duplex (IEC958)" msgstr "Digital stereo duplex (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +#: spa/plugins/alsa/acp/alsa-mixer.c:4696 msgid "Multichannel Duplex" msgstr "Multikanalduplex" -#: spa/plugins/alsa/acp/alsa-mixer.c:4633 +#: spa/plugins/alsa/acp/alsa-mixer.c:4697 msgid "Stereo Duplex" msgstr "Stereo duplex" -#: spa/plugins/alsa/acp/alsa-mixer.c:4634 +#: spa/plugins/alsa/acp/alsa-mixer.c:4698 msgid "Mono Chat + 7.1 Surround" msgstr "Mono Chatt + 7.1 Surround" -#: spa/plugins/alsa/acp/alsa-mixer.c:4735 +#: spa/plugins/alsa/acp/alsa-mixer.c:4799 #, c-format msgid "%s Output" msgstr "%s-utgång" -#: spa/plugins/alsa/acp/alsa-mixer.c:4743 +#: spa/plugins/alsa/acp/alsa-mixer.c:4807 #, c-format msgid "%s Input" msgstr "%s-ingång" @@ -627,116 +676,115 @@ msgstr[1] "" "Förmodligen är detta ett fel i ALSA-drivrutinen ”%s”. Vänligen rapportera " "problemet till ALSA-utvecklarna." -#: spa/plugins/alsa/acp/channelmap.h:457 +#: spa/plugins/alsa/acp/channelmap.h:460 msgid "(invalid)" msgstr "(ogiltig)" -#: spa/plugins/alsa/acp/compat.c:193 +#: spa/plugins/alsa/acp/compat.c:194 msgid "Built-in Audio" msgstr "Inbyggt ljud" -#: spa/plugins/alsa/acp/compat.c:198 +#: spa/plugins/alsa/acp/compat.c:199 msgid "Modem" msgstr "Modem" -#: spa/plugins/bluez5/bluez5-device.c:1813 +#: spa/plugins/bluez5/bluez5-device.c:2032 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "Audio gateway (A2DP-källa & HSP/HFP AG)" -#: spa/plugins/bluez5/bluez5-device.c:1841 +#: spa/plugins/bluez5/bluez5-device.c:2061 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 +#: spa/plugins/bluez5/bluez5-device.c:2104 #, 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:1884 +#: spa/plugins/bluez5/bluez5-device.c:2107 #, 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:1892 +#: spa/plugins/bluez5/bluez5-device.c:2115 msgid "High Fidelity Playback (A2DP Sink)" msgstr "High fidelity-uppspelning (A2DP-utgång)" -#: spa/plugins/bluez5/bluez5-device.c:1894 +#: spa/plugins/bluez5/bluez5-device.c:2117 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "High fidelity duplex (A2DP-källa/utgång)" -#: spa/plugins/bluez5/bluez5-device.c:1944 +#: spa/plugins/bluez5/bluez5-device.c:2194 #, 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:1949 +#: spa/plugins/bluez5/bluez5-device.c:2199 #, 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:1953 +#: spa/plugins/bluez5/bluez5-device.c:2203 #, 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:1962 +#: spa/plugins/bluez5/bluez5-device.c:2212 msgid "High Fidelity Playback (BAP Sink)" msgstr "High fidelity-uppspelning (BAP-utgång)" -#: spa/plugins/bluez5/bluez5-device.c:1966 +#: spa/plugins/bluez5/bluez5-device.c:2216 msgid "High Fidelity Input (BAP Source)" msgstr "High fidelity-ingång (BAP-källa)" -#: spa/plugins/bluez5/bluez5-device.c:1969 +#: spa/plugins/bluez5/bluez5-device.c:2219 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "High fidelity duplex (BAP-källa/utgång)" -#: spa/plugins/bluez5/bluez5-device.c:2015 +#: spa/plugins/bluez5/bluez5-device.c:2259 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "Headset-huvudenhet (HSP/HFP, kodek %s)" -#: 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 +#: spa/plugins/bluez5/bluez5-device.c:2411 +#: spa/plugins/bluez5/bluez5-device.c:2416 +#: spa/plugins/bluez5/bluez5-device.c:2423 +#: spa/plugins/bluez5/bluez5-device.c:2429 +#: spa/plugins/bluez5/bluez5-device.c:2435 +#: spa/plugins/bluez5/bluez5-device.c:2441 +#: spa/plugins/bluez5/bluez5-device.c:2447 +#: spa/plugins/bluez5/bluez5-device.c:2453 +#: spa/plugins/bluez5/bluez5-device.c:2459 msgid "Handsfree" msgstr "Handsfree" -#: spa/plugins/bluez5/bluez5-device.c:2141 +#: spa/plugins/bluez5/bluez5-device.c:2417 msgid "Handsfree (HFP)" msgstr "Handsfree (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2158 -msgid "Headphone" -msgstr "Hörlurar" - -#: spa/plugins/bluez5/bluez5-device.c:2164 +#: spa/plugins/bluez5/bluez5-device.c:2440 msgid "Portable" msgstr "Bärbar" -#: spa/plugins/bluez5/bluez5-device.c:2170 +#: spa/plugins/bluez5/bluez5-device.c:2446 msgid "Car" msgstr "Bil" -#: spa/plugins/bluez5/bluez5-device.c:2176 +#: spa/plugins/bluez5/bluez5-device.c:2452 msgid "HiFi" msgstr "HiFi" -#: spa/plugins/bluez5/bluez5-device.c:2182 +#: spa/plugins/bluez5/bluez5-device.c:2458 msgid "Phone" msgstr "Telefon" -#: spa/plugins/bluez5/bluez5-device.c:2189 +#: spa/plugins/bluez5/bluez5-device.c:2465 msgid "Bluetooth" msgstr "Bluetooth" -#: spa/plugins/bluez5/bluez5-device.c:2190 -msgid "Bluetooth (HFP)" -msgstr "Bluetooth (HFP)" +#: spa/plugins/bluez5/bluez5-device.c:2466 +msgid "Bluetooth Handsfree" +msgstr "Bluetooth-handsfree" + +#~ msgid "Headphone" +#~ msgstr "Hörlurar" diff --git a/po/zh_CN.po b/po/zh_CN.po index 8be588b03..3236996bb 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -13,8 +13,8 @@ msgstr "" "Project-Id-Version: pipewire.master-tx\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" "issues\n" -"POT-Creation-Date: 2026-02-11 16:53+0000\n" -"PO-Revision-Date: 2026-02-13 09:36+0800\n" +"POT-Creation-Date: 2026-04-08 13:01+0000\n" +"PO-Revision-Date: 2026-04-09 08:06+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 49.0\n" +"X-Generator: Gtranslator 50.0\n" "Plural-Forms: nplurals=1; plural=0;\n" #: src/daemon/pipewire.c:29 @@ -65,46 +65,46 @@ msgstr "虚拟输出" msgid "Tunnel for %s@%s" msgstr "用于 %s@%s 的隧道" -#: src/modules/module-zeroconf-discover.c:326 +#: src/modules/module-zeroconf-discover.c:290 msgid "Unknown device" msgstr "未知设备" -#: src/modules/module-zeroconf-discover.c:338 +#: src/modules/module-zeroconf-discover.c:302 #, c-format msgid "%s on %s@%s" msgstr "%2$s@%3$s 上的 %1$s" -#: src/modules/module-zeroconf-discover.c:342 +#: src/modules/module-zeroconf-discover.c:306 #, c-format msgid "%s on %s" msgstr "%2$s 上的 %1$s" -#: src/tools/pw-cat.c:264 +#: src/tools/pw-cat.c:269 #, c-format msgid "Supported formats:\n" msgstr "支持的格式:\n" -#: src/tools/pw-cat.c:749 +#: src/tools/pw-cat.c:754 #, c-format msgid "Supported channel layouts:\n" msgstr "支持的声道布局:\n" -#: src/tools/pw-cat.c:759 +#: src/tools/pw-cat.c:764 #, c-format msgid "Supported channel layout aliases:\n" msgstr "支持的声道布局别名:\n" -#: src/tools/pw-cat.c:761 +#: src/tools/pw-cat.c:766 #, c-format msgid " %s -> %s\n" msgstr " %s -> %s\n" -#: src/tools/pw-cat.c:766 +#: src/tools/pw-cat.c:771 #, c-format msgid "Supported channel names:\n" msgstr "支持的声道名称:\n" -#: src/tools/pw-cat.c:1177 +#: src/tools/pw-cat.c:1183 #, c-format msgid "" "%s [options] [|-]\n" @@ -119,7 +119,7 @@ msgstr "" " -v, --verbose 输出详细操作\n" "\n" -#: src/tools/pw-cat.c:1184 +#: src/tools/pw-cat.c:1190 #, c-format msgid "" " -R, --remote Remote daemon name\n" @@ -129,6 +129,8 @@ msgid "" " --target Set node target serial or name " "(default %s)\n" " 0 means don't link\n" +" -C --monitor Capture monitor ports (in recording " +"mode)\n" " --latency Set node latency (default %s)\n" " Xunit (unit = s, ms, us, ns)\n" " or direct samples (256)\n" @@ -143,6 +145,7 @@ msgstr "" " --media-role 设置媒体角色 (默认 %s)\n" " --target 设置节点目标序列或名称 (默认 %s)\n" " 设为 0 则不链接节点\n" +" -C --monitor 捕获监视器端口 (录制模式)\n" " --latency 设置节点延迟 (默认 %s)\n" " 时间 (单位可为 s, ms, us, ns)\n" " 或样本数 (如256)\n" @@ -151,7 +154,7 @@ msgstr "" " -P --properties 设置节点属性\n" "\n" -#: src/tools/pw-cat.c:1202 +#: src/tools/pw-cat.c:1209 #, c-format msgid "" " --rate Sample rate (default %u)\n" @@ -198,7 +201,7 @@ msgstr "" " -n, --sample-count COUNT 计数采样后停止\n" "\n" -#: src/tools/pw-cat.c:1227 +#: src/tools/pw-cat.c:1234 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" @@ -218,7 +221,7 @@ msgstr "" " -c, --midi-clip MIDI 剪辑模式\n" "\n" -#: src/tools/pw-cat.c:1827 +#: src/tools/pw-cat.c:1839 #, c-format msgid "Supported containers and extensions:\n" msgstr "支持的容器和扩展:\n" @@ -245,12 +248,12 @@ msgstr "" msgid "Pro Audio" msgstr "专业音频" -#: spa/plugins/alsa/acp/acp.c:537 spa/plugins/alsa/acp/alsa-mixer.c:4699 -#: spa/plugins/bluez5/bluez5-device.c:2021 +#: spa/plugins/alsa/acp/acp.c:535 spa/plugins/alsa/acp/alsa-mixer.c:4699 +#: spa/plugins/bluez5/bluez5-device.c:2163 msgid "Off" msgstr "关" -#: spa/plugins/alsa/acp/acp.c:620 +#: spa/plugins/alsa/acp/acp.c:618 #, c-format msgid "%s [ALSA UCM error]" msgstr "%s [ALSA UCM 错误]" @@ -278,7 +281,7 @@ msgstr "输入插孔" #: spa/plugins/alsa/acp/alsa-mixer.c:2726 #: spa/plugins/alsa/acp/alsa-mixer.c:2810 -#: spa/plugins/bluez5/bluez5-device.c:2422 +#: spa/plugins/bluez5/bluez5-device.c:2596 msgid "Microphone" msgstr "话筒" @@ -344,15 +347,15 @@ msgid "No Bass Boost" msgstr "无重低音增强" #: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:2428 +#: spa/plugins/bluez5/bluez5-device.c:2602 msgid "Speaker" msgstr "扬声器" #. 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:2434 -#: spa/plugins/bluez5/bluez5-device.c:2501 +#: spa/plugins/bluez5/bluez5-device.c:2608 +#: spa/plugins/bluez5/bluez5-device.c:2675 msgid "Headphones" msgstr "模拟耳机" @@ -462,7 +465,7 @@ msgstr "立体声" #: spa/plugins/alsa/acp/alsa-mixer.c:4535 #: spa/plugins/alsa/acp/alsa-mixer.c:4693 -#: spa/plugins/bluez5/bluez5-device.c:2410 +#: spa/plugins/bluez5/bluez5-device.c:2584 msgid "Headset" msgstr "耳机" @@ -657,101 +660,109 @@ msgstr "内置音频" msgid "Modem" msgstr "调制解调器" -#: spa/plugins/bluez5/bluez5-device.c:2032 +#: spa/plugins/bluez5/bluez5-device.c:2174 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "音频网关 (A2DP 信源 或 HSP/HFP 网关)" -#: spa/plugins/bluez5/bluez5-device.c:2061 +#: spa/plugins/bluez5/bluez5-device.c:2203 msgid "Audio Streaming for Hearing Aids (ASHA Sink)" msgstr "助听器音频流 (ASHA 信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2104 +#: spa/plugins/bluez5/bluez5-device.c:2246 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "高保真回放 (A2DP 信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2107 +#: spa/plugins/bluez5/bluez5-device.c:2249 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "高保真双工 (A2DP 信源/信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2115 +#: spa/plugins/bluez5/bluez5-device.c:2257 msgid "High Fidelity Playback (A2DP Sink)" msgstr "高保真回放 (A2DP 信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2117 +#: spa/plugins/bluez5/bluez5-device.c:2259 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "高保真双工 (A2DP 信源/信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2194 +#: spa/plugins/bluez5/bluez5-device.c:2281 +msgid "Auto: Prefer Quality (A2DP)" +msgstr "自动:质量优先 (A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:2286 +msgid "Auto: Prefer Latency (A2DP)" +msgstr "自动:延迟优先 (A2DP)" + +#: spa/plugins/bluez5/bluez5-device.c:2366 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "高保真回放 (BAP 信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2199 +#: spa/plugins/bluez5/bluez5-device.c:2371 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "高保真输入 (BAP 信源, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2203 +#: spa/plugins/bluez5/bluez5-device.c:2375 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "高保真双工 (BAP 信源/信宿, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2212 +#: spa/plugins/bluez5/bluez5-device.c:2384 msgid "High Fidelity Playback (BAP Sink)" msgstr "高保真回放 (BAP 信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2216 +#: spa/plugins/bluez5/bluez5-device.c:2388 msgid "High Fidelity Input (BAP Source)" msgstr "高保真输入 (BAP 信源)" -#: spa/plugins/bluez5/bluez5-device.c:2219 +#: spa/plugins/bluez5/bluez5-device.c:2391 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "高保真双工 (BAP 信源/信宿)" -#: spa/plugins/bluez5/bluez5-device.c:2259 +#: spa/plugins/bluez5/bluez5-device.c:2431 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "头戴式耳机单元 (HSP/HFP, 编码 %s)" -#: spa/plugins/bluez5/bluez5-device.c:2411 -#: spa/plugins/bluez5/bluez5-device.c:2416 -#: spa/plugins/bluez5/bluez5-device.c:2423 -#: spa/plugins/bluez5/bluez5-device.c:2429 -#: spa/plugins/bluez5/bluez5-device.c:2435 -#: spa/plugins/bluez5/bluez5-device.c:2441 -#: spa/plugins/bluez5/bluez5-device.c:2447 -#: spa/plugins/bluez5/bluez5-device.c:2453 -#: spa/plugins/bluez5/bluez5-device.c:2459 +#: spa/plugins/bluez5/bluez5-device.c:2585 +#: spa/plugins/bluez5/bluez5-device.c:2590 +#: spa/plugins/bluez5/bluez5-device.c:2597 +#: spa/plugins/bluez5/bluez5-device.c:2603 +#: spa/plugins/bluez5/bluez5-device.c:2609 +#: spa/plugins/bluez5/bluez5-device.c:2615 +#: spa/plugins/bluez5/bluez5-device.c:2621 +#: spa/plugins/bluez5/bluez5-device.c:2627 +#: spa/plugins/bluez5/bluez5-device.c:2633 msgid "Handsfree" msgstr "免提" -#: spa/plugins/bluez5/bluez5-device.c:2417 +#: spa/plugins/bluez5/bluez5-device.c:2591 msgid "Handsfree (HFP)" msgstr "免提(HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2440 +#: spa/plugins/bluez5/bluez5-device.c:2614 msgid "Portable" msgstr "便携式" -#: spa/plugins/bluez5/bluez5-device.c:2446 +#: spa/plugins/bluez5/bluez5-device.c:2620 msgid "Car" msgstr "车内" -#: spa/plugins/bluez5/bluez5-device.c:2452 +#: spa/plugins/bluez5/bluez5-device.c:2626 msgid "HiFi" msgstr "高保真" -#: spa/plugins/bluez5/bluez5-device.c:2458 +#: spa/plugins/bluez5/bluez5-device.c:2632 msgid "Phone" msgstr "电话" -#: spa/plugins/bluez5/bluez5-device.c:2465 +#: spa/plugins/bluez5/bluez5-device.c:2639 msgid "Bluetooth" msgstr "蓝牙" -#: spa/plugins/bluez5/bluez5-device.c:2466 +#: spa/plugins/bluez5/bluez5-device.c:2640 msgid "Bluetooth Handsfree" msgstr "蓝牙免提" diff --git a/po/zh_TW.po b/po/zh_TW.po index 1a768a992..ab41369cf 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -4,110 +4,221 @@ # # Cheng-Chia Tseng , 2010, 2012. # pan93412 , 2020. +# SPDX-FileCopyrightText: 2026 Kisaragi Hiu msgid "" msgstr "" "Project-Id-Version: PipeWire Volume Control\n" "Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/" "issues/new\n" -"POT-Creation-Date: 2021-04-18 16:54+0800\n" -"PO-Revision-Date: 2020-01-11 13:49+0800\n" -"Last-Translator: pan93412 \n" -"Language-Team: Chinese \n" +"POT-Creation-Date: 2026-03-11 22:03+0900\n" +"PO-Revision-Date: 2026-03-11 21:24+0900\n" +"Last-Translator: Kisaragi Hiu \n" +"Language-Team: Chinese (Taiwan) \n" "Language: zh_TW\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Lokalize 19.12.0\n" +"X-Generator: Lokalize 26.03.70\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: src/daemon/pipewire.c:43 +#: 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 [選項]\n" +" -h, --help 顯示此說明\n" +" -v, --verbose 提高訊息詳細程度一階\n" +" --version 顯示版本\n" +" -c, --config 載入設定檔案(預設 %s)\n" +" -P --properties 設定前後文屬性\n" #: src/daemon/pipewire.desktop.in:4 msgid "PipeWire Media System" -msgstr "" +msgstr "PipeWire 媒體系統" #: src/daemon/pipewire.desktop.in:5 msgid "Start the PipeWire Media System" -msgstr "" +msgstr "啟動 PipeWire 媒體系統" -#: src/examples/media-session/alsa-monitor.c:526 -#: spa/plugins/alsa/acp/compat.c:187 -msgid "Built-in Audio" -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/examples/media-session/alsa-monitor.c:530 -#: spa/plugins/alsa/acp/compat.c:192 -msgid "Modem" -msgstr "數據機" +#: src/modules/module-fallback-sink.c:40 +msgid "Dummy Output" +msgstr "虛擬輸出" -#: src/examples/media-session/alsa-monitor.c:539 +#: src/modules/module-pulse-tunnel.c:761 +#, c-format +msgid "Tunnel for %s@%s" +msgstr "%s@%s 的穿隧道" + +#: src/modules/module-zeroconf-discover.c:290 msgid "Unknown device" -msgstr "" +msgstr "未知裝置" -#: src/tools/pw-cat.c:991 +#: src/modules/module-zeroconf-discover.c:302 +#, c-format +msgid "%s on %s@%s" +msgstr "%s 於 %s@%s" + +#: src/modules/module-zeroconf-discover.c:306 +#, c-format +msgid "%s on %s" +msgstr "%s 於 %s" + +#: src/tools/pw-cat.c:269 +#, c-format +msgid "Supported formats:\n" +msgstr "支援的格式:\n" + +#: src/tools/pw-cat.c:754 +#, c-format +msgid "Supported channel layouts:\n" +msgstr "支援的頻道配置:\n" + +#: src/tools/pw-cat.c:764 +#, c-format +msgid "Supported channel layout aliases:\n" +msgstr "支援的頻道配置別名:\n" + +#: src/tools/pw-cat.c:766 +#, c-format +msgid " %s -> %s\n" +msgstr " %s -> %s\n" + +#: src/tools/pw-cat.c:771 +#, c-format +msgid "Supported channel names:\n" +msgstr "支援的頻道名稱:\n" + +#: src/tools/pw-cat.c:1182 #, c-format msgid "" -"%s [options] \n" +"%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 +#: src/tools/pw-cat.c:1189 #, 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 (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" -" --list-targets List available targets for --target\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 設定節點目標序號或名稱(預設 %s)\n" +" 0 代表不要連結\n" +" --latency 設定節點延遲(預設 %s)\n" +" X單位(單位為 s, ms, us 或 ns)\n" +" 或直接指定樣本數 (256)\n" +" 取樣率取自來源檔案\n" +" -P --properties 設定節點屬性\n" +"\n" -#: src/tools/pw-cat.c:1016 +#: src/tools/pw-cat.c:1207 #, 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 (default %u)\n" +" --channels Number of channels (default %u)\n" " --channel-map Channel map\n" -" one of: \"stereo\", " -"\"surround-51\",... or\n" +" a channel layout: \"Stereo\", " +"\"5.1\",... or\n" " comma separated list of channel " "names: eg. \"FL,FR\"\n" -" --format Sample format %s (req. for rec) " -"(default %s)\n" +" --list-layouts List supported channel layouts\n" +" --list-channel-names List supported channel maps\n" +" --format Sample format (default %s)\n" +" --list-formats List supported sample formats\n" +" --container Container format\n" +" --list-containers List supported containers and " +"extensions\n" " --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 取樣率(預設 %u)\n" +" --channels 頻道數量(預設 %u)\n" +" --channel-map 頻道映射\n" +" 可以是頻道配置名稱,像是 " +"\"Stereo\"、\"5.1\" 等等,或是\n" +" 以逗號分隔的頻道名稱清單,像是 " +"\"FL,FR\"\n" +" --list-layouts 列出支援的頻道配置\n" +" --list-channel-names 列出支援的頻道映射\n" +" --format 取樣格式(預設 %s)\n" +" --list-formats 列出支援的取樣格式\n" +" --container 容器格式\n" +" --list-containers 列出支援的容器與副檔名\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 樣本數 在「樣本數」個樣本之後停止\n" +"\n" -#: src/tools/pw-cat.c:1033 +#: src/tools/pw-cat.c:1232 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" +" -r, --record 錄製模式\n" +" -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:2932 +#: src/tools/pw-cat.c:1837 +#, c-format +msgid "Supported containers and extensions:\n" +msgstr "支援的容器與副檔名:\n" + +#: src/tools/pw-cli.c:2386 #, c-format msgid "" "%s [options] [command]\n" @@ -115,357 +226,363 @@ msgid "" " --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:290 +#: spa/plugins/alsa/acp/acp.c:361 msgid "Pro Audio" -msgstr "" +msgstr "Pro Audio" -#: spa/plugins/alsa/acp/acp.c:411 spa/plugins/alsa/acp/alsa-mixer.c:4704 -#: spa/plugins/bluez5/bluez5-device.c:1000 +#: spa/plugins/alsa/acp/acp.c:535 spa/plugins/alsa/acp/alsa-mixer.c:4699 +#: spa/plugins/bluez5/bluez5-device.c:2021 msgid "Off" msgstr "關閉" -#: spa/plugins/alsa/acp/channelmap.h:466 -msgid "(invalid)" -msgstr "(無效)" +#: spa/plugins/alsa/acp/acp.c:618 +#, c-format +msgid "%s [ALSA UCM error]" +msgstr "%s [ALSA UCM 錯誤]" -#: spa/plugins/alsa/acp/alsa-mixer.c:2709 +#: spa/plugins/alsa/acp/alsa-mixer.c:2721 msgid "Input" msgstr "輸入" -#: spa/plugins/alsa/acp/alsa-mixer.c:2710 +#: spa/plugins/alsa/acp/alsa-mixer.c:2722 msgid "Docking Station Input" msgstr "Docking Station 輸入" -#: spa/plugins/alsa/acp/alsa-mixer.c:2711 +#: spa/plugins/alsa/acp/alsa-mixer.c:2723 msgid "Docking Station Microphone" msgstr "Docking Station 麥克風" -#: spa/plugins/alsa/acp/alsa-mixer.c:2712 +#: spa/plugins/alsa/acp/alsa-mixer.c:2724 msgid "Docking Station Line In" msgstr "Docking Station 線路輸入" -#: spa/plugins/alsa/acp/alsa-mixer.c:2713 -#: spa/plugins/alsa/acp/alsa-mixer.c:2804 +#: spa/plugins/alsa/acp/alsa-mixer.c:2725 +#: spa/plugins/alsa/acp/alsa-mixer.c:2816 msgid "Line In" msgstr "線路輸入" -#: spa/plugins/alsa/acp/alsa-mixer.c:2714 -#: spa/plugins/alsa/acp/alsa-mixer.c:2798 -#: spa/plugins/bluez5/bluez5-device.c:1145 +#: spa/plugins/alsa/acp/alsa-mixer.c:2726 +#: spa/plugins/alsa/acp/alsa-mixer.c:2810 +#: spa/plugins/bluez5/bluez5-device.c:2422 msgid "Microphone" msgstr "麥克風" -#: spa/plugins/alsa/acp/alsa-mixer.c:2715 -#: spa/plugins/alsa/acp/alsa-mixer.c:2799 +#: spa/plugins/alsa/acp/alsa-mixer.c:2727 +#: spa/plugins/alsa/acp/alsa-mixer.c:2811 msgid "Front Microphone" msgstr "前方麥克風" -#: spa/plugins/alsa/acp/alsa-mixer.c:2716 -#: spa/plugins/alsa/acp/alsa-mixer.c:2800 +#: spa/plugins/alsa/acp/alsa-mixer.c:2728 +#: spa/plugins/alsa/acp/alsa-mixer.c:2812 msgid "Rear Microphone" msgstr "後方麥克風" -#: spa/plugins/alsa/acp/alsa-mixer.c:2717 +#: spa/plugins/alsa/acp/alsa-mixer.c:2729 msgid "External Microphone" msgstr "外接麥克風" -#: spa/plugins/alsa/acp/alsa-mixer.c:2718 -#: spa/plugins/alsa/acp/alsa-mixer.c:2802 +#: spa/plugins/alsa/acp/alsa-mixer.c:2730 +#: spa/plugins/alsa/acp/alsa-mixer.c:2814 msgid "Internal Microphone" msgstr "內建麥克風" -#: spa/plugins/alsa/acp/alsa-mixer.c:2719 -#: spa/plugins/alsa/acp/alsa-mixer.c:2805 +#: spa/plugins/alsa/acp/alsa-mixer.c:2731 +#: spa/plugins/alsa/acp/alsa-mixer.c:2817 msgid "Radio" msgstr "無線電" -#: spa/plugins/alsa/acp/alsa-mixer.c:2720 -#: spa/plugins/alsa/acp/alsa-mixer.c:2806 +#: spa/plugins/alsa/acp/alsa-mixer.c:2732 +#: spa/plugins/alsa/acp/alsa-mixer.c:2818 msgid "Video" msgstr "視訊" -#: spa/plugins/alsa/acp/alsa-mixer.c:2721 +#: spa/plugins/alsa/acp/alsa-mixer.c:2733 msgid "Automatic Gain Control" msgstr "自動增益控制" -#: spa/plugins/alsa/acp/alsa-mixer.c:2722 +#: spa/plugins/alsa/acp/alsa-mixer.c:2734 msgid "No Automatic Gain Control" msgstr "無自動增益控制" -#: spa/plugins/alsa/acp/alsa-mixer.c:2723 +#: spa/plugins/alsa/acp/alsa-mixer.c:2735 msgid "Boost" msgstr "增強" -#: spa/plugins/alsa/acp/alsa-mixer.c:2724 +#: spa/plugins/alsa/acp/alsa-mixer.c:2736 msgid "No Boost" msgstr "無增強" -#: spa/plugins/alsa/acp/alsa-mixer.c:2725 +#: spa/plugins/alsa/acp/alsa-mixer.c:2737 msgid "Amplifier" msgstr "擴大器" -#: spa/plugins/alsa/acp/alsa-mixer.c:2726 +#: spa/plugins/alsa/acp/alsa-mixer.c:2738 msgid "No Amplifier" msgstr "無擴大器" -#: spa/plugins/alsa/acp/alsa-mixer.c:2727 +#: spa/plugins/alsa/acp/alsa-mixer.c:2739 msgid "Bass Boost" msgstr "低音增強" -#: spa/plugins/alsa/acp/alsa-mixer.c:2728 +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 msgid "No Bass Boost" msgstr "無低音增強" -#: spa/plugins/alsa/acp/alsa-mixer.c:2729 -#: spa/plugins/bluez5/bluez5-device.c:1150 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:2428 msgid "Speaker" msgstr "喇叭" -#: spa/plugins/alsa/acp/alsa-mixer.c:2730 -#: spa/plugins/alsa/acp/alsa-mixer.c:2808 +#. 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:2434 +#: spa/plugins/bluez5/bluez5-device.c:2501 msgid "Headphones" msgstr "頭戴式耳機" -#: spa/plugins/alsa/acp/alsa-mixer.c:2797 +#: spa/plugins/alsa/acp/alsa-mixer.c:2809 msgid "Analog Input" msgstr "類比輸入" -#: spa/plugins/alsa/acp/alsa-mixer.c:2801 +#: spa/plugins/alsa/acp/alsa-mixer.c:2813 msgid "Dock Microphone" msgstr "臺座麥克風" -#: spa/plugins/alsa/acp/alsa-mixer.c:2803 +#: spa/plugins/alsa/acp/alsa-mixer.c:2815 msgid "Headset Microphone" msgstr "耳麥麥克風" -#: spa/plugins/alsa/acp/alsa-mixer.c:2807 +#: spa/plugins/alsa/acp/alsa-mixer.c:2819 msgid "Analog Output" msgstr "類比輸出" -#: spa/plugins/alsa/acp/alsa-mixer.c:2809 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:2821 msgid "Headphones 2" -msgstr "頭戴式耳機" +msgstr "頭戴式耳機 2" -#: spa/plugins/alsa/acp/alsa-mixer.c:2810 +#: spa/plugins/alsa/acp/alsa-mixer.c:2822 msgid "Headphones Mono Output" msgstr "頭戴式耳機單聲道輸出" -#: spa/plugins/alsa/acp/alsa-mixer.c:2811 +#: spa/plugins/alsa/acp/alsa-mixer.c:2823 msgid "Line Out" msgstr "線路輸出" -#: spa/plugins/alsa/acp/alsa-mixer.c:2812 +#: spa/plugins/alsa/acp/alsa-mixer.c:2824 msgid "Analog Mono Output" msgstr "類比單聲道輸出" -#: spa/plugins/alsa/acp/alsa-mixer.c:2813 +#: spa/plugins/alsa/acp/alsa-mixer.c:2825 msgid "Speakers" msgstr "喇叭" -#: spa/plugins/alsa/acp/alsa-mixer.c:2814 +#: spa/plugins/alsa/acp/alsa-mixer.c:2826 msgid "HDMI / DisplayPort" msgstr "HDMI / DisplayPort" -#: spa/plugins/alsa/acp/alsa-mixer.c:2815 +#: spa/plugins/alsa/acp/alsa-mixer.c:2827 msgid "Digital Output (S/PDIF)" msgstr "數位輸出 (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2816 +#: spa/plugins/alsa/acp/alsa-mixer.c:2828 msgid "Digital Input (S/PDIF)" msgstr "數位輸入 (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2817 +#: spa/plugins/alsa/acp/alsa-mixer.c:2829 msgid "Multichannel Input" msgstr "多聲道輸入" -#: spa/plugins/alsa/acp/alsa-mixer.c:2818 +#: spa/plugins/alsa/acp/alsa-mixer.c:2830 msgid "Multichannel Output" msgstr "多聲道輸出" -#: spa/plugins/alsa/acp/alsa-mixer.c:2819 +#: spa/plugins/alsa/acp/alsa-mixer.c:2831 msgid "Game Output" msgstr "遊戲輸出" -#: spa/plugins/alsa/acp/alsa-mixer.c:2820 -#: spa/plugins/alsa/acp/alsa-mixer.c:2821 +#: spa/plugins/alsa/acp/alsa-mixer.c:2832 +#: spa/plugins/alsa/acp/alsa-mixer.c:2833 msgid "Chat Output" msgstr "聊天輸出" -#: spa/plugins/alsa/acp/alsa-mixer.c:2822 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:2834 msgid "Chat Input" -msgstr "聊天輸出" +msgstr "聊天輸入" -#: spa/plugins/alsa/acp/alsa-mixer.c:2823 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:2835 msgid "Virtual Surround 7.1" -msgstr "虛擬環繞聲 sink" +msgstr "虛擬環繞聲 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4527 +#: spa/plugins/alsa/acp/alsa-mixer.c:4522 msgid "Analog Mono" msgstr "類比單聲道" -#: spa/plugins/alsa/acp/alsa-mixer.c:4528 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:4523 msgid "Analog Mono (Left)" -msgstr "類比單聲道" +msgstr "類比單聲道(左)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4529 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:4524 msgid "Analog Mono (Right)" -msgstr "類比單聲道" +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:4530 -#: spa/plugins/alsa/acp/alsa-mixer.c:4538 -#: spa/plugins/alsa/acp/alsa-mixer.c:4539 +#: 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 "類比立體聲" -#: spa/plugins/alsa/acp/alsa-mixer.c:4531 +#: spa/plugins/alsa/acp/alsa-mixer.c:4526 msgid "Mono" msgstr "單聲道" -#: spa/plugins/alsa/acp/alsa-mixer.c:4532 +#: spa/plugins/alsa/acp/alsa-mixer.c:4527 msgid "Stereo" msgstr "立體聲" -#: spa/plugins/alsa/acp/alsa-mixer.c:4540 -#: spa/plugins/alsa/acp/alsa-mixer.c:4698 -#: spa/plugins/bluez5/bluez5-device.c:1135 +#: spa/plugins/alsa/acp/alsa-mixer.c:4535 +#: spa/plugins/alsa/acp/alsa-mixer.c:4693 +#: spa/plugins/bluez5/bluez5-device.c:2410 msgid "Headset" msgstr "耳麥" -#: spa/plugins/alsa/acp/alsa-mixer.c:4541 -#: spa/plugins/alsa/acp/alsa-mixer.c:4699 -#, fuzzy +#: spa/plugins/alsa/acp/alsa-mixer.c:4536 +#: spa/plugins/alsa/acp/alsa-mixer.c:4694 msgid "Speakerphone" -msgstr "喇叭" +msgstr "會議揚聲器" -#: spa/plugins/alsa/acp/alsa-mixer.c:4542 -#: spa/plugins/alsa/acp/alsa-mixer.c:4543 +#: spa/plugins/alsa/acp/alsa-mixer.c:4537 +#: spa/plugins/alsa/acp/alsa-mixer.c:4538 msgid "Multichannel" msgstr "多聲道" -#: spa/plugins/alsa/acp/alsa-mixer.c:4544 +#: spa/plugins/alsa/acp/alsa-mixer.c:4539 msgid "Analog Surround 2.1" msgstr "類比環繞聲 2.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4545 +#: spa/plugins/alsa/acp/alsa-mixer.c:4540 msgid "Analog Surround 3.0" msgstr "類比環繞聲 3.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4546 +#: spa/plugins/alsa/acp/alsa-mixer.c:4541 msgid "Analog Surround 3.1" msgstr "類比環繞聲 3.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4547 +#: spa/plugins/alsa/acp/alsa-mixer.c:4542 msgid "Analog Surround 4.0" msgstr "類比環繞聲 4.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4548 +#: spa/plugins/alsa/acp/alsa-mixer.c:4543 msgid "Analog Surround 4.1" msgstr "類比環繞聲 4.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4549 +#: spa/plugins/alsa/acp/alsa-mixer.c:4544 msgid "Analog Surround 5.0" msgstr "類比環繞聲 5.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4550 +#: spa/plugins/alsa/acp/alsa-mixer.c:4545 msgid "Analog Surround 5.1" msgstr "類比環繞聲 5.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4551 +#: spa/plugins/alsa/acp/alsa-mixer.c:4546 msgid "Analog Surround 6.0" msgstr "類比環繞聲 6.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4552 +#: spa/plugins/alsa/acp/alsa-mixer.c:4547 msgid "Analog Surround 6.1" msgstr "類比環繞聲 6.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4553 +#: spa/plugins/alsa/acp/alsa-mixer.c:4548 msgid "Analog Surround 7.0" msgstr "類比環繞聲 7.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4554 +#: spa/plugins/alsa/acp/alsa-mixer.c:4549 msgid "Analog Surround 7.1" msgstr "類比環繞聲 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4555 +#: spa/plugins/alsa/acp/alsa-mixer.c:4550 msgid "Digital Stereo (IEC958)" msgstr "數位立體聲 (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4556 +#: spa/plugins/alsa/acp/alsa-mixer.c:4551 msgid "Digital Surround 4.0 (IEC958/AC3)" msgstr "數位環繞聲 4.0 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4557 +#: spa/plugins/alsa/acp/alsa-mixer.c:4552 msgid "Digital Surround 5.1 (IEC958/AC3)" msgstr "數位環繞聲 5.1 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4558 +#: spa/plugins/alsa/acp/alsa-mixer.c:4553 msgid "Digital Surround 5.1 (IEC958/DTS)" msgstr "數位環繞聲 5.1 (IEC958/DTS)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4559 +#: spa/plugins/alsa/acp/alsa-mixer.c:4554 msgid "Digital Stereo (HDMI)" msgstr "數位立體聲 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4560 +#: spa/plugins/alsa/acp/alsa-mixer.c:4555 msgid "Digital Surround 5.1 (HDMI)" msgstr "數位環繞聲 5.1 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4561 +#: spa/plugins/alsa/acp/alsa-mixer.c:4556 msgid "Chat" -msgstr "" +msgstr "聊天" -#: spa/plugins/alsa/acp/alsa-mixer.c:4562 +#: spa/plugins/alsa/acp/alsa-mixer.c:4557 msgid "Game" -msgstr "" +msgstr "遊戲" -#: spa/plugins/alsa/acp/alsa-mixer.c:4696 +#: spa/plugins/alsa/acp/alsa-mixer.c:4691 msgid "Analog Mono Duplex" msgstr "類比單聲道雙工" -#: spa/plugins/alsa/acp/alsa-mixer.c:4697 +#: spa/plugins/alsa/acp/alsa-mixer.c:4692 msgid "Analog Stereo Duplex" msgstr "類比立體聲雙工" -#: spa/plugins/alsa/acp/alsa-mixer.c:4700 +#: spa/plugins/alsa/acp/alsa-mixer.c:4695 msgid "Digital Stereo Duplex (IEC958)" msgstr "數位立體聲雙工 (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4701 +#: spa/plugins/alsa/acp/alsa-mixer.c:4696 msgid "Multichannel Duplex" msgstr "多聲道雙工" -#: spa/plugins/alsa/acp/alsa-mixer.c:4702 +#: spa/plugins/alsa/acp/alsa-mixer.c:4697 msgid "Stereo Duplex" msgstr "立體聲雙工" -#: spa/plugins/alsa/acp/alsa-mixer.c:4703 +#: spa/plugins/alsa/acp/alsa-mixer.c:4698 msgid "Mono Chat + 7.1 Surround" -msgstr "" +msgstr "單聲道聊天 + 7.1 立體聲" -#: spa/plugins/alsa/acp/alsa-mixer.c:4806 +#: spa/plugins/alsa/acp/alsa-mixer.c:4799 #, c-format msgid "%s Output" msgstr "%s 輸出" -#: spa/plugins/alsa/acp/alsa-mixer.c:4813 +#: spa/plugins/alsa/acp/alsa-mixer.c:4807 #, c-format msgid "%s Input" msgstr "%s 輸入" -#: spa/plugins/alsa/acp/alsa-util.c:1175 spa/plugins/alsa/acp/alsa-util.c:1269 +#: 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 " @@ -481,23 +598,23 @@ msgstr[0] "" "snd_pcm_avail() 傳回超出預期的大值:%lu bytes (%lu ms)。\n" "這很能是 ALSA 驅動程式「%s」的臭蟲。請回報這個問題給 ALSA 開發者。" -#: spa/plugins/alsa/acp/alsa-util.c:1241 +#: 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] "" "snd_pcm_delay() 傳回超出預期的大值:%li bytes (%s%lu ms)。\n" "這很能是 ALSA 驅動程式「%s」的臭蟲。請回報這個問題給 ALSA 開發者。" -#: spa/plugins/alsa/acp/alsa-util.c:1288 +#: spa/plugins/alsa/acp/alsa-util.c:1346 #, c-format msgid "" "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail " @@ -508,7 +625,7 @@ msgstr "" "snd_pcm_avail_delay() 傳回超出預期的大值:延遲 %lu 少於可用的 %lu。\n" "這很能是 ALSA 驅動程式「%s」的臭蟲。請回報這個問題給 ALSA 開發者。" -#: spa/plugins/alsa/acp/alsa-util.c:1331 +#: spa/plugins/alsa/acp/alsa-util.c:1389 #, c-format msgid "" "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte " @@ -524,62 +641,115 @@ msgstr[0] "" "snd_pcm_mmap_begin() 傳回超出預期的大值:%lu bytes (%lu ms)。\n" "這很能是 ALSA 驅動程式「%s」的臭蟲。請回報這個問題給 ALSA 開發者。" -#: spa/plugins/bluez5/bluez5-device.c:1010 -msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" -msgstr "" +#: spa/plugins/alsa/acp/channelmap.h:460 +msgid "(invalid)" +msgstr "(無效)" -#: spa/plugins/bluez5/bluez5-device.c:1033 +#: spa/plugins/alsa/acp/compat.c:194 +msgid "Built-in Audio" +msgstr "內部音效" + +#: spa/plugins/alsa/acp/compat.c:199 +msgid "Modem" +msgstr "數據機" + +#: spa/plugins/bluez5/bluez5-device.c:2032 +msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" +msgstr "音訊閘道 (A2DP Source & HSP/HFP AG)" + +#: spa/plugins/bluez5/bluez5-device.c:2061 +msgid "Audio Streaming for Hearing Aids (ASHA Sink)" +msgstr "助聽器的音訊串流 (ASHA Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:2104 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" -msgstr "" +msgstr "高傳真播放裝置 (A2DP Sink,編碼器 %s)" -#: spa/plugins/bluez5/bluez5-device.c:1035 +#: spa/plugins/bluez5/bluez5-device.c:2107 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" -msgstr "" +msgstr "高傳真雙工裝置 (A2DP Source/Sink,編碼器 %s)" -#: spa/plugins/bluez5/bluez5-device.c:1041 +#: spa/plugins/bluez5/bluez5-device.c:2115 msgid "High Fidelity Playback (A2DP Sink)" -msgstr "" +msgstr "高傳真播放裝置 (A2DP Sink)" -#: spa/plugins/bluez5/bluez5-device.c:1043 +#: spa/plugins/bluez5/bluez5-device.c:2117 msgid "High Fidelity Duplex (A2DP Source/Sink)" -msgstr "" +msgstr "高傳真雙工裝置 (A2DP Source/Sink)" -#: spa/plugins/bluez5/bluez5-device.c:1070 +#: spa/plugins/bluez5/bluez5-device.c:2194 +#, c-format +msgid "High Fidelity Playback (BAP Sink, codec %s)" +msgstr "高傳真播放裝置 (BAP Sink,編碼器 %s)" + +#: spa/plugins/bluez5/bluez5-device.c:2199 +#, c-format +msgid "High Fidelity Input (BAP Source, codec %s)" +msgstr "高傳真輸入裝置 (BAP Source,編碼器 %s)" + +#: spa/plugins/bluez5/bluez5-device.c:2203 +#, c-format +msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" +msgstr "高傳真雙工裝置 (BAP Source/Sink,編碼器 %s)" + +#: spa/plugins/bluez5/bluez5-device.c:2212 +msgid "High Fidelity Playback (BAP Sink)" +msgstr "高傳真播放裝置 (BAP Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:2216 +msgid "High Fidelity Input (BAP Source)" +msgstr "高傳真輸入裝置 (BAP Source)" + +#: spa/plugins/bluez5/bluez5-device.c:2219 +msgid "High Fidelity Duplex (BAP Source/Sink)" +msgstr "高傳真雙工裝置 (BAP Source/Sink)" + +#: spa/plugins/bluez5/bluez5-device.c:2259 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" -msgstr "" +msgstr "耳麥頭戴單元 (HSP/HFP,編碼器 %s)" -#: spa/plugins/bluez5/bluez5-device.c:1074 -msgid "Headset Head Unit (HSP/HFP)" -msgstr "" - -#: spa/plugins/bluez5/bluez5-device.c:1140 +#: spa/plugins/bluez5/bluez5-device.c:2411 +#: spa/plugins/bluez5/bluez5-device.c:2416 +#: spa/plugins/bluez5/bluez5-device.c:2423 +#: spa/plugins/bluez5/bluez5-device.c:2429 +#: spa/plugins/bluez5/bluez5-device.c:2435 +#: spa/plugins/bluez5/bluez5-device.c:2441 +#: spa/plugins/bluez5/bluez5-device.c:2447 +#: spa/plugins/bluez5/bluez5-device.c:2453 +#: spa/plugins/bluez5/bluez5-device.c:2459 msgid "Handsfree" msgstr "免持裝置" -#: spa/plugins/bluez5/bluez5-device.c:1155 -msgid "Headphone" -msgstr "頭戴式耳機" +#: spa/plugins/bluez5/bluez5-device.c:2417 +msgid "Handsfree (HFP)" +msgstr "免持裝置 (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:1160 +#: spa/plugins/bluez5/bluez5-device.c:2440 msgid "Portable" msgstr "可攜裝置" -#: spa/plugins/bluez5/bluez5-device.c:1165 +#: spa/plugins/bluez5/bluez5-device.c:2446 msgid "Car" msgstr "汽車" -#: spa/plugins/bluez5/bluez5-device.c:1170 +#: spa/plugins/bluez5/bluez5-device.c:2452 msgid "HiFi" msgstr "HiFi" -#: spa/plugins/bluez5/bluez5-device.c:1175 +#: spa/plugins/bluez5/bluez5-device.c:2458 msgid "Phone" msgstr "手機" -#: spa/plugins/bluez5/bluez5-device.c:1181 -#, fuzzy +#: spa/plugins/bluez5/bluez5-device.c:2465 msgid "Bluetooth" -msgstr "藍牙輸入" +msgstr "藍牙" + +#: spa/plugins/bluez5/bluez5-device.c:2466 +msgid "Bluetooth Handsfree" +msgstr "藍牙免持裝置" + +#~ msgid "Headphone" +#~ msgstr "頭戴式耳機" diff --git a/spa/include/spa/param/audio/dsd-utils.h b/spa/include/spa/param/audio/dsd-utils.h index 2dab7cc19..9af62d99d 100644 --- a/spa/include/spa/param/audio/dsd-utils.h +++ b/spa/include/spa/param/audio/dsd-utils.h @@ -44,7 +44,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 (info->channels > max_position) - return -ECHRNG; + return -EINVAL; if (position == NULL || spa_pod_copy_array(position, SPA_TYPE_Id, info->position, max_position) != info->channels) { SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); diff --git a/spa/include/spa/param/audio/layout-types.h b/spa/include/spa/param/audio/layout-types.h index 33650202a..45cca3a6f 100644 --- a/spa/include/spa/param/audio/layout-types.h +++ b/spa/include/spa/param/audio/layout-types.h @@ -87,7 +87,7 @@ spa_audio_layout_info_parse_name(struct spa_audio_layout_info *layout, size_t si uint32_t i, n_pos; if (spa_atou32(name+3, &n_pos, 10)) { if (n_pos > max_position) - return -ECHRNG; + return -EINVAL; for (i = 0; i < 0x1000 && i < n_pos; i++) layout->position[i] = SPA_AUDIO_CHANNEL_AUX0 + i; for (; i < n_pos; i++) @@ -99,7 +99,7 @@ spa_audio_layout_info_parse_name(struct spa_audio_layout_info *layout, size_t si 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; + return -EINVAL; *layout = i->layout; return i->layout.n_channels; } diff --git a/spa/include/spa/param/audio/raw-json.h b/spa/include/spa/param/audio/raw-json.h index 6b1b25164..11540a6b0 100644 --- a/spa/include/spa/param/audio/raw-json.h +++ b/spa/include/spa/param/audio/raw-json.h @@ -88,14 +88,14 @@ spa_audio_info_raw_ext_update(struct spa_audio_info_raw *info, size_t size, } else if (spa_streq(key, SPA_KEY_AUDIO_CHANNELS)) { if (spa_atou32(val, &v, 0) && (force || info->channels == 0)) { if (v > max_position) - return -ECHRNG; + return -EINVAL; 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; + return -EINVAL; info->channels = v; SPA_FLAG_CLEAR(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); } @@ -105,7 +105,7 @@ spa_audio_info_raw_ext_update(struct spa_audio_info_raw *info, size_t size, if (spa_audio_parse_position_n(val, strlen(val), info->position, max_position, &v) > 0) { if (v > max_position) - return -ECHRNG; + return -EINVAL; info->channels = v; SPA_FLAG_CLEAR(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); } diff --git a/spa/include/spa/param/audio/raw-utils.h b/spa/include/spa/param/audio/raw-utils.h index 3f81b4a92..1e4ae2758 100644 --- a/spa/include/spa/param/audio/raw-utils.h +++ b/spa/include/spa/param/audio/raw-utils.h @@ -46,7 +46,7 @@ spa_format_audio_raw_ext_parse(const struct spa_pod *format, struct spa_audio_in 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; + return -EINVAL; if (position == NULL || spa_pod_copy_array(position, SPA_TYPE_Id, info->position, max_position) != info->channels) { SPA_FLAG_SET(info->flags, SPA_AUDIO_FLAG_UNPOSITIONED); diff --git a/spa/include/spa/pod/body.h b/spa/include/spa/pod/body.h index 4ab5d8e5f..90eadb82e 100644 --- a/spa/include/spa/pod/body.h +++ b/spa/include/spa/pod/body.h @@ -111,8 +111,15 @@ SPA_API_POD_BODY int spa_pod_choice_n_values(uint32_t choice_type, uint32_t *min 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) + if (offset < 0) return -EINVAL; + /* On 32-bit platforms, off_t is a signed 32-bit type (since it tracks pointer + * width), and consequently can never exceed UINT32_MAX. Skip the upper-bound + * check on 32-bit platforms to avoid a compiler warning. */ +#if __SIZEOF_POINTER__ > 4 + if (offset > (int64_t)UINT32_MAX) + return -EINVAL; +#endif if (size < sizeof(struct spa_pod) || size > maxsize || maxsize - size < (uint32_t)offset) diff --git a/spa/include/spa/support/cpu.h b/spa/include/spa/support/cpu.h index c69338855..2faf42154 100644 --- a/spa/include/spa/support/cpu.h +++ b/spa/include/spa/support/cpu.h @@ -62,6 +62,7 @@ struct spa_cpu { struct spa_interface iface; }; #define SPA_CPU_FLAG_BMI2 (1<<18) /**< Bit Manipulation Instruction Set 2 */ #define SPA_CPU_FLAG_AVX512 (1<<19) /**< AVX-512 */ #define SPA_CPU_FLAG_SLOW_UNALIGNED (1<<20) /**< unaligned loads/stores are slow */ +#define SPA_CPU_FLAG_SLOW_GATHER (1<<21) /**< gather functions are slow */ /* PPC specific */ #define SPA_CPU_FLAG_ALTIVEC (1<<0) /**< standard */ diff --git a/spa/include/spa/support/system.h b/spa/include/spa/support/system.h index 07a31a55f..56ceea648 100644 --- a/spa/include/spa/support/system.h +++ b/spa/include/spa/support/system.h @@ -41,7 +41,7 @@ struct itimerspec; #define SPA_TYPE_INTERFACE_System SPA_TYPE_INFO_INTERFACE_BASE "System" #define SPA_TYPE_INTERFACE_DataSystem SPA_TYPE_INFO_INTERFACE_BASE "DataSystem" -#define SPA_VERSION_SYSTEM 0 +#define SPA_VERSION_SYSTEM 1 struct spa_system { struct spa_interface iface; }; /* IO events */ @@ -59,11 +59,18 @@ struct spa_system { struct spa_interface iface; }; struct spa_poll_event { uint32_t events; - void *data; + union { + void *data; + uint64_t data_u64; + }; +#ifdef __x86_64__ +} __attribute__((packed)); +#else }; +#endif struct spa_system_methods { -#define SPA_VERSION_SYSTEM_METHODS 0 +#define SPA_VERSION_SYSTEM_METHODS 1 uint32_t version; /* read/write/ioctl */ @@ -151,7 +158,7 @@ SPA_API_SYSTEM int spa_system_pollfd_del(struct spa_system *object, int pfd, int SPA_API_SYSTEM int spa_system_pollfd_wait(struct spa_system *object, int pfd, struct spa_poll_event *ev, int n_ev, int timeout) { - return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_wait, 0, pfd, ev, n_ev, timeout); + return spa_api_method_fast_r(int, -ENOTSUP, spa_system, &object->iface, pollfd_wait, 1, pfd, ev, n_ev, timeout); } SPA_API_SYSTEM int spa_system_timerfd_create(struct spa_system *object, int clockid, int flags) diff --git a/spa/include/spa/utils/endian.h b/spa/include/spa/utils/endian.h index 2d002d453..0793ed412 100644 --- a/spa/include/spa/utils/endian.h +++ b/spa/include/spa/utils/endian.h @@ -5,7 +5,7 @@ #ifndef SPA_ENDIAN_H #define SPA_ENDIAN_H -#if defined(__FreeBSD__) || defined(__MidnightBSD__) +#if defined(__MidnightBSD__) #include #define bswap_16 bswap16 #define bswap_32 bswap32 diff --git a/spa/include/spa/utils/json-builder.h b/spa/include/spa/utils/json-builder.h new file mode 100644 index 000000000..b41ea9774 --- /dev/null +++ b/spa/include/spa/utils/json-builder.h @@ -0,0 +1,445 @@ +/* Simple Plugin API */ +/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef SPA_UTILS_JSON_BUILDER_H +#define SPA_UTILS_JSON_BUILDER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + +#ifndef SPA_API_JSON_BUILDER + #ifdef SPA_API_IMPL + #define SPA_API_JSON_BUILDER SPA_API_IMPL + #else + #define SPA_API_JSON_BUILDER static inline + #endif +#endif + +/** \defgroup spa_json_builder JSON builder + * JSON builder functions + */ + +/** + * \addtogroup spa_json_builder + * \{ + */ + +struct spa_json_builder { + FILE *f; +#define SPA_JSON_BUILDER_FLAG_CLOSE (1<<0) +#define SPA_JSON_BUILDER_FLAG_INDENT (1<<1) +#define SPA_JSON_BUILDER_FLAG_SPACE (1<<2) +#define SPA_JSON_BUILDER_FLAG_PRETTY (SPA_JSON_BUILDER_FLAG_INDENT|SPA_JSON_BUILDER_FLAG_SPACE) +#define SPA_JSON_BUILDER_FLAG_COLOR (1<<3) +#define SPA_JSON_BUILDER_FLAG_SIMPLE (1<<4) +#define SPA_JSON_BUILDER_FLAG_RAW (1<<5) + uint32_t flags; + uint32_t indent_off; + uint32_t level; + uint32_t indent; + uint32_t count; + const char *delim; + const char *comma; + const char *key_sep; +#define SPA_JSON_BUILDER_COLOR_NORMAL 0 +#define SPA_JSON_BUILDER_COLOR_KEY 1 +#define SPA_JSON_BUILDER_COLOR_LITERAL 2 +#define SPA_JSON_BUILDER_COLOR_NUMBER 3 +#define SPA_JSON_BUILDER_COLOR_STRING 4 +#define SPA_JSON_BUILDER_COLOR_CONTAINER 5 + const char *color[8]; +}; + +SPA_API_JSON_BUILDER int spa_json_builder_file(struct spa_json_builder *b, FILE *f, uint32_t flags) +{ + bool color = flags & SPA_JSON_BUILDER_FLAG_COLOR; + bool simple = flags & SPA_JSON_BUILDER_FLAG_SIMPLE; + bool space = flags & SPA_JSON_BUILDER_FLAG_SPACE; + spa_zero(*b); + b->f = f; + b->flags = flags; + b->indent = 2; + b->delim = ""; + b->comma = simple ? space ? "" : " " : ","; + b->key_sep = simple ? space ? " =" : "=" : ":"; + b->color[0] = (color ? SPA_ANSI_RESET : ""); + b->color[1] = (color ? SPA_ANSI_BRIGHT_BLUE : ""); + b->color[2] = (color ? SPA_ANSI_BRIGHT_MAGENTA : ""); + b->color[3] = (color ? SPA_ANSI_BRIGHT_CYAN : ""); + b->color[4] = (color ? SPA_ANSI_BRIGHT_GREEN : ""); + b->color[5] = (color ? SPA_ANSI_BRIGHT_YELLOW : ""); + return 0; +} + +SPA_API_JSON_BUILDER int spa_json_builder_memstream(struct spa_json_builder *b, + char **mem, size_t *size, uint32_t flags) +{ + FILE *f; + spa_zero(*b); + if ((f = open_memstream(mem, size)) == NULL) + return -errno; + return spa_json_builder_file(b, f, flags | SPA_JSON_BUILDER_FLAG_CLOSE); +} + +SPA_API_JSON_BUILDER int spa_json_builder_membuf(struct spa_json_builder *b, + char *mem, size_t size, uint32_t flags) +{ + FILE *f; + spa_zero(*b); + if ((f = fmemopen(mem, size, "w")) == NULL) + return -errno; + return spa_json_builder_file(b, f, flags | SPA_JSON_BUILDER_FLAG_CLOSE); +} + +SPA_API_JSON_BUILDER void spa_json_builder_close(struct spa_json_builder *b) +{ + if (b->flags & SPA_JSON_BUILDER_FLAG_CLOSE) + fclose(b->f); +} + +SPA_API_JSON_BUILDER int spa_json_builder_encode_string(struct spa_json_builder *b, + bool raw, const char *before, const char *val, int size, const char *after) +{ + FILE *f = b->f; + int i, len; + if (raw) { + len = fprintf(f, "%s%.*s%s", before, size, val, after) - 1; + } else { + len = fprintf(f, "%s\"", before); + for (i = 0; i < size && val[i]; i++) { + char v = val[i]; + switch (v) { + case '\n': len += fprintf(f, "\\n"); break; + case '\r': len += fprintf(f, "\\r"); break; + case '\b': len += fprintf(f, "\\b"); break; + case '\t': len += fprintf(f, "\\t"); break; + case '\f': len += fprintf(f, "\\f"); break; + case '\\': + case '"': len += fprintf(f, "\\%c", v); break; + default: + if (v > 0 && v < 0x20) + len += fprintf(f, "\\u%04x", v); + else + len += fprintf(f, "%c", v); + break; + } + } + len += fprintf(f, "\"%s", after); + } + return len-1; +} + +SPA_API_JSON_BUILDER +void spa_json_builder_add_simple(struct spa_json_builder *b, const char *key, int key_len, + char type, const char *val, int val_len) +{ + bool indent = b->indent_off == 0 && (b->flags & SPA_JSON_BUILDER_FLAG_INDENT); + bool space = b->flags & SPA_JSON_BUILDER_FLAG_SPACE; + bool force_raw = b->flags & SPA_JSON_BUILDER_FLAG_RAW; + bool raw = true, simple = b->flags & SPA_JSON_BUILDER_FLAG_SIMPLE; + int color; + + if (val == NULL || val_len == 0) { + val = "null"; + val_len = 4; + type = 'l'; + } + if (type == 0) { + if (spa_json_is_container(val, val_len)) + type = simple ? 'C' : 'S'; + else if (val_len > 0 && (*val == '}' || *val == ']')) + type = 'e'; + else if (spa_json_is_null(val, val_len) || + spa_json_is_bool(val, val_len)) + type = 'l'; + else if (spa_json_is_string(val, val_len)) + type = 's'; + else if (spa_json_is_json_number(val, val_len)) + type = 'd'; + else if (simple && (spa_json_is_float(val, val_len) || + spa_json_is_int(val, val_len))) + type = 'd'; + else + type = 'S'; + } + switch (type) { + case 'e': + b->level -= b->indent; + b->delim = ""; + break; + } + + fprintf(b->f, "%s%s%*s", b->delim, b->count == 0 ? "" : indent ? "\n" : space ? " " : "", + indent ? b->level : 0, ""); + if (key) { + bool key_raw = force_raw || (simple && spa_json_make_simple_string(&key, &key_len)) || + spa_json_is_string(key, key_len); + spa_json_builder_encode_string(b, key_raw, + b->color[1], key, key_len, b->color[0]); + fprintf(b->f, "%s%s", b->key_sep, space ? " " : ""); + } + b->delim = b->comma; + switch (type) { + case 'c': + color = SPA_JSON_BUILDER_COLOR_NORMAL; + val_len = 1; + b->delim = ""; + b->level += b->indent; + if (val[1] == '-') b->indent_off++; + break; + case 'e': + color = SPA_JSON_BUILDER_COLOR_NORMAL; + val_len = 1; + if (val[1] == '-') b->indent_off--; + break; + case 'l': + color = SPA_JSON_BUILDER_COLOR_LITERAL; + break; + case 'd': + color = SPA_JSON_BUILDER_COLOR_NUMBER; + break; + case 's': + color = SPA_JSON_BUILDER_COLOR_STRING; + break; + case 'C': + color = SPA_JSON_BUILDER_COLOR_CONTAINER; + break; + default: + color = SPA_JSON_BUILDER_COLOR_STRING; + raw = force_raw || (simple && spa_json_make_simple_string(&val, &val_len)); + break; + } + spa_json_builder_encode_string(b, raw, b->color[color], val, val_len, b->color[0]); + b->count++; +} + +SPA_API_JSON_BUILDER void spa_json_builder_object_push(struct spa_json_builder *b, + const char *key, const char *val) +{ + spa_json_builder_add_simple(b, key, INT_MAX, 'c', val, INT_MAX); +} +SPA_API_JSON_BUILDER void spa_json_builder_pop(struct spa_json_builder *b, + const char *val) +{ + spa_json_builder_add_simple(b, NULL, 0, 'e', val, INT_MAX); +} +SPA_API_JSON_BUILDER void spa_json_builder_object_null(struct spa_json_builder *b, + const char *key) +{ + spa_json_builder_add_simple(b, key, INT_MAX, 'l', "null", 4); +} +SPA_API_JSON_BUILDER void spa_json_builder_object_bool(struct spa_json_builder *b, + const char *key, bool val) +{ + spa_json_builder_add_simple(b, key, INT_MAX, 'l', val ? "true" : "false", INT_MAX); +} +SPA_API_JSON_BUILDER void spa_json_builder_object_int(struct spa_json_builder *b, + const char *key, int64_t val) +{ + char str[128]; + snprintf(str, sizeof(str), "%" PRIi64, val); + spa_json_builder_add_simple(b, key, INT_MAX, 'd', str, INT_MAX); +} +SPA_API_JSON_BUILDER void spa_json_builder_object_uint(struct spa_json_builder *b, + const char *key, uint64_t val) +{ + char str[128]; + snprintf(str, sizeof(str), "%" PRIu64, val); + spa_json_builder_add_simple(b, key, INT_MAX, 'd', str, INT_MAX); +} +SPA_API_JSON_BUILDER void spa_json_builder_object_double(struct spa_json_builder *b, + const char *key, double val) +{ + char str[64]; + spa_json_format_float(str, sizeof(str), (float)val); + spa_json_builder_add_simple(b, key, INT_MAX, 'd', str, INT_MAX); +} +SPA_API_JSON_BUILDER void spa_json_builder_object_string(struct spa_json_builder *b, + const char *key, const char *val) +{ + spa_json_builder_add_simple(b, key, INT_MAX, 'S', val, INT_MAX); +} +SPA_API_JSON_BUILDER SPA_PRINTF_FUNC(3,0) +void spa_json_builder_object_stringv(struct spa_json_builder *b, + const char *key, const char *fmt, va_list va) +{ + char *val; + if (vasprintf(&val, fmt, va) > 0) { + spa_json_builder_object_string(b, key, val); + free(val); + } +} + +SPA_API_JSON_BUILDER SPA_PRINTF_FUNC(3,4) +void spa_json_builder_object_stringf(struct spa_json_builder *b, + const char *key, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + spa_json_builder_object_stringv(b, key, fmt, va); + va_end(va); +} + +SPA_API_JSON_BUILDER void spa_json_builder_object_value_iter(struct spa_json_builder *b, + struct spa_json *it, const char *key, int key_len, const char *val, int len) +{ + struct spa_json sub; + if (spa_json_is_array(val, len)) { + spa_json_builder_add_simple(b, key, key_len, 'c', "[", 1); + spa_json_enter(it, &sub); + while ((len = spa_json_next(&sub, &val)) > 0) + spa_json_builder_object_value_iter(b, &sub, NULL, 0, val, len); + spa_json_builder_pop(b, "]"); + } + else if (spa_json_is_object(val, len)) { + const char *k; + int kl; + spa_json_builder_add_simple(b, key, key_len, 'c', "{", 1); + spa_json_enter(it, &sub); + while ((kl = spa_json_next(&sub, &k)) > 0) { + if ((len = spa_json_next(&sub, &val)) < 0) + break; + spa_json_builder_object_value_iter(b, &sub, k, kl, val, len); + } + spa_json_builder_pop(b, "}"); + } + else { + spa_json_builder_add_simple(b, key, key_len, 0, val, len); + } +} +SPA_API_JSON_BUILDER void spa_json_builder_object_value_full(struct spa_json_builder *b, + bool recurse, const char *key, int key_len, const char *val, int val_len) +{ + if (!recurse || val == NULL) { + spa_json_builder_add_simple(b, key, key_len, 0, val, val_len); + } else { + struct spa_json it[1]; + const char *v; + if (spa_json_begin(&it[0], val, val_len, &v) >= 0) + spa_json_builder_object_value_iter(b, &it[0], key, key_len, val, val_len); + } +} +SPA_API_JSON_BUILDER void spa_json_builder_object_value(struct spa_json_builder *b, + bool recurse, const char *key, const char *val) +{ + spa_json_builder_object_value_full(b, recurse, key, key ? strlen(key) : 0, + val, val ? strlen(val) : 0); +} +SPA_API_JSON_BUILDER SPA_PRINTF_FUNC(4,5) +void spa_json_builder_object_valuef(struct spa_json_builder *b, + bool recurse, const char *key, const char *fmt, ...) +{ + va_list va; + char *val; + va_start(va, fmt); + if (vasprintf(&val, fmt, va) > 0) { + spa_json_builder_object_value(b, recurse, key, val); + free(val); + } + va_end(va); +} + + +/* array functions */ +SPA_API_JSON_BUILDER void spa_json_builder_array_push(struct spa_json_builder *b, + const char *val) +{ + spa_json_builder_object_push(b, NULL, val); +} +SPA_API_JSON_BUILDER void spa_json_builder_array_null(struct spa_json_builder *b) +{ + spa_json_builder_object_null(b, NULL); +} +SPA_API_JSON_BUILDER void spa_json_builder_array_bool(struct spa_json_builder *b, + bool val) +{ + spa_json_builder_object_bool(b, NULL, val); +} +SPA_API_JSON_BUILDER void spa_json_builder_array_int(struct spa_json_builder *b, + int64_t val) +{ + spa_json_builder_object_int(b, NULL, val); +} +SPA_API_JSON_BUILDER void spa_json_builder_array_uint(struct spa_json_builder *b, + uint64_t val) +{ + spa_json_builder_object_uint(b, NULL, val); +} +SPA_API_JSON_BUILDER void spa_json_builder_array_double(struct spa_json_builder *b, + double val) +{ + spa_json_builder_object_double(b, NULL, val); +} +SPA_API_JSON_BUILDER void spa_json_builder_array_string(struct spa_json_builder *b, + const char *val) +{ + spa_json_builder_object_string(b, NULL, val); +} +SPA_API_JSON_BUILDER SPA_PRINTF_FUNC(2,3) +void spa_json_builder_array_stringf(struct spa_json_builder *b, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + spa_json_builder_object_stringv(b, NULL, fmt, va); + va_end(va); +} +SPA_API_JSON_BUILDER void spa_json_builder_array_value(struct spa_json_builder *b, + bool recurse, const char *val) +{ + spa_json_builder_object_value(b, recurse, NULL, val); +} +SPA_API_JSON_BUILDER SPA_PRINTF_FUNC(3,4) +void spa_json_builder_array_valuef(struct spa_json_builder *b, bool recurse, const char *fmt, ...) +{ + va_list va; + char *val; + va_start(va, fmt); + if (vasprintf(&val, fmt, va) > 0) { + spa_json_builder_object_value(b, recurse, NULL, val); + free(val); + } + va_end(va); +} + +SPA_API_JSON_BUILDER char *spa_json_builder_reformat(const char *json, uint32_t flags) +{ + struct spa_json_builder b; + char *mem; + size_t size; + int res; + if ((res = spa_json_builder_memstream(&b, &mem, &size, flags)) < 0) { + errno = -res; + return NULL; + } + spa_json_builder_array_value(&b, true, json); + spa_json_builder_close(&b); + return mem; +} + +/** + * \} + */ + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* SPA_UTILS_JSON_BUILDER_H */ diff --git a/spa/include/spa/utils/json-core.h b/spa/include/spa/utils/json-core.h index 5616bffe1..9745000cf 100644 --- a/spa/include/spa/utils/json-core.h +++ b/spa/include/spa/utils/json-core.h @@ -232,7 +232,7 @@ SPA_API_JSON int spa_json_next(struct spa_json * iter, const char **value) switch (cur) { case '\0': case '\t': case ' ': case '\r': case '\n': - case '"': case '#': + case '"': case '#': case '{': case '[': case ':': case ',': case '=': case ']': case '}': iter->state = __STRUCT | flag; if (iter->depth > 0) @@ -399,6 +399,10 @@ SPA_API_JSON int spa_json_is_container(const char *val, int len) { return len > 0 && (*val == '{' || *val == '['); } +SPA_API_JSON int spa_json_is_container_end(const char *val, int len) +{ + return len > 0 && (*val == '}' || *val == ']'); +} /* object */ SPA_API_JSON int spa_json_is_object(const char *val, int len) @@ -421,20 +425,11 @@ SPA_API_JSON bool spa_json_is_null(const char *val, int len) /* float */ SPA_API_JSON int spa_json_parse_float(const char *val, int len, float *result) { - char buf[96]; - char *end; - int pos; + char buf[96], *end; if (len <= 0 || len >= (int)sizeof(buf)) return 0; - for (pos = 0; pos < len; ++pos) { - switch (val[pos]) { - case '+': case '-': case '0' ... '9': case '.': case 'e': case 'E': break; - default: return 0; - } - } - memcpy(buf, val, len); buf[len] = '\0'; @@ -462,8 +457,7 @@ SPA_API_JSON char *spa_json_format_float(char *str, int size, float val) /* int */ SPA_API_JSON int spa_json_parse_int(const char *val, int len, int *result) { - char buf[64]; - char *end; + char buf[64], *end; if (len <= 0 || len >= (int)sizeof(buf)) return 0; @@ -480,6 +474,39 @@ SPA_API_JSON bool spa_json_is_int(const char *val, int len) return spa_json_parse_int(val, len, &dummy); } +SPA_API_JSON bool spa_json_is_json_number(const char *val, int len) +{ + static const int8_t trans[9][7] = { + /* '1-9' '0' '-' '+' '.' 'eE' other */ + /* 0 */ {-1, -1, -1, -1, 6, 7, -1 }, /* after '0' */ + /* 1 */ { 1, 1, -1, -1, 6, 7, -1 }, /* in integer */ + /* 2 */ { 2, 2, -1, -1, -1, 7, -1 }, /* in fraction */ + /* 3 */ { 3, 3, -1, -1, -1, -1, -1 }, /* in exponent */ + /* 4 */ { 1, 0, 5, -1, -1, -1, -1 }, /* start */ + /* 5 */ { 1, 0, -1, -1, -1, -1, -1 }, /* after '-' */ + /* 6 */ { 2, 2, -1, -1, -1, -1, -1 }, /* after '.' */ + /* 7 */ { 3, 3, 8, 8, -1, -1, -1 }, /* after 'e'/'E' */ + /* 8 */ { 3, 3, -1, -1, -1, -1, -1 }, /* after exp sign */ + }; + static const int8_t char_class[128] = { + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, /* 0-15 */ + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, /* 16-31 */ + 6,6,6,6,6,6,6,6,6,6,6,3,6,2,4,6, /* 32-47: + - . */ + 1,0,0,0,0,0,0,0,0,0,6,6,6,6,6,6, /* 48-63: 0-9 */ + 6,6,6,6,6,5,6,6,6,6,6,6,6,6,6,6, /* 64-79: E */ + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, /* 80-95 */ + 6,6,6,6,6,5,6,6,6,6,6,6,6,6,6,6, /* 96-111: e */ + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, /* 112-127 */ + }; + int i, state = 4; + + for (i = 0; i < len; i++) { + if ((state = trans[state][char_class[val[i]&0x7f]]) < 0) + return false; + } + return state < 4; +} + /* bool */ SPA_API_JSON bool spa_json_is_true(const char *val, int len) { @@ -510,6 +537,46 @@ SPA_API_JSON bool spa_json_is_string(const char *val, int len) { return len > 1 && *val == '"'; } +SPA_API_JSON bool spa_json_is_simple_string(const char *val, int size) +{ + int i; + static const char *REJECT = "\"\\'=:,{}[]()#"; + for (i = 0; i < size && val[i]; i++) { + if (val[i] <= 0x20 || strchr(REJECT, val[i]) != NULL) + return false; + } + return true; +} +SPA_API_JSON bool spa_json_make_simple_string(const char **val, int *len) +{ + int i, l = *len; + const char *v = *val; + static const char *REJECT = "\"\\'=:,{}[]()#"; + int trimmed = 0, bad = 0; + for (i = 0; i < l && v[i]; i++) { + if (i == 0 && v[0] == '\"') + trimmed++; + else if ((i+1 == l || !v[i+1]) && v[i] == '\"') + trimmed++; + else if (v[i] <= 0x20 || strchr(REJECT, v[i]) != NULL) + bad++; + } + if (trimmed == 0 && bad == 0 && i > 0) + return true; + else if (trimmed == 2) { + if (bad == 0 && i > 2 && + !spa_json_is_null(&v[1], i-2) && + !spa_json_is_bool(&v[1], i-2) && + !spa_json_is_float(&v[1], i-2) && + !spa_json_is_container(&v[1], i-2) && + !spa_json_is_container_end(&v[1], i-2)) { + (*len) = i-2; + (*val)++; + } + return true; + } + return false; +} SPA_API_JSON int spa_json_parse_hex(const char *p, int num, uint32_t *res) { diff --git a/spa/include/spa/utils/string.h b/spa/include/spa/utils/string.h index bab60de2b..35e4ea026 100644 --- a/spa/include/spa/utils/string.h +++ b/spa/include/spa/utils/string.h @@ -380,17 +380,26 @@ SPA_API_STRING void spa_strbuf_init(struct spa_strbuf *buf, char *buffer, size_t buf->buffer[0] = '\0'; } +SPA_PRINTF_FUNC(2, 0) +SPA_API_STRING int spa_strbuf_appendv(struct spa_strbuf *buf, const char *fmt, va_list args) +{ + size_t remain = buf->maxsize - buf->pos; + int written = vsnprintf(&buf->buffer[buf->pos], remain, fmt, args); + if (written > 0) + buf->pos += SPA_MIN(remain, (size_t)written); + return written; +} + SPA_PRINTF_FUNC(2, 3) SPA_API_STRING int spa_strbuf_append(struct spa_strbuf *buf, const char *fmt, ...) { - size_t remain = buf->maxsize - buf->pos; - ssize_t written; va_list args; + int written; + va_start(args, fmt); - written = vsnprintf(&buf->buffer[buf->pos], remain, fmt, args); + written = spa_strbuf_appendv(buf, fmt, args); va_end(args); - if (written > 0) - buf->pos += SPA_MIN(remain, (size_t)written); + return written; } diff --git a/spa/lib/lib.c b/spa/lib/lib.c index 0aa35fae3..3ded776ad 100644 --- a/spa/lib/lib.c +++ b/spa/lib/lib.c @@ -2,7 +2,6 @@ #undef SPA_AUDIO_MAX_CHANNELS #define SPA_API_IMPL SPA_EXPORT -#include #include #include #include @@ -126,16 +125,17 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include #include #include +#include #include #include #include @@ -158,6 +158,7 @@ #include #include #include +#include #include #include #include diff --git a/spa/plugins/aec/aec-webrtc.cpp b/spa/plugins/aec/aec-webrtc.cpp index 7b8cafcde..2e451d851 100644 --- a/spa/plugins/aec/aec-webrtc.cpp +++ b/spa/plugins/aec/aec-webrtc.cpp @@ -39,7 +39,7 @@ struct impl_data { std::unique_ptr play_buffer, rec_buffer, out_buffer; }; -SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.eac.webrtc"); +SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.aec.webrtc"); #undef SPA_LOG_TOPIC_DEFAULT #define SPA_LOG_TOPIC_DEFAULT &log_topic @@ -221,6 +221,11 @@ static int webrtc_init2(void *object, const struct spa_dict *args, }}; #endif + if (out_info->channels != 1 && rec_info->channels != out_info->channels) { + spa_log_error(impl->log, "Source channels must be equal to capture channels or 1"); + return -EINVAL; + } + #if defined(HAVE_WEBRTC) auto apm = std::unique_ptr(webrtc::AudioProcessing::Create(config)); #elif defined(HAVE_WEBRTC1) diff --git a/spa/plugins/alsa/acp-tool.c b/spa/plugins/alsa/acp-tool.c index c12401100..824d69df6 100644 --- a/spa/plugins/alsa/acp-tool.c +++ b/spa/plugins/alsa/acp-tool.c @@ -10,7 +10,9 @@ #include #include #include +#ifdef __linux__ #include +#endif #include #include diff --git a/spa/plugins/alsa/acp/acp.c b/spa/plugins/alsa/acp/acp.c index bee8d1ef4..8f5ea18c2 100644 --- a/spa/plugins/alsa/acp/acp.c +++ b/spa/plugins/alsa/acp/acp.c @@ -485,13 +485,11 @@ static int add_pro_profile(pa_card *impl, uint32_t index) 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); pa_proplist_setf(m->output_proplist, "api.alsa.auto-link", "true"); pa_proplist_setf(m->output_proplist, "api.alsa.disable-tsched", "true"); } PA_IDXSET_FOREACH(m, ap->input_mappings, idx) { pa_proplist_setf(m->input_proplist, "node.group", "pro-audio-%u", index); - pa_proplist_setf(m->input_proplist, "node.link-group", "pro-audio-%u", index); pa_proplist_setf(m->input_proplist, "api.alsa.auto-link", "true"); pa_proplist_setf(m->input_proplist, "api.alsa.disable-tsched", "true"); } diff --git a/spa/plugins/alsa/acp/compat.h b/spa/plugins/alsa/acp/compat.h index f7592e1a6..87151d197 100644 --- a/spa/plugins/alsa/acp/compat.h +++ b/spa/plugins/alsa/acp/compat.h @@ -429,14 +429,14 @@ static PA_PRINTF_FUNC(1,0) inline char *pa_vsprintf_malloc(const char *fmt, va_l #define pa_fopen_cloexec(f,m) fopen(f,m"e") -static inline char *pa_path_get_filename(const char *p) +static inline const char *pa_path_get_filename(const char *p) { - char *fn; + const char *fn; if (!p) return NULL; if ((fn = strrchr(p, PA_PATH_SEP_CHAR))) return fn+1; - return (char*) p; + return p; } static inline bool pa_is_path_absolute(const char *fn) diff --git a/spa/plugins/alsa/alsa-seq-bridge.c b/spa/plugins/alsa/alsa-seq-bridge.c index 06c5bc28f..d5b0019a8 100644 --- a/spa/plugins/alsa/alsa-seq-bridge.c +++ b/spa/plugins/alsa/alsa-seq-bridge.c @@ -227,7 +227,7 @@ static void emit_port_info(struct seq_state *this, struct seq_port *port, bool f if (full) port->info.change_mask = port->info_all; if (port->info.change_mask) { - struct spa_dict_item items[6]; + struct spa_dict_item items[7]; uint32_t n_items = 0; int card_id; snd_seq_port_info_t *info; @@ -284,6 +284,9 @@ static void emit_port_info(struct seq_state *this, struct seq_port *port, bool f snprintf(card, sizeof(card), "%d", card_id); items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_CARD, card); } + if (this->ump) + items[n_items++] = SPA_DICT_ITEM_INIT("control.ump", "true"); + port->info.props = &SPA_DICT_INIT(items, n_items); spa_node_emit_port_info(&this->hooks, @@ -501,6 +504,7 @@ impl_node_port_enum_params(void *object, int seq, struct seq_state *this = object; struct seq_port *port; struct spa_pod *param; + struct spa_pod_frame f[1]; struct spa_pod_builder b = { 0 }; uint8_t buffer[1024]; struct spa_result_node_params result; @@ -524,10 +528,18 @@ impl_node_port_enum_params(void *object, int seq, case SPA_PARAM_EnumFormat: if (result.index > 0) return 0; - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + 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(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), + 0); + if (port->control_types != 0) { + spa_pod_builder_add(&b, + SPA_FORMAT_CONTROL_types, SPA_POD_Int(port->control_types), + 0); + } + param = spa_pod_builder_pop(&b, &f[0]); break; case SPA_PARAM_Format: @@ -535,10 +547,18 @@ impl_node_port_enum_params(void *object, int seq, return -EIO; if (result.index > 0) return 0; - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_Format, SPA_PARAM_Format, + 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(SPA_MEDIA_TYPE_application), - SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control)); + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), + 0); + if (port->control_types != 0) { + spa_pod_builder_add(&b, + SPA_FORMAT_CONTROL_types, SPA_POD_Int(port->control_types), + 0); + } + param = spa_pod_builder_pop(&b, &f[0]); break; case SPA_PARAM_Buffers: @@ -955,7 +975,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; + this->ump = false; for (i = 0; info && i < info->n_items; i++) { const char *k = info->items[i].key; diff --git a/spa/plugins/alsa/alsa-seq.c b/spa/plugins/alsa/alsa-seq.c index 8a4ba369c..c25b49420 100644 --- a/spa/plugins/alsa/alsa-seq.c +++ b/spa/plugins/alsa/alsa-seq.c @@ -620,9 +620,8 @@ static int process_read(struct seq_state *state) { struct seq_stream *stream = &state->streams[SPA_DIRECTION_OUTPUT]; const bool ump = state->ump; - uint32_t *data; + void *data; uint8_t midi1_data[MAX_EVENT_SIZE]; - uint32_t ump_data[MAX_EVENT_SIZE]; long size; int res = -1; struct seq_port *port; @@ -633,9 +632,6 @@ static int process_read(struct seq_state *state) 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; if (ump) { @@ -702,7 +698,7 @@ static int process_read(struct seq_state *state) #ifdef HAVE_ALSA_UMP snd_seq_ump_event_t *ev = event; - data = (uint32_t*)&ev->ump[0]; + data = &ev->ump[0]; size = spa_ump_message_size(snd_ump_msg_hdr_type(ev->ump[0])) * 4; #else spa_assert_not_reached(); @@ -715,34 +711,21 @@ static int process_read(struct seq_state *state) spa_log_warn(state->log, "decode failed: %s", snd_strerror(size)); continue; } - - midi1_ptr = midi1_data; - midi1_size = size; + data = midi1_data; } - 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_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, ump ? SPA_CONTROL_UMP : SPA_CONTROL_Midi ); + spa_pod_builder_bytes(&port->builder, data, size); - 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); + /* 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; } done: @@ -819,7 +802,6 @@ static int process_write(struct seq_state *state) const void *c_body; uint64_t out_time; snd_seq_real_time_t out_rt; - bool first = true; if (io->status != SPA_STATUS_HAVE_DATA || io->buffer_id >= port->n_buffers) @@ -844,9 +826,6 @@ static int process_write(struct seq_state *state) size_t body_size; uint8_t *body; - if (c.type != SPA_CONTROL_UMP) - continue; - body = (uint8_t*)c_body; body_size = c.value.size; @@ -861,6 +840,9 @@ static int process_write(struct seq_state *state) #ifdef HAVE_ALSA_UMP snd_seq_ump_event_t ev; + if (c.type != SPA_CONTROL_UMP) + continue; + 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); @@ -878,26 +860,26 @@ static int process_write(struct seq_state *state) #endif } else { snd_seq_event_t ev; - uint8_t data[MAX_EVENT_SIZE]; - int size; - uint64_t st = 0; + int size = 0; + long s; + + if (c.type != SPA_CONTROL_Midi) + continue; while (body_size > 0) { - if ((size = spa_ump_to_midi((const uint32_t **)&body, &body_size, - data, sizeof(data), &st)) <= 0) - break; - - if (first) + if (size == 0) snd_seq_ev_clear(&ev); - if ((size = snd_midi_event_encode(stream->codec, data, size, &ev)) < 0) { + if ((s = snd_midi_event_encode(stream->codec, body, body_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; + size = 0; continue; } - first = false; + body += s; + body_size -= s; + size += s; 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. */ @@ -913,7 +895,7 @@ static int process_write(struct seq_state *state) spa_log_warn(state->log, "failed to output event: %s", snd_strerror(err)); } - first = true; + size = 0; } } } diff --git a/spa/plugins/alsa/alsa-seq.h b/spa/plugins/alsa/alsa-seq.h index 354fa1d5d..3100b9587 100644 --- a/spa/plugins/alsa/alsa-seq.h +++ b/spa/plugins/alsa/alsa-seq.h @@ -82,6 +82,8 @@ struct seq_port { struct spa_pod_builder builder; struct spa_pod_frame frame; + uint32_t control_types; + struct spa_audio_info current_format; unsigned int have_format:1; unsigned int active:1; diff --git a/spa/plugins/alsa/alsa-udev.c b/spa/plugins/alsa/alsa-udev.c index 8537c9760..9c3e12f20 100644 --- a/spa/plugins/alsa/alsa-udev.c +++ b/spa/plugins/alsa/alsa-udev.c @@ -48,6 +48,7 @@ struct card { unsigned int accessible:1; unsigned int ignored:1; unsigned int emitted:1; + unsigned int wireless_disconnected:1; /* Local SPA object IDs. (Global IDs are produced by PipeWire * out of this using its registry.) Compress-Offload or PCM @@ -59,6 +60,10 @@ struct card { * is used because 0 is a valid ALSA card number. */ uint32_t pcm_device_id; uint32_t compress_offload_device_id; + + /* Syspath of the USB interface that has wireless_status (e.g. + * /sys/devices/.../1-5:1.3). Empty string when not applicable. */ + char wireless_status_syspath[256]; }; static uint32_t calc_pcm_device_id(struct card *card) @@ -92,6 +97,8 @@ struct impl { struct spa_source source; struct spa_source notify; + struct udev_monitor *usb_umonitor; + struct spa_source usb_source; unsigned int use_acp:1; unsigned int expose_busy:1; int use_ucm; @@ -353,6 +360,87 @@ static int check_udev_environment(struct udev *udev, const char *devname) return ret; } +static void check_wireless_status(struct impl *this, struct card *card) +{ + char path[PATH_MAX]; + char buf[32]; + size_t sz; + bool was_disconnected; + + if (card->wireless_status_syspath[0] == '\0') + return; + + was_disconnected = card->wireless_disconnected; + + spa_scnprintf(path, sizeof(path), "%s/wireless_status", card->wireless_status_syspath); + + spa_autoptr(FILE) f = fopen(path, "re"); + if (f == NULL) + return; + + sz = fread(buf, 1, sizeof(buf) - 1, f); + buf[sz] = '\0'; + + card->wireless_disconnected = spa_strstartswith(buf, "disconnected"); + + if (card->wireless_disconnected != was_disconnected) + spa_log_info(this->log, "card %u: wireless headset %s", + card->card_nr, + card->wireless_disconnected ? "disconnected" : "connected"); +} + +static void find_wireless_status(struct impl *this, struct card *card) +{ + const char *bus, *parent_syspath, *parent_sysname; + struct udev_device *parent; + struct dirent *entry; + char path[PATH_MAX]; + size_t sysname_len; + + bus = udev_device_get_property_value(card->udev_device, "ID_BUS"); + if (!spa_streq(bus, "usb")) + return; + + /* udev_device_get_parent_* returns a borrowed reference owned by the child; do not unref it. */ + parent = udev_device_get_parent_with_subsystem_devtype(card->udev_device, "usb", "usb_device"); + if (parent == NULL) + return; + + parent_syspath = udev_device_get_syspath(parent); + parent_sysname = udev_device_get_sysname(parent); + if (parent_syspath == NULL || parent_sysname == NULL) + return; + + sysname_len = strlen(parent_sysname); + + spa_autoptr(DIR) dir = opendir(parent_syspath); + if (dir == NULL) + return; + + while ((entry = readdir(dir)) != NULL) { + /* USB interface directories are named ":." */ + if (strncmp(entry->d_name, parent_sysname, sysname_len) != 0 || + entry->d_name[sysname_len] != ':') + continue; + + spa_scnprintf(path, sizeof(path), "%s/%s/wireless_status", + parent_syspath, entry->d_name); + if (access(path, R_OK) < 0) + continue; + + spa_scnprintf(card->wireless_status_syspath, + sizeof(card->wireless_status_syspath), + "%s/%s", parent_syspath, entry->d_name); + + check_wireless_status(this, card); + + spa_log_debug(this->log, "card %u: found wireless_status at %s (%s)", + card->card_nr, card->wireless_status_syspath, + card->wireless_disconnected ? "disconnected" : "connected"); + return; + } +} + static int check_pcm_device_availability(struct impl *this, struct card *card, int *num_pcm_devices) { @@ -538,6 +626,9 @@ static int emit_added_object_info(struct impl *this, struct card *card) if ((str = udev_device_get_property_value(udev_device, "ACP_PROFILE_SET")) && *str) items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_PROFILE_SET, str); + if ((str = udev_device_get_property_value(udev_device, "ACP_IGNORE_DB")) && *str) + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_API_ALSA_IGNORE_DB, str); + if ((str = udev_device_get_property_value(udev_device, "SOUND_CLASS")) && *str) items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_DEVICE_CLASS, str); @@ -731,7 +822,11 @@ static void process_card(struct impl *this, enum action action, struct card *car return; check_access(this, card); - if (card->accessible && !card->emitted) { + check_wireless_status(this, card); + + bool effective_accessible = card->accessible && !card->wireless_disconnected; + + if (effective_accessible && !card->emitted) { int res = emit_added_object_info(this, card); if (res < 0) { if (card->ignored) @@ -750,7 +845,7 @@ static void process_card(struct impl *this, enum action action, struct card *car card->card_nr); card->unavailable = false; } - } else if (!card->accessible && card->emitted) { + } else if (!effective_accessible && card->emitted) { card->emitted = false; if (card->pcm_device_id != ID_DEVICE_NOT_SUPPORTED) @@ -787,8 +882,11 @@ static void process_udev_device(struct impl *this, enum action action, struct ud return; card = find_card(this, card_nr); - if (action == ACTION_CHANGE && !card) + if (action == ACTION_CHANGE && !card) { card = add_card(this, card_nr, udev_device); + if (card) + find_wireless_status(this, card); + } if (!card) return; @@ -915,6 +1013,41 @@ static void impl_on_fd_events(struct spa_source *source) udev_device_unref(udev_device); } +static void impl_on_usb_events(struct spa_source *source) +{ + struct impl *this = source->data; + struct udev_device *udev_device; + const char *action, *syspath; + unsigned int i; + + udev_device = udev_monitor_receive_device(this->usb_umonitor); + if (udev_device == NULL) + return; + + if ((action = udev_device_get_action(udev_device)) == NULL) + action = "change"; + + if (!spa_streq(action, "change")) + goto done; + + syspath = udev_device_get_syspath(udev_device); + if (syspath == NULL) + goto done; + + for (i = 0; i < this->n_cards; i++) { + struct card *card = &this->cards[i]; + if (card->wireless_status_syspath[0] == '\0') + continue; + if (!spa_streq(card->wireless_status_syspath, syspath)) + continue; + spa_log_debug(this->log, "wireless_status change for card %u", card->card_nr); + process_card(this, ACTION_CHANGE, card); + } + +done: + udev_device_unref(udev_device); +} + static int start_monitor(struct impl *this) { int res; @@ -938,6 +1071,20 @@ static int start_monitor(struct impl *this) spa_log_debug(this->log, "monitor %p", this->umonitor); spa_loop_add_source(this->main_loop, &this->source); + this->usb_umonitor = udev_monitor_new_from_netlink(this->udev, "udev"); + if (this->usb_umonitor != NULL) { + udev_monitor_filter_add_match_subsystem_devtype(this->usb_umonitor, + "usb", "usb_interface"); + udev_monitor_enable_receiving(this->usb_umonitor); + + this->usb_source.func = impl_on_usb_events; + this->usb_source.data = this; + this->usb_source.fd = udev_monitor_get_fd(this->usb_umonitor); + this->usb_source.mask = SPA_IO_IN | SPA_IO_ERR; + + spa_loop_add_source(this->main_loop, &this->usb_source); + } + if ((res = start_inotify(this)) < 0) return res; @@ -955,6 +1102,12 @@ static int stop_monitor(struct impl *this) udev_monitor_unref(this->umonitor); this->umonitor = NULL; + if (this->usb_umonitor != NULL) { + spa_loop_remove_source(this->main_loop, &this->usb_source); + udev_monitor_unref(this->usb_umonitor); + this->usb_umonitor = NULL; + } + stop_inotify(this); return 0; diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index e9355ca41..482200854 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -268,6 +269,7 @@ struct impl { struct spa_list active_graphs; struct filter_graph graphs[MAX_GRAPH]; struct spa_process_latency_info latency; + char *graph_descs[MAX_GRAPH]; int in_filter_props; int filter_props_count; @@ -722,6 +724,34 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, 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.center-level"), + SPA_PROP_INFO_description, SPA_POD_String("Center up/downmix level"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.center_level, 0.0, 10.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 20: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.surround-level"), + SPA_PROP_INFO_description, SPA_POD_String("Surround up/downmix level"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.surround_level, 0.0, 10.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + case 21: + *param = spa_pod_builder_add_object(b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("channelmix.lfe-level"), + SPA_PROP_INFO_description, SPA_POD_String("LFE up/downmix level"), + SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float( + this->mix.lfe_level, 0.0, 10.0), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; + + case 22: *param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_name, SPA_POD_String("channelmix.hilbert-taps"), @@ -730,7 +760,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, this->mix.hilbert_taps, 0, MAX_TAPS), SPA_PROP_INFO_params, SPA_POD_Bool(true)); break; - case 20: + case 23: 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"), @@ -749,14 +779,14 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, spa_pod_builder_pop(b, &f[1]); *param = spa_pod_builder_pop(b, &f[0]); break; - case 21: + case 24: *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: + case 25: *param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_quality), @@ -765,7 +795,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, 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: + case 26: *param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_name, SPA_POD_String("resample.disable"), @@ -773,7 +803,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->resample_disabled), SPA_PROP_INFO_params, SPA_POD_Bool(true)); break; - case 24: + case 27: *param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_name, SPA_POD_String("dither.noise"), @@ -781,7 +811,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, 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: + case 28: 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"), @@ -799,7 +829,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, spa_pod_builder_pop(b, &f[1]); *param = spa_pod_builder_pop(b, &f[0]); break; - case 26: + case 29: *param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_name, SPA_POD_String("debug.wav-path"), @@ -807,7 +837,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, SPA_PROP_INFO_type, SPA_POD_String(p->wav_path), SPA_PROP_INFO_params, SPA_POD_Bool(true)); break; - case 27: + case 30: *param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_name, SPA_POD_String("channelmix.lock-volumes"), @@ -815,7 +845,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->lock_volumes), SPA_PROP_INFO_params, SPA_POD_Bool(true)); break; - case 28: + case 31: *param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph.disable"), @@ -823,7 +853,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, SPA_PROP_INFO_type, SPA_POD_CHOICE_Bool(p->filter_graph_disabled), SPA_PROP_INFO_params, SPA_POD_Bool(true)); break; - case 29: + case 32: *param = spa_pod_builder_add_object(b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_name, SPA_POD_String("audioconvert.filter-graph.N"), @@ -834,7 +864,7 @@ static int node_param_prop_info(struct impl *this, uint32_t id, uint32_t index, 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); + index - 33, b, param); } return 0; } @@ -846,6 +876,7 @@ static int node_param_props(struct impl *this, uint32_t id, uint32_t index, { struct props *p = &this->props; struct spa_pod_frame f[2]; + struct filter_graph *g; switch (index) { case 0: @@ -900,6 +931,12 @@ static int node_param_props(struct impl *this, uint32_t id, uint32_t index, 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.center-level"); + spa_pod_builder_float(b, this->mix.center_level); + spa_pod_builder_string(b, "channelmix.surround-level"); + spa_pod_builder_float(b, this->mix.surround_level); + spa_pod_builder_string(b, "channelmix.lfe-level"); + spa_pod_builder_float(b, this->mix.lfe_level); 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"); @@ -918,8 +955,12 @@ static int node_param_props(struct impl *this, uint32_t id, uint32_t index, 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_list_for_each(g, &this->active_graphs, link) { + char key[64]; + snprintf(key, sizeof(key), "audioconvert.filter-graph.%d", g->order); + spa_pod_builder_string(b, key); + spa_pod_builder_string(b, this->graph_descs[g->order]); + } spa_pod_builder_pop(b, &f[1]); *param = spa_pod_builder_pop(b, &f[0]); break; @@ -953,7 +994,7 @@ static int impl_node_enum_params(void *object, int seq, struct impl *this = object; struct spa_pod *param; struct spa_pod_builder b = { 0 }; - uint8_t buffer[4096]; + uint8_t buffer[16384]; struct spa_result_node_params result; uint32_t count = 0; int res = 0; @@ -1409,6 +1450,7 @@ static int load_filter_graph(struct impl *impl, const char *graph, int order) g->removing = true; spa_log_info(impl->log, "removing filter-graph order:%d", order); } + free(impl->graph_descs[order]); } if (graph != NULL && graph[0] != '\0') { @@ -1434,6 +1476,8 @@ static int load_filter_graph(struct impl *impl, const char *graph, int order) spa_list_remove(&pending->link); insert_graph(&impl->active_graphs, pending); + impl->graph_descs[order] = spa_json_builder_reformat(graph, 0); + spa_log_info(impl->log, "loading filter-graph order:%d", order); } if (impl->setup) @@ -1480,6 +1524,12 @@ static int audioconvert_set_param(struct impl *this, const char *k, const char * spa_atof(s, &this->mix.rear_delay); else if (spa_streq(k, "channelmix.stereo-widen")) spa_atof(s, &this->mix.widen); + else if (spa_streq(k, "channelmix.center-level")) + spa_atof(s, &this->mix.center_level); + else if (spa_streq(k, "channelmix.surround-level")) + spa_atof(s, &this->mix.surround_level); + else if (spa_streq(k, "channelmix.lfe-level")) + spa_atof(s, &this->mix.lfe_level); else if (spa_streq(k, "channelmix.hilbert-taps")) spa_atou32(s, &this->mix.hilbert_taps, 0); else if (spa_streq(k, "channelmix.upmix-method")) @@ -2115,7 +2165,7 @@ static int setup_in_convert(struct impl *this) return res; spa_log_debug(this->log, "%p: got converter features %08x:%08x passthrough:%d remap:%d %s", this, - this->cpu_flags, in->conv.cpu_flags, in->conv.is_passthrough, + this->cpu_flags, in->conv.func_cpu_flags, in->conv.is_passthrough, remap, in->conv.func_name); return 0; @@ -2272,7 +2322,7 @@ static int setup_channelmix(struct impl *this, uint32_t channels, uint32_t *posi set_volume(this); spa_log_debug(this->log, "%p: got channelmix features %08x:%08x flags:%08x %s", - this, this->cpu_flags, this->mix.cpu_flags, + this, this->cpu_flags, this->mix.func_cpu_flags, this->mix.flags, this->mix.func_name); return 0; } @@ -2320,7 +2370,7 @@ static int setup_resample(struct impl *this) res = resample_native_init(&this->resample); spa_log_debug(this->log, "%p: got resample features %08x:%08x %s", - this, this->cpu_flags, this->resample.cpu_flags, + this, this->cpu_flags, this->resample.func_cpu_flags, this->resample.func_name); return res; } @@ -2412,7 +2462,7 @@ static int setup_out_convert(struct impl *this) spa_log_debug(this->log, "%p: got converter features %08x:%08x quant:%d:%d" " passthrough:%d remap:%d %s", this, - this->cpu_flags, out->conv.cpu_flags, out->conv.method, + this->cpu_flags, out->conv.func_cpu_flags, out->conv.method, out->conv.noise_bits, out->conv.is_passthrough, remap, out->conv.func_name); return 0; @@ -4234,6 +4284,7 @@ static void free_dir(struct dir *dir) static int impl_clear(struct spa_handle *handle) { struct impl *this; + int i; spa_return_val_if_fail(handle != NULL, -EINVAL); @@ -4245,6 +4296,10 @@ static int impl_clear(struct spa_handle *handle) free_tmp(this); clean_filter_handles(this, true); + for (i = 0; i < MAX_GRAPH; i++) { + if (this->graph_descs[i]) + free(this->graph_descs[i]); + } if (this->resample.free) resample_free(&this->resample); @@ -4299,18 +4354,13 @@ impl_init(const struct spa_handle_factory *factory, struct filter_graph *g = &this->graphs[i]; g->impl = this; spa_list_append(&this->free_graphs, &g->link); + this->graph_descs[i] = NULL; } this->rate_limit.interval = 2 * SPA_NSEC_PER_SEC; this->rate_limit.burst = 1; - this->mix.options = CHANNELMIX_OPTION_UPMIX | CHANNELMIX_OPTION_MIX_LFE; - this->mix.upmix = CHANNELMIX_UPMIX_NONE; - this->mix.log = this->log; - this->mix.lfe_cutoff = 0.0f; - this->mix.fc_cutoff = 0.0f; - this->mix.rear_delay = 0.0f; - this->mix.widen = 0.0f; + channelmix_reset(&this->mix); for (i = 0; info && i < info->n_items; i++) { const char *k = info->items[i].key; diff --git a/spa/plugins/audioconvert/benchmark-fmt-ops.c b/spa/plugins/audioconvert/benchmark-fmt-ops.c index 9ea43ec65..e59f1f56b 100644 --- a/spa/plugins/audioconvert/benchmark-fmt-ops.c +++ b/spa/plugins/audioconvert/benchmark-fmt-ops.c @@ -51,9 +51,9 @@ static void run_test1(const char *name, const char *impl, bool in_packed, bool o void *op[n_channels]; struct timespec ts; uint64_t count, t1, t2; - struct convert conv; - - conv.n_channels = n_channels; + struct convert conv = { + .n_channels = n_channels, + }; for (j = 0; j < n_channels; j++) { ip[j] = &samp_in[j * n_samples * 4]; diff --git a/spa/plugins/audioconvert/channelmix-ops-avx.c b/spa/plugins/audioconvert/channelmix-ops-avx.c new file mode 100644 index 000000000..08d8e2b00 --- /dev/null +++ b/spa/plugins/audioconvert/channelmix-ops-avx.c @@ -0,0 +1,63 @@ +/* Spa */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "channelmix-ops.h" + +#include +#include +#include + +static inline void clear_avx(float *d, uint32_t n_samples) +{ + memset(d, 0, n_samples * sizeof(float)); +} + +static inline void copy_avx(float *d, const float *s, uint32_t n_samples) +{ + spa_memcpy(d, s, n_samples * sizeof(float)); +} + +static inline void vol_avx(float *d, const float *s, float vol, uint32_t n_samples) +{ + uint32_t n, unrolled; + if (vol == 0.0f) { + clear_avx(d, n_samples); + } else if (vol == 1.0f) { + copy_avx(d, s, n_samples); + } else { + __m256 t[4]; + const __m256 v = _mm256_set1_ps(vol); + + if (SPA_IS_ALIGNED(d, 32) && + SPA_IS_ALIGNED(s, 32)) + unrolled = n_samples & ~31; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 32) { + t[0] = _mm256_load_ps(&s[n]); + t[1] = _mm256_load_ps(&s[n+8]); + t[2] = _mm256_load_ps(&s[n+16]); + t[3] = _mm256_load_ps(&s[n+24]); + _mm256_store_ps(&d[n], _mm256_mul_ps(t[0], v)); + _mm256_store_ps(&d[n+8], _mm256_mul_ps(t[1], v)); + _mm256_store_ps(&d[n+16], _mm256_mul_ps(t[2], v)); + _mm256_store_ps(&d[n+24], _mm256_mul_ps(t[3], v)); + } + for(; n < n_samples; n++) { + __m128 v = _mm_set1_ps(vol); + _mm_store_ss(&d[n], _mm_mul_ss(_mm_load_ss(&s[n]), v)); + } + } +} + +void channelmix_copy_avx(struct channelmix *mix, void * SPA_RESTRICT dst[], + const void * SPA_RESTRICT src[], uint32_t n_samples) +{ + uint32_t i, n_dst = mix->dst_chan; + float **d = (float **)dst; + const float **s = (const float **)src; + for (i = 0; i < n_dst; i++) + vol_avx(d[i], s[i], mix->matrix[i][i], n_samples); +} diff --git a/spa/plugins/audioconvert/channelmix-ops.c b/spa/plugins/audioconvert/channelmix-ops.c index 12edb4b5a..574d0dd1b 100644 --- a/spa/plugins/audioconvert/channelmix-ops.c +++ b/spa/plugins/audioconvert/channelmix-ops.c @@ -36,6 +36,11 @@ static const struct channelmix_info { uint32_t cpu_flags; } channelmix_table[] = { +#if defined (HAVE_AVX) + MAKE(2, MASK_MONO, 2, MASK_MONO, channelmix_copy_avx, SPA_CPU_FLAG_AVX), + MAKE(2, MASK_STEREO, 2, MASK_STEREO, channelmix_copy_avx, SPA_CPU_FLAG_AVX), + MAKE(EQ, 0, EQ, 0, channelmix_copy_avx, SPA_CPU_FLAG_AVX), +#endif #if defined (HAVE_SSE) MAKE(2, MASK_MONO, 2, MASK_MONO, channelmix_copy_sse, SPA_CPU_FLAG_SSE), MAKE(2, MASK_STEREO, 2, MASK_STEREO, channelmix_copy_sse, SPA_CPU_FLAG_SSE), @@ -193,9 +198,9 @@ static int make_matrix(struct channelmix *mix) uint32_t dst_chan = mix->dst_chan; uint64_t unassigned, keep; uint32_t i, j, ic, jc, matrix_encoding = MATRIX_NORMAL; - float clev = SQRT1_2; - float slev = SQRT1_2; - float llev = 0.5f; + float clev = mix->center_level; + float slev = mix->surround_level; + float llev = mix->lfe_level; float maxsum = 0.0f; bool filter_fc = false, filter_lfe = false, matched = false, normalize; #define _MATRIX(s,d) matrix[_CH(s)][_CH(d)] @@ -720,7 +725,7 @@ done: if (src_paired == 0) src_paired = ~0LU; - for (jc = 0, ic = 0, i = 0; ic < dst_chan; i++) { + for (jc = 0, ic = 0, i = 0; ic < dst_chan && i < MAX_CHANNELS; i++) { float sum = 0.0f; char str1[1024], str2[1024]; struct spa_strbuf sb1, sb2; @@ -730,7 +735,7 @@ done: if (i < CHANNEL_BITS && (dst_paired & (1UL << i)) == 0) continue; - for (jc = 0, j = 0; jc < src_chan; j++) { + for (jc = 0, j = 0; jc < src_chan && j < MAX_CHANNELS; j++) { if (j < CHANNEL_BITS && (src_paired & (1UL << j)) == 0) continue; @@ -869,6 +874,21 @@ static void impl_channelmix_free(struct channelmix *mix) mix->process = NULL; } +void channelmix_reset(struct channelmix *mix) +{ + spa_zero(*mix); + mix->options = CHANNELMIX_DEFAULT_OPTIONS; + mix->upmix = CHANNELMIX_DEFAULT_UPMIX; + mix->lfe_cutoff = CHANNELMIX_DEFAULT_LFE_CUTOFF; + mix->fc_cutoff = CHANNELMIX_DEFAULT_FC_CUTOFF; + mix->rear_delay = CHANNELMIX_DEFAULT_REAR_DELAY; + mix->center_level = CHANNELMIX_DEFAULT_CENTER_LEVEL; + mix->surround_level = CHANNELMIX_DEFAULT_SURROUND_LEVEL; + mix->lfe_level = CHANNELMIX_DEFAULT_LFE_LEVEL; + mix->widen = CHANNELMIX_DEFAULT_WIDEN; + mix->hilbert_taps = CHANNELMIX_DEFAULT_HILBERT_TAPS; +} + int channelmix_init(struct channelmix *mix) { const struct channelmix_info *info; @@ -885,8 +905,8 @@ int channelmix_init(struct channelmix *mix) mix->free = impl_channelmix_free; mix->process = info->process; mix->set_volume = impl_channelmix_set_volume; - mix->cpu_flags = info->cpu_flags; mix->delay = (uint32_t)(mix->rear_delay * mix->freq / 1000.0f); + mix->func_cpu_flags = info->cpu_flags; mix->func_name = info->name; spa_zero(mix->taps_mem); diff --git a/spa/plugins/audioconvert/channelmix-ops.h b/spa/plugins/audioconvert/channelmix-ops.h index 6ea2b9451..c66eaddd3 100644 --- a/spa/plugins/audioconvert/channelmix-ops.h +++ b/spa/plugins/audioconvert/channelmix-ops.h @@ -28,6 +28,17 @@ #define CHANNELMIX_OPS_MAX_ALIGN 16 +#define CHANNELMIX_DEFAULT_OPTIONS (CHANNELMIX_OPTION_UPMIX | CHANNELMIX_OPTION_MIX_LFE) +#define CHANNELMIX_DEFAULT_UPMIX CHANNELMIX_UPMIX_NONE +#define CHANNELMIX_DEFAULT_LFE_CUTOFF 0.0f +#define CHANNELMIX_DEFAULT_FC_CUTOFF 0.0f +#define CHANNELMIX_DEFAULT_REAR_DELAY 0.0f +#define CHANNELMIX_DEFAULT_CENTER_LEVEL 0.707106781f +#define CHANNELMIX_DEFAULT_SURROUND_LEVEL 0.707106781f +#define CHANNELMIX_DEFAULT_LFE_LEVEL 0.5f +#define CHANNELMIX_DEFAULT_WIDEN 0.0f +#define CHANNELMIX_DEFAULT_HILBERT_TAPS 0 + struct channelmix { uint32_t src_chan; uint32_t dst_chan; @@ -44,6 +55,7 @@ struct channelmix { uint32_t upmix; struct spa_log *log; + uint32_t func_cpu_flags; const char *func_name; #define CHANNELMIX_FLAG_ZERO (1<<0) /**< all zero components */ @@ -59,6 +71,9 @@ struct channelmix { float fc_cutoff; /* in Hz, 0 is disabled */ float rear_delay; /* in ms, 0 is disabled */ float widen; /* stereo widen. 0 is disabled */ + float center_level; /* center down/upmix level, sqrt(1/2) */ + float lfe_level; /* lfe down/upmix level, 1/2 */ + float surround_level; /* surround down/upmix level, sqrt(1/2) */ uint32_t hilbert_taps; /* to phase shift, 0 disabled */ struct lr4 lr4[MAX_CHANNELS]; @@ -79,6 +94,7 @@ struct channelmix { void *data; }; +void channelmix_reset(struct channelmix *mix); int channelmix_init(struct channelmix *mix); static const struct channelmix_upmix_info { @@ -139,4 +155,8 @@ DEFINE_FUNCTION(f32_5p1_4, sse); DEFINE_FUNCTION(f32_7p1_4, sse); #endif +#if defined (HAVE_AVX) +DEFINE_FUNCTION(copy, avx); +#endif + #undef DEFINE_FUNCTION diff --git a/spa/plugins/audioconvert/fmt-ops-avx2.c b/spa/plugins/audioconvert/fmt-ops-avx2.c index a939da458..9c3dce52d 100644 --- a/spa/plugins/audioconvert/fmt-ops-avx2.c +++ b/spa/plugins/audioconvert/fmt-ops-avx2.c @@ -4,6 +4,8 @@ #include "fmt-ops.h" +#include + #include // GCC: workaround for missing AVX intrinsic: "_mm256_setr_m128()" // (see https://stackoverflow.com/questions/32630458/setting-m256i-to-the-value-of-two-m128i-values) @@ -30,6 +32,38 @@ _mm256_srli_epi16(x, 8)); \ }) +#define _MM_TRANS_1x4_PS(v0,v1,v2,v3) \ +({ \ + v1 = _mm_shuffle_ps(v0, v0, _MM_SHUFFLE(0, 3, 2, 1)); \ + v2 = _mm_shuffle_ps(v0, v0, _MM_SHUFFLE(1, 0, 3, 2)); \ + v3 = _mm_shuffle_ps(v0, v0, _MM_SHUFFLE(2, 1, 0, 3)); \ +}) +#define _MM_TRANS_1x4_EPI32(v0,v1,v2,v3) \ +({ \ + v1 = _mm_shuffle_epi32(v0, _MM_SHUFFLE(0, 3, 2, 1)); \ + v2 = _mm_shuffle_epi32(v0, _MM_SHUFFLE(1, 0, 3, 2)); \ + v3 = _mm_shuffle_epi32(v0, _MM_SHUFFLE(2, 1, 0, 3)); \ +}) + +#define _MM_STOREM_PS(d0,d1,d2,d3,v) \ +({ \ + __m128 o[3]; \ + _MM_TRANS_1x4_PS(v, o[0], o[1], o[2]); \ + _mm_store_ss(d0, v); \ + _mm_store_ss(d1, o[0]); \ + _mm_store_ss(d2, o[1]); \ + _mm_store_ss(d3, o[2]); \ +}) +#define _MM_STOREM_EPI32(d0,d1,d2,d3,v) \ +({ \ + __m128i o[3]; \ + _MM_TRANS_1x4_EPI32(v, o[0], o[1], o[2]); \ + *d0 = _mm_cvtsi128_si32(v); \ + *d1 = _mm_cvtsi128_si32(o[0]); \ + *d2 = _mm_cvtsi128_si32(o[1]); \ + *d3 = _mm_cvtsi128_si32(o[2]); \ +}) + static void conv_s16_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) @@ -253,7 +287,7 @@ conv_s16s_to_f32d_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const } static void -conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, +conv_s24_to_f32d_1s_gather_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) { const int8_t *s = src; @@ -289,7 +323,7 @@ conv_s24_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA } static void -conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, +conv_s24_to_f32d_2s_gather_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) { const int8_t *s = src; @@ -341,7 +375,7 @@ conv_s24_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA } } static void -conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, +conv_s24_to_f32d_4s_gather_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) { const int8_t *s = src; @@ -397,18 +431,13 @@ conv_s24_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA s += 12 * n_channels; } for(; n < n_samples; n++) { - out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+0))); - out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+1))); - out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+2))); - out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*((int24_t*)s+3))); - out[0] = _mm_mul_ss(out[0], factor); - out[1] = _mm_mul_ss(out[1], factor); - out[2] = _mm_mul_ss(out[2], factor); - out[3] = _mm_mul_ss(out[3], factor); - _mm_store_ss(&d0[n], out[0]); - _mm_store_ss(&d1[n], out[1]); - _mm_store_ss(&d2[n], out[2]); - _mm_store_ss(&d3[n], out[3]); + in[0] = _mm_setr_epi32(s24_to_s32(*((int24_t*)s+0)), + s24_to_s32(*((int24_t*)s+1)), + s24_to_s32(*((int24_t*)s+2)), + s24_to_s32(*((int24_t*)s+3))); + out[0] = _mm_cvtepi32_ps(in[0]); + out[0] = _mm_mul_ps(out[0], factor); + _MM_STOREM_PS(&d0[n], &d1[n], &d2[n], &d3[n], out[0]); s += 3 * n_channels; } } @@ -420,16 +449,22 @@ conv_s24_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const voi const int8_t *s = src[0]; uint32_t i = 0, n_channels = conv->n_channels; - for(; i + 3 < n_channels; i += 4) - conv_s24_to_f32d_4s_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples); - for(; i + 1 < n_channels; i += 2) - conv_s24_to_f32d_2s_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples); - for(; i < n_channels; i++) - conv_s24_to_f32d_1s_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples); + if (conv->cpu_flags & SPA_CPU_FLAG_SLOW_GATHER) { +#if defined (HAVE_SSE2) + conv_s24_to_f32d_sse2(conv, dst, src, n_samples); +#endif + } else { + for(; i + 3 < n_channels; i += 4) + conv_s24_to_f32d_4s_gather_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_s24_to_f32d_2s_gather_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_s24_to_f32d_1s_gather_avx2(conv, &dst[i], &s[3*i], n_channels, n_samples); + } } static void -conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, +conv_s32_to_f32d_4s_gather_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) { const int32_t *s = src; @@ -473,24 +508,17 @@ conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA } for(; n < n_samples; n++) { __m128 out[4], factor = _mm_set1_ps(1.0f / S32_SCALE_I2F); - out[0] = _mm_cvtsi32_ss(factor, s[0]); - out[1] = _mm_cvtsi32_ss(factor, s[1]); - out[2] = _mm_cvtsi32_ss(factor, s[2]); - out[3] = _mm_cvtsi32_ss(factor, s[3]); - out[0] = _mm_mul_ss(out[0], factor); - out[1] = _mm_mul_ss(out[1], factor); - out[2] = _mm_mul_ss(out[2], factor); - out[3] = _mm_mul_ss(out[3], factor); - _mm_store_ss(&d0[n], out[0]); - _mm_store_ss(&d1[n], out[1]); - _mm_store_ss(&d2[n], out[2]); - _mm_store_ss(&d3[n], out[3]); + __m128i in[1]; + in[0] = _mm_setr_epi32(s[0], s[1], s[2], s[3]); + out[0] = _mm_cvtepi32_ps(in[0]); + out[0] = _mm_mul_ps(out[0], factor); + _MM_STOREM_PS(&d0[n], &d1[n], &d2[n], &d3[n], out[0]); s += n_channels; } } static void -conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, +conv_s32_to_f32d_2s_gather_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) { const int32_t *s = src; @@ -535,7 +563,7 @@ conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA } static void -conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, +conv_s32_to_f32d_1s_gather_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) { const int32_t *s = src; @@ -575,6 +603,169 @@ conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA } } + +static void +conv_s32_to_f32d_2s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s = src; + float *d0 = dst[0], *d1 = dst[1]; + uint32_t n, unrolled; + __m256i in[4]; + __m256 out[4], t[4], factor = _mm256_set1_ps(1.0f / S32_SCALE_I2F); + + if (SPA_IS_ALIGNED(d0, 32) && + SPA_IS_ALIGNED(d1, 32)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_setr_epi64x( + *((uint64_t*)&s[0*n_channels]), + *((uint64_t*)&s[1*n_channels]), + *((uint64_t*)&s[4*n_channels]), + *((uint64_t*)&s[5*n_channels])); + in[1] = _mm256_setr_epi64x( + *((uint64_t*)&s[2*n_channels]), + *((uint64_t*)&s[3*n_channels]), + *((uint64_t*)&s[6*n_channels]), + *((uint64_t*)&s[7*n_channels])); + + out[0] = _mm256_cvtepi32_ps(in[0]); + out[1] = _mm256_cvtepi32_ps(in[1]); + + out[0] = _mm256_mul_ps(out[0], factor); /* a0 b0 a1 b1 a4 b4 a5 b5 */ + out[1] = _mm256_mul_ps(out[1], factor); /* a2 b2 a3 b3 a6 b6 a7 b7 */ + + t[0] = _mm256_unpacklo_ps(out[0], out[1]); /* a0 a2 b0 b2 a4 a6 b4 b6 */ + t[1] = _mm256_unpackhi_ps(out[0], out[1]); /* a1 a3 b1 b3 a5 a7 b5 b7 */ + + out[0] = _mm256_unpacklo_ps(t[0], t[1]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + out[1] = _mm256_unpackhi_ps(t[0], t[1]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + + _mm256_store_ps(&d0[n], out[0]); + _mm256_store_ps(&d1[n], out[1]); + + s += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 out[2], factor = _mm_set1_ps(1.0f / S32_SCALE_I2F); + out[0] = _mm_cvtsi32_ss(factor, s[0]); + out[1] = _mm_cvtsi32_ss(factor, s[1]); + out[0] = _mm_mul_ss(out[0], factor); + out[1] = _mm_mul_ss(out[1], factor); + _mm_store_ss(&d0[n], out[0]); + _mm_store_ss(&d1[n], out[1]); + s += n_channels; + } +} + +static void +conv_s32_to_f32d_1s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s = src; + float *d0 = dst[0]; + uint32_t n, unrolled; + __m256i in[2]; + __m256 out[2], factor = _mm256_set1_ps(1.0f / S32_SCALE_I2F); + + if (SPA_IS_ALIGNED(d0, 32)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_setr_epi32( + s[0*n_channels], s[1*n_channels], + s[2*n_channels], s[3*n_channels], + s[4*n_channels], s[5*n_channels], + s[6*n_channels], s[7*n_channels]); + out[0] = _mm256_cvtepi32_ps(in[0]); + out[0] = _mm256_mul_ps(out[0], factor); + _mm256_store_ps(&d0[n+0], out[0]); + s += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 out, factor = _mm_set1_ps(1.0f / S32_SCALE_I2F); + out = _mm_cvtsi32_ss(factor, s[0]); + out = _mm_mul_ss(out, factor); + _mm_store_ss(&d0[n], out); + s += n_channels; + } +} + +static void +conv_s32_to_f32d_4s_avx2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s = src; + float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3]; + uint32_t n, unrolled; + __m256i in[4]; + __m256 out[4], t[4], factor = _mm256_set1_ps(1.0f / S32_SCALE_I2F); + + if (SPA_IS_ALIGNED(d0, 32) && + SPA_IS_ALIGNED(d1, 32) && + SPA_IS_ALIGNED(d2, 32) && + SPA_IS_ALIGNED(d3, 32)) + unrolled = n_samples & ~7; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 8) { + in[0] = _mm256_setr_m128i( + _mm_loadu_si128((__m128i*)&s[0*n_channels]), + _mm_loadu_si128((__m128i*)&s[4*n_channels])); + in[1] = _mm256_setr_m128i( + _mm_loadu_si128((__m128i*)&s[1*n_channels]), + _mm_loadu_si128((__m128i*)&s[5*n_channels])); + in[2] = _mm256_setr_m128i( + _mm_loadu_si128((__m128i*)&s[2*n_channels]), + _mm_loadu_si128((__m128i*)&s[6*n_channels])); + in[3] = _mm256_setr_m128i( + _mm_loadu_si128((__m128i*)&s[3*n_channels]), + _mm_loadu_si128((__m128i*)&s[7*n_channels])); + + out[0] = _mm256_cvtepi32_ps(in[0]); /* a0 b0 c0 d0 a4 b4 c4 d4 */ + out[1] = _mm256_cvtepi32_ps(in[1]); /* a1 b1 c1 d1 a5 b5 c5 d5 */ + out[2] = _mm256_cvtepi32_ps(in[2]); /* a2 b2 c2 d2 a6 b6 c6 d6 */ + out[3] = _mm256_cvtepi32_ps(in[3]); /* a3 b3 c3 d3 a7 b7 c7 d7 */ + + out[0] = _mm256_mul_ps(out[0], factor); + out[1] = _mm256_mul_ps(out[1], factor); + out[2] = _mm256_mul_ps(out[2], factor); + out[3] = _mm256_mul_ps(out[3], factor); + + t[0] = _mm256_unpacklo_ps(out[0], out[2]); /* a0 a2 b0 b2 a4 a6 b4 b6 */ + t[1] = _mm256_unpackhi_ps(out[0], out[2]); /* c0 c2 d0 d2 c4 c6 d4 d6 */ + t[2] = _mm256_unpacklo_ps(out[1], out[3]); /* a1 a3 b1 b3 a5 a7 b5 b7 */ + t[3] = _mm256_unpackhi_ps(out[1], out[3]); /* c1 c3 d1 d3 c5 c7 d5 d7 */ + + out[0] = _mm256_unpacklo_ps(t[0], t[2]); /* a0 a1 a2 a3 a4 a5 a6 a7 */ + out[1] = _mm256_unpackhi_ps(t[0], t[2]); /* b0 b1 b2 b3 b4 b5 b6 b7 */ + out[2] = _mm256_unpacklo_ps(t[1], t[3]); /* c0 c1 c2 c3 c4 c5 c6 c7 */ + out[3] = _mm256_unpackhi_ps(t[1], t[3]); /* d0 d1 d2 d3 d4 d5 d6 d7 */ + + _mm256_store_ps(&d0[n], out[0]); + _mm256_store_ps(&d1[n], out[1]); + _mm256_store_ps(&d2[n], out[2]); + _mm256_store_ps(&d3[n], out[3]); + + s += 8*n_channels; + } + for(; n < n_samples; n++) { + __m128 out[4], factor = _mm_set1_ps(1.0f / S32_SCALE_I2F); + __m128i in[1]; + in[0] = _mm_setr_epi32(s[0], s[1], s[2], s[3]); + out[0] = _mm_cvtepi32_ps(in[0]); + out[0] = _mm_mul_ps(out[0], factor); + _MM_STOREM_PS(&d0[n], &d1[n], &d2[n], &d3[n], out[0]); + s += n_channels; + } +} + void conv_s32_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], uint32_t n_samples) @@ -582,12 +773,21 @@ conv_s32_to_f32d_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const voi const int32_t *s = src[0]; uint32_t i = 0, n_channels = conv->n_channels; - for(; i + 3 < n_channels; i += 4) - conv_s32_to_f32d_4s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); - for(; i + 1 < n_channels; i += 2) - conv_s32_to_f32d_2s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); - for(; i < n_channels; i++) - conv_s32_to_f32d_1s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); + if (conv->cpu_flags & SPA_CPU_FLAG_SLOW_GATHER) { + for(; i + 3 < n_channels; i += 4) + conv_s32_to_f32d_4s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_s32_to_f32d_2s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_s32_to_f32d_1s_avx2(conv, &dst[i], &s[i], n_channels, n_samples); + } else { + for(; i + 3 < n_channels; i += 4) + conv_s32_to_f32d_4s_gather_avx2(conv, &dst[i], &s[i], n_channels, n_samples); + for(; i + 1 < n_channels; i += 2) + conv_s32_to_f32d_2s_gather_avx2(conv, &dst[i], &s[i], n_channels, n_samples); + for(; i < n_channels; i++) + conv_s32_to_f32d_1s_gather_avx2(conv, &dst[i], &s[i], n_channels, n_samples); + } } static void @@ -612,14 +812,10 @@ conv_f32d_to_s32_1s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale); in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); out[0] = _mm_cvtps_epi32(in[0]); - out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); - out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); - out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); - - d[0*n_channels] = _mm_cvtsi128_si32(out[0]); - d[1*n_channels] = _mm_cvtsi128_si32(out[1]); - d[2*n_channels] = _mm_cvtsi128_si32(out[2]); - d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + _MM_STOREM_EPI32(&d[0*n_channels], + &d[1*n_channels], + &d[2*n_channels], + &d[3*n_channels], out[0]); d += 4*n_channels; } for(; n < n_samples; n++) { @@ -774,15 +970,7 @@ conv_f32d_to_s32_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R __m128 int_min = _mm_set1_ps(S32_MIN_F2I); __m128 int_max = _mm_set1_ps(S32_MAX_F2I); - in[0] = _mm_load_ss(&s0[n]); - in[1] = _mm_load_ss(&s1[n]); - in[2] = _mm_load_ss(&s2[n]); - in[3] = _mm_load_ss(&s3[n]); - - in[0] = _mm_unpacklo_ps(in[0], in[2]); - in[1] = _mm_unpacklo_ps(in[1], in[3]); - in[0] = _mm_unpacklo_ps(in[0], in[1]); - + in[0] = _mm_setr_ps(s0[n], s1[n], s2[n], s3[n]); in[0] = _mm_mul_ps(in[0], scale); in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); out[0] = _mm_cvtps_epi32(in[0]); @@ -972,18 +1160,16 @@ conv_f32d_to_s16_4s_avx2(void *data, void * SPA_RESTRICT dst, const void * SPA_R __m128 int_max = _mm_set1_ps(S16_MAX); __m128 int_min = _mm_set1_ps(S16_MIN); - in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); - in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); - in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale); - in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale); - in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); - in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); - in[2] = _MM_CLAMP_SS(in[2], int_min, int_max); - in[3] = _MM_CLAMP_SS(in[3], int_min, int_max); + in[0] = _mm_setr_ps(s0[n], s1[n], s2[n], s3[n]); + in[0] = _mm_mul_ps(in[0], int_scale); + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + + _MM_TRANS_1x4_PS(in[0], in[1], in[2], in[3]); d[0] = _mm_cvtss_si32(in[0]); d[1] = _mm_cvtss_si32(in[1]); d[2] = _mm_cvtss_si32(in[2]); d[3] = _mm_cvtss_si32(in[3]); + d += n_channels; } } @@ -1055,14 +1241,10 @@ conv_f32d_to_s16_4_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const v __m128 int_max = _mm_set1_ps(S16_MAX); __m128 int_min = _mm_set1_ps(S16_MIN); - in[0] = _mm_mul_ss(_mm_load_ss(&s0[n]), int_scale); - in[1] = _mm_mul_ss(_mm_load_ss(&s1[n]), int_scale); - in[2] = _mm_mul_ss(_mm_load_ss(&s2[n]), int_scale); - in[3] = _mm_mul_ss(_mm_load_ss(&s3[n]), int_scale); - in[0] = _MM_CLAMP_SS(in[0], int_min, int_max); - in[1] = _MM_CLAMP_SS(in[1], int_min, int_max); - in[2] = _MM_CLAMP_SS(in[2], int_min, int_max); - in[3] = _MM_CLAMP_SS(in[3], int_min, int_max); + in[0] = _mm_setr_ps(s0[n], s1[n], s2[n], s3[n]); + in[0] = _mm_mul_ps(in[0], int_scale); + in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); + _MM_TRANS_1x4_PS(in[0], in[1], in[2], in[3]); d[0] = _mm_cvtss_si32(in[0]); d[1] = _mm_cvtss_si32(in[1]); d[2] = _mm_cvtss_si32(in[2]); @@ -1185,3 +1367,4 @@ conv_f32d_to_s16s_2_avx2(struct convert *conv, void * SPA_RESTRICT dst[], const d += 2; } } + diff --git a/spa/plugins/audioconvert/fmt-ops-sse2.c b/spa/plugins/audioconvert/fmt-ops-sse2.c index ee5c89c06..dcba6d2e1 100644 --- a/spa/plugins/audioconvert/fmt-ops-sse2.c +++ b/spa/plugins/audioconvert/fmt-ops-sse2.c @@ -26,6 +26,72 @@ a = _mm_shufflehi_epi16(a, _MM_SHUFFLE(2, 3, 0, 1)); \ }) +#define spa_read_unaligned(ptr, type) \ +__extension__ ({ \ + __typeof__(type) _val; \ + memcpy(&_val, (ptr), sizeof(_val)); \ + _val; \ +}) + +#define spa_write_unaligned(ptr, type, val) \ +__extension__ ({ \ + __typeof__(type) _val = (val); \ + memcpy((ptr), &_val, sizeof(_val)); \ +}) + +#define _MM_TRANS_1x4_PS(v0,v1,v2,v3) \ +({ \ + v1 = _mm_shuffle_ps(v0, v0, _MM_SHUFFLE(0, 3, 2, 1)); \ + v2 = _mm_shuffle_ps(v0, v0, _MM_SHUFFLE(1, 0, 3, 2)); \ + v3 = _mm_shuffle_ps(v0, v0, _MM_SHUFFLE(2, 1, 0, 3)); \ +}) + +#define _MM_TRANS_1x4_EPI32(v0,v1,v2,v3) \ +({ \ + v1 = _mm_shuffle_epi32(v0, _MM_SHUFFLE(0, 3, 2, 1)); \ + v2 = _mm_shuffle_epi32(v0, _MM_SHUFFLE(1, 0, 3, 2)); \ + v3 = _mm_shuffle_epi32(v0, _MM_SHUFFLE(2, 1, 0, 3)); \ +}) +#if 0 +#define _MM_STOREM_PS(d0,d1,d2,d3,v) \ +({ \ + *d0 = v[0]; \ + *d1 = v[1]; \ + *d2 = v[2]; \ + *d3 = v[3]; \ +}) +#else +#define _MM_STOREM_PS(d0,d1,d2,d3,v) \ +({ \ + __m128 o[3]; \ + _MM_TRANS_1x4_PS(v, o[0], o[1], o[2]); \ + _mm_store_ss(d0, v); \ + _mm_store_ss(d1, o[0]); \ + _mm_store_ss(d2, o[1]); \ + _mm_store_ss(d3, o[2]); \ +}) +#endif + +#define _MM_STOREM_EPI32(d0,d1,d2,d3,v) \ +({ \ + __m128i o[3]; \ + _MM_TRANS_1x4_EPI32(v, o[0], o[1], o[2]); \ + *d0 = _mm_cvtsi128_si32(v); \ + *d1 = _mm_cvtsi128_si32(o[0]); \ + *d2 = _mm_cvtsi128_si32(o[1]); \ + *d3 = _mm_cvtsi128_si32(o[2]); \ +}) + +#define _MM_STOREUM_EPI32(d0,d1,d2,d3,v) \ +({ \ + __m128i o[3]; \ + _MM_TRANS_1x4_EPI32(v, o[0], o[1], o[2]); \ + spa_write_unaligned(d0, uint32_t, _mm_cvtsi128_si32(v)); \ + spa_write_unaligned(d1, uint32_t, _mm_cvtsi128_si32(o[0])); \ + spa_write_unaligned(d2, uint32_t, _mm_cvtsi128_si32(o[1])); \ + spa_write_unaligned(d3, uint32_t, _mm_cvtsi128_si32(o[2])); \ +}) + static void conv_s16_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) @@ -233,18 +299,6 @@ conv_s16s_to_f32d_2_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const } } -#define spa_read_unaligned(ptr, type) \ -__extension__ ({ \ - __typeof__(type) _val; \ - memcpy(&_val, (ptr), sizeof(_val)); \ - _val; \ -}) - -#define spa_write_unaligned(ptr, type, val) \ -__extension__ ({ \ - __typeof__(type) _val = (val); \ - memcpy((ptr), &_val, sizeof(_val)); \ -}) void conv_s24_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) @@ -416,18 +470,13 @@ conv_s24_to_f32d_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA s += 4 * n_channels; } for(; n < n_samples; n++) { - out[0] = _mm_cvtsi32_ss(factor, s24_to_s32(*s)); - out[1] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+1))); - out[2] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+2))); - out[3] = _mm_cvtsi32_ss(factor, s24_to_s32(*(s+3))); - out[0] = _mm_mul_ss(out[0], factor); - out[1] = _mm_mul_ss(out[1], factor); - out[2] = _mm_mul_ss(out[2], factor); - out[3] = _mm_mul_ss(out[3], factor); - _mm_store_ss(&d0[n], out[0]); - _mm_store_ss(&d1[n], out[1]); - _mm_store_ss(&d2[n], out[2]); - _mm_store_ss(&d3[n], out[3]); + in[0] = _mm_setr_epi32(s24_to_s32(*s), + s24_to_s32(*(s+1)), + s24_to_s32(*(s+2)), + s24_to_s32(*(s+3))); + out[0] = _mm_cvtepi32_ps(in[0]); + out[0] = _mm_mul_ps(out[0], factor); + _MM_STOREM_PS(&d0[n], &d1[n], &d2[n], &d3[n], out[0]); s += n_channels; } } @@ -447,6 +496,59 @@ conv_s24_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const voi conv_s24_to_f32d_1s_sse2(conv, &dst[i], &s[3*i], n_channels, n_samples); } +static void +conv_s32_to_f32d_4s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, + uint32_t n_channels, uint32_t n_samples) +{ + const int32_t *s = src; + float *d0 = dst[0], *d1 = dst[1], *d2 = dst[2], *d3 = dst[3]; + uint32_t n, unrolled; + __m128i in[4]; + __m128 out[4], factor = _mm_set1_ps(1.0f / S32_SCALE_I2F); + + if (SPA_IS_ALIGNED(d0, 16) && + SPA_IS_ALIGNED(d1, 16) && + SPA_IS_ALIGNED(d2, 16) && + SPA_IS_ALIGNED(d3, 16) && + SPA_IS_ALIGNED(s, 16) && (n_channels & 3) == 0) + unrolled = n_samples & ~3; + else + unrolled = 0; + + for(n = 0; n < unrolled; n += 4) { + in[0] = _mm_load_si128((__m128i*)(s + 0*n_channels)); + in[1] = _mm_load_si128((__m128i*)(s + 1*n_channels)); + in[2] = _mm_load_si128((__m128i*)(s + 2*n_channels)); + in[3] = _mm_load_si128((__m128i*)(s + 3*n_channels)); + + out[0] = _mm_cvtepi32_ps(in[0]); + out[1] = _mm_cvtepi32_ps(in[1]); + out[2] = _mm_cvtepi32_ps(in[2]); + out[3] = _mm_cvtepi32_ps(in[3]); + + out[0] = _mm_mul_ps(out[0], factor); + out[1] = _mm_mul_ps(out[1], factor); + out[2] = _mm_mul_ps(out[2], factor); + out[3] = _mm_mul_ps(out[3], factor); + + _MM_TRANSPOSE4_PS(out[0], out[1], out[2], out[3]); + + _mm_store_ps(&d0[n], out[0]); + _mm_store_ps(&d1[n], out[1]); + _mm_store_ps(&d2[n], out[2]); + _mm_store_ps(&d3[n], out[3]); + + s += 4*n_channels; + } + for(; n < n_samples; n++) { + in[0] = _mm_setr_epi32(s[0], s[1], s[2], s[3]); + out[0] = _mm_cvtepi32_ps(in[0]); + out[0] = _mm_mul_ps(out[0], factor); + _MM_STOREM_PS(&d0[n], &d1[n], &d2[n], &d3[n], out[0]); + s += n_channels; + } +} + static void conv_s32_to_f32d_1s_sse2(void *data, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src, uint32_t n_channels, uint32_t n_samples) @@ -487,6 +589,8 @@ conv_s32_to_f32d_sse2(struct convert *conv, void * SPA_RESTRICT dst[], const voi const int32_t *s = src[0]; uint32_t i = 0, n_channels = conv->n_channels; + for(; i + 3 < n_channels; i += 4) + conv_s32_to_f32d_4s_sse2(conv, &dst[i], &s[i], n_channels, n_samples); for(; i < n_channels; i++) conv_s32_to_f32d_1s_sse2(conv, &dst[i], &s[i], n_channels, n_samples); } @@ -513,14 +617,10 @@ conv_f32d_to_s32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R in[0] = _mm_mul_ps(_mm_load_ps(&s0[n]), scale); in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); out[0] = _mm_cvtps_epi32(in[0]); - out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); - out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); - out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); - - d[0*n_channels] = _mm_cvtsi128_si32(out[0]); - d[1*n_channels] = _mm_cvtsi128_si32(out[1]); - d[2*n_channels] = _mm_cvtsi128_si32(out[2]); - d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + _MM_STOREM_EPI32(&d[0*n_channels], + &d[1*n_channels], + &d[2*n_channels], + &d[3*n_channels], out[0]); d += 4*n_channels; } for(; n < n_samples; n++) { @@ -630,15 +730,7 @@ conv_f32d_to_s32_4s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R d += 4*n_channels; } for(; n < n_samples; n++) { - in[0] = _mm_load_ss(&s0[n]); - in[1] = _mm_load_ss(&s1[n]); - in[2] = _mm_load_ss(&s2[n]); - in[3] = _mm_load_ss(&s3[n]); - - in[0] = _mm_unpacklo_ps(in[0], in[2]); - in[1] = _mm_unpacklo_ps(in[1], in[3]); - in[0] = _mm_unpacklo_ps(in[0], in[1]); - + in[0] = _mm_setr_ps(s0[n], s1[n], s2[n], s3[n]); in[0] = _mm_mul_ps(in[0], scale); in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); out[0] = _mm_cvtps_epi32(in[0]); @@ -754,14 +846,10 @@ conv_f32d_to_s32_1s_noise_sse2(struct convert *conv, void * SPA_RESTRICT dst, co in[0] = _mm_add_ps(in[0], _mm_load_ps(&noise[n])); in[0] = _MM_CLAMP_PS(in[0], int_min, int_max); out[0] = _mm_cvtps_epi32(in[0]); - out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); - out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); - out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); - - d[0*n_channels] = _mm_cvtsi128_si32(out[0]); - d[1*n_channels] = _mm_cvtsi128_si32(out[1]); - d[2*n_channels] = _mm_cvtsi128_si32(out[2]); - d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + _MM_STOREM_EPI32(&d[0*n_channels], + &d[1*n_channels], + &d[2*n_channels], + &d[3*n_channels], out[0]); d += 4*n_channels; } for(; n < n_samples; n++) { @@ -810,14 +898,10 @@ conv_interleave_32_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA for(n = 0; n < unrolled; n += 4) { out[0] = _mm_load_si128((__m128i*)&s0[n]); - out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); - out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); - out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); - - d[0*n_channels] = _mm_cvtsi128_si32(out[0]); - d[1*n_channels] = _mm_cvtsi128_si32(out[1]); - d[2*n_channels] = _mm_cvtsi128_si32(out[2]); - d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + _MM_STOREM_EPI32(&d[0*n_channels], + &d[1*n_channels], + &d[2*n_channels], + &d[3*n_channels], out[0]); d += 4*n_channels; } for(; n < n_samples; n++) { @@ -893,14 +977,10 @@ conv_interleave_32s_1s_sse2(void *data, void * SPA_RESTRICT dst, const void * SP for(n = 0; n < unrolled; n += 4) { out[0] = _mm_load_si128((__m128i*)&s0[n]); out[0] = _MM_BSWAP_EPI32(out[0]); - out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); - out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); - out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); - - d[0*n_channels] = _mm_cvtsi128_si32(out[0]); - d[1*n_channels] = _mm_cvtsi128_si32(out[1]); - d[2*n_channels] = _mm_cvtsi128_si32(out[2]); - d[3*n_channels] = _mm_cvtsi128_si32(out[3]); + _MM_STOREM_EPI32(&d[0*n_channels], + &d[1*n_channels], + &d[2*n_channels], + &d[3*n_channels], out[0]); d += 4*n_channels; } for(; n < n_samples; n++) { @@ -1257,14 +1337,10 @@ conv_f32d_to_s16_2s_sse2(void *data, void * SPA_RESTRICT dst, const void * SPA_R t[1] = _mm_packs_epi32(t[1], t[1]); out[0] = _mm_unpacklo_epi16(t[0], t[1]); - out[1] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(0, 3, 2, 1)); - out[2] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(1, 0, 3, 2)); - out[3] = _mm_shuffle_epi32(out[0], _MM_SHUFFLE(2, 1, 0, 3)); - - spa_write_unaligned(d + 0*n_channels, uint32_t, _mm_cvtsi128_si32(out[0])); - spa_write_unaligned(d + 1*n_channels, uint32_t, _mm_cvtsi128_si32(out[1])); - spa_write_unaligned(d + 2*n_channels, uint32_t, _mm_cvtsi128_si32(out[2])); - spa_write_unaligned(d + 3*n_channels, uint32_t, _mm_cvtsi128_si32(out[3])); + _MM_STOREUM_EPI32(&d[0*n_channels], + &d[1*n_channels], + &d[2*n_channels], + &d[3*n_channels], out[0]); d += 4*n_channels; } for(; n < n_samples; n++) { diff --git a/spa/plugins/audioconvert/fmt-ops.c b/spa/plugins/audioconvert/fmt-ops.c index 3fc2c5f0a..34de40445 100644 --- a/spa/plugins/audioconvert/fmt-ops.c +++ b/spa/plugins/audioconvert/fmt-ops.c @@ -631,7 +631,7 @@ int convert_init(struct convert *conv) conv->random[i] = random(); conv->is_passthrough = conv->src_fmt == conv->dst_fmt; - conv->cpu_flags = info->cpu_flags; + conv->func_cpu_flags = info->cpu_flags; conv->update_noise = ninfo->noise; conv->process = info->process; conv->clear = cinfo ? cinfo->clear : NULL; diff --git a/spa/plugins/audioconvert/fmt-ops.h b/spa/plugins/audioconvert/fmt-ops.h index f738e3858..24b4b1aaf 100644 --- a/spa/plugins/audioconvert/fmt-ops.h +++ b/spa/plugins/audioconvert/fmt-ops.h @@ -219,6 +219,7 @@ struct convert { uint32_t n_channels; uint32_t rate; uint32_t cpu_flags; + uint32_t func_cpu_flags; const char *func_name; unsigned int is_passthrough:1; diff --git a/spa/plugins/audioconvert/meson.build b/spa/plugins/audioconvert/meson.build index bd60872b6..71d5d56cd 100644 --- a/spa/plugins/audioconvert/meson.build +++ b/spa/plugins/audioconvert/meson.build @@ -44,7 +44,7 @@ endif if have_sse2 audioconvert_sse2 = static_library('audioconvert_sse2', ['fmt-ops-sse2.c' ], - c_args : [sse2_args, '-O3', '-DHAVE_SSE2'], + c_args : [sse2_args, '-O3', '-DHAVE_SSE2', simd_cargs], dependencies : [ spa_dep ], install : false ) @@ -55,7 +55,7 @@ if have_ssse3 audioconvert_ssse3 = static_library('audioconvert_ssse3', ['fmt-ops-ssse3.c', 'resample-native-ssse3.c' ], - c_args : [ssse3_args, '-O3', '-DHAVE_SSSE3'], + c_args : [ssse3_args, '-O3', '-DHAVE_SSSE3', simd_cargs], dependencies : [ spa_dep ], install : false ) @@ -65,17 +65,27 @@ endif if have_sse41 audioconvert_sse41 = static_library('audioconvert_sse41', ['fmt-ops-sse41.c'], - c_args : [sse41_args, '-O3', '-DHAVE_SSE41'], + c_args : [sse41_args, '-O3', '-DHAVE_SSE41', simd_cargs], dependencies : [ spa_dep ], install : false ) simd_cargs += ['-DHAVE_SSE41'] simd_dependencies += audioconvert_sse41 endif +if have_avx + audioconvert_avx = static_library('audioconvert_avx', + ['channelmix-ops-avx.c'], + c_args : [avx_args, '-O3', '-DHAVE_AVX', simd_cargs], + dependencies : [ spa_dep ], + install : false + ) + simd_cargs += ['-DHAVE_AVX'] + simd_dependencies += audioconvert_avx +endif if have_avx2 and have_fma audioconvert_avx2_fma = static_library('audioconvert_avx2_fma', ['resample-native-avx2.c'], - c_args : [avx2_args, fma_args, '-O3', '-DHAVE_AVX2', '-DHAVE_FMA'], + c_args : [avx2_args, fma_args, '-O3', '-DHAVE_AVX2', '-DHAVE_FMA', simd_cargs], dependencies : [ spa_dep ], install : false ) @@ -85,7 +95,7 @@ endif if have_avx2 audioconvert_avx2 = static_library('audioconvert_avx2', ['fmt-ops-avx2.c'], - c_args : [avx2_args, '-O3', '-DHAVE_AVX2'], + c_args : [avx2_args, '-O3', '-DHAVE_AVX2', simd_cargs], dependencies : [ spa_dep ], install : false ) diff --git a/spa/plugins/audioconvert/peaks-ops.c b/spa/plugins/audioconvert/peaks-ops.c index 29b93a081..f7a897f90 100644 --- a/spa/plugins/audioconvert/peaks-ops.c +++ b/spa/plugins/audioconvert/peaks-ops.c @@ -60,7 +60,7 @@ int peaks_init(struct peaks *peaks) if (info == NULL) return -ENOTSUP; - peaks->cpu_flags = info->cpu_flags; + peaks->func_cpu_flags = info->cpu_flags; peaks->func_name = info->name; peaks->free = impl_peaks_free; peaks->min_max = info->min_max; diff --git a/spa/plugins/audioconvert/peaks-ops.h b/spa/plugins/audioconvert/peaks-ops.h index 24092a4f7..40b20cfbc 100644 --- a/spa/plugins/audioconvert/peaks-ops.h +++ b/spa/plugins/audioconvert/peaks-ops.h @@ -14,6 +14,7 @@ extern struct spa_log_topic resample_log_topic; struct peaks { uint32_t cpu_flags; + uint32_t func_cpu_flags; const char *func_name; struct spa_log *log; diff --git a/spa/plugins/audioconvert/resample-native.c b/spa/plugins/audioconvert/resample-native.c index 3604c5b45..5bb33ffc1 100644 --- a/spa/plugins/audioconvert/resample-native.c +++ b/spa/plugins/audioconvert/resample-native.c @@ -576,7 +576,7 @@ int resample_native_init(struct resample *r) r, c->cutoff, r->quality, c->window, r->i_rate, r->o_rate, gcd, n_taps, n_phases, r->cpu_flags, d->info->cpu_flags); - r->cpu_flags = d->info->cpu_flags; + r->func_cpu_flags = d->info->cpu_flags; impl_native_reset(r); impl_native_update_rate(r, 1.0); diff --git a/spa/plugins/audioconvert/resample.h b/spa/plugins/audioconvert/resample.h index fec3bf963..7b6e58415 100644 --- a/spa/plugins/audioconvert/resample.h +++ b/spa/plugins/audioconvert/resample.h @@ -38,6 +38,7 @@ struct resample { #define RESAMPLE_OPTION_PREFILL (1<<0) uint32_t options; uint32_t cpu_flags; + uint32_t func_cpu_flags; const char *func_name; uint32_t channels; diff --git a/spa/plugins/audioconvert/test-audioconvert.c b/spa/plugins/audioconvert/test-audioconvert.c index de3ebb8b5..de18d524b 100644 --- a/spa/plugins/audioconvert/test-audioconvert.c +++ b/spa/plugins/audioconvert/test-audioconvert.c @@ -54,7 +54,7 @@ static int setup_context(struct context *ctx) size_t size; int res; struct spa_support support[1]; - struct spa_dict_item items[6]; + struct spa_dict_item items[9]; const struct spa_handle_factory *factory; void *iface; @@ -76,10 +76,13 @@ static int setup_context(struct context *ctx) items[3] = SPA_DICT_ITEM_INIT("channelmix.lfe-cutoff", "150"); items[4] = SPA_DICT_ITEM_INIT("channelmix.fc-cutoff", "12000"); items[5] = SPA_DICT_ITEM_INIT("channelmix.rear-delay", "12.0"); + items[6] = SPA_DICT_ITEM_INIT("channelmix.center-level", "0.707106781"); + items[7] = SPA_DICT_ITEM_INIT("channelmix.surround-level", "0.707106781"); + items[8] = SPA_DICT_ITEM_INIT("channelmix.lfe-level", "0.5"); res = spa_handle_factory_init(factory, ctx->convert_handle, - &SPA_DICT_INIT(items, 6), + &SPA_DICT_INIT(items, 9), support, 1); spa_assert_se(res >= 0); diff --git a/spa/plugins/audioconvert/test-channelmix.c b/spa/plugins/audioconvert/test-channelmix.c index b68c956bf..529db880f 100644 --- a/spa/plugins/audioconvert/test-channelmix.c +++ b/spa/plugins/audioconvert/test-channelmix.c @@ -45,7 +45,7 @@ static void test_mix(uint32_t src_chan, uint32_t src_mask, uint32_t dst_chan, ui spa_log_debug(&logger.log, "start %d->%d (%08x -> %08x)", src_chan, dst_chan, src_mask, dst_mask); - spa_zero(mix); + channelmix_reset(&mix); mix.options = options; mix.src_chan = src_chan; mix.dst_chan = dst_chan; @@ -340,7 +340,7 @@ static void test_n_m_impl(void) src[i] = src_data[i]; } - spa_zero(mix); + channelmix_reset(&mix); mix.src_chan = 16; mix.dst_chan = 12; mix.log = &logger.log; diff --git a/spa/plugins/audioconvert/test-fmt-ops.c b/spa/plugins/audioconvert/test-fmt-ops.c index 17a26a351..d5ca414ef 100644 --- a/spa/plugins/audioconvert/test-fmt-ops.c +++ b/spa/plugins/audioconvert/test-fmt-ops.c @@ -45,9 +45,9 @@ static void run_test(const char *name, void *tp[N_CHANNELS]; int i, j; const uint8_t *in8 = in, *out8 = out; - struct convert conv; - - conv.n_channels = N_CHANNELS; + struct convert conv = { + .n_channels = N_CHANNELS, + }; for (j = 0; j < N_SAMPLES; j++) { memcpy(&samp_in[j * in_size], &in8[(j % n_samples) * in_size], in_size); diff --git a/spa/plugins/audioconvert/volume-ops.c b/spa/plugins/audioconvert/volume-ops.c index bf6aa6909..b76ab4bec 100644 --- a/spa/plugins/audioconvert/volume-ops.c +++ b/spa/plugins/audioconvert/volume-ops.c @@ -56,7 +56,7 @@ int volume_init(struct volume *vol) if (info == NULL) return -ENOTSUP; - vol->cpu_flags = info->cpu_flags; + vol->func_cpu_flags = info->cpu_flags; vol->func_name = info->name; vol->free = impl_volume_free; vol->process = info->process; diff --git a/spa/plugins/audioconvert/volume-ops.h b/spa/plugins/audioconvert/volume-ops.h index a50ee9a6f..51642110f 100644 --- a/spa/plugins/audioconvert/volume-ops.h +++ b/spa/plugins/audioconvert/volume-ops.h @@ -13,6 +13,7 @@ struct volume { uint32_t cpu_flags; + uint32_t func_cpu_flags; const char *func_name; struct spa_log *log; diff --git a/spa/plugins/audiomixer/audiomixer.c b/spa/plugins/audiomixer/audiomixer.c index 99b0658a0..54549bce8 100644 --- a/spa/plugins/audiomixer/audiomixer.c +++ b/spa/plugins/audiomixer/audiomixer.c @@ -725,7 +725,7 @@ static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, port->io[0] = info->data; port->io[1] = info->data; } - if (!port->active) { + if (port->direction == SPA_DIRECTION_INPUT && !port->active) { spa_list_append(&info->impl->mix_list, &port->mix_link); port->active = true; } diff --git a/spa/plugins/audiomixer/mixer-dsp.c b/spa/plugins/audiomixer/mixer-dsp.c index 5bd4e1a1e..698426d21 100644 --- a/spa/plugins/audiomixer/mixer-dsp.c +++ b/spa/plugins/audiomixer/mixer-dsp.c @@ -718,7 +718,7 @@ static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, port->io[0] = info->data; port->io[1] = info->data; } - if (!port->active) { + if (port->direction == SPA_DIRECTION_INPUT && !port->active) { spa_list_append(&info->impl->mix_list, &port->mix_link); port->active = true; } diff --git a/spa/plugins/audiotestsrc/audiotestsrc.c b/spa/plugins/audiotestsrc/audiotestsrc.c index 5e7c521b8..3414e8b18 100644 --- a/spa/plugins/audiotestsrc/audiotestsrc.c +++ b/spa/plugins/audiotestsrc/audiotestsrc.c @@ -41,13 +41,11 @@ enum wave_type { #define DEFAULT_RATE 48000 #define DEFAULT_CHANNELS 2 -#define DEFAULT_LIVE true #define DEFAULT_WAVE WAVE_SINE #define DEFAULT_FREQ 440.0 #define DEFAULT_VOLUME 1.0 struct props { - bool live; uint32_t wave; float freq; float volume; @@ -55,7 +53,6 @@ struct props { static void reset_props(struct props *props) { - props->live = DEFAULT_LIVE; props->wave = DEFAULT_WAVE; props->freq = DEFAULT_FREQ; props->volume = DEFAULT_VOLUME; @@ -118,9 +115,7 @@ struct impl { struct spa_hook_list hooks; struct spa_callbacks callbacks; - bool async; struct spa_source timer_source; - struct itimerspec timerspec; bool started; uint64_t start_time; @@ -162,13 +157,6 @@ static int impl_node_enum_params(void *object, int seq, 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_live), - SPA_PROP_INFO_description, SPA_POD_String("Configure live mode of the source"), - SPA_PROP_INFO_type, SPA_POD_Bool(p->live)); - break; - case 1: spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); spa_pod_builder_add(&b, SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_waveType), @@ -184,14 +172,14 @@ static int impl_node_enum_params(void *object, int seq, spa_pod_builder_pop(&b, &f[1]); param = spa_pod_builder_pop(&b, &f[0]); break; - case 2: + case 1: param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_frequency), SPA_PROP_INFO_description, SPA_POD_String("Select the frequency"), SPA_PROP_INFO_type, SPA_POD_CHOICE_RANGE_Float(p->freq, 0.0, 50000000.0)); break; - case 3: + case 2: param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_PropInfo, id, SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_volume), @@ -211,7 +199,6 @@ static int impl_node_enum_params(void *object, int seq, case 0: param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Props, id, - SPA_PROP_live, SPA_POD_Bool(p->live), SPA_PROP_waveType, SPA_POD_Int(p->wave), SPA_PROP_frequency, SPA_POD_Float(p->freq), SPA_PROP_volume, SPA_POD_Float(p->volume)); @@ -265,7 +252,6 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, if (id == SPA_PARAM_Props) { struct props *p = &this->props; - struct port *port = &this->port; if (param == NULL) { reset_props(p); @@ -273,15 +259,9 @@ 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_waveType, SPA_POD_OPT_Int(&p->wave), SPA_PROP_frequency, SPA_POD_OPT_Float(&p->freq), SPA_PROP_volume, SPA_POD_OPT_Float(&p->volume)); - - if (p->live) - port->info.flags |= SPA_PORT_FLAG_LIVE; - else - port->info.flags &= ~SPA_PORT_FLAG_LIVE; } else return -ENOENT; @@ -316,23 +296,15 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) static void set_timer(struct impl *this, bool enabled) { - if (this->async || this->props.live) { - if (enabled) { - if (this->props.live) { - uint64_t next_time = this->start_time + this->elapsed_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; - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 1; - } - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - } - spa_system_timerfd_settime(this->data_system, - this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + struct itimerspec ts = {0}; + + if (enabled) { + uint64_t next_time = this->start_time + this->elapsed_time; + ts.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + ts.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, &ts, NULL); } static int read_timer(struct impl *this) @@ -340,14 +312,12 @@ static int read_timer(struct impl *this) uint64_t expirations; int res = 0; - if (this->async || this->props.live) { - if ((res = spa_system_timerfd_read(this->data_system, - this->timer_source.fd, &expirations)) < 0) { - if (res != -EAGAIN) - spa_log_error(this->log, "%p: timerfd error: %s", - this, spa_strerror(res)); - } + if ((res = spa_system_timerfd_read(this->data_system, this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, "%p: timerfd error: %s", + this, spa_strerror(res)); } + return 0; } @@ -471,10 +441,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman return 0; clock_gettime(CLOCK_MONOTONIC, &now); - if (this->props.live) - this->start_time = SPA_TIMESPEC_TO_NSEC(&now); - else - this->start_time = 0; + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); this->sample_count = 0; this->elapsed_time = 0; @@ -895,9 +862,6 @@ static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t i b->outstanding = false; spa_list_append(&port->empty, &b->link); - - if (!this->props.live && !this->following) - set_timer(this, true); } static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) @@ -971,7 +935,7 @@ static int impl_node_process(void *object) io->buffer_id = SPA_ID_INVALID; } - if (!this->props.live || this->following) + if (this->following) return make_buffer(this); else return SPA_STATUS_OK; @@ -1105,10 +1069,6 @@ impl_init(const struct spa_handle_factory *factory, CLOCK_MONOTONIC, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); this->timer_source.mask = SPA_IO_IN; this->timer_source.rmask = 0; - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - this->timerspec.it_interval.tv_sec = 0; - this->timerspec.it_interval.tv_nsec = 0; if (this->data_loop) spa_loop_add_source(this->data_loop, &this->timer_source); @@ -1117,9 +1077,7 @@ impl_init(const struct spa_handle_factory *factory, 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_NO_REF; - if (this->props.live) - port->info.flags |= SPA_PORT_FLAG_LIVE; + port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_LIVE; 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/bluez5/a2dp-codec-aac.c b/spa/plugins/bluez5/a2dp-codec-aac.c index c49f7a616..f4cbecd9b 100644 --- a/spa/plugins/bluez5/a2dp-codec-aac.c +++ b/spa/plugins/bluez5/a2dp-codec-aac.c @@ -106,8 +106,8 @@ static int codec_fill_caps(const struct media_codec *codec, uint32_t flags, static const struct media_codec_config aac_frequencies[] = { - { AAC_SAMPLING_FREQ_48000, 48000, 11 }, { AAC_SAMPLING_FREQ_44100, 44100, 10 }, + { AAC_SAMPLING_FREQ_48000, 48000, 11 }, { AAC_SAMPLING_FREQ_96000, 96000, 9 }, { AAC_SAMPLING_FREQ_88200, 88200, 8 }, { AAC_SAMPLING_FREQ_64000, 64000, 7 }, @@ -194,75 +194,6 @@ static int codec_select_config(const struct media_codec *codec, uint32_t flags, return sizeof(conf); } -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) -{ - a2dp_aac_t conf; - struct spa_pod_frame f[2]; - struct spa_pod_choice *choice; - uint32_t position[2]; - uint32_t i = 0; - - if (caps_size < sizeof(conf)) - return -EINVAL; - - memcpy(&conf, caps, sizeof(conf)); - - 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), - 0); - spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); - - spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); - choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); - i = 0; - SPA_FOR_EACH_ELEMENT_VAR(aac_frequencies, f) { - if (AAC_GET_FREQUENCY(conf) & f->config) { - if (i++ == 0) - spa_pod_builder_int(b, f->value); - spa_pod_builder_int(b, f->value); - } - } - if (i > 1) - choice->body.type = SPA_CHOICE_Enum; - spa_pod_builder_pop(b, &f[1]); - - if (i == 0) - return -EINVAL; - - if (SPA_FLAG_IS_SET(conf.channels, AAC_CHANNELS_1 | AAC_CHANNELS_2)) { - spa_pod_builder_add(b, - SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2), - 0); - } else if (conf.channels & AAC_CHANNELS_1) { - position[0] = SPA_AUDIO_CHANNEL_MONO; - spa_pod_builder_add(b, - SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1), - SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), - SPA_TYPE_Id, 1, position), - 0); - } else if (conf.channels & AAC_CHANNELS_2) { - position[0] = SPA_AUDIO_CHANNEL_FL; - position[1] = SPA_AUDIO_CHANNEL_FR; - spa_pod_builder_add(b, - SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2), - SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), - SPA_TYPE_Id, 2, position), - 0); - } else - return -EINVAL; - - *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) @@ -283,8 +214,10 @@ static int codec_validate_config(const struct media_codec *codec, uint32_t flags /* * A2DP v1.3.2, 4.5.2: only one bit shall be set in bitfields. * However, there is a report (#1342) of device setting multiple - * bits for AAC object type. It's not clear if this was due to - * a BlueZ bug, but we can be lax here and below in codec_init. + * bits for AAC object type. In addition AirPods set multiple bits. + * + * Some devices also set multiple bits in frequencies & channels. + * For these, pick a "preferred" choice. */ if (!(conf.object_type & (AAC_OBJECT_TYPE_MPEG2_AAC_LC | AAC_OBJECT_TYPE_MPEG4_AAC_LC | @@ -315,6 +248,35 @@ static int codec_validate_config(const struct media_codec *codec, uint32_t flags return 0; } +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_audio_info info; + struct spa_pod_frame f[1]; + int res; + + if ((res = codec_validate_config(codec, flags, caps, caps_size, &info)) < 0) + return res; + + 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(info.media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(info.media_subtype), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(info.info.raw.format), + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info.info.raw.rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info.info.raw.channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, info.info.raw.channels, info.info.raw.position), + 0); + + *param = spa_pod_builder_pop(b, &f[0]); + return *param == NULL ? -EIO : 1; +} + static void *codec_init_props(const struct media_codec *codec, uint32_t flags, const struct spa_dict *settings) { struct props *p = calloc(1, sizeof(struct props)); @@ -324,7 +286,7 @@ static void *codec_init_props(const struct media_codec *codec, uint32_t flags, c return NULL; if (settings == NULL || (str = spa_dict_lookup(settings, "bluez5.a2dp.aac.bitratemode")) == NULL) - str = "0"; + str = "5"; p->bitratemode = SPA_CLAMP(atoi(str), 0, 5); return p; @@ -369,14 +331,14 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, goto error; /* If object type has multiple bits set (invalid per spec, see above), - * assume the device usually means AAC-LC. + * assume the device usually means MPEG2 AAC LC which is mandatory. */ - if (conf->object_type & AAC_OBJECT_TYPE_MPEG4_AAC_LC) { - res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_AAC_LC); + 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_MPEG2_AAC_LC) { - res = aacEncoder_SetParam(this->aacenc, AACENC_AOT, AOT_MP2_AAC_LC); + } else 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_MPEG4_AAC_ELD) { @@ -418,8 +380,12 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, // Fragmentation is not implemented yet, // so make sure every encoded AAC frame fits in (mtu - header) this->max_bitrate = ((this->mtu - sizeof(struct rtp_header)) * 8 * this->rate) / 1024; - this->max_bitrate = SPA_MIN(this->max_bitrate, get_valid_aac_bitrate(conf)); - this->cur_bitrate = this->max_bitrate; + this->cur_bitrate = SPA_MIN(this->max_bitrate, get_valid_aac_bitrate(conf)); + spa_log_debug(log, "AAC: max (peak) bitrate: %d, cur bitrate: %d, mode: %d (vbr: %d)", + this->max_bitrate, + this->cur_bitrate, + bitratemode, + conf->vbr); res = aacEncoder_SetParam(this->aacenc, AACENC_BITRATE, this->cur_bitrate); if (res != AACENC_OK) @@ -429,6 +395,15 @@ static void *codec_init(const struct media_codec *codec, uint32_t flags, if (res != AACENC_OK) goto error; + // Assume >110 kbit/s as a "high bitrate" CBR and increase the + // band pass cutout up to 19.3 kHz (as in mode 5 VBR). + if (!conf->vbr && this->cur_bitrate > 110000) { + res = aacEncoder_SetParam(this->aacenc, AACENC_BANDWIDTH, + 19293); + if (res != AACENC_OK) + goto error; + } + res = aacEncoder_SetParam(this->aacenc, AACENC_TRANSMUX, TT_MP4_LATM_MCP1); if (res != AACENC_OK) goto error; diff --git a/spa/plugins/bluez5/a2dp-codec-sbc.c b/spa/plugins/bluez5/a2dp-codec-sbc.c index b4ebfc2a2..f1b86e76b 100644 --- a/spa/plugins/bluez5/a2dp-codec-sbc.c +++ b/spa/plugins/bluez5/a2dp-codec-sbc.c @@ -343,77 +343,27 @@ 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) { - a2dp_sbc_t conf; - struct spa_pod_frame f[2]; - struct spa_pod_choice *choice; - uint32_t i = 0; - uint32_t position[2]; + struct spa_audio_info info; + struct spa_pod_frame f[1]; + int res; - if (caps_size < sizeof(conf)) - return -EINVAL; - - memcpy(&conf, caps, sizeof(conf)); + if ((res = codec_validate_config(codec, flags, caps, caps_size, &info)) < 0) + return res; 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_mediaType, SPA_POD_Id(info.media_type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(info.media_subtype), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(info.info.raw.format), + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(info.info.raw.rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(info.info.raw.channels), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), + SPA_TYPE_Id, info.info.raw.channels, info.info.raw.position), 0); - spa_pod_builder_prop(b, SPA_FORMAT_AUDIO_rate, 0); - spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_None, 0); - choice = (struct spa_pod_choice*)spa_pod_builder_frame(b, &f[1]); - i = 0; - if (conf.frequency & SBC_SAMPLING_FREQ_48000) { - if (i++ == 0) - spa_pod_builder_int(b, 48000); - spa_pod_builder_int(b, 48000); - } - if (conf.frequency & SBC_SAMPLING_FREQ_44100) { - if (i++ == 0) - spa_pod_builder_int(b, 44100); - spa_pod_builder_int(b, 44100); - } - if (conf.frequency & SBC_SAMPLING_FREQ_32000) { - if (i++ == 0) - spa_pod_builder_int(b, 32000); - spa_pod_builder_int(b, 32000); - } - if (conf.frequency & SBC_SAMPLING_FREQ_16000) { - if (i++ == 0) - spa_pod_builder_int(b, 16000); - spa_pod_builder_int(b, 16000); - } - if (i > 1) - choice->body.type = SPA_CHOICE_Enum; - spa_pod_builder_pop(b, &f[1]); - - if (conf.channel_mode & SBC_CHANNEL_MODE_MONO && - conf.channel_mode & (SBC_CHANNEL_MODE_JOINT_STEREO | - SBC_CHANNEL_MODE_STEREO | SBC_CHANNEL_MODE_DUAL_CHANNEL)) { - spa_pod_builder_add(b, - SPA_FORMAT_AUDIO_channels, SPA_POD_CHOICE_RANGE_Int(2, 1, 2), - 0); - } else if (conf.channel_mode & SBC_CHANNEL_MODE_MONO) { - position[0] = SPA_AUDIO_CHANNEL_MONO; - spa_pod_builder_add(b, - SPA_FORMAT_AUDIO_channels, SPA_POD_Int(1), - SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), - SPA_TYPE_Id, 1, position), - 0); - } else { - position[0] = SPA_AUDIO_CHANNEL_FL; - position[1] = SPA_AUDIO_CHANNEL_FR; - spa_pod_builder_add(b, - SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2), - SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(uint32_t), - SPA_TYPE_Id, 2, position), - 0); - } *param = spa_pod_builder_pop(b, &f[0]); return *param == NULL ? -EIO : 1; } diff --git a/spa/plugins/bluez5/backend-native.c b/spa/plugins/bluez5/backend-native.c index 4d14183e7..64a4c25c1 100644 --- a/spa/plugins/bluez5/backend-native.c +++ b/spa/plugins/bluez5/backend-native.c @@ -1969,6 +1969,9 @@ static void hfp_hf_remove_disconnected_calls(struct rfcomm *rfcomm) struct updated_call *updated_call; bool found; + if (!rfcomm->telephony_ag) + return; + 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) { @@ -2097,6 +2100,8 @@ 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 (!rfcomm->telephony_ag) { + /* noop */ } 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"); @@ -2245,7 +2250,8 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) rfcomm->hfp_hf_in_progress = false; } } - } else if (sscanf(token, "+CLIP: \"%16[^\"]\",%u", number, &type) == 2) { + } else if (sscanf(token, "+CLIP: \"%16[^\"]\",%u", number, &type) == 2 + && rfcomm->telephony_ag) { struct spa_bt_telephony_call *call; spa_list_for_each(call, &rfcomm->telephony_ag->call_list, link) { if (call->state == CALL_STATE_INCOMING && !spa_streq(number, call->line_identification)) { @@ -2256,7 +2262,8 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) break; } } - } else if (sscanf(token, "+CCWA: \"%16[^\"]\",%u", number, &type) == 2) { + } else if (sscanf(token, "+CCWA: \"%16[^\"]\",%u", number, &type) == 2 + && rfcomm->telephony_ag) { struct spa_bt_telephony_call *call; bool found = false; @@ -2273,7 +2280,7 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) if (call == NULL) spa_log_warn(backend->log, "failed to create waiting call"); } - } else if (spa_strstartswith(token, "+CLCC:")) { + } else if (spa_strstartswith(token, "+CLCC:") && rfcomm->telephony_ag) { struct spa_bt_telephony_call *call; size_t pos; char *token_end; @@ -2421,17 +2428,19 @@ 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, + if (backend->telephony) { + 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->media_codec->codec_id; - rfcomm->telephony_ag->transport.state = rfcomm->transport->state; + if (rfcomm->transport) { + 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); } - telephony_ag_register(rfcomm->telephony_ag); rfcomm_send_cmd(rfcomm, hfp_hf_clip, NULL, "AT+CLIP=1"); break; @@ -2478,7 +2487,7 @@ static bool rfcomm_hfp_hf(struct rfcomm *rfcomm, char* token) break; case hfp_hf_chld1_hangup: /* For HFP/HF/TWC/BV-03-C - see 0e92ab9307e05758b3f70b4c0648e29c1d1e50be */ - if (!rfcomm->hfp_hf_clcc) { + if (!rfcomm->hfp_hf_clcc && rfcomm->telephony_ag) { 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) { diff --git a/spa/plugins/bluez5/bap-codec-lc3.c b/spa/plugins/bluez5/bap-codec-lc3.c index 74761f26b..881af4e14 100644 --- a/spa/plugins/bluez5/bap-codec-lc3.c +++ b/spa/plugins/bluez5/bap-codec-lc3.c @@ -126,22 +126,22 @@ static const struct bap_qos bap_qos_configs[] = { BAP_QOS("48_6_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 5, 20, 40000, 27, "low-latency"), /* 48_6_1 */ /* BAP v1.0.1 Table 5.2; high-reliability */ - BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 13, 75, 40000, 10, "high-reliabilty"), /* 8_1_2 */ - BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 13, 95, 40000, 0, "high-reliabilty"), /* 8_2_2 */ - BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 13, 75, 40000, 11, "high-reliabilty"), /* 16_1_2 */ - BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 13, 95, 40000, 1, "high-reliabilty"), /* 16_2_2 */ - BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 13, 75, 40000, 12, "high-reliabilty"), /* 24_1_2 */ - BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 13, 95, 40000, 2, "high-reliabilty"), /* 24_2_2 */ - BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 13, 75, 40000, 13, "high-reliabilty"), /* 32_1_2 */ - BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 13, 95, 40000, 3, "high-reliabilty"), /* 32_2_2 */ - BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 13, 80, 40000, 54, "high-reliabilty"), /* 441_1_2 */ - BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 13, 85, 40000, 44, "high-reliabilty"), /* 441_2_2 */ - BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 13, 75, 40000, 55, "high-reliabilty"), /* 48_1_2 */ - BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 13, 95, 40000, 45, "high-reliabilty"), /* 48_2_2 */ - BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 13, 75, 40000, 56, "high-reliabilty"), /* 48_3_2 */ - BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 13, 100, 40000, 46, "high-reliabilty"), /* 48_4_2 */ - BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 13, 75, 40000, 57, "high-reliabilty"), /* 48_5_2 */ - BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 13, 100, 40000, 47, "high-reliabilty"), /* 48_6_2 */ + BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 13, 75, 40000, 10, "high-reliability"), /* 8_1_2 */ + BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 13, 95, 40000, 0, "high-reliability"), /* 8_2_2 */ + BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 13, 75, 40000, 11, "high-reliability"), /* 16_1_2 */ + BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 13, 95, 40000, 1, "high-reliability"), /* 16_2_2 */ + BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 13, 75, 40000, 12, "high-reliability"), /* 24_1_2 */ + BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 13, 95, 40000, 2, "high-reliability"), /* 24_2_2 */ + BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 13, 75, 40000, 13, "high-reliability"), /* 32_1_2 */ + BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 13, 95, 40000, 3, "high-reliability"), /* 32_2_2 */ + BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 13, 80, 40000, 54, "high-reliability"), /* 441_1_2 */ + BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 13, 85, 40000, 44, "high-reliability"), /* 441_2_2 */ + BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 13, 75, 40000, 55, "high-reliability"), /* 48_1_2 */ + BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 13, 95, 40000, 45, "high-reliability"), /* 48_2_2 */ + BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 13, 75, 40000, 56, "high-reliability"), /* 48_3_2 */ + BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 13, 100, 40000, 46, "high-reliability"), /* 48_4_2 */ + BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 13, 75, 40000, 57, "high-reliability"), /* 48_5_2 */ + BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 13, 100, 40000, 47, "high-reliability"), /* 48_6_2 */ }; static const struct bap_qos bap_bcast_qos_configs[] = { @@ -167,22 +167,22 @@ static const struct bap_qos bap_bcast_qos_configs[] = { BAP_QOS("48_6_1", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 4, 20, 40000, 27, "low-latency"), /* 48_6_1 */ /* BAP v1.0.1 Table 6.4; high-reliability */ - BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 4, 45, 40000, 10, "high-reliabilty"), /* 8_1_2 */ - BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 4, 60, 40000, 0, "high-reliabilty"), /* 8_2_2 */ - BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 4, 45, 40000, 11, "high-reliabilty"), /* 16_1_2 */ - BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 4, 60, 40000, 1, "high-reliabilty"), /* 16_2_2 */ - BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 4, 45, 40000, 12, "high-reliabilty"), /* 24_1_2 */ - BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 4, 60, 40000, 2, "high-reliabilty"), /* 24_2_2 */ - BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 4, 45, 40000, 13, "high-reliabilty"), /* 32_1_2 */ - BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 4, 60, 40000, 3, "high-reliabilty"), /* 32_2_2 */ - BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 4, 54, 40000, 14, "high-reliabilty"), /* 441_1_2 */ - BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 4, 60, 40000, 4, "high-reliabilty"), /* 441_2_2 */ - BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 4, 50, 40000, 15, "high-reliabilty"), /* 48_1_2 */ - BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 4, 65, 40000, 5, "high-reliabilty"), /* 48_2_2 */ - BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 4, 50, 40000, 16, "high-reliabilty"), /* 48_3_2 */ - BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 4, 65, 40000, 6, "high-reliabilty"), /* 48_4_2 */ - BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 4, 50, 40000, 17, "high-reliabilty"), /* 48_5_2 */ - BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 4, 65, 40000, 7, "high-reliabilty"), /* 48_6_2 */ + BAP_QOS("8_1_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_7_5, false, 26, 4, 45, 40000, 10, "high-reliability"), /* 8_1_2 */ + BAP_QOS("8_2_2", LC3_CONFIG_FREQ_8KHZ, LC3_CONFIG_DURATION_10, false, 30, 4, 60, 40000, 0, "high-reliability"), /* 8_2_2 */ + BAP_QOS("16_1_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_7_5, false, 30, 4, 45, 40000, 11, "high-reliability"), /* 16_1_2 */ + BAP_QOS("16_2_2", LC3_CONFIG_FREQ_16KHZ, LC3_CONFIG_DURATION_10, false, 40, 4, 60, 40000, 1, "high-reliability"), /* 16_2_2 */ + BAP_QOS("24_1_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_7_5, false, 45, 4, 45, 40000, 12, "high-reliability"), /* 24_1_2 */ + BAP_QOS("24_2_2", LC3_CONFIG_FREQ_24KHZ, LC3_CONFIG_DURATION_10, false, 60, 4, 60, 40000, 2, "high-reliability"), /* 24_2_2 */ + BAP_QOS("32_1_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_7_5, false, 60, 4, 45, 40000, 13, "high-reliability"), /* 32_1_2 */ + BAP_QOS("32_2_2", LC3_CONFIG_FREQ_32KHZ, LC3_CONFIG_DURATION_10, false, 80, 4, 60, 40000, 3, "high-reliability"), /* 32_2_2 */ + BAP_QOS("441_1_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_7_5, true, 97, 4, 54, 40000, 14, "high-reliability"), /* 441_1_2 */ + BAP_QOS("441_2_2", LC3_CONFIG_FREQ_44KHZ, LC3_CONFIG_DURATION_10, true, 130, 4, 60, 40000, 4, "high-reliability"), /* 441_2_2 */ + BAP_QOS("48_1_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 75, 4, 50, 40000, 15, "high-reliability"), /* 48_1_2 */ + BAP_QOS("48_2_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 100, 4, 65, 40000, 5, "high-reliability"), /* 48_2_2 */ + BAP_QOS("48_3_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 90, 4, 50, 40000, 16, "high-reliability"), /* 48_3_2 */ + BAP_QOS("48_4_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 120, 4, 65, 40000, 6, "high-reliability"), /* 48_4_2 */ + BAP_QOS("48_5_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_7_5, false, 117, 4, 50, 40000, 17, "high-reliability"), /* 48_5_2 */ + BAP_QOS("48_6_2", LC3_CONFIG_FREQ_48KHZ, LC3_CONFIG_DURATION_10, false, 155, 4, 65, 40000, 7, "high-reliability"), /* 48_6_2 */ }; static unsigned int get_rate_mask(uint8_t rate) { @@ -1503,6 +1503,10 @@ static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps, struct ltv_writer writer = LTV_WRITER(caps, *caps_size); const struct bap_qos *preset = NULL; + uint32_t retransmissions = 0; + uint8_t rtn_manual_set = 0; + uint32_t max_transport_latency = 0; + uint32_t presentation_delay = 0; *caps_size = 0; if (settings) { @@ -1511,6 +1515,14 @@ static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps, sscanf(settings->items[i].value, "%"PRIu32, &channel_allocation); if (spa_streq(settings->items[i].key, "preset")) preset_name = settings->items[i].value; + if (spa_streq(settings->items[i].key, "max_transport_latency")) + spa_atou32(settings->items[i].value, &max_transport_latency, 0); + if (spa_streq(settings->items[i].key, "presentation_delay")) + spa_atou32(settings->items[i].value, &presentation_delay, 0); + if (spa_streq(settings->items[i].key, "retransmissions")) { + spa_atou32(settings->items[i].value, &retransmissions, 0); + rtn_manual_set = 1; + } } } @@ -1537,9 +1549,9 @@ static int codec_get_bis_config(const struct media_codec *codec, uint8_t *caps, else qos->framing = 0; qos->sdu = preset->framelen * get_channel_count(channel_allocation); - qos->retransmission = preset->retransmission; - qos->latency = preset->latency; - qos->delay = preset->delay; + qos->retransmission = rtn_manual_set ? retransmissions : preset->retransmission; + qos->latency = max_transport_latency ? max_transport_latency : preset->latency; + qos->delay = presentation_delay ? presentation_delay : preset->delay; qos->phy = 2; qos->interval = (preset->frame_duration == LC3_CONFIG_DURATION_7_5 ? 7500 : 10000); diff --git a/spa/plugins/bluez5/bluez5-dbus.c b/spa/plugins/bluez5/bluez5-dbus.c index 21a5e53de..22d971c37 100644 --- a/spa/plugins/bluez5/bluez5-dbus.c +++ b/spa/plugins/bluez5/bluez5-dbus.c @@ -188,9 +188,16 @@ struct spa_bt_metadata { uint8_t value[METADATA_MAX_LEN - 1]; }; +#define RTN_MAX 0x1E +#define MAX_TRANSPORT_LATENCY_MIN 0x5 +#define MAX_TRANSPORT_LATENCY_MAX 0x0FA0 + struct spa_bt_bis { struct spa_list link; char qos_preset[255]; + int retransmissions; + int rtn_manual_set; + int max_transport_latency; int channel_allocation; struct spa_list metadata_list; }; @@ -587,18 +594,35 @@ static enum spa_bt_profile get_codec_profile(const struct media_codec *codec, { switch (direction) { case SPA_BT_MEDIA_SOURCE: - return codec->kind == MEDIA_CODEC_BAP ? SPA_BT_PROFILE_BAP_SOURCE : SPA_BT_PROFILE_A2DP_SOURCE; + if (codec->kind == MEDIA_CODEC_A2DP) + return SPA_BT_PROFILE_A2DP_SOURCE; + else if (codec->kind == MEDIA_CODEC_BAP) + return SPA_BT_PROFILE_BAP_SOURCE; + else if (codec->kind == MEDIA_CODEC_HFP) + return SPA_BT_PROFILE_HEADSET_AUDIO; + else + return SPA_BT_PROFILE_NULL; case SPA_BT_MEDIA_SINK: - if (codec->kind == MEDIA_CODEC_ASHA) + if (codec->kind == MEDIA_CODEC_A2DP) + return SPA_BT_PROFILE_A2DP_SINK; + else if (codec->kind == MEDIA_CODEC_ASHA) return SPA_BT_PROFILE_ASHA_SINK; else if (codec->kind == MEDIA_CODEC_BAP) return SPA_BT_PROFILE_BAP_SINK; + else if (codec->kind == MEDIA_CODEC_HFP) + return SPA_BT_PROFILE_HEADSET_AUDIO; else - return SPA_BT_PROFILE_A2DP_SINK; + return SPA_BT_PROFILE_NULL; case SPA_BT_MEDIA_SOURCE_BROADCAST: - return SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; + if (codec->kind == MEDIA_CODEC_BAP) + return SPA_BT_PROFILE_BAP_BROADCAST_SOURCE; + else + return SPA_BT_PROFILE_NULL; case SPA_BT_MEDIA_SINK_BROADCAST: - return SPA_BT_PROFILE_BAP_BROADCAST_SINK; + if (codec->kind == MEDIA_CODEC_BAP) + return SPA_BT_PROFILE_BAP_BROADCAST_SINK; + else + return SPA_BT_PROFILE_NULL; default: spa_assert_not_reached(); } @@ -720,14 +744,12 @@ static const char *bap_features_get_uuid(struct bap_features *feat, size_t i) /** Get feature name at \a i, or NULL if uuid doesn't match */ static const char *bap_features_get_name(struct bap_features *feat, size_t i, const char *uuid) { - char *pos; - if (i >= feat->dict.n_items) return NULL; if (!spa_streq(feat->dict.items[i].value, uuid)) return NULL; - pos = strchr(feat->dict.items[i].key, ':'); + const char *pos = strchr(feat->dict.items[i].key, ':'); if (!pos) return NULL; return pos + 1; @@ -1336,7 +1358,6 @@ static struct spa_bt_adapter *adapter_find(struct spa_bt_monitor *monitor, const static int parse_modalias(const char *modalias, uint16_t *source, uint16_t *vendor, uint16_t *product, uint16_t *version) { - char *pos; unsigned int src, i, j, k; if (spa_strstartswith(modalias, "bluetooth:")) @@ -1346,7 +1367,7 @@ static int parse_modalias(const char *modalias, uint16_t *source, uint16_t *vend else return -EINVAL; - pos = strchr(modalias, ':'); + const char *pos = strchr(modalias, ':'); if (pos == NULL) return -EINVAL; @@ -2780,16 +2801,18 @@ bool spa_bt_device_supports_media_codec(struct spa_bt_device *device, const stru bool is_bap = codec->kind == MEDIA_CODEC_BAP; size_t i; - 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; + if (!is_media_codec_enabled(monitor, codec)) + return false; return spa_bt_backend_supports_codec(monitor->backend, device, codec->codec_id) == 1; } + codec_target_profile = get_codec_target_profile(monitor, codec); + if (!codec_target_profile) + return false; + 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") && @@ -6176,8 +6199,11 @@ static void configure_bis(struct spa_bt_monitor *monitor, struct bap_codec_qos qos; struct spa_bt_metadata *metadata_entry; struct spa_dict settings; - struct spa_dict_item setting_items[2]; + struct spa_dict_item setting_items[4]; + uint32_t n_items = 0; char channel_allocation[64] = {0}; + char retransmissions[3] = {0}; + char max_transport_latency[5] = {0}; int mse = 0; int options = 0; @@ -6202,12 +6228,27 @@ static void configure_bis(struct spa_bt_monitor *monitor, metadata_size += metadata_entry->length - 1; } + spa_log_debug(monitor->log, "bis->channel_allocation %d", bis->channel_allocation); - if (bis->channel_allocation) + if (bis->channel_allocation) { spa_scnprintf(channel_allocation, sizeof(channel_allocation), "%"PRIu32, bis->channel_allocation); - setting_items[0] = SPA_DICT_ITEM_INIT("channel_allocation", channel_allocation); - setting_items[1] = SPA_DICT_ITEM_INIT("preset", bis->qos_preset); - settings = SPA_DICT_INIT(setting_items, 2); + } + spa_log_debug(monitor->log, "bis->rtn_manual_set %d", bis->rtn_manual_set); + spa_log_debug(monitor->log, "bis->retransmissions %d", bis->retransmissions); + if (bis->rtn_manual_set) { + spa_scnprintf(retransmissions, sizeof(retransmissions), "%"PRIu8, bis->retransmissions); + setting_items[n_items++] = SPA_DICT_ITEM_INIT("retransmissions", retransmissions); + } + spa_log_debug(monitor->log, "bis->max_transport_latency %d", bis->max_transport_latency); + if (bis->max_transport_latency) { + spa_scnprintf(max_transport_latency, sizeof(max_transport_latency), "%"PRIu32, bis->max_transport_latency); + setting_items[n_items++] = SPA_DICT_ITEM_INIT("max_transport_latency", max_transport_latency); + } + + setting_items[n_items++] = SPA_DICT_ITEM_INIT("preset", bis->qos_preset); + setting_items[n_items++] = SPA_DICT_ITEM_INIT("channel_allocation", channel_allocation); + + settings = SPA_DICT_INIT(setting_items, n_items); caps_size = sizeof(caps); ret = codec->get_bis_config(codec, caps, &caps_size, &settings, &qos); @@ -7081,7 +7122,7 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const memcpy(big_entry->broadcast_code, bcode, strlen(bcode)); spa_log_debug(monitor->log, "big_entry->broadcast_code %s", big_entry->broadcast_code); } else if (spa_streq(key, "adapter")) { - if (spa_json_get_string(&it[1], big_entry->adapter, sizeof(big_entry->adapter)) <= 0) + if (spa_json_get_string(&it[0], big_entry->adapter, sizeof(big_entry->adapter)) <= 0) goto parse_failed; spa_log_debug(monitor->log, "big_entry->adapter %s", big_entry->adapter); } else if (spa_streq(key, "encryption")) { @@ -7110,6 +7151,20 @@ static void parse_broadcast_source_config(struct spa_bt_monitor *monitor, const if (spa_json_get_string(&it[1], bis_entry->qos_preset, sizeof(bis_entry->qos_preset)) <= 0) goto parse_failed; spa_log_debug(monitor->log, "bis_entry->qos_preset %s", bis_entry->qos_preset); + } else if (spa_streq(bis_key, "retransmissions")) { + if (spa_json_get_int(&it[2], &bis_entry->retransmissions) <= 0) + goto parse_failed; + if (bis_entry->retransmissions > RTN_MAX) + goto parse_failed; + bis_entry->rtn_manual_set = 1; + spa_log_debug(monitor->log, "bis_entry->retransmissions %d", bis_entry->retransmissions); + } else if (spa_streq(bis_key, "max_transport_latency")) { + if (spa_json_get_int(&it[2], &bis_entry->max_transport_latency) <= 0) + goto parse_failed; + if (bis_entry->max_transport_latency < MAX_TRANSPORT_LATENCY_MIN && + bis_entry->max_transport_latency > MAX_TRANSPORT_LATENCY_MAX) + goto parse_failed; + spa_log_debug(monitor->log, "bis_entry->max_transport_latency %d", bis_entry->max_transport_latency); } else if (spa_streq(bis_key, "audio_channel_allocation")) { if (spa_json_get_int(&it[1], &bis_entry->channel_allocation) <= 0) goto parse_failed; diff --git a/spa/plugins/bluez5/bluez5-device.c b/spa/plugins/bluez5/bluez5-device.c index fc659f655..897241954 100644 --- a/spa/plugins/bluez5/bluez5-device.c +++ b/spa/plugins/bluez5/bluez5-device.c @@ -59,6 +59,8 @@ enum device_profile { DEVICE_PROFILE_OFF = 0, DEVICE_PROFILE_AG, DEVICE_PROFILE_A2DP, + DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY, + DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY, DEVICE_PROFILE_HSP_HFP, DEVICE_PROFILE_BAP, DEVICE_PROFILE_BAP_SINK, @@ -67,6 +69,12 @@ enum device_profile { DEVICE_PROFILE_LAST, }; +enum codec_order { + CODEC_ORDER_NONE = 0, + CODEC_ORDER_QUALITY, + CODEC_ORDER_LATENCY, +}; + enum { ROUTE_INPUT = 0, ROUTE_OUTPUT, @@ -204,9 +212,89 @@ static bool profile_is_bap(enum device_profile profile) return false; } -static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec id, const struct media_codec **codecs, size_t size) +static bool profile_is_a2dp(enum device_profile profile) +{ + switch (profile) { + case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: + return true; + default: + break; + } + return false; +} + +static size_t get_media_codec_quality_priority (const struct media_codec *mc) +{ + /* From lowest quality to highest quality */ + static const enum spa_bluetooth_audio_codec quality_priorities[] = { + SPA_BLUETOOTH_AUDIO_CODEC_START, + SPA_BLUETOOTH_AUDIO_CODEC_SBC, + SPA_BLUETOOTH_AUDIO_CODEC_APTX, + SPA_BLUETOOTH_AUDIO_CODEC_AAC, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_G, + SPA_BLUETOOTH_AUDIO_CODEC_LC3PLUS_HR, + SPA_BLUETOOTH_AUDIO_CODEC_APTX_HD, + SPA_BLUETOOTH_AUDIO_CODEC_LDAC, + }; + size_t i; + + for (i = 0; i < SPA_N_ELEMENTS(quality_priorities); ++i) { + if (quality_priorities[i] == mc->id) + return i; + } + + return 0; +} + +static size_t get_media_codec_latency_priority (const struct media_codec *mc) +{ + /* From highest latency to lowest latency */ + static const enum spa_bluetooth_audio_codec latency_priorities[] = { + SPA_BLUETOOTH_AUDIO_CODEC_START, + SPA_BLUETOOTH_AUDIO_CODEC_SBC, + SPA_BLUETOOTH_AUDIO_CODEC_APTX, + SPA_BLUETOOTH_AUDIO_CODEC_OPUS_G, + SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM_DUPLEX, + SPA_BLUETOOTH_AUDIO_CODEC_FASTSTREAM, + SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL_DUPLEX, + SPA_BLUETOOTH_AUDIO_CODEC_APTX_LL, + }; + size_t i; + + for (i = 0; i < SPA_N_ELEMENTS(latency_priorities); ++i) { + if (latency_priorities[i] == mc->id) + return i; + } + + return 0; +} + +static int media_codec_quality_cmp(const void *a, const void *b) { + const struct media_codec *ca = *(const struct media_codec **)a; + const struct media_codec *cb = *(const struct media_codec **)b; + size_t ca_prio = get_media_codec_quality_priority (ca); + size_t cb_prio = get_media_codec_quality_priority (cb); + if (ca_prio > cb_prio) return -1; + if (ca_prio < cb_prio) return 1; + return 0; +} + +static int media_codec_latency_cmp(const void *a, const void *b) { + const struct media_codec *ca = *(const struct media_codec **)a; + const struct media_codec *cb = *(const struct media_codec **)b; + size_t ca_prio = get_media_codec_latency_priority (ca); + size_t cb_prio = get_media_codec_latency_priority (cb); + if (ca_prio > cb_prio) return -1; + if (ca_prio < cb_prio) return 1; + return 0; +} + +static void get_media_codecs(struct impl *this, enum codec_order order, enum spa_bluetooth_audio_codec id, const struct media_codec **codecs, size_t size) { const struct media_codec * const *c; + size_t n = 0; spa_assert(size > 0); spa_assert(this->supported_codecs); @@ -216,12 +304,24 @@ static void get_media_codecs(struct impl *this, enum spa_bluetooth_audio_codec i continue; if ((*c)->id == id || id == 0) { - *codecs++ = *c; + codecs[n++] = *c; --size; } } - *codecs = NULL; + codecs[n] = NULL; + + switch (order) { + case CODEC_ORDER_QUALITY: + qsort(codecs, n, sizeof(struct media_codec *), media_codec_quality_cmp); + break; + case CODEC_ORDER_LATENCY: + qsort(codecs, n, sizeof(struct media_codec *), media_codec_latency_cmp); + break; + case CODEC_ORDER_NONE: + default: + break; + } } static const struct media_codec *get_supported_media_codec(struct impl *this, enum spa_bluetooth_audio_codec id, @@ -380,6 +480,8 @@ 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_A2DP_AUTO_PREFER_QUALITY + && impl->profile != DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY && impl->profile != DEVICE_PROFILE_BAP && impl->profile != DEVICE_PROFILE_BAP_SINK && impl->profile != DEVICE_PROFILE_BAP_SOURCE @@ -1262,6 +1364,8 @@ static int emit_nodes(struct impl *this) } break; case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: if (this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_SOURCE) { t = find_transport(this, SPA_BT_PROFILE_A2DP_SOURCE); if (t) { @@ -1464,13 +1568,13 @@ 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_is_bap(profile) && is_bap_client(this))) + if ((profile_is_a2dp (profile) || (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)); + get_media_codecs(this, CODEC_ORDER_NONE, codec, codecs, SPA_N_ELEMENTS(codecs)); this->switching_codec = true; @@ -1487,6 +1591,14 @@ static int set_profile(struct impl *this, uint32_t profile, enum spa_bluetooth_a case DEVICE_PROFILE_A2DP: profiles = this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_DUPLEX; break; + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + get_media_codecs(this, CODEC_ORDER_QUALITY, 0, codecs, SPA_N_ELEMENTS(codecs)); + profiles = this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_DUPLEX; + break; + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: + get_media_codecs(this, CODEC_ORDER_LATENCY, 0, codecs, SPA_N_ELEMENTS(codecs)); + profiles = this->bt_dev->connected_profiles & SPA_BT_PROFILE_A2DP_DUPLEX; + break; default: profiles = 0; break; @@ -1646,6 +1758,8 @@ static void profiles_changed(void *userdata, uint32_t connected_change) nodes_changed); break; case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: nodes_changed = (connected_change & SPA_BT_PROFILE_A2DP_DUPLEX); spa_log_debug(this->log, "profiles changed: A2DP nodes changed: %d", nodes_changed); @@ -1801,6 +1915,8 @@ static uint32_t profile_direction_mask(struct impl *this, uint32_t index, enum s switch (index) { case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: if (device->connected_profiles & SPA_BT_PROFILE_A2DP_SINK) have_output = true; @@ -1850,6 +1966,8 @@ static uint32_t get_profile_from_index(struct impl *this, uint32_t index, uint32 switch (profile) { case DEVICE_PROFILE_OFF: case DEVICE_PROFILE_AG: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: *codec = 0; *next = (profile + 1) << 16; return profile; @@ -1884,6 +2002,8 @@ static uint32_t get_index_from_profile(struct impl *this, uint32_t profile, enum switch (profile) { case DEVICE_PROFILE_OFF: case DEVICE_PROFILE_AG: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: return (profile << 16); case DEVICE_PROFILE_ASHA: @@ -1977,15 +2097,37 @@ static void set_initial_profile(struct impl *this) t = find_transport(this, i); if (t) { - if (i == SPA_BT_PROFILE_A2DP_SOURCE || i == SPA_BT_PROFILE_BAP_SOURCE) + if (i == SPA_BT_PROFILE_A2DP_SOURCE || i == SPA_BT_PROFILE_BAP_SOURCE) { this->profile = DEVICE_PROFILE_AG; - else if (i == SPA_BT_PROFILE_BAP_SINK) + this->props.codec = t->media_codec->id; + } else if (i == SPA_BT_PROFILE_BAP_SINK) { this->profile = DEVICE_PROFILE_BAP; - else if (i == SPA_BT_PROFILE_ASHA_SINK) + this->props.codec = t->media_codec->id; + } else if (i == SPA_BT_PROFILE_ASHA_SINK) { this->profile = DEVICE_PROFILE_ASHA; - else - this->profile = DEVICE_PROFILE_A2DP; - this->props.codec = t->media_codec->id; + this->props.codec = t->media_codec->id; + } else { + const struct media_codec *codecs[64]; + const struct media_codec *quality_codec = NULL; + int j; + + get_media_codecs(this, CODEC_ORDER_QUALITY, 0, codecs, SPA_N_ELEMENTS(codecs)); + for (j = 0; codecs[j] != NULL; ++j) { + if (codecs[j]->kind == MEDIA_CODEC_A2DP) { + quality_codec = codecs[j]; + break; + } + } + + if (quality_codec) { + this->profile = DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY; + this->props.codec = quality_codec->id; + } else { + this->profile = DEVICE_PROFILE_A2DP; + this->props.codec = t->media_codec->id; + } + } + spa_log_debug(this->log, "initial profile media profile:%d codec:%d", this->profile, this->props.codec); return; @@ -2123,6 +2265,36 @@ static struct spa_pod *build_profile(struct impl *this, struct spa_pod_builder * n_source++; break; } + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: + { + uint32_t profile; + + /* make this device profile visible only if there is an A2DP sink */ + profile = device->connected_profiles & (SPA_BT_PROFILE_A2DP_SINK | SPA_BT_PROFILE_A2DP_SOURCE); + if (!(profile & SPA_BT_PROFILE_A2DP_SINK)) + return NULL; + + switch (profile_index) { + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + name = "a2dp-auto-prefer-quality"; + desc = _("Auto: Prefer Quality (A2DP)"); + priority = 255; + break; + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: + name = "a2dp-auto-prefer-latency"; + desc = _("Auto: Prefer Latency (A2DP)"); + priority = 254; + break; + default: + return NULL; + } + + n_sink++; + if (this->autoswitch_routes && (device->connected_profiles & SPA_BT_PROFILE_HEADSET_HEAD_UNIT)) + n_source++; + break; + } case DEVICE_PROFILE_BAP_SINK: case DEVICE_PROFILE_BAP_SOURCE: /* These are client-only */ @@ -2324,6 +2496,8 @@ static bool profile_has_route(uint32_t profile, uint32_t route) case DEVICE_PROFILE_AG: break; case DEVICE_PROFILE_A2DP: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_QUALITY: + case DEVICE_PROFILE_A2DP_AUTO_PREFER_LATENCY: switch (route) { case ROUTE_INPUT: case ROUTE_OUTPUT: @@ -2623,7 +2797,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 || profile_is_bap(this->profile)) && + if ((profile_is_a2dp (this->profile) || 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); @@ -2659,7 +2833,7 @@ next: c = this->supported_codecs[*j]; - if (!(this->profile == DEVICE_PROFILE_A2DP && c->kind == MEDIA_CODEC_A2DP) && + if (!(profile_is_a2dp (this->profile) && 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)) @@ -3240,7 +3414,7 @@ static int impl_set_param(void *object, return 0; } - if (this->profile == DEVICE_PROFILE_A2DP || profile_is_bap(this->profile) || + if (profile_is_a2dp (this->profile) || 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) { diff --git a/spa/plugins/bluez5/iso-io.c b/spa/plugins/bluez5/iso-io.c index 2cc65a2bf..ce1fd7d0c 100644 --- a/spa/plugins/bluez5/iso-io.c +++ b/spa/plugins/bluez5/iso-io.c @@ -411,7 +411,7 @@ static void group_on_timeout(struct spa_source *source) /* Ensure controller fill level */ fill_count = UINT_MAX; spa_list_for_each(stream, &group->streams, link) { - if (!stream->sink || !group->started) + if (!stream->sink || !group->started || !stream->tx_latency.enabled) continue; if (stream->tx_latency.queue < MIN_FILL) fill_count = SPA_MIN(fill_count, MIN_FILL - stream->tx_latency.queue); diff --git a/spa/plugins/bluez5/media-source.c b/spa/plugins/bluez5/media-source.c index da2e57b5a..e1c01d90c 100644 --- a/spa/plugins/bluez5/media-source.c +++ b/spa/plugins/bluez5/media-source.c @@ -1784,7 +1784,7 @@ static uint32_t get_samples(struct impl *this, int64_t *duration_ns) static void update_target_latency(struct impl *this) { struct port *port = &this->port; - int32_t target; + int32_t target = 0; int samples; if (this->transport == NULL || !port->have_format) @@ -1803,7 +1803,7 @@ static void update_target_latency(struct impl *this) */ if (this->decode_buffer_target) target = this->decode_buffer_target; - else + else if (this->transport->iso_io) target = spa_bt_iso_io_get_source_target_latency(this->transport->iso_io); spa_bt_decode_buffer_set_target_latency(&port->buffer, target); diff --git a/spa/plugins/bluez5/midi-node.c b/spa/plugins/bluez5/midi-node.c index 7146d6f8a..671035b34 100644 --- a/spa/plugins/bluez5/midi-node.c +++ b/spa/plugins/bluez5/midi-node.c @@ -23,7 +23,6 @@ #include #include #include -#include #include #include @@ -450,7 +449,7 @@ static void midi_event_recv(void *user_data, uint16_t timestamp, uint8_t *data, struct impl *this = user_data; struct port *port = &this->ports[PORT_OUT]; struct time_sync *sync = &port->sync; - uint64_t time, state = 0; + uint64_t time; int res; spa_assert(size > 0); @@ -460,19 +459,11 @@ static void midi_event_recv(void *user_data, uint16_t timestamp, uint8_t *data, spa_log_trace(this->log, "%p: event:0x%x size:%d timestamp:%d time:%"PRIu64"", this, (int)data[0], (int)size, (int)timestamp, (uint64_t)time); - 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; - - res = midi_event_ringbuffer_push(&this->event_rbuf, time, (uint8_t*)ump, ump_size); - if (res < 0) { - midi_event_ringbuffer_init(&this->event_rbuf); - spa_log_warn(this->log, "%p: MIDI receive buffer overflow: %s", - this, spa_strerror(res)); - } + res = midi_event_ringbuffer_push(&this->event_rbuf, time, data, size); + if (res < 0) { + midi_event_ringbuffer_init(&this->event_rbuf); + spa_log_warn(this->log, "%p: MIDI receive buffer overflow: %s", + this, spa_strerror(res)); } } @@ -713,7 +704,7 @@ static int process_output(struct impl *this) offset = time * this->rate / SPA_NSEC_PER_SEC; offset = SPA_CLAMP(offset, 0u, this->duration - 1); - spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_UMP); + spa_pod_builder_control(&port->builder, offset, SPA_CONTROL_Midi); buf = spa_pod_builder_reserve_bytes(&port->builder, size); if (buf) { midi_event_ringbuffer_pop(&this->event_rbuf, buf, size); @@ -786,37 +777,28 @@ static int write_data(struct impl *this, struct spa_data *d) time = 0; 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; + const uint8_t *event = c_body; + uint32_t size = c.value.size; - if (c.type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_Midi) continue; time = SPA_MAX(time, this->current_time + c.offset * SPA_NSEC_PER_SEC / this->rate); - 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) diff --git a/spa/plugins/control/mixer.c b/spa/plugins/control/mixer.c index caf3a6b06..1b106d3f7 100644 --- a/spa/plugins/control/mixer.c +++ b/spa/plugins/control/mixer.c @@ -72,6 +72,7 @@ struct impl { struct spa_node node; uint32_t quantum_limit; + uint32_t control_types; struct spa_log *log; @@ -473,9 +474,9 @@ static int port_set_format(void *object, if (!port->have_format) { this->n_formats++; port->have_format = true; - port->types = types; - spa_log_debug(this->log, "%p: set format on port %d:%d", - this, direction, port_id); + port->types = types == 0 ? this->control_types : types; + spa_log_debug(this->log, "%p: set format on port %d:%d types:%08x %08x", + this, direction, port_id, port->types, this->control_types); } } port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; @@ -589,7 +590,7 @@ static int do_port_set_io(struct spa_loop *loop, bool async, uint32_t seq, port->io[0] = info->data; port->io[1] = info->data; } - if (!port->active) { + if (port->direction == SPA_DIRECTION_INPUT && !port->active) { spa_list_append(&info->impl->mix_list, &port->mix_link); port->active = true; } @@ -955,6 +956,8 @@ impl_init(const struct spa_handle_factory *factory, } this->quantum_limit = 8192; + /* by default we convert to midi1 */ + this->control_types = 1u<n_items; i++) { const char *k = info->items[i].key; @@ -962,6 +965,14 @@ impl_init(const struct spa_handle_factory *factory, if (spa_streq(k, "clock.quantum-limit")) { spa_atou32(s, &this->quantum_limit, 0); } + else if (spa_streq(k, "control.ump")) { + if (spa_atob(s)) + /* we convert to UMP when forced */ + this->control_types = 1u<control_types = 0; + } } spa_hook_list_init(&this->hooks); diff --git a/spa/plugins/filter-graph/audio-dsp-avx2.c b/spa/plugins/filter-graph/audio-dsp-avx2.c index 76c7b17d5..346b26ab3 100644 --- a/spa/plugins/filter-graph/audio-dsp-avx2.c +++ b/spa/plugins/filter-graph/audio-dsp-avx2.c @@ -140,10 +140,10 @@ static void dsp_add_n_gain_avx2(void *obj, float *dst, for (i = 1; i < n_src; i++) { g = _mm256_set1_ps(gain[i]); - in[0] = _mm256_add_ps(in[0], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+ 0]))); - in[1] = _mm256_add_ps(in[1], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+ 8]))); - in[2] = _mm256_add_ps(in[2], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+16]))); - in[3] = _mm256_add_ps(in[3], _mm256_mul_ps(g, _mm256_load_ps(&s[i][n+24]))); + in[0] = _mm256_fmadd_ps(g, _mm256_load_ps(&s[i][n+ 0]), in[0]); + in[1] = _mm256_fmadd_ps(g, _mm256_load_ps(&s[i][n+ 8]), in[1]); + in[2] = _mm256_fmadd_ps(g, _mm256_load_ps(&s[i][n+16]), in[2]); + in[3] = _mm256_fmadd_ps(g, _mm256_load_ps(&s[i][n+24]), in[3]); } _mm256_store_ps(&d[n+ 0], in[0]); _mm256_store_ps(&d[n+ 8], in[1]); @@ -237,13 +237,12 @@ void dsp_sum_avx2(void *obj, float *r, const float *a, const float *b, uint32_t inline static __m256 _mm256_mul_pz(__m256 ab, __m256 cd) { - __m256 aa, bb, dc, x0, x1; + __m256 aa, bb, dc, x1; aa = _mm256_moveldup_ps(ab); bb = _mm256_movehdup_ps(ab); - x0 = _mm256_mul_ps(aa, cd); dc = _mm256_shuffle_ps(cd, cd, _MM_SHUFFLE(2,3,0,1)); x1 = _mm256_mul_ps(bb, dc); - return _mm256_addsub_ps(x0, x1); + return _mm256_fmaddsub_ps(aa, cd, x1); } void dsp_fft_cmul_avx2(void *obj, void *fft, @@ -308,12 +307,10 @@ void dsp_fft_cmuladd_avx2(void *obj, void *fft, bb[1] = _mm256_load_ps(&b[2*i+8]); /* br2 bi2 br3 bi3 */ dd[0] = _mm256_mul_pz(aa[0], bb[0]); dd[1] = _mm256_mul_pz(aa[1], bb[1]); - dd[0] = _mm256_mul_ps(dd[0], s); - dd[1] = _mm256_mul_ps(dd[1], s); t[0] = _mm256_load_ps(&src[2*i]); t[1] = _mm256_load_ps(&src[2*i+8]); - t[0] = _mm256_add_ps(t[0], dd[0]); - t[1] = _mm256_add_ps(t[1], dd[1]); + t[0] = _mm256_fmadd_ps(dd[0], s, t[0]); + t[1] = _mm256_fmadd_ps(dd[1], s, t[1]); _mm256_store_ps(&dst[2*i], t[0]); _mm256_store_ps(&dst[2*i+8], t[1]); } diff --git a/spa/plugins/filter-graph/audio-dsp.c b/spa/plugins/filter-graph/audio-dsp.c index 133b53db5..d0c4ef008 100644 --- a/spa/plugins/filter-graph/audio-dsp.c +++ b/spa/plugins/filter-graph/audio-dsp.c @@ -24,7 +24,7 @@ struct dsp_info { static const struct dsp_info dsp_table[] = { #if defined (HAVE_AVX2) - { SPA_CPU_FLAG_AVX2, + { SPA_CPU_FLAG_AVX2 | SPA_CPU_FLAG_FMA3, .funcs.clear = dsp_clear_c, .funcs.copy = dsp_copy_c, .funcs.mix_gain = dsp_mix_gain_avx2, diff --git a/spa/plugins/filter-graph/audio-plugin.h b/spa/plugins/filter-graph/audio-plugin.h index 9f9d1bce6..fffdc0de5 100644 --- a/spa/plugins/filter-graph/audio-plugin.h +++ b/spa/plugins/filter-graph/audio-plugin.h @@ -69,6 +69,7 @@ struct spa_fga_descriptor { void (*connect_port) (void *instance, unsigned long port, void *data); void (*control_changed) (void *instance); + void (*control_sync) (void *instance); void (*activate) (void *instance); void (*deactivate) (void *instance); diff --git a/spa/plugins/filter-graph/convolver.c b/spa/plugins/filter-graph/convolver.c index a077c6ec1..788b118e3 100644 --- a/spa/plugins/filter-graph/convolver.c +++ b/spa/plugins/filter-graph/convolver.c @@ -171,7 +171,10 @@ static int convolver1_run(struct spa_fga_dsp *dsp, struct convolver1 *conv, cons if (conv->segCount > 1) { if (inputBufferFill == 0) { - int indexAudio = (conv->current + 1) % conv->segCount; + int indexAudio = conv->current; + + if (++indexAudio == conv->segCount) + indexAudio = 0; spa_fga_dsp_fft_cmul(dsp, conv->fft, conv->pre_mult, conv->segmentsIr[1], @@ -179,7 +182,8 @@ static int convolver1_run(struct spa_fga_dsp *dsp, struct convolver1 *conv, cons conv->fftComplexSize, conv->scale); for (i = 2; i < conv->segCount; i++) { - indexAudio = (conv->current + i) % conv->segCount; + if (++indexAudio == conv->segCount) + indexAudio = 0; spa_fga_dsp_fft_cmuladd(dsp, conv->fft, conv->pre_mult, @@ -214,9 +218,10 @@ static int convolver1_run(struct spa_fga_dsp *dsp, struct convolver1 *conv, cons SPA_SWAP(conv->fft_buffer[0], conv->fft_buffer[1]); - conv->current = (conv->current > 0) ? (conv->current - 1) : (conv->segCount - 1); + if (conv->current == 0) + conv->current = conv->segCount; + conv->current--; } - processed += processing; } conv->inputBufferFill = inputBufferFill; diff --git a/spa/plugins/filter-graph/filter-graph.c b/spa/plugins/filter-graph/filter-graph.c index 52609f2c6..4b47d0c9f 100644 --- a/spa/plugins/filter-graph/filter-graph.c +++ b/spa/plugins/filter-graph/filter-graph.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -221,6 +222,7 @@ struct impl { struct spa_cpu *cpu; struct spa_fga_dsp *dsp; struct spa_plugin_loader *loader; + struct spa_loop *data_loop; uint64_t info_all; struct spa_filter_graph_info info; @@ -552,19 +554,25 @@ static int impl_get_props(void *object, struct spa_pod_builder *b, struct spa_po struct descriptor *desc = node->desc; const struct spa_fga_descriptor *d = desc->desc; struct spa_fga_port *p = &d->ports[port->p]; + float v, min, max; if (node->name[0] != '\0') snprintf(name, sizeof(name), "%s:%s", node->name, p->name); else snprintf(name, sizeof(name), "%s", p->name); + if (port->control_initialized) + v = port->control_current; + else + get_ranges(impl, p, &v, &min, &max); + spa_pod_builder_string(b, name); if (p->hint & SPA_FGA_HINT_BOOLEAN) { - spa_pod_builder_bool(b, port->control_data[0] <= 0.0f ? false : true); + spa_pod_builder_bool(b, v <= 0.0f ? false : true); } else if (p->hint & SPA_FGA_HINT_INTEGER) { - spa_pod_builder_int(b, (int32_t)port->control_data[0]); + spa_pod_builder_int(b, (int32_t)v); } else { - spa_pod_builder_float(b, port->control_data[0]); + spa_pod_builder_float(b, v); } } spa_pod_builder_pop(b, &f[1]); @@ -698,21 +706,46 @@ static int impl_reset(void *object) return 0; } -static void node_control_changed(struct node *node) +static int +do_emit_node_control_sync(struct spa_loop *loop, bool async, uint32_t seq, const void *data, + size_t size, void *user_data) { - const struct spa_fga_descriptor *d = node->desc->desc; + struct impl *impl = user_data; + struct graph *graph = &impl->graph; + struct node *node; + uint32_t i; + spa_list_for_each(node, &graph->node_list, link) { + const struct spa_fga_descriptor *d = node->desc->desc; + if (!node->control_changed || d->control_sync == NULL) + continue; + for (i = 0; i < node->n_hndl; i++) { + if (node->hndl[i] != NULL) + d->control_sync(node->hndl[i]); + } + } + return 0; +} + +static void emit_node_control_changed(struct impl *impl) +{ + struct graph *graph = &impl->graph; + struct node *node; uint32_t i; - if (!node->control_changed) - return; + spa_loop_locked(impl->data_loop, do_emit_node_control_sync, 1, NULL, 0, impl); - for (i = 0; i < node->n_hndl; i++) { - if (node->hndl[i] == NULL) + spa_list_for_each(node, &graph->node_list, link) { + const struct spa_fga_descriptor *d = node->desc->desc; + if (!node->control_changed) continue; - if (d->control_changed) - d->control_changed(node->hndl[i]); + if (d->control_changed != NULL) { + for (i = 0; i < node->n_hndl; i++) { + if (node->hndl[i] != NULL) + d->control_changed(node->hndl[i]); + } + } + node->control_changed = false; } - node->control_changed = false; } static int sync_volume(struct graph *graph, struct volume *vol) @@ -826,11 +859,7 @@ static int impl_set_props(void *object, enum spa_direction direction, const stru spa_pod_dynamic_builder_clean(&b); if (changed > 0) { - struct node *node; - - spa_list_for_each(node, &graph->node_list, link) - node_control_changed(node); - + emit_node_control_changed(impl); spa_filter_graph_emit_props_changed(&impl->hooks, SPA_DIRECTION_INPUT); } return 0; @@ -1035,8 +1064,8 @@ static struct descriptor *descriptor_load(struct impl *impl, const char *type, } } else if (SPA_FGA_IS_PORT_CONTROL(fp->flags)) { if (SPA_FGA_IS_PORT_INPUT(fp->flags)) { - spa_log_info(impl->log, "using port %lu ('%s') as control %d", p, - fp->name, desc->n_control); + spa_log_info(impl->log, "using port %lu ('%s') as control %d %f/%f/%f", p, + fp->name, desc->n_control, fp->def, fp->min, fp->max); desc->control[desc->n_control++] = p; } else if (SPA_FGA_IS_PORT_OUTPUT(fp->flags)) { @@ -1622,6 +1651,7 @@ static int impl_activate(void *object, const struct spa_dict *props) goto error; } } + node->control_changed = true; } /* then link ports */ @@ -1695,10 +1725,10 @@ static int impl_activate(void *object, const struct spa_dict *props) for (i = 0; i < node->n_hndl; i++) { if (d->activate) d->activate(node->hndl[i]); - if (node->control_changed && d->control_changed) - d->control_changed(node->hndl[i]); } } + emit_node_control_changed(impl); + /* calculate latency */ sort_reset(graph); while ((node = sort_next_node(graph)) != NULL) { @@ -2346,6 +2376,7 @@ impl_init(const struct spa_handle_factory *factory, spa_log_topic_init(impl->log, &log_topic); impl->cpu = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_CPU); + impl->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop); impl->max_align = spa_cpu_get_max_align(impl->cpu); impl->dsp = spa_fga_dsp_new(impl->cpu ? spa_cpu_get_flags(impl->cpu) : 0); diff --git a/spa/plugins/filter-graph/meson.build b/spa/plugins/filter-graph/meson.build index 94ee0bd25..20b90f4c4 100644 --- a/spa/plugins/filter-graph/meson.build +++ b/spa/plugins/filter-graph/meson.build @@ -18,16 +18,16 @@ if have_sse simd_cargs += ['-DHAVE_SSE'] simd_dependencies += filter_graph_sse endif -if have_avx2 - filter_graph_avx2 = static_library('filter_graph_avx2', +if have_avx2 and have_fma + filter_graph_avx2_fma = static_library('filter_graph_avx2_fma', ['audio-dsp-avx2.c' ], include_directories : [configinc], - c_args : [avx2_args, fma_args,'-O3', '-DHAVE_AVX2'], + c_args : [avx2_args, fma_args, '-O3', '-DHAVE_AVX2', '-DHAVE_FMA'], dependencies : [ spa_dep ], install : false ) - simd_cargs += ['-DHAVE_AVX2'] - simd_dependencies += filter_graph_avx2 + simd_cargs += ['-DHAVE_AVX2', '-DHAVE_FMA'] + simd_dependencies += filter_graph_avx2_fma endif if have_neon filter_graph_neon = static_library('filter_graph_neon', diff --git a/spa/plugins/filter-graph/plugin_builtin.c b/spa/plugins/filter-graph/plugin_builtin.c index 3bcde30c9..3c15673db 100644 --- a/spa/plugins/filter-graph/plugin_builtin.c +++ b/spa/plugins/filter-graph/plugin_builtin.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -542,7 +543,12 @@ static void bq_run(void *Instance, unsigned long samples) struct biquad *bq = &impl->bq; float *out = impl->port[0]; float *in = impl->port[1]; + spa_fga_dsp_biquad_run(impl->dsp, bq, 1, 0, &out, (const float **)&in, 1, samples); +} +static void bq_control_sync(void * Instance) +{ + struct builtin *impl = Instance; if (impl->type == BQ_NONE) { float b0, b1, b2, a0, a1, a2; b0 = impl->port[5][0]; @@ -562,7 +568,6 @@ static void bq_run(void *Instance, unsigned long samples) if (impl->freq != freq || impl->Q != Q || impl->gain != gain) bq_freq_update(impl, impl->type, freq, Q, gain); } - spa_fga_dsp_biquad_run(impl->dsp, bq, 1, 0, &out, (const float **)&in, 1, samples); } /** bq_lowpass */ @@ -574,6 +579,7 @@ static const struct spa_fga_descriptor bq_lowpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -588,6 +594,7 @@ static const struct spa_fga_descriptor bq_highpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -602,6 +609,7 @@ static const struct spa_fga_descriptor bq_bandpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -616,6 +624,7 @@ static const struct spa_fga_descriptor bq_lowshelf_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -630,6 +639,7 @@ static const struct spa_fga_descriptor bq_highshelf_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -644,6 +654,7 @@ static const struct spa_fga_descriptor bq_peaking_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -658,6 +669,7 @@ static const struct spa_fga_descriptor bq_notch_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -673,6 +685,7 @@ static const struct spa_fga_descriptor bq_allpass_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -687,6 +700,7 @@ static const struct spa_fga_descriptor bq_raw_desc = { .instantiate = bq_instantiate, .connect_port = builtin_connect_port, + .control_sync = bq_control_sync, .activate = bq_activate, .run = bq_run, .cleanup = builtin_cleanup, @@ -1453,6 +1467,7 @@ static struct spa_fga_port clamp_ports[] = { { .index = 3, .name = "Control", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -FLT_MAX, .max = FLT_MAX }, { .index = 4, .name = "Min", @@ -1510,6 +1525,7 @@ static struct spa_fga_port linear_ports[] = { { .index = 3, .name = "Control", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -FLT_MAX, .max = FLT_MAX }, { .index = 4, .name = "Mult", @@ -1577,6 +1593,7 @@ static struct spa_fga_port recip_ports[] = { { .index = 3, .name = "Control", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -FLT_MAX, .max = FLT_MAX }, }; @@ -1626,6 +1643,7 @@ static struct spa_fga_port exp_ports[] = { { .index = 3, .name = "Control", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -FLT_MAX, .max = FLT_MAX }, { .index = 4, .name = "Base", @@ -1683,6 +1701,7 @@ static struct spa_fga_port log_ports[] = { { .index = 3, .name = "Control", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -FLT_MAX, .max = FLT_MAX }, { .index = 4, .name = "Base", @@ -2472,10 +2491,12 @@ static struct spa_fga_port ramp_ports[] = { { .index = 1, .name = "Start", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -FLT_MAX, .max = FLT_MAX }, { .index = 2, .name = "Stop", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = -FLT_MAX, .max = FLT_MAX }, { .index = 3, .name = "Current", @@ -2484,6 +2505,7 @@ static struct spa_fga_port ramp_ports[] = { { .index = 4, .name = "Duration (s)", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 1.0f, .min = 0.0f, .max = FLT_MAX }, }; @@ -2643,6 +2665,7 @@ static struct spa_fga_port debug_ports[] = { { .index = 2, .name = "Control", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -FLT_MAX, .max = FLT_MAX }, { .index = 3, .name = "Notify", @@ -2989,7 +3012,7 @@ static struct spa_fga_port noisegate_ports[] = { { .index = 2, .name = "Level", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, - .def = NAN + .def = NAN, .min = -FLT_MAX, .max = FLT_MAX }, { .index = 3, .name = "Open Threshold", @@ -3230,6 +3253,7 @@ static struct spa_fga_port null_ports[] = { { .index = 1, .name = "Control", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = -FLT_MAX, .max = FLT_MAX }, }; diff --git a/spa/plugins/filter-graph/plugin_ebur128.c b/spa/plugins/filter-graph/plugin_ebur128.c index fb79de590..02fbcbe53 100644 --- a/spa/plugins/filter-graph/plugin_ebur128.c +++ b/spa/plugins/filter-graph/plugin_ebur128.c @@ -399,6 +399,7 @@ static struct spa_fga_port lufs2gain_ports[] = { { .index = 0, .name = "LUFS", .flags = SPA_FGA_PORT_INPUT | SPA_FGA_PORT_CONTROL, + .def = 0.0f, .min = 0.0f, .max = FLT_MAX }, { .index = 1, .name = "Gain", diff --git a/spa/plugins/filter-graph/plugin_ladspa.c b/spa/plugins/filter-graph/plugin_ladspa.c index d5c8ef488..6335d002f 100644 --- a/spa/plugins/filter-graph/plugin_ladspa.c +++ b/spa/plugins/filter-graph/plugin_ladspa.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -113,8 +114,14 @@ static void ladspa_port_update_ranges(struct descriptor *dd, struct spa_fga_port LADSPA_PortRangeHintDescriptor hint = d->PortRangeHints[p].HintDescriptor; LADSPA_Data lower, upper; - lower = d->PortRangeHints[p].LowerBound; - upper = d->PortRangeHints[p].UpperBound; + if (hint & LADSPA_HINT_BOUNDED_BELOW) + lower = d->PortRangeHints[p].LowerBound; + else + lower = -FLT_MAX; + if (hint & LADSPA_HINT_BOUNDED_ABOVE) + upper = d->PortRangeHints[p].UpperBound; + else + upper = FLT_MAX; port->hint = 0; if (hint & LADSPA_HINT_TOGGLED) @@ -226,43 +233,49 @@ static inline const char *split_walk(const char *str, const char *delimiter, siz return s; } -static int load_ladspa_plugin(struct plugin *impl, const char *path) +static void make_search_paths(const char **path, const char **search_dirs) +{ + const char *p; + + while ((p = strstr(*path, "../")) != NULL) + *path = p + 3; + + *search_dirs = getenv("LADSPA_PATH"); + if (!*search_dirs) + *search_dirs = "/usr/lib64/ladspa:/usr/lib/ladspa:" LIBDIR; +} + +static int load_ladspa_plugin(struct plugin *impl, const char *path, const char *search_dirs) { int res = -ENOENT; + const char *p, *state = NULL; + char filename[PATH_MAX]; + size_t len; - if (path[0] != '/') { - const char *search_dirs, *p, *state = NULL; - char filename[PATH_MAX]; - size_t len; + /* + * set the errno for the case when `ladspa_handle_load_by_path()` + * is never called, which can only happen if the supplied + * LADSPA_PATH contains too long paths + */ + res = -ENAMETOOLONG; - search_dirs = getenv("LADSPA_PATH"); - if (!search_dirs) - search_dirs = "/usr/lib64/ladspa:/usr/lib/ladspa:" LIBDIR; + while ((p = split_walk(search_dirs, ":", &len, &state))) { + int namelen; - /* - * set the errno for the case when `ladspa_handle_load_by_path()` - * is never called, which can only happen if the supplied - * LADSPA_PATH contains too long paths - */ - res = -ENAMETOOLONG; - - while ((p = split_walk(search_dirs, ":", &len, &state))) { - int namelen; - - if (len >= sizeof(filename)) - continue; + if (len == 0 || len >= sizeof(filename)) + continue; + if (strncmp(path, p, len) == 0 && path[len] == '/') + namelen = snprintf(filename, sizeof(filename), "%s", path); + else namelen = snprintf(filename, sizeof(filename), "%.*s/%s.so", (int) len, p, path); - if (namelen < 0 || (size_t) namelen >= sizeof(filename)) - continue; - res = ladspa_handle_load_by_path(impl, filename); - if (res >= 0) - break; - } - } - else { - res = ladspa_handle_load_by_path(impl, path); + if (namelen < 0 || (size_t) namelen >= sizeof(filename)) + continue; + + res = ladspa_handle_load_by_path(impl, filename); + if (res >= 0) + break; } return res; } @@ -310,7 +323,7 @@ impl_init(const struct spa_handle_factory *factory, struct plugin *impl; uint32_t i; int res; - const char *path = NULL; + const char *path = NULL, *search_dirs; handle->get_interface = impl_get_interface; handle->clear = impl_clear; @@ -328,9 +341,11 @@ impl_init(const struct spa_handle_factory *factory, if (path == NULL) return -EINVAL; - if ((res = load_ladspa_plugin(impl, path)) < 0) { - spa_log_error(impl->log, "failed to load plugin '%s': %s", - path, spa_strerror(res)); + make_search_paths(&path, &search_dirs); + + if ((res = load_ladspa_plugin(impl, path, search_dirs)) < 0) { + spa_log_error(impl->log, "failed to load plugin '%s' in '%s': %s", + path, search_dirs, spa_strerror(res)); return res; } diff --git a/spa/plugins/filter-graph/plugin_lv2.c b/spa/plugins/filter-graph/plugin_lv2.c index 712b728e2..b2d3fc6cc 100644 --- a/spa/plugins/filter-graph/plugin_lv2.c +++ b/spa/plugins/filter-graph/plugin_lv2.c @@ -560,6 +560,17 @@ static const struct spa_fga_descriptor *lv2_plugin_make_desc(void *plugin, const fp->min = mins[i]; fp->max = maxes[i]; fp->def = controls[i]; + + if (isnan(fp->min)) + fp->min = -FLT_MAX; + if (isnan(fp->max)) + fp->max = FLT_MAX; + if (isnan(fp->def)) + fp->def = 0.0f; + if (fp->max <= fp->min) + fp->max = FLT_MAX; + if (fp->def <= fp->min) + fp->min = -FLT_MAX; } return &desc->desc; } diff --git a/spa/plugins/filter-graph/plugin_onnx.c b/spa/plugins/filter-graph/plugin_onnx.c index 3ef12e963..13fccee56 100644 --- a/spa/plugins/filter-graph/plugin_onnx.c +++ b/spa/plugins/filter-graph/plugin_onnx.c @@ -593,6 +593,10 @@ static const struct spa_fga_descriptor *onnx_plugin_make_desc(void *plugin, cons fp->flags |= SPA_FGA_PORT_OUTPUT; fp->name = ti->data_name; + fp->min = -FLT_MAX; + fp->max = FLT_MAX; + fp->def = 0.0f; + ti->data_index = desc->desc.n_ports; desc->desc.n_ports++; diff --git a/spa/plugins/libcamera/libcamera-device.cpp b/spa/plugins/libcamera/libcamera-device.cpp index 2d6532f82..6517860fc 100644 --- a/spa/plugins/libcamera/libcamera-device.cpp +++ b/spa/plugins/libcamera/libcamera-device.cpp @@ -5,6 +5,7 @@ /* SPDX-License-Identifier: MIT */ #include +#include #include #include @@ -25,7 +26,6 @@ #include #include -#include using namespace libcamera; @@ -50,7 +50,7 @@ struct impl { std::string device_id); }; -const libcamera::Span cameraDevice(const Camera& camera) +std::span cameraDevice(const Camera& camera) { if (auto devices = camera.properties().get(properties::SystemDevices)) return devices.value(); diff --git a/spa/plugins/libcamera/libcamera-source.cpp b/spa/plugins/libcamera/libcamera-source.cpp index f0eaa68f0..fba3d8b0b 100644 --- a/spa/plugins/libcamera/libcamera-source.cpp +++ b/spa/plugins/libcamera/libcamera-source.cpp @@ -184,9 +184,6 @@ struct impl { 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) { @@ -194,6 +191,9 @@ struct impl { camera->id().c_str(), spa_strerror(res)); } + if (source.fd >= 0) + spa_system_close(system, std::exchange(source.fd, -1)); + completed_requests_rb = SPA_RINGBUFFER_INIT(); active = false; @@ -2163,7 +2163,7 @@ impl::impl(spa_log *log, spa_loop *data_loop, spa_system *system, &impl_node, this); params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); - params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_WRITE); params[NODE_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); params[NODE_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); diff --git a/spa/plugins/meson.build b/spa/plugins/meson.build index 42aec7ed3..f774c1036 100644 --- a/spa/plugins/meson.build +++ b/spa/plugins/meson.build @@ -1,4 +1,4 @@ -if alsa_dep.found() and host_machine.system() == 'linux' +if alsa_dep.found() subdir('alsa') endif if get_option('avb').require(host_machine.system() == 'linux', error_message: 'AVB support is only available on Linux').allowed() diff --git a/spa/plugins/support/cpu-x86.c b/spa/plugins/support/cpu-x86.c index c1c53855d..0fb866671 100644 --- a/spa/plugins/support/cpu-x86.c +++ b/spa/plugins/support/cpu-x86.c @@ -78,6 +78,8 @@ x86_init(struct impl *impl) if ((ebx & AVX512_BITS) == AVX512_BITS) flags |= SPA_CPU_FLAG_AVX512; } + if (max_level < 0x16) + flags |= SPA_CPU_FLAG_SLOW_GATHER; /* Check cpuid level of extended features. */ __cpuid (0x80000000, ext_level, ebx, ecx, edx); diff --git a/spa/plugins/support/logger.c b/spa/plugins/support/logger.c index c6e6ca4b8..7d0b14c1f 100644 --- a/spa/plugins/support/logger.c +++ b/spa/plugins/support/logger.c @@ -2,12 +2,16 @@ /* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ /* SPDX-License-Identifier: MIT */ +#include "config.h" + #include +#include #include #include #include #include #include +#include #include #include @@ -67,16 +71,10 @@ impl_log_logtv(void *object, const char *fmt, va_list args) { -#define RESERVED_LENGTH 24 - struct impl *impl = object; - char timestamp[18] = {0}; - char topicstr[32] = {0}; - char filename[64] = {0}; - char location[1000 + RESERVED_LENGTH], *p, *s; + char location[1024]; static const char * const levels[] = { "-", "E", "W", "I", "D", "T", "*T*" }; const char *prefix = "", *suffix = ""; - int size, len; bool do_trace; if ((do_trace = (level == SPA_LOG_LEVEL_TRACE && impl->have_source))) @@ -93,8 +91,18 @@ impl_log_logtv(void *object, suffix = SPA_ANSI_RESET; } - p = location; - len = sizeof(location) - RESERVED_LENGTH; + struct spa_strbuf msg; + spa_strbuf_init(&msg, location, sizeof(location)); + + spa_strbuf_append(&msg, "%s[%s]", prefix, levels[level]); + +#ifdef HAVE_GETTID + static thread_local pid_t tid; + if (SPA_UNLIKELY(tid == 0)) + tid = gettid(); + + spa_strbuf_append(&msg, "[%jd]", (intmax_t) tid); +#endif if (impl->local_timestamp) { char buf[64]; @@ -104,67 +112,52 @@ impl_log_logtv(void *object, clock_gettime(impl->clock_id, &now); localtime_r(&now.tv_sec, &now_tm); strftime(buf, sizeof(buf), "%H:%M:%S", &now_tm); - spa_scnprintf(timestamp, sizeof(timestamp), "[%s.%06d]", buf, + spa_strbuf_append(&msg, "[%s.%06d]", buf, (int)(now.tv_nsec / SPA_NSEC_PER_USEC)); } else if (impl->timestamp) { struct timespec now; clock_gettime(impl->clock_id, &now); - spa_scnprintf(timestamp, sizeof(timestamp), "[%05jd.%06jd]", + spa_strbuf_append(&msg, "[%05jd.%06jd]", (intmax_t) (now.tv_sec & 0x1FFFFFFF) % 100000, (intmax_t) now.tv_nsec / 1000); } if (topic && topic->topic) - spa_scnprintf(topicstr, sizeof(topicstr), " %-12s | ", topic->topic); + spa_strbuf_append(&msg, " %-12s | ", topic->topic); if (impl->line && line != 0) { - s = strrchr(file, '/'); - spa_scnprintf(filename, sizeof(filename), "[%16.16s:%5i %s()]", + const char *s = strrchr(file, '/'); + spa_strbuf_append(&msg, "[%16.16s:%5i %s()]", s ? s + 1 : file, line, func); } - size = spa_scnprintf(p, len, "%s[%s]%s%s%s ", prefix, levels[level], - timestamp, topicstr, filename); - /* - * it is assumed that at this point `size` <= `len`, - * which is reasonable as long as file names and function names - * don't become very long - */ - size += spa_vscnprintf(p + size, len - size, fmt, args); + spa_strbuf_append(&msg, " "); + spa_strbuf_appendv(&msg, fmt, args); + spa_strbuf_append(&msg, "%s\n", suffix); - /* - * `RESERVED_LENGTH` bytes are reserved for printing the suffix - * (at the moment it's "... (truncated)\x1B[0m\n" at its longest - 21 bytes), - * its length must be less than `RESERVED_LENGTH` (including the null byte), - * otherwise a stack buffer overrun could ensue - */ + if (SPA_UNLIKELY(msg.pos >= msg.maxsize)) { + static const char truncated_text[] = "... (truncated)"; + size_t suffix_length = strlen(suffix) + strlen(truncated_text) + 1 + 1; - /* if the message could not fit entirely... */ - if (size >= len - 1) { - size = len - 1; /* index of the null byte */ - len = sizeof(location); - size += spa_scnprintf(p + size, len - size, "... (truncated)"); + spa_assert(msg.maxsize >= suffix_length); + msg.pos = msg.maxsize - suffix_length; + + spa_strbuf_append(&msg, "%s%s\n", truncated_text, suffix); + spa_assert(msg.pos < msg.maxsize); } - else { - len = sizeof(location); - } - - size += spa_scnprintf(p + size, len - size, "%s\n", suffix); if (SPA_UNLIKELY(do_trace)) { uint32_t index; spa_ringbuffer_get_write_index(&impl->trace_rb, &index); spa_ringbuffer_write_data(&impl->trace_rb, impl->trace_data, TRACE_BUFFER, - index & (TRACE_BUFFER - 1), location, size); - spa_ringbuffer_write_update(&impl->trace_rb, index + size); + index & (TRACE_BUFFER - 1), msg.buffer, msg.pos); + spa_ringbuffer_write_update(&impl->trace_rb, index + msg.pos); if (spa_system_eventfd_write(impl->system, impl->source.fd, 1) < 0) fprintf(impl->file, "error signaling eventfd: %s\n", strerror(errno)); } else - fputs(location, impl->file); - -#undef RESERVED_LENGTH + fputs(msg.buffer, impl->file); } static SPA_PRINTF_FUNC(6,0) void diff --git a/spa/plugins/support/loop.c b/spa/plugins/support/loop.c index e5e49849f..5e35f6dcd 100644 --- a/spa/plugins/support/loop.c +++ b/spa/plugins/support/loop.c @@ -462,12 +462,12 @@ again: * this invoking thread but we need to serialize the flushing here with * a mutex */ if (loop_thread == 0) - pthread_mutex_lock(&impl->lock); + spa_assert_se(pthread_mutex_lock(&impl->lock) == 0); flush_all_queues(impl); if (loop_thread == 0) - pthread_mutex_unlock(&impl->lock); + spa_assert_se(pthread_mutex_unlock(&impl->lock) == 0); res = item->res; } else { @@ -482,9 +482,9 @@ again: recurse = impl->recurse; while (impl->recurse > 0) { impl->recurse--; - pthread_mutex_unlock(&impl->lock); + spa_assert_se(pthread_mutex_unlock(&impl->lock) == 0); } - pthread_mutex_unlock(&impl->lock); + spa_assert_se(pthread_mutex_unlock(&impl->lock) == 0); } if ((res = spa_system_eventfd_read(impl->system, queue->ack_fd, &count)) < 0) @@ -492,7 +492,7 @@ again: queue, queue->ack_fd, spa_strerror(res)); for (i = 0; i < recurse; i++) { - pthread_mutex_lock(&impl->lock); + spa_assert_se(pthread_mutex_lock(&impl->lock) == 0); impl->recurse++; } @@ -569,9 +569,14 @@ static int loop_locked(void *object, spa_invoke_func_t func, uint32_t seq, { struct impl *impl = object; int res; - pthread_mutex_lock(&impl->lock); + + res = pthread_mutex_lock(&impl->lock); + if (res) + return -res; + res = func(&impl->loop, false, seq, data, size, user_data); - pthread_mutex_unlock(&impl->lock); + spa_assert_se(pthread_mutex_unlock(&impl->lock) == 0); + return res; } @@ -598,7 +603,7 @@ static void loop_enter(void *object) struct impl *impl = object; pthread_t thread_id = pthread_self(); - pthread_mutex_lock(&impl->lock); + spa_assert_se(pthread_mutex_lock(&impl->lock) == 0); if (impl->enter_count == 0) { spa_return_if_fail(impl->thread == 0); impl->thread = thread_id; @@ -625,7 +630,7 @@ static void loop_leave(void *object) impl->thread = 0; flush_all_queues(impl); } - pthread_mutex_unlock(&impl->lock); + spa_assert_se(pthread_mutex_unlock(&impl->lock) == 0); } static int loop_check(void *object) @@ -644,7 +649,7 @@ static int loop_check(void *object) /* we could take the lock, check if we actually locked it somewhere */ res = impl->recurse > 0 ? 1 : -EPERM; - pthread_mutex_unlock(&impl->lock); + spa_assert_se(pthread_mutex_unlock(&impl->lock) == 0); return res; } static int loop_lock(void *object) @@ -722,13 +727,20 @@ static int loop_accept(void *object) } struct cancellation_handler_data { - struct spa_poll_event *ep; - int ep_count; + struct impl *impl; + const struct spa_poll_event *ep; + volatile int ep_count; + volatile int unlocked; + volatile int locked; }; static void cancellation_handler(void *closure) { const struct cancellation_handler_data *data = closure; + struct impl *impl = data->impl; + + if (data->unlocked && !data->locked) + spa_assert_se(pthread_mutex_lock(&impl->lock) == 0); for (int i = 0; i < data->ep_count; i++) { struct spa_source *s = data->ep[i].data; @@ -745,20 +757,24 @@ static int loop_iterate_cancel(void *object, int timeout) struct spa_poll_event ep[MAX_EP], *e; int i, nfds; uint32_t remove_count; + struct cancellation_handler_data cdata = { impl, ep, 0, 0, 0 }; + + spa_return_val_if_fail(impl->enter_count > 0, -EPERM); + + pthread_cleanup_push(cancellation_handler, &cdata); remove_count = impl->remove_count; spa_loop_control_hook_before(&impl->hooks_list); - pthread_mutex_unlock(&impl->lock); + spa_assert_se((cdata.unlocked = (pthread_mutex_unlock(&impl->lock) == 0))); nfds = spa_system_pollfd_wait(impl->system, impl->poll_fd, ep, SPA_N_ELEMENTS(ep), timeout); - pthread_mutex_lock(&impl->lock); + spa_assert_se((cdata.locked = (pthread_mutex_lock(&impl->lock) == 0))); spa_loop_control_hook_after(&impl->hooks_list); if (remove_count != impl->remove_count) nfds = 0; - struct cancellation_handler_data cdata = { ep, nfds }; - pthread_cleanup_push(cancellation_handler, &cdata); + cdata.ep_count = nfds; /* 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 @@ -794,13 +810,15 @@ static int loop_iterate(void *object, int timeout) int i, nfds; uint32_t remove_count; + spa_return_val_if_fail(impl->enter_count > 0, -EPERM); + remove_count = impl->remove_count; spa_loop_control_hook_before(&impl->hooks_list); - pthread_mutex_unlock(&impl->lock); + spa_assert_se(pthread_mutex_unlock(&impl->lock) == 0); nfds = spa_system_pollfd_wait(impl->system, impl->poll_fd, ep, SPA_N_ELEMENTS(ep), timeout); - pthread_mutex_lock(&impl->lock); + spa_assert_se(pthread_mutex_lock(&impl->lock) == 0); spa_loop_control_hook_after(&impl->hooks_list); if (remove_count != impl->remove_count) return 0; diff --git a/spa/plugins/support/node-driver.c b/spa/plugins/support/node-driver.c index fa9cf3426..f0585a711 100644 --- a/spa/plugins/support/node-driver.c +++ b/spa/plugins/support/node-driver.c @@ -91,7 +91,6 @@ struct impl { struct spa_io_clock *clock; struct spa_source timer_source; - struct itimerspec timerspec; int clock_fd; bool started; @@ -182,13 +181,16 @@ static void set_timeout(struct impl *this, uint64_t next_time) * 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.) */ + struct itimerspec ts; 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; + ts.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = next_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->timer_source.fd, SPA_FD_TIMER_ABSTIME | - SPA_FD_TIMER_CANCEL_ON_SET, &this->timerspec, NULL); + SPA_FD_TIMER_CANCEL_ON_SET, &ts, NULL); } static inline uint64_t gettime_nsec(struct impl *this, clockid_t clock_id) @@ -1043,10 +1045,6 @@ impl_init(const struct spa_handle_factory *factory, this->timer_source.mask = SPA_IO_IN; this->timer_source.rmask = 0; - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - this->timerspec.it_interval.tv_sec = 0; - this->timerspec.it_interval.tv_nsec = 0; spa_loop_add_source(this->data_loop, &this->timer_source); diff --git a/spa/plugins/support/null-audio-sink.c b/spa/plugins/support/null-audio-sink.c index b804023b3..610adf9ce 100644 --- a/spa/plugins/support/null-audio-sink.c +++ b/spa/plugins/support/null-audio-sink.c @@ -114,7 +114,6 @@ struct impl { unsigned int started:1; unsigned int following:1; struct spa_source timer_source; - struct itimerspec timerspec; uint64_t next_time; }; @@ -179,11 +178,15 @@ static int impl_node_enum_params(void *object, int seq, static void set_timeout(struct impl *this, uint64_t next_time) { + struct itimerspec ts; + 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; + ts.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + ts.it_value.tv_nsec = next_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->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &ts, NULL); } static int set_timers(struct impl *this) @@ -929,10 +932,6 @@ impl_init(const struct spa_handle_factory *factory, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); this->timer_source.mask = SPA_IO_IN; this->timer_source.rmask = 0; - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - this->timerspec.it_interval.tv_sec = 0; - this->timerspec.it_interval.tv_nsec = 0; spa_loop_add_source(this->data_loop, &this->timer_source); diff --git a/spa/plugins/support/system.c b/spa/plugins/support/system.c index 767e0b43d..cd9e1c6eb 100644 --- a/spa/plugins/support/system.c +++ b/spa/plugins/support/system.c @@ -30,6 +30,10 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.system"); # define TFD_TIMER_CANCEL_ON_SET (1 << 1) #endif +SPA_STATIC_ASSERT(sizeof(struct spa_poll_event) == sizeof(struct epoll_event)); +SPA_STATIC_ASSERT(offsetof(struct spa_poll_event, events) == offsetof(struct epoll_event, events)); +SPA_STATIC_ASSERT(offsetof(struct spa_poll_event, data) == offsetof(struct epoll_event, data.ptr)); + struct impl { struct spa_handle handle; struct spa_system system; @@ -132,16 +136,9 @@ static int impl_pollfd_del(void *object, int pfd, int fd) static int impl_pollfd_wait(void *object, int pfd, struct spa_poll_event *ev, int n_ev, int timeout) { - struct epoll_event ep[n_ev]; - int i, nfds; - - if (SPA_UNLIKELY((nfds = epoll_wait(pfd, ep, n_ev, timeout)) < 0)) + int nfds; + if (SPA_UNLIKELY((nfds = epoll_wait(pfd, (struct epoll_event*)ev, n_ev, timeout)) < 0)) return -errno; - - for (i = 0; i < nfds; i++) { - ev[i].events = ep[i].events; - ev[i].data = ep[i].data.ptr; - } return nfds; } diff --git a/spa/plugins/test/fakesink.c b/spa/plugins/test/fakesink.c index 71550300c..31667a1de 100644 --- a/spa/plugins/test/fakesink.c +++ b/spa/plugins/test/fakesink.c @@ -26,10 +26,6 @@ #define SPA_LOG_TOPIC_DEFAULT &log_topic SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.fakesink"); -struct props { - bool live; -}; - #define MAX_BUFFERS 16 #define MAX_PORTS 1 @@ -68,13 +64,11 @@ struct impl { uint64_t info_all; struct spa_node_info info; struct spa_param_info params[1]; - struct props props; struct spa_hook_list hooks; struct spa_callbacks callbacks; struct spa_source timer_source; - struct itimerspec timerspec; bool started; uint64_t start_time; @@ -87,13 +81,6 @@ struct impl { #define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS) -#define DEFAULT_LIVE false - -static void reset_props(struct impl *this, struct props *props) -{ - props->live = DEFAULT_LIVE; -} - static int impl_node_enum_params(void *object, int seq, uint32_t id, uint32_t start, uint32_t num, const struct spa_pod *filter) @@ -116,14 +103,6 @@ static int impl_node_enum_params(void *object, int seq, spa_pod_builder_init(&b, buffer, sizeof(buffer)); switch (id) { - case SPA_PARAM_Props: - if (result.index > 0) - return 0; - - param = spa_pod_builder_add_object(&b, - SPA_TYPE_OBJECT_Props, id, - SPA_PROP_live, SPA_POD_Bool(this->props.live)); - break; default: return -ENOENT; } @@ -151,26 +130,7 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, spa_return_val_if_fail(this != NULL, -EINVAL); - switch (id) { - case SPA_PARAM_Props: - { - struct port *port = &this->port; - - if (param == NULL) { - reset_props(this, &this->props); - return 0; - } - spa_pod_parse_object(param, - SPA_TYPE_OBJECT_Props, NULL, - SPA_PROP_live, SPA_POD_OPT_Bool(&this->props.live)); - - if (this->props.live) - port->info.flags |= SPA_PORT_FLAG_LIVE; - else - port->info.flags &= ~SPA_PORT_FLAG_LIVE; - break; - } default: return -ENOENT; } @@ -179,23 +139,15 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, static void set_timer(struct impl *this, bool enabled) { - if (this->callbacks.funcs || this->props.live) { - if (enabled) { - if (this->props.live) { - uint64_t next_time = this->start_time + this->elapsed_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; - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 1; - } - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - } - spa_system_timerfd_settime(this->data_system, - this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + struct itimerspec ts = {0}; + + if (enabled) { + uint64_t next_time = this->start_time + this->elapsed_time; + ts.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + ts.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, &ts, NULL); } static inline int read_timer(struct impl *this) @@ -203,14 +155,12 @@ static inline int read_timer(struct impl *this) uint64_t expirations; int res = 0; - if (this->callbacks.funcs || this->props.live) { - if ((res = spa_system_timerfd_read(this->data_system, - this->timer_source.fd, &expirations)) < 0) { - if (res != -EAGAIN) - spa_log_error(this->log, "%p: timerfd error: %s", - this, spa_strerror(res)); - } + if ((res = spa_system_timerfd_read(this->data_system, this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, "%p: timerfd error: %s", + this, spa_strerror(res)); } + return res; } @@ -298,10 +248,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman return 0; clock_gettime(CLOCK_MONOTONIC, &now); - if (this->props.live) - this->start_time = SPA_TIMESPEC_TO_NSEC(&now); - else - this->start_time = 0; + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); this->buffer_count = 0; this->elapsed_time = 0; @@ -651,10 +598,8 @@ static int impl_node_process(void *object) io->buffer_id = SPA_ID_INVALID; io->status = SPA_STATUS_OK; } - if (this->callbacks.funcs == NULL) - return consume_buffer(this); - else - return SPA_STATUS_OK; + + return SPA_STATUS_OK; } static const struct spa_node_methods impl_node = { @@ -758,8 +703,6 @@ impl_init(const struct spa_handle_factory *factory, this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); this->info.params = this->params; this->info.n_params = 1; - reset_props(this, &this->props); - this->timer_source.func = on_input; this->timer_source.data = this; @@ -767,10 +710,6 @@ impl_init(const struct spa_handle_factory *factory, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); this->timer_source.mask = SPA_IO_IN; this->timer_source.rmask = 0; - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - this->timerspec.it_interval.tv_sec = 0; - this->timerspec.it_interval.tv_nsec = 0; if (this->data_loop) spa_loop_add_source(this->data_loop, &this->timer_source); @@ -779,9 +718,7 @@ impl_init(const struct spa_handle_factory *factory, 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_NO_REF; - if (this->props.live) - port->info.flags |= SPA_PORT_FLAG_LIVE; + port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_LIVE; port->params[0] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); port->params[1] = SPA_PARAM_INFO(SPA_PARAM_IO, 0); port->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); diff --git a/spa/plugins/test/fakesrc.c b/spa/plugins/test/fakesrc.c index 28b37dab4..db28d9c3c 100644 --- a/spa/plugins/test/fakesrc.c +++ b/spa/plugins/test/fakesrc.c @@ -27,7 +27,6 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.fakesrc"); struct props { - bool live; uint32_t pattern; }; @@ -75,7 +74,6 @@ struct impl { struct spa_callbacks callbacks; struct spa_source timer_source; - struct itimerspec timerspec; bool started; uint64_t start_time; @@ -89,12 +87,10 @@ struct impl { #define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_OUTPUT && (p) < MAX_PORTS) -#define DEFAULT_LIVE false #define DEFAULT_PATTERN 0 static void reset_props(struct impl *this, struct props *props) { - props->live = DEFAULT_LIVE; props->pattern = DEFAULT_PATTERN; } @@ -129,7 +125,6 @@ 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_Int(2, p->pattern, p->pattern)); break; } @@ -164,7 +159,6 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, case SPA_PARAM_Props: { struct props *p = &this->props; - struct port *port = &this->port; if (param == NULL) { reset_props(this, p); @@ -172,13 +166,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_Int(&p->pattern)); - - if (p->live) - port->info.flags |= SPA_PORT_FLAG_LIVE; - else - port->info.flags &= ~SPA_PORT_FLAG_LIVE; break; } default: @@ -194,23 +182,15 @@ static int fill_buffer(struct impl *this, struct buffer *b) static void set_timer(struct impl *this, bool enabled) { - if (this->callbacks.funcs || this->props.live) { - if (enabled) { - if (this->props.live) { - uint64_t next_time = this->start_time + this->elapsed_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; - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 1; - } - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - } - spa_system_timerfd_settime(this->data_system, - this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + struct itimerspec ts = {0}; + + if (enabled) { + uint64_t next_time = this->start_time + this->elapsed_time; + ts.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + ts.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, &ts, NULL); } static inline int read_timer(struct impl *this) @@ -218,14 +198,12 @@ static inline int read_timer(struct impl *this) uint64_t expirations; int res = 0; - if (this->callbacks.funcs || this->props.live) { - if ((res = spa_system_timerfd_read(this->data_system, - this->timer_source.fd, &expirations)) < 0) { - if (res != -EAGAIN) - spa_log_error(this->log, "%p: timerfd error: %s", - this, spa_strerror(res)); - } + if ((res = spa_system_timerfd_read(this->data_system, this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, "%p: timerfd error: %s", + this, spa_strerror(res)); } + return res; } @@ -311,10 +289,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman return 0; clock_gettime(CLOCK_MONOTONIC, &now); - if (this->props.live) - this->start_time = SPA_TIMESPEC_TO_NSEC(&now); - else - this->start_time = 0; + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); this->buffer_count = 0; this->elapsed_time = 0; @@ -682,10 +657,7 @@ static int impl_node_process(void *object) io->buffer_id = SPA_ID_INVALID; } - if (this->callbacks.funcs == NULL) - return make_buffer(this); - else - return SPA_STATUS_OK; + return SPA_STATUS_OK; } static const struct spa_node_methods impl_node = { @@ -797,10 +769,6 @@ impl_init(const struct spa_handle_factory *factory, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); this->timer_source.mask = SPA_IO_IN; this->timer_source.rmask = 0; - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - this->timerspec.it_interval.tv_sec = 0; - this->timerspec.it_interval.tv_nsec = 0; if (this->data_loop) spa_loop_add_source(this->data_loop, &this->timer_source); @@ -809,9 +777,7 @@ impl_init(const struct spa_handle_factory *factory, 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_NO_REF; - if (this->props.live) - port->info.flags |= SPA_PORT_FLAG_LIVE; + port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_LIVE; port->params[0] = SPA_PARAM_INFO(SPA_PARAM_Meta, SPA_PARAM_INFO_READ); port->params[1] = SPA_PARAM_INFO(SPA_PARAM_IO, 0); port->params[2] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); diff --git a/spa/plugins/v4l2/v4l2-device.c b/spa/plugins/v4l2/v4l2-device.c index 2379d2105..f29252d16 100644 --- a/spa/plugins/v4l2/v4l2-device.c +++ b/spa/plugins/v4l2/v4l2-device.c @@ -98,9 +98,9 @@ static int emit_info(struct impl *this, bool full) (this->dev.cap.version >> 8) & 0xFF, (this->dev.cap.version) & 0xFF); ADD_ITEM(SPA_KEY_API_V4L2_CAP_VERSION, version); - snprintf(capabilities, sizeof(capabilities), "%08x", this->dev.cap.capabilities); + snprintf(capabilities, sizeof(capabilities), "0x%08x", this->dev.cap.capabilities); ADD_ITEM(SPA_KEY_API_V4L2_CAP_CAPABILITIES, capabilities); - snprintf(device_caps, sizeof(device_caps), "%08x", this->dev.cap.device_caps); + snprintf(device_caps, sizeof(device_caps), "0x%08x", this->dev.cap.device_caps); ADD_ITEM(SPA_KEY_API_V4L2_CAP_DEVICE_CAPS, device_caps); #undef ADD_ITEM info.props = &SPA_DICT_INIT(items, n_items); diff --git a/spa/plugins/videotestsrc/videotestsrc.c b/spa/plugins/videotestsrc/videotestsrc.c index db5c90113..a8d7059c8 100644 --- a/spa/plugins/videotestsrc/videotestsrc.c +++ b/spa/plugins/videotestsrc/videotestsrc.c @@ -35,17 +35,14 @@ enum pattern { PATTERN_SNOW, }; -#define DEFAULT_LIVE true #define DEFAULT_PATTERN PATTERN_SMPTE_SNOW struct props { - bool live; uint32_t pattern; }; static void reset_props(struct props *props) { - props->live = DEFAULT_LIVE; props->pattern = DEFAULT_PATTERN; } @@ -97,9 +94,7 @@ struct impl { struct spa_hook_list hooks; struct spa_callbacks callbacks; - bool async; struct spa_source *timer_source; - struct itimerspec timerspec; bool started; uint64_t start_time; @@ -141,13 +136,6 @@ static int impl_node_enum_params(void *object, int seq, 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_live), - SPA_PROP_INFO_description, SPA_POD_String("Configure live mode of the source"), - SPA_PROP_INFO_type, SPA_POD_Bool(p->live)); - break; - case 1: spa_pod_builder_push_object(&b, &f[0], SPA_TYPE_OBJECT_PropInfo, id); spa_pod_builder_add(&b, SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_patternType), @@ -176,7 +164,6 @@ static int impl_node_enum_params(void *object, int seq, case 0: 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_Int(p->pattern)); break; default: @@ -231,7 +218,6 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, case SPA_PARAM_Props: { struct props *p = &this->props; - struct port *port = &this->port; if (param == NULL) { reset_props(p); @@ -239,13 +225,8 @@ 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_Int(&p->pattern)); - if (p->live) - port->info.flags |= SPA_PORT_FLAG_LIVE; - else - port->info.flags &= ~SPA_PORT_FLAG_LIVE; break; } default: @@ -263,22 +244,15 @@ static int fill_buffer(struct impl *this, struct buffer *b) static void set_timer(struct impl *this, bool enabled) { - if (this->async || this->props.live) { - if (enabled) { - if (this->props.live) { - uint64_t next_time = this->start_time + this->elapsed_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; - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 1; - } - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - } - spa_loop_utils_update_timer(this->loop_utils, this->timer_source, &this->timerspec.it_value, &this->timerspec.it_interval, true); + struct timespec ts = {0}; + + if (enabled) { + uint64_t next_time = this->start_time + this->elapsed_time; + ts.tv_sec = next_time / SPA_NSEC_PER_SEC; + ts.tv_nsec = next_time % SPA_NSEC_PER_SEC; } + + spa_loop_utils_update_timer(this->loop_utils, this->timer_source, &ts, NULL, true); } static int make_buffer(struct impl *this) @@ -358,10 +332,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman return 0; clock_gettime(CLOCK_MONOTONIC, &now); - if (this->props.live) - this->start_time = SPA_TIMESPEC_TO_NSEC(&now); - else - this->start_time = 0; + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); this->frame_count = 0; this->elapsed_time = 0; @@ -755,9 +726,6 @@ static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t i b->outstanding = false; spa_list_append(&port->empty, &b->link); - - if (!this->props.live) - set_timer(this, true); } static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) @@ -795,10 +763,7 @@ static int impl_node_process(void *object) io->buffer_id = SPA_ID_INVALID; } - if (!this->props.live) - return make_buffer(this); - else - return SPA_STATUS_OK; + return SPA_STATUS_OK; } static const struct spa_node_methods impl_node = { @@ -907,18 +872,12 @@ impl_init(const struct spa_handle_factory *factory, reset_props(&this->props); this->timer_source = spa_loop_utils_add_timer(this->loop_utils, on_output, this); - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - this->timerspec.it_interval.tv_sec = 0; - this->timerspec.it_interval.tv_nsec = 0; 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_NO_REF; - if (this->props.live) - port->info.flags |= SPA_PORT_FLAG_LIVE; + port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_LIVE; 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/vulkan/vulkan-blit-utils.c b/spa/plugins/vulkan/vulkan-blit-utils.c index f8a60497e..e0bd6cfe5 100644 --- a/spa/plugins/vulkan/vulkan-blit-utils.c +++ b/spa/plugins/vulkan/vulkan-blit-utils.c @@ -12,8 +12,10 @@ #include #include #include -#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) +#ifdef __linux__ #include +#else +#include #endif #include #include diff --git a/spa/plugins/vulkan/vulkan-compute-source.c b/spa/plugins/vulkan/vulkan-compute-source.c index 1daf3b47c..aa6f4a60f 100644 --- a/spa/plugins/vulkan/vulkan-compute-source.c +++ b/spa/plugins/vulkan/vulkan-compute-source.c @@ -33,17 +33,6 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.vulkan.compute-source"); #define FRAMES_TO_TIME(this,f) ((this->position->video.framerate.denom * (f) * SPA_NSEC_PER_SEC) / \ (this->position->video.framerate.num)) -#define DEFAULT_LIVE true - -struct props { - bool live; -}; - -static void reset_props(struct props *props) -{ - props->live = DEFAULT_LIVE; -} - struct buffer { uint32_t id; #define BUFFER_FLAG_OUT (1<<0) @@ -95,14 +84,11 @@ struct impl { #define IDX_Props 1 #define N_NODE_PARAMS 2 struct spa_param_info params[N_NODE_PARAMS]; - struct props props; struct spa_hook_list hooks; struct spa_callbacks callbacks; - bool async; struct spa_source timer_source; - struct itimerspec timerspec; bool started; uint64_t start_time; @@ -138,38 +124,6 @@ static int impl_node_enum_params(void *object, int seq, 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_live), - SPA_PROP_INFO_description, SPA_POD_String("Configure live mode of the source"), - SPA_PROP_INFO_type, SPA_POD_Bool(p->live)); - 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_live, SPA_POD_Bool(p->live)); - break; - default: - return 0; - } - break; - } default: return -ENOENT; } @@ -214,25 +168,6 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, spa_return_val_if_fail(this != NULL, -EINVAL); switch (id) { - case SPA_PARAM_Props: - { - struct props *p = &this->props; - struct port *port = &this->port; - - if (param == NULL) { - reset_props(p); - return 0; - } - spa_pod_parse_object(param, - SPA_TYPE_OBJECT_Props, NULL, - SPA_PROP_live, SPA_POD_OPT_Bool(&p->live)); - - if (p->live) - port->info.flags |= SPA_PORT_FLAG_LIVE; - else - port->info.flags &= ~SPA_PORT_FLAG_LIVE; - break; - } default: return -ENOENT; } @@ -242,23 +177,15 @@ static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, static void set_timer(struct impl *this, bool enabled) { - if (this->async || this->props.live) { - if (enabled) { - if (this->props.live) { - uint64_t next_time = this->start_time + this->elapsed_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; - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 1; - } - } else { - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - } - spa_system_timerfd_settime(this->data_system, - this->timer_source.fd, SPA_FD_TIMER_ABSTIME, &this->timerspec, NULL); + struct itimerspec ts = {0}; + + if (enabled) { + uint64_t next_time = this->start_time + this->elapsed_time; + ts.it_value.tv_sec = next_time / SPA_NSEC_PER_SEC; + ts.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, &ts, NULL); } static int read_timer(struct impl *this) @@ -266,14 +193,12 @@ static int read_timer(struct impl *this) uint64_t expirations; int res = 0; - if (this->async || this->props.live) { - if ((res = spa_system_timerfd_read(this->data_system, - this->timer_source.fd, &expirations)) < 0) { - if (res != -EAGAIN) - spa_log_error(this->log, "%p: timerfd error: %s", - this, spa_strerror(res)); - } + if ((res = spa_system_timerfd_read(this->data_system, this->timer_source.fd, &expirations)) < 0) { + if (res != -EAGAIN) + spa_log_error(this->log, "%p: timerfd error: %s", + this, spa_strerror(res)); } + return res; } @@ -348,9 +273,6 @@ static inline void reuse_buffer(struct impl *this, struct port *port, uint32_t i SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_OUT); spa_list_append(&port->empty, &b->link); - - if (!this->props.live) - set_timer(this, true); } } @@ -409,10 +331,7 @@ static int impl_node_send_command(void *object, const struct spa_command *comman return 0; clock_gettime(CLOCK_MONOTONIC, &now); - if (this->props.live) - this->start_time = SPA_TIMESPEC_TO_NSEC(&now); - else - this->start_time = 0; + this->start_time = SPA_TIMESPEC_TO_NSEC(&now); this->frame_count = 0; this->elapsed_time = 0; @@ -874,10 +793,7 @@ static int impl_node_process(void *object) io->buffer_id = SPA_ID_INVALID; } - if (!this->props.live) - return make_buffer(this); - else - return SPA_STATUS_OK; + return SPA_STATUS_OK; } static const struct spa_node_methods impl_node = { @@ -985,7 +901,6 @@ impl_init(const struct spa_handle_factory *factory, 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; - reset_props(&this->props); this->timer_source.func = on_output; this->timer_source.data = this; @@ -993,10 +908,6 @@ impl_init(const struct spa_handle_factory *factory, SPA_FD_CLOEXEC | SPA_FD_NONBLOCK); this->timer_source.mask = SPA_IO_IN; this->timer_source.rmask = 0; - this->timerspec.it_value.tv_sec = 0; - this->timerspec.it_value.tv_nsec = 0; - this->timerspec.it_interval.tv_sec = 0; - this->timerspec.it_interval.tv_nsec = 0; if (this->data_loop) spa_loop_add_source(this->data_loop, &this->timer_source); @@ -1006,9 +917,7 @@ impl_init(const struct spa_handle_factory *factory, SPA_PORT_CHANGE_MASK_PARAMS | SPA_PORT_CHANGE_MASK_PROPS; port->info = SPA_PORT_INFO_INIT(); - port->info.flags = SPA_PORT_FLAG_NO_REF; - if (this->props.live) - port->info.flags |= SPA_PORT_FLAG_LIVE; + port->info.flags = SPA_PORT_FLAG_NO_REF | SPA_PORT_FLAG_LIVE; 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); diff --git a/spa/plugins/vulkan/vulkan-compute-utils.c b/spa/plugins/vulkan/vulkan-compute-utils.c index 49705bee8..503542483 100644 --- a/spa/plugins/vulkan/vulkan-compute-utils.c +++ b/spa/plugins/vulkan/vulkan-compute-utils.c @@ -11,8 +11,10 @@ #include #include #include -#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) +#ifdef __linux__ #include +#else +#include #endif #include #include diff --git a/spa/plugins/vulkan/vulkan-utils.c b/spa/plugins/vulkan/vulkan-utils.c index 6a0f693dc..88a230dd6 100644 --- a/spa/plugins/vulkan/vulkan-utils.c +++ b/spa/plugins/vulkan/vulkan-utils.c @@ -11,8 +11,10 @@ #include #include #include -#if !defined(__FreeBSD__) && !defined(__MidnightBSD__) +#ifdef __linux__ #include +#else +#include #endif #include #include diff --git a/spa/tests/benchmark-aec.c b/spa/tests/benchmark-aec.c index 3ac0fc62e..37d4a8375 100644 --- a/spa/tests/benchmark-aec.c +++ b/spa/tests/benchmark-aec.c @@ -6,7 +6,6 @@ #include #include -#include #include #include #include diff --git a/spa/tools/spa-json-dump.c b/spa/tools/spa-json-dump.c index ee3b42da7..ed81330d4 100644 --- a/spa/tools/spa-json-dump.c +++ b/spa/tools/spa-json-dump.c @@ -15,27 +15,28 @@ #include #include +#include #include #define DEFAULT_INDENT 2 struct data { const char *filename; - FILE *file; + + FILE *out; + struct spa_json_builder builder; void *data; size_t size; - - int indent; - bool simple_string; - const char *comma; - const char *key_sep; }; -#define OPTIONS "hi:s" +#define OPTIONS "hNC:Ri:s" static const struct option long_options[] = { { "help", no_argument, NULL, 'h'}, + { "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' }, @@ -53,72 +54,21 @@ static void show_usage(struct data *d, const char *name, bool is_error) " -h, --help Show this help\n" "\n"); fprintf(fp, + " -N, --no-colors disable color output\n" + " -C, --color[=WHEN] whether to enable color support. WHEN is `never`, `always`, or `auto`\n" + " -R, --raw force raw output\n" " -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) +static int dump(struct data *d, struct spa_json *it, const char *key, const char *value, 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]; - switch (v) { - case '\n': - fprintf(f, "\\n"); - break; - case '\r': - fprintf(f, "\\r"); - break; - case '\b': - fprintf(f, "\\b"); - break; - case '\t': - fprintf(f, "\\t"); - break; - case '\f': - fprintf(f, "\\f"); - break; - case '\\': case '"': - fprintf(f, "\\%c", v); - break; - default: - if (v > 0 && v < 0x20) - fprintf(f, "\\u%04x", v); - else - fprintf(f, "%c", v); - break; - } - } - fprintf(f, "\""); -} - -static int dump(struct data *d, int indent, struct spa_json *it, const char *value, int len) -{ - FILE *file = d->file; + struct spa_json_builder *b = &d->builder; struct spa_json sub; bool toplevel = false; - int count = 0, res; - char key[1024]; + int res; if (!value) { toplevel = true; @@ -127,29 +77,22 @@ static int dump(struct data *d, int indent, struct spa_json *it, const char *val } if (spa_json_is_array(value, len)) { - fprintf(file, "["); + spa_json_builder_object_push(b, key, "["); spa_json_enter(it, &sub); while ((len = spa_json_next(&sub, &value)) > 0) { - fprintf(file, "%s\n%*s", count++ > 0 ? d->comma : "", - indent+d->indent, ""); - if ((res = dump(d, indent+d->indent, &sub, value, len)) < 0) + if ((res = dump(d, &sub, NULL, value, len)) < 0) return res; } - fprintf(file, "%s%*s]", count > 0 ? "\n" : "", - count > 0 ? indent : 0, ""); + spa_json_builder_pop(b, "]"); } else if (spa_json_is_object(value, len)) { - fprintf(file, "{"); + char k[1024]; + spa_json_builder_object_push(b, key, "{"); if (!toplevel) spa_json_enter(it, &sub); else sub = *it; - while ((len = spa_json_object_next(&sub, key, sizeof(key), &value)) > 0) { - fprintf(file, "%s\n%*s", - 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); + while ((len = spa_json_object_next(&sub, k, sizeof(k), &value)) > 0) { + res = dump(d, &sub, k, value, len); if (res < 0) { if (toplevel) *it = sub; @@ -158,18 +101,10 @@ static int dump(struct data *d, int indent, struct spa_json *it, const char *val } if (toplevel) *it = sub; - fprintf(file, "%s%*s}", count > 0 ? "\n" : "", - count > 0 ? indent : 0, ""); - } else if (spa_json_is_string(value, len) || - spa_json_is_null(value, len) || - spa_json_is_bool(value, len) || - spa_json_is_int(value, len) || - spa_json_is_float(value, len)) { - fprintf(file, "%.*s", len, value); + spa_json_builder_pop(b, "}"); } else { - encode_string(d, value, len); + spa_json_builder_add_simple(b, key, INT_MAX, 0, value, len); } - if (spa_json_get_error(it, NULL, NULL)) return -EINVAL; @@ -192,12 +127,12 @@ static int process_json(struct data *d) len = 0; } - res = dump(d, 0, &it, value, len); + res = dump(d, &it, NULL, value, len); if (spa_json_next(&it, &value) < 0) res = -EINVAL; - fprintf(d->file, "\n"); - fflush(d->file); + fprintf(d->builder.f, "\n"); + fflush(d->builder.f); if (res < 0) { struct spa_error_location loc; @@ -257,30 +192,50 @@ int main(int argc, char *argv[]) int c; int longopt_index = 0; int fd, res, exit_code = EXIT_FAILURE; + int flags = 0, indent = -1; struct data d; struct stat sbuf; + bool raw = false, colors = false; spa_zero(d); - d.file = stdout; d.filename = "-"; - d.simple_string = false; - d.comma = ","; - d.key_sep = ":"; - d.indent = DEFAULT_INDENT; + d.out = stdout; + + if (getenv("NO_COLOR") == NULL && isatty(fileno(d.out))) + colors = true; + setlinebuf(d.out); 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 'N' : + colors = false; + break; + case 'C' : + if (optarg == NULL || !strcmp(optarg, "auto")) + break; /* nothing to do, tty detection was done + before parsing options */ + else if (!strcmp(optarg, "never")) + colors = false; + else if (!strcmp(optarg, "always")) + colors = true; + else { + fprintf(stderr, "Unknown color: %s\n", optarg); + show_usage(&d, argv[0], true); + return -1; + } + break; + case 'R': + raw = true; + break; case 'i': - d.indent = atoi(optarg); + indent = atoi(optarg); break; case 's': - d.simple_string = true; - d.comma = ""; - d.key_sep = " ="; + flags |= SPA_JSON_BUILDER_FLAG_SIMPLE; break; default: show_usage(&d, argv[0], true); @@ -308,6 +263,15 @@ int main(int argc, char *argv[]) } d.size = sbuf.st_size; + if (!raw) + flags |= SPA_JSON_BUILDER_FLAG_PRETTY; + if (colors) + flags |= SPA_JSON_BUILDER_FLAG_COLOR; + + spa_json_builder_file(&d.builder, d.out, flags); + if (indent >= 0) + d.builder.indent = indent; + res = process_json(&d); if (res < 0) exit_code = EXIT_FAILURE; diff --git a/src/daemon/client.conf.in b/src/daemon/client.conf.in index 46874af93..e0baeda92 100644 --- a/src/daemon/client.conf.in +++ b/src/daemon/client.conf.in @@ -101,6 +101,9 @@ stream.properties = { #channelmix.lfe-cutoff = 150 #channelmix.fc-cutoff = 12000 #channelmix.rear-delay = 12.0 + #channelmix.center-level = 0.707106781 + #channelmix.surround-level = 0.707106781 + #channelmix.lfe-level = 0.5 #channelmix.stereo-widen = 0.0 #channelmix.hilbert-taps = 0 #dither.noise = 0 diff --git a/src/daemon/filter-chain/source-rnnoise.conf b/src/daemon/filter-chain/source-rnnoise.conf index f3c2c71be..83142dd29 100644 --- a/src/daemon/filter-chain/source-rnnoise.conf +++ b/src/daemon/filter-chain/source-rnnoise.conf @@ -21,7 +21,6 @@ context.modules = [ # listed in the environment variable LADSPA_PATH or # /usr/lib64/ladspa, /usr/lib/ladspa or the system library directory # as a fallback. - # You might want to use an absolute path here to avoid problems. plugin = "librnnoise_ladspa" label = noise_suppressor_stereo control = { diff --git a/src/daemon/minimal.conf.in b/src/daemon/minimal.conf.in index 82647e9ca..7ab93e92b 100644 --- a/src/daemon/minimal.conf.in +++ b/src/daemon/minimal.conf.in @@ -100,6 +100,10 @@ context.modules = [ } flags = [ ifexists nofail ] } + # the graph scheduler + { name = libpipewire-module-scheduler-v1 + condition = [ { module.scheduler-v1 = !false } ] + } # The native communication protocol. { name = libpipewire-module-protocol-native } @@ -328,6 +332,9 @@ context.objects = [ #channelmix.fc-cutoff = 12000 #channelmix.rear-delay = 12.0 #channelmix.stereo-widen = 0.0 + #channelmix.center-level = 0.707106781 + #channelmix.surround-level = 0.707106781 + #channelmix.lfe-level = 0.5 #channelmix.hilbert-taps = 0 #channelmix.disable = false #dither.noise = 0 diff --git a/src/daemon/pipewire-avb.conf.in b/src/daemon/pipewire-avb.conf.in index da158f212..e43015780 100644 --- a/src/daemon/pipewire-avb.conf.in +++ b/src/daemon/pipewire-avb.conf.in @@ -60,6 +60,9 @@ stream.properties = { #channelmix.fc-cutoff = 6000 #channelmix.rear-delay = 12.0 #channelmix.stereo-widen = 0.1 + #channelmix.center-level = 0.707106781 + #channelmix.surround-level = 0.707106781 + #channelmix.lfe-level = 0.5 #channelmix.hilbert-taps = 0 } diff --git a/src/daemon/pipewire-pulse.conf.in b/src/daemon/pipewire-pulse.conf.in index 8c21e37df..db646db0b 100644 --- a/src/daemon/pipewire-pulse.conf.in +++ b/src/daemon/pipewire-pulse.conf.in @@ -88,6 +88,9 @@ stream.properties = { #channelmix.fc-cutoff = 12000 #channelmix.rear-delay = 12.0 #channelmix.stereo-widen = 0.0 + #channelmix.center-level = 0.707106781 + #channelmix.surround-level = 0.707106781 + #channelmix.lfe-level = 0.5 #channelmix.hilbert-taps = 0 #dither.noise = 0 } diff --git a/src/daemon/pipewire.conf.in b/src/daemon/pipewire.conf.in index c3eb7120f..a9142cede 100644 --- a/src/daemon/pipewire.conf.in +++ b/src/daemon/pipewire.conf.in @@ -121,6 +121,10 @@ context.modules = [ flags = [ ifexists nofail ] condition = [ { module.rt = !false } ] } + # the graph scheduler + { name = libpipewire-module-scheduler-v1 + condition = [ { module.scheduler-v1 = !false } ] + } # The native communication protocol. { name = libpipewire-module-protocol-native diff --git a/src/examples/audio-dsp-filter.c b/src/examples/audio-dsp-filter.c index 8a43147a6..589646d61 100644 --- a/src/examples/audio-dsp-filter.c +++ b/src/examples/audio-dsp-filter.c @@ -108,6 +108,7 @@ int main(int argc, char *argv[]) PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Filter", PW_KEY_MEDIA_ROLE, "DSP", + PW_KEY_NODE_PASSIVE, "follow", NULL), &filter_events, &data); diff --git a/src/examples/audio-dsp-src.c b/src/examples/audio-dsp-src.c index 135d2e27e..0ef4a0e53 100644 --- a/src/examples/audio-dsp-src.c +++ b/src/examples/audio-dsp-src.c @@ -18,7 +18,6 @@ #define M_PI_M2f (float)(M_PI+M_PI) -#define DEFAULT_RATE 44100 #define DEFAULT_FREQ 440 #define DEFAULT_VOLUME 0.7f @@ -61,7 +60,9 @@ static void on_process(void *userdata, struct spa_io_position *position) return; for (i = 0; i < n_samples; i++) { - out_port->accumulator += M_PI_M2f * DEFAULT_FREQ / DEFAULT_RATE; + out_port->accumulator += M_PI_M2f * DEFAULT_FREQ * + position->clock.rate.num / position->clock.rate.denom; + if (out_port->accumulator >= M_PI_M2f) out_port->accumulator -= M_PI_M2f; diff --git a/src/examples/video-src-alloc.c b/src/examples/video-src-alloc.c index add9597ed..c89ce8d78 100644 --- a/src/examples/video-src-alloc.c +++ b/src/examples/video-src-alloc.c @@ -192,7 +192,11 @@ static void on_stream_state_changed(void *_data, enum pw_stream_state old, enum interval.tv_sec = 0; interval.tv_nsec = 40 * SPA_NSEC_PER_MSEC; - if (pw_stream_is_driving(data->stream)) + printf("driving:%d lazy:%d\n", + pw_stream_is_driving(data->stream), + pw_stream_is_lazy(data->stream)); + + if (pw_stream_is_driving(data->stream) != pw_stream_is_lazy(data->stream)) pw_loop_update_timer(pw_thread_loop_get_loop(data->loop), data->timer, &timeout, &interval, false); break; @@ -390,6 +394,7 @@ int main(int argc, char *argv[]) "video-src-alloc", pw_properties_new( PW_KEY_MEDIA_CLASS, "Video/Source", + PW_KEY_NODE_SUPPORTS_REQUEST, "1", NULL), &stream_events, &data); diff --git a/src/examples/video-src-fixate.c b/src/examples/video-src-fixate.c index 6d08074de..b16d810ad 100644 --- a/src/examples/video-src-fixate.c +++ b/src/examples/video-src-fixate.c @@ -18,7 +18,11 @@ #include #include #include +#ifdef __linux__ #include +#else +#include +#endif #include #include diff --git a/src/gst/gstpipewiresrc.c b/src/gst/gstpipewiresrc.c index b0de17dfd..3c57028b4 100644 --- a/src/gst/gstpipewiresrc.c +++ b/src/gst/gstpipewiresrc.c @@ -531,6 +531,7 @@ gst_pipewire_src_init (GstPipeWireSrc * src) src->autoconnect = DEFAULT_AUTOCONNECT; src->min_latency = 0; src->max_latency = GST_CLOCK_TIME_NONE; + src->last_buffer_clock_time = GST_CLOCK_TIME_NONE; src->n_buffers = 0; src->flushing_on_remove_buffer = FALSE; src->on_disconnect = DEFAULT_ON_DISCONNECT; @@ -838,6 +839,17 @@ static GstBuffer *dequeue_buffer(GstPipeWireSrc *pwsrc) video_size += d->chunk->size; } + + /* If the buffer number is smaller than the plane number, + * update the stride and offset for the remaining planes. + */ + if (n_datas && n_datas < n_planes) { + for (i = n_datas; i < n_planes; i++) { + meta->stride[i] = gst_video_format_info_extrapolate_stride (info->finfo, i, b->buffer->datas[0].chunk->stride); + meta->offset[i] = meta->offset[i-1] + + meta->stride[i-1] * GST_VIDEO_FORMAT_INFO_SCALE_HEIGHT (info->finfo, i-1, GST_VIDEO_INFO_HEIGHT(info)); + } + } } if (b->buffer->n_datas != gst_buffer_n_memory(data->buf)) { @@ -1073,10 +1085,6 @@ wait_negotiated (GstPipeWireSrc *this) GST_DEBUG_OBJECT (this, "waiting for NEGOTIATED, now %s", pw_stream_state_as_string (state)); if (state == PW_STREAM_STATE_ERROR) break; - if (this->flushing) { - state = PW_STREAM_STATE_ERROR; - break; - } if (this->negotiated) break; @@ -1598,16 +1606,31 @@ gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer) GST_LOG_OBJECT (pwsrc, "EOS, send last buffer"); break; } else if (timeout && pwsrc->last_buffer != NULL) { + buf = gst_buffer_copy (pwsrc->last_buffer); 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); if (buf != NULL) { - if (pwsrc->resend_last || pwsrc->keepalive_time > 0) - gst_buffer_replace (&pwsrc->last_buffer, buf); + if (pwsrc->resend_last || pwsrc->keepalive_time > 0) { + GstClock *clock; + GstBuffer *old; + + old = pwsrc->last_buffer; + pwsrc->last_buffer = gst_buffer_copy (buf); + gst_buffer_unref (old); + gst_buffer_add_parent_buffer_meta (pwsrc->last_buffer, buf); + + clock = gst_element_get_clock (GST_ELEMENT_CAST (pwsrc)); + if (clock != NULL) { + pwsrc->last_buffer_clock_time = gst_clock_get_time (clock); + gst_object_unref (clock); + } else { + pwsrc->last_buffer_clock_time = GST_CLOCK_TIME_NONE; + } + } break; } } @@ -1632,21 +1655,33 @@ gst_pipewire_src_create (GstPushSrc * psrc, GstBuffer ** buffer) if (update_time) { GstClock *clock; - GstClockTime pts, dts; + GstClockTime current_clock_time; clock = gst_element_get_clock (GST_ELEMENT_CAST (pwsrc)); if (clock != NULL) { - pts = dts = gst_clock_get_time (clock); + current_clock_time = gst_clock_get_time (clock); gst_object_unref (clock); } else { - pts = dts = GST_CLOCK_TIME_NONE; + current_clock_time = GST_CLOCK_TIME_NONE; } - GST_BUFFER_PTS (*buffer) = pts; - GST_BUFFER_DTS (*buffer) = dts; + if (GST_CLOCK_TIME_IS_VALID (current_clock_time) && + GST_CLOCK_TIME_IS_VALID (pwsrc->last_buffer_clock_time) && + GST_CLOCK_TIME_IS_VALID (GST_BUFFER_PTS (*buffer)) && + GST_CLOCK_TIME_IS_VALID (GST_BUFFER_DTS (*buffer))) { + GstClockTime diff; + + diff = current_clock_time - pwsrc->last_buffer_clock_time; + + GST_BUFFER_PTS (*buffer) += diff; + GST_BUFFER_DTS (*buffer) += diff; + } else { + GST_BUFFER_PTS (*buffer) = GST_BUFFER_DTS (*buffer) = current_clock_time; + } GST_LOG_OBJECT (pwsrc, "Sending keepalive buffer pts/dts: %" GST_TIME_FORMAT - " (%" G_GUINT64_FORMAT ")", GST_TIME_ARGS (pts), pts); + " (%" G_GUINT64_FORMAT ")", GST_TIME_ARGS (current_clock_time), + current_clock_time); } return GST_FLOW_OK; diff --git a/src/gst/gstpipewiresrc.h b/src/gst/gstpipewiresrc.h index 869877fcb..4b0f57e0e 100644 --- a/src/gst/gstpipewiresrc.h +++ b/src/gst/gstpipewiresrc.h @@ -83,6 +83,7 @@ struct _GstPipeWireSrc { GstClockTime max_latency; GstBuffer *last_buffer; + GstClockTime last_buffer_clock_time; enum spa_meta_videotransform_value transform_value; diff --git a/src/modules/meson.build b/src/modules/meson.build index 59f46ae13..11b29a117 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -46,6 +46,7 @@ module_sources = [ 'module-vban-recv.c', 'module-vban-send.c', 'module-session-manager.c', + 'module-scheduler-v1.c', 'module-zeroconf-discover.c', 'module-roc-source.c', 'module-roc-sink.c', @@ -295,6 +296,17 @@ pipewire_module_protocol_native = shared_library('pipewire-module-protocol-nativ dependencies : pipewire_module_protocol_deps, ) +zeroconf_sources = [] +zeroconf_deps = [] +if avahi_dep.found() + zeroconf_sources += [ + 'zeroconf-utils/zeroconf.c', + 'zeroconf-utils/avahi-poll.c', + ] + zeroconf_deps += avahi_dep + cdata.set('HAVE_AVAHI', true) +endif + pipewire_module_protocol_pulse_deps = pipewire_module_protocol_deps pipewire_module_protocol_pulse_sources = [ @@ -371,10 +383,9 @@ endif if avahi_dep.found() pipewire_module_protocol_pulse_sources += [ 'module-protocol-pulse/modules/module-zeroconf-publish.c', - 'module-zeroconf-discover/avahi-poll.c', ] - pipewire_module_protocol_pulse_deps += avahi_dep - cdata.set('HAVE_AVAHI', true) + pipewire_module_protocol_pulse_sources += zeroconf_sources + pipewire_module_protocol_pulse_deps += zeroconf_deps endif if gsettings_gio_dep.found() @@ -532,6 +543,15 @@ pipewire_module_adapter = shared_library('pipewire-module-adapter', dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep], ) +pipewire_module_scheduler_v1 = shared_library('pipewire-module-scheduler-v1', + [ 'module-scheduler-v1.c' ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep], +) + pipewire_module_session_manager = shared_library('pipewire-module-session-manager', [ 'module-session-manager.c', 'module-session-manager/client-endpoint/client-endpoint.c', @@ -559,12 +579,12 @@ if build_module_zeroconf_discover pipewire_module_zeroconf_discover = shared_library('pipewire-module-zeroconf-discover', [ 'module-zeroconf-discover.c', 'module-protocol-pulse/format.c', - 'module-zeroconf-discover/avahi-poll.c' ], + zeroconf_sources ], include_directories : [configinc], install : true, install_dir : modules_install_dir, install_rpath: modules_install_dir, - dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, avahi_dep], + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, zeroconf_deps], ) endif summary({'zeroconf-discover': build_module_zeroconf_discover}, bool_yn: true, section: 'Optional Modules') @@ -589,12 +609,12 @@ build_module_raop_discover = avahi_dep.found() if build_module_raop_discover pipewire_module_raop_discover = shared_library('pipewire-module-raop-discover', [ 'module-raop-discover.c', - 'module-zeroconf-discover/avahi-poll.c' ], + zeroconf_sources ], include_directories : [configinc], install : true, install_dir : modules_install_dir, install_rpath: modules_install_dir, - dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, avahi_dep], + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, zeroconf_deps], ) endif summary({'raop-discover (needs Avahi)': build_module_raop_discover}, bool_yn: true, section: 'Optional Modules') @@ -603,12 +623,12 @@ build_module_snapcast_discover = avahi_dep.found() if build_module_snapcast_discover pipewire_module_snapcast_discover = shared_library('pipewire-module-snapcast-discover', [ 'module-snapcast-discover.c', - 'module-zeroconf-discover/avahi-poll.c' ], + zeroconf_sources ], include_directories : [configinc], install : true, install_dir : modules_install_dir, install_rpath: modules_install_dir, - dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, avahi_dep], + dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, zeroconf_deps], ) endif summary({'snapcast-discover (needs Avahi)': build_module_snapcast_discover}, bool_yn: true, section: 'Optional Modules') @@ -651,13 +671,13 @@ pipewire_module_rtp_sink = shared_library('pipewire-module-rtp-sink', build_module_rtp_session = avahi_dep.found() if build_module_rtp_session pipewire_module_rtp_session = shared_library('pipewire-module-rtp-session', - [ 'module-zeroconf-discover/avahi-poll.c', - 'module-rtp-session.c' ], + [ 'module-rtp-session.c', + zeroconf_sources ], include_directories : [configinc], install : true, install_dir : modules_install_dir, install_rpath: modules_install_dir, - dependencies : [pipewire_module_rtp_common_dep, avahi_dep], + dependencies : [pipewire_module_rtp_common_dep, zeroconf_deps], ) endif @@ -690,6 +710,35 @@ pipewire_module_vban_recv = shared_library('pipewire-module-vban-recv', dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep], ) +pipewire_module_sendspin_sources = [] +pipewire_module_sendspin_deps = [ mathlib, dl_lib, rt_lib, pipewire_dep ] + +if avahi_dep.found() + pipewire_module_sendspin_sources += zeroconf_sources + pipewire_module_sendspin_deps += zeroconf_deps +endif + +pipewire_module_sendspin_recv = shared_library('pipewire-module-sendspin-recv', + [ 'module-sendspin-recv.c', + 'module-sendspin/websocket.c', + pipewire_module_sendspin_sources ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : pipewire_module_sendspin_deps, +) +pipewire_module_sendspin_send = shared_library('pipewire-module-sendspin-send', + [ 'module-sendspin-send.c', + 'module-sendspin/websocket.c', + pipewire_module_sendspin_sources ], + include_directories : [configinc], + install : true, + install_dir : modules_install_dir, + install_rpath: modules_install_dir, + dependencies : pipewire_module_sendspin_deps, +) + build_module_roc = roc_dep.found() if build_module_roc pipewire_module_roc_sink = shared_library('pipewire-module-roc-sink', diff --git a/src/modules/module-avb/acmp.c b/src/modules/module-avb/acmp.c index eacfb6133..73a84ba89 100644 --- a/src/modules/module-avb/acmp.c +++ b/src/modules/module-avb/acmp.c @@ -174,13 +174,14 @@ static int handle_connect_tx_command(struct acmp *acmp, uint64_t now, const void return 0; memcpy(buf, m, len); + AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE); + stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id)); if (stream == NULL) { status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX; goto done; } - AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE); reply->stream_id = htobe64(stream->id); stream_activate(stream, ntohs(reply->talker_unique_id), now); @@ -251,14 +252,14 @@ static int handle_disconnect_tx_command(struct acmp *acmp, uint64_t now, const v return 0; memcpy(buf, m, len); + AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE); + stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id)); if (stream == NULL) { status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX; goto done; } - AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE); - stream_deactivate(stream, now); done: diff --git a/src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c b/src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c index 0149d633b..73b7d2548 100644 --- a/src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c +++ b/src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c @@ -99,7 +99,7 @@ int handle_cmd_lock_entity_milan_v12(struct aecp *aecp, int64_t now, const void desc = server_find_descriptor(server, desc_type, desc_id); if (desc == NULL) - return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len); + return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len); entity_state = desc->ptr; lock = &entity_state->state.lock_state; @@ -148,7 +148,7 @@ int handle_cmd_lock_entity_milan_v12(struct aecp *aecp, int64_t now, const void // If the lock is taken again by device if (ctrler_id == lock->locked_id) { lock->base_info.expire_timeout += - AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND; + AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND * SPA_NSEC_PER_SEC; lock->is_locked = true; } else { diff --git a/src/modules/module-avb/aecp-aem.c b/src/modules/module-avb/aecp-aem.c index fccf6b178..e655f02c5 100644 --- a/src/modules/module-avb/aecp-aem.c +++ b/src/modules/module-avb/aecp-aem.c @@ -27,7 +27,8 @@ static int handle_acquire_entity_avb_legacy(struct aecp *aecp, int64_t now, const void *m, int len) { struct server *server = aecp->server; - const struct avb_packet_aecp_aem *p = m; + const struct avb_ethernet_header *h = m; + const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void); const struct avb_packet_aecp_aem_acquire *ae; const struct descriptor *desc; uint16_t desc_type, desc_id; @@ -53,7 +54,8 @@ static int handle_lock_entity_avb_legacy(struct aecp *aecp, int64_t now, const void *m, int len) { struct server *server = aecp->server; - const struct avb_packet_aecp_aem *p = m; + const struct avb_ethernet_header *h = m; + const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void); const struct avb_packet_aecp_aem_acquire *ae; const struct descriptor *desc; uint16_t desc_type, desc_id; diff --git a/src/modules/module-avb/avb-transport-loopback.h b/src/modules/module-avb/avb-transport-loopback.h new file mode 100644 index 000000000..18508b99a --- /dev/null +++ b/src/modules/module-avb/avb-transport-loopback.h @@ -0,0 +1,184 @@ +/* AVB support */ +/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */ +/* SPDX-License-Identifier: MIT */ + +#ifndef AVB_TRANSPORT_LOOPBACK_H +#define AVB_TRANSPORT_LOOPBACK_H + +#include +#include +#include +#include +#include +#include +#include + +#include "internal.h" +#include "packets.h" + +#define AVB_LOOPBACK_MAX_PACKETS 64 +#define AVB_LOOPBACK_MAX_PACKET_SIZE 2048 + +struct avb_loopback_packet { + uint8_t dest[6]; + uint16_t type; + size_t size; + uint8_t data[AVB_LOOPBACK_MAX_PACKET_SIZE]; +}; + +struct avb_loopback_transport { + struct avb_loopback_packet packets[AVB_LOOPBACK_MAX_PACKETS]; + int packet_count; + int packet_read; +}; + +static inline int avb_loopback_setup(struct server *server) +{ + struct avb_loopback_transport *t; + static const uint8_t test_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 }; + + t = calloc(1, sizeof(*t)); + if (t == NULL) + return -errno; + + server->transport_data = t; + + memcpy(server->mac_addr, test_mac, 6); + server->ifindex = 1; + server->entity_id = (uint64_t)server->mac_addr[0] << 56 | + (uint64_t)server->mac_addr[1] << 48 | + (uint64_t)server->mac_addr[2] << 40 | + (uint64_t)0xff << 32 | + (uint64_t)0xfe << 24 | + (uint64_t)server->mac_addr[3] << 16 | + (uint64_t)server->mac_addr[4] << 8 | + (uint64_t)server->mac_addr[5]; + + return 0; +} + +static inline int avb_loopback_send_packet(struct server *server, + const uint8_t dest[6], uint16_t type, void *data, size_t size) +{ + struct avb_loopback_transport *t = server->transport_data; + struct avb_loopback_packet *pkt; + struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data; + + if (t->packet_count >= AVB_LOOPBACK_MAX_PACKETS) + return -ENOSPC; + if (size > AVB_LOOPBACK_MAX_PACKET_SIZE) + return -EMSGSIZE; + + /* Fill in the ethernet header like the raw transport does */ + memcpy(hdr->dest, dest, 6); + memcpy(hdr->src, server->mac_addr, 6); + hdr->type = htons(type); + + pkt = &t->packets[t->packet_count % AVB_LOOPBACK_MAX_PACKETS]; + memcpy(pkt->dest, dest, 6); + pkt->type = type; + pkt->size = size; + memcpy(pkt->data, data, size); + t->packet_count++; + + return 0; +} + +/** + * Return a dummy fd for protocol handlers that create their own sockets. + * Uses eventfd so pw_loop_add_io() has a valid fd to work with. + */ +static inline int avb_loopback_make_socket(struct server *server, + uint16_t type, const uint8_t mac[6]) +{ + int fd; + + fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (fd < 0) + return -errno; + + return fd; +} + +static inline void avb_loopback_destroy(struct server *server) +{ + free(server->transport_data); + server->transport_data = NULL; +} + +/** + * Create a dummy stream socket using eventfd. + * No AF_PACKET, no ioctls, no privileges needed. + */ +static inline int avb_loopback_stream_setup_socket(struct server *server, + struct stream *stream) +{ + int fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (fd < 0) + return -errno; + + spa_zero(stream->sock_addr); + stream->sock_addr.sll_family = AF_PACKET; + stream->sock_addr.sll_halen = ETH_ALEN; + + return fd; +} + +/** + * No-op stream send — pretend the send succeeded. + * Audio data is consumed from the ringbuffer but goes nowhere. + */ +static inline ssize_t avb_loopback_stream_send(struct server *server, + struct stream *stream, struct msghdr *msg, int flags) +{ + ssize_t total = 0; + for (size_t i = 0; i < msg->msg_iovlen; i++) + total += msg->msg_iov[i].iov_len; + return total; +} + +static const struct avb_transport_ops avb_transport_loopback = { + .setup = avb_loopback_setup, + .send_packet = avb_loopback_send_packet, + .make_socket = avb_loopback_make_socket, + .destroy = avb_loopback_destroy, + .stream_setup_socket = avb_loopback_stream_setup_socket, + .stream_send = avb_loopback_stream_send, +}; + +/** Get the number of captured sent packets */ +static inline int avb_loopback_get_packet_count(struct server *server) +{ + struct avb_loopback_transport *t = server->transport_data; + return t->packet_count - t->packet_read; +} + +/** Read the next captured sent packet, returns packet size or -1 */ +static inline int avb_loopback_get_packet(struct server *server, + void *buf, size_t bufsize) +{ + struct avb_loopback_transport *t = server->transport_data; + struct avb_loopback_packet *pkt; + + if (t->packet_read >= t->packet_count) + return -1; + + pkt = &t->packets[t->packet_read % AVB_LOOPBACK_MAX_PACKETS]; + t->packet_read++; + + if (pkt->size > bufsize) + return -1; + + memcpy(buf, pkt->data, pkt->size); + return pkt->size; +} + +/** Clear all captured packets */ +static inline void avb_loopback_clear_packets(struct server *server) +{ + struct avb_loopback_transport *t = server->transport_data; + t->packet_count = 0; + t->packet_read = 0; +} + +#endif /* AVB_TRANSPORT_LOOPBACK_H */ diff --git a/src/modules/module-avb/avdecc.c b/src/modules/module-avb/avdecc.c index 7cd7e8e7a..8729d6421 100644 --- a/src/modules/module-avb/avdecc.c +++ b/src/modules/module-avb/avdecc.c @@ -84,7 +84,7 @@ static void on_socket_data(void *data, int fd, uint32_t mask) } } -int avb_server_send_packet(struct server *server, const uint8_t dest[6], +static int raw_send_packet(struct server *server, const uint8_t dest[6], uint16_t type, void *data, size_t size) { struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data; @@ -101,6 +101,12 @@ int avb_server_send_packet(struct server *server, const uint8_t dest[6], return res; } +int avb_server_send_packet(struct server *server, const uint8_t dest[6], + uint16_t type, void *data, size_t size) +{ + return server->transport->send_packet(server, dest, type, data, size); +} + static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_t mac[6]) { struct sock_fprog filter; @@ -136,7 +142,7 @@ static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_ return 0; } -int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]) +static int raw_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]) { int fd, res; struct ifreq req; @@ -209,13 +215,20 @@ error_close: return res; } -static int setup_socket(struct server *server) +int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]) +{ + if (server->transport && server->transport->make_socket) + return server->transport->make_socket(server, type, mac); + return raw_make_socket(server, type, mac); +} + +static int raw_transport_setup(struct server *server) { struct impl *impl = server->impl; int fd, res; static const uint8_t bmac[6] = AVB_BROADCAST_MAC; - fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac); + fd = raw_make_socket(server, AVB_TSN_ETH, bmac); if (fd < 0) return fd; @@ -244,6 +257,119 @@ error_no_source: return res; } +static int raw_stream_setup_socket(struct server *server, struct stream *stream) +{ + int fd, res; + char buf[128]; + struct ifreq req; + + fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL)); + if (fd < 0) { + pw_log_error("socket() failed: %m"); + return -errno; + } + + spa_zero(req); + snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname); + res = ioctl(fd, SIOCGIFINDEX, &req); + if (res < 0) { + pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname); + res = -errno; + goto error_close; + } + + spa_zero(stream->sock_addr); + stream->sock_addr.sll_family = AF_PACKET; + stream->sock_addr.sll_protocol = htons(ETH_P_TSN); + stream->sock_addr.sll_ifindex = req.ifr_ifindex; + + if (stream->direction == SPA_DIRECTION_OUTPUT) { + struct sock_txtime txtime_cfg; + + res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio, + sizeof(stream->prio)); + if (res < 0) { + pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio); + res = -errno; + goto error_close; + } + + txtime_cfg.clockid = CLOCK_TAI; + txtime_cfg.flags = 0; + res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg, + sizeof(txtime_cfg)); + if (res < 0) { + pw_log_error("setsockopt(SO_TXTIME) failed: %m"); + res = -errno; + goto error_close; + } + } else { + struct packet_mreq mreq; + + res = bind(fd, (struct sockaddr *) &stream->sock_addr, sizeof(stream->sock_addr)); + if (res < 0) { + pw_log_error("bind() failed: %m"); + res = -errno; + goto error_close; + } + + spa_zero(mreq); + mreq.mr_ifindex = req.ifr_ifindex; + mreq.mr_type = PACKET_MR_MULTICAST; + mreq.mr_alen = ETH_ALEN; + memcpy(&mreq.mr_address, stream->addr, ETH_ALEN); + res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, + &mreq, sizeof(struct packet_mreq)); + + pw_log_info("join %s", avb_utils_format_addr(buf, 128, stream->addr)); + + if (res < 0) { + pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m"); + res = -errno; + goto error_close; + } + } + return fd; + +error_close: + close(fd); + return res; +} + +static ssize_t raw_stream_send(struct server *server, struct stream *stream, + struct msghdr *msg, int flags) +{ + return sendmsg(stream->source->fd, msg, flags); +} + +int avb_server_stream_setup_socket(struct server *server, struct stream *stream) +{ + return server->transport->stream_setup_socket(server, stream); +} + +ssize_t avb_server_stream_send(struct server *server, struct stream *stream, + struct msghdr *msg, int flags) +{ + return server->transport->stream_send(server, stream, msg, flags); +} + +static void raw_transport_destroy(struct server *server) +{ + struct impl *impl = server->impl; + if (server->source) + pw_loop_destroy_source(impl->loop, server->source); + server->source = NULL; +} + +const struct avb_transport_ops avb_transport_raw = { + .setup = raw_transport_setup, + .send_packet = raw_send_packet, + .make_socket = raw_make_socket, + .destroy = raw_transport_destroy, + .stream_setup_socket = raw_stream_setup_socket, + .stream_send = raw_stream_send, +}; + struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props) { struct server *server; @@ -266,10 +392,14 @@ struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props) spa_hook_list_init(&server->listener_list); spa_list_init(&server->descriptors); + spa_list_init(&server->streams); server->debug_messages = false; - if ((res = setup_socket(server)) < 0) + if (server->transport == NULL) + server->transport = &avb_transport_raw; + + if ((res = server->transport->setup(server)) < 0) goto error_free; @@ -315,12 +445,10 @@ void avdecc_server_add_listener(struct server *server, struct spa_hook *listener void avdecc_server_free(struct server *server) { - struct impl *impl = server->impl; - server_destroy_descriptors(server); spa_list_remove(&server->link); - if (server->source) - pw_loop_destroy_source(impl->loop, server->source); + if (server->transport) + server->transport->destroy(server); pw_timer_queue_cancel(&server->timer); spa_hook_list_clean(&server->listener_list); free(server->ifname); diff --git a/src/modules/module-avb/internal.h b/src/modules/module-avb/internal.h index 82ced2f21..bb4961674 100644 --- a/src/modules/module-avb/internal.h +++ b/src/modules/module-avb/internal.h @@ -5,6 +5,8 @@ #ifndef AVB_INTERNAL_H #define AVB_INTERNAL_H +#include + #include #ifdef __cplusplus @@ -17,6 +19,22 @@ struct avb_mrp; #define AVB_TSN_ETH 0x22f0 #define AVB_BROADCAST_MAC { 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 }; +struct stream; + +struct avb_transport_ops { + int (*setup)(struct server *server); + int (*send_packet)(struct server *server, const uint8_t dest[6], + uint16_t type, void *data, size_t size); + int (*make_socket)(struct server *server, uint16_t type, + const uint8_t mac[6]); + void (*destroy)(struct server *server); + + /* stream data plane ops */ + int (*stream_setup_socket)(struct server *server, struct stream *stream); + ssize_t (*stream_send)(struct server *server, struct stream *stream, + struct msghdr *msg, int flags); +}; + struct impl { struct pw_loop *loop; struct pw_timer_queue *timer_queue; @@ -77,12 +95,16 @@ struct server { uint64_t entity_id; int ifindex; + const struct avb_transport_ops *transport; + void *transport_data; + struct spa_source *source; struct pw_timer timer; struct spa_hook_list listener_list; struct spa_list descriptors; + struct spa_list streams; unsigned debug_messages:1; @@ -102,7 +124,6 @@ static inline void server_destroy_descriptors(struct server *server) struct descriptor *d, *t; spa_list_for_each_safe(d, t, &server->descriptors, link) { - free(d->ptr); spa_list_remove(&d->link); free(d); } @@ -145,11 +166,17 @@ void avdecc_server_free(struct server *server); void avdecc_server_add_listener(struct server *server, struct spa_hook *listener, const struct server_events *events, void *data); +extern const struct avb_transport_ops avb_transport_raw; + int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]); int avb_server_send_packet(struct server *server, const uint8_t dest[6], uint16_t type, void *data, size_t size); +int avb_server_stream_setup_socket(struct server *server, struct stream *stream); +ssize_t avb_server_stream_send(struct server *server, struct stream *stream, + struct msghdr *msg, int flags); + struct aecp { struct server *server; struct spa_hook server_listener; diff --git a/src/modules/module-avb/mrp.c b/src/modules/module-avb/mrp.c index 73d5275ca..c6505d41b 100644 --- a/src/modules/module-avb/mrp.c +++ b/src/modules/module-avb/mrp.c @@ -302,6 +302,8 @@ const char *avb_mrp_notify_name(uint8_t notify) const char *avb_mrp_send_name(uint8_t send) { switch(send) { + case 0: + return "none"; case AVB_MRP_SEND_NEW: return "new"; case AVB_MRP_SEND_JOININ: diff --git a/src/modules/module-avb/mrp.h b/src/modules/module-avb/mrp.h index 78683412c..399343267 100644 --- a/src/modules/module-avb/mrp.h +++ b/src/modules/module-avb/mrp.h @@ -88,13 +88,13 @@ struct avb_packet_mrp_footer { #define AVB_MRP_ATTRIBUTE_EVENT_LV 5 #define AVB_MRP_ATTRIBUTE_EVENT_LVA 6 -#define AVB_MRP_SEND_NEW 0 -#define AVB_MRP_SEND_JOININ 1 -#define AVB_MRP_SEND_IN 2 -#define AVB_MRP_SEND_JOINMT 3 -#define AVB_MRP_SEND_MT 4 -#define AVB_MRP_SEND_LV 5 -#define AVB_MRP_SEND_LVA 6 +#define AVB_MRP_SEND_NEW 1 +#define AVB_MRP_SEND_JOININ 2 +#define AVB_MRP_SEND_IN 3 +#define AVB_MRP_SEND_JOINMT 4 +#define AVB_MRP_SEND_MT 5 +#define AVB_MRP_SEND_LV 6 +#define AVB_MRP_SEND_LVA 7 #define AVB_MRP_NOTIFY_NEW 1 #define AVB_MRP_NOTIFY_JOIN 2 diff --git a/src/modules/module-avb/msrp.c b/src/modules/module-avb/msrp.c index 92d1e65b4..611ddd537 100644 --- a/src/modules/module-avb/msrp.c +++ b/src/modules/module-avb/msrp.c @@ -91,7 +91,7 @@ static int encode_talker(struct msrp *msrp, struct attr *a, void *m) *t = a->attr.attr.talker; ev = SPA_PTROFF(t, sizeof(*t), uint8_t); - *ev = a->attr.mrp->pending_send * 6 * 6; + *ev = (a->attr.mrp->pending_send - 1) * 6 * 6; f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer); f->end_mark = 0; @@ -170,7 +170,7 @@ static int encode_listener(struct msrp *msrp, struct attr *a, void *m) *l = a->attr.attr.listener; ev = SPA_PTROFF(l, sizeof(*l), uint8_t); - *ev = a->attr.mrp->pending_send * 6 * 6; + *ev = (a->attr.mrp->pending_send - 1) * 6 * 6; ev = SPA_PTROFF(ev, sizeof(*ev), uint8_t); *ev = a->attr.param * 4 * 4 * 4; @@ -226,7 +226,7 @@ static int encode_domain(struct msrp *msrp, struct attr *a, void *m) *d = a->attr.attr.domain; ev = SPA_PTROFF(d, sizeof(*d), uint8_t); - *ev = a->attr.mrp->pending_send * 36; + *ev = (a->attr.mrp->pending_send - 1) * 36; f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer); f->end_mark = 0; @@ -332,7 +332,8 @@ static void msrp_notify(void *data, uint64_t now, uint8_t notify) { struct attr *a = data; struct msrp *msrp = a->msrp; - return dispatch[a->attr.type].notify(msrp, now, a, notify); + if (dispatch[a->attr.type].notify) + dispatch[a->attr.type].notify(msrp, now, a, notify); } static const struct avb_mrp_attribute_events mrp_attr_events = { diff --git a/src/modules/module-avb/mvrp.c b/src/modules/module-avb/mvrp.c index 20862c2ae..e2e501a9e 100644 --- a/src/modules/module-avb/mvrp.c +++ b/src/modules/module-avb/mvrp.c @@ -84,7 +84,7 @@ static int encode_vid(struct mvrp *mvrp, struct attr *a, void *m) *d = a->attr.attr.vid; ev = SPA_PTROFF(d, sizeof(*d), uint8_t); - *ev = a->attr.mrp->pending_send * 36; + *ev = (a->attr.mrp->pending_send - 1) * 36; f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer); f->end_mark = 0; @@ -171,7 +171,8 @@ static void mvrp_notify(void *data, uint64_t now, uint8_t notify) { struct attr *a = data; struct mvrp *mvrp = a->mvrp; - return dispatch[a->attr.type].notify(mvrp, now, a, notify); + if (dispatch[a->attr.type].notify) + dispatch[a->attr.type].notify(mvrp, now, a, notify); } static const struct avb_mrp_attribute_events mrp_attr_events = { diff --git a/src/modules/module-avb/stream.c b/src/modules/module-avb/stream.c index f7101bdf0..26a3a795b 100644 --- a/src/modules/module-avb/stream.c +++ b/src/modules/module-avb/stream.c @@ -116,9 +116,10 @@ static int flush_write(struct stream *stream, uint64_t current_time) p->timestamp = ptime; p->dbc = dbc; - n = sendmsg(stream->source->fd, &stream->msg, MSG_NOSIGNAL); + n = avb_server_stream_send(stream->server, stream, + &stream->msg, MSG_NOSIGNAL); if (n < 0 || n != (ssize_t)stream->pdu_size) { - pw_log_error("sendmsg() failed %zd != %zd: %m", + pw_log_error("stream send failed %zd != %zd: %m", n, stream->pdu_size); } txtime += stream->pdu_period; @@ -331,6 +332,8 @@ struct stream *server_create_stream(struct server *server, struct stream *stream stream->talker_attr->attr.talker.rank = AVB_MSRP_RANK_DEFAULT; stream->talker_attr->attr.talker.accumulated_latency = htonl(95); + spa_list_append(&server->streams, &stream->link); + return stream; error_free_stream: @@ -348,82 +351,7 @@ void stream_destroy(struct stream *stream) static int setup_socket(struct stream *stream) { - struct server *server = stream->server; - int fd, res; - char buf[128]; - struct ifreq req; - - fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL)); - if (fd < 0) { - pw_log_error("socket() failed: %m"); - return -errno; - } - - spa_zero(req); - snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname); - res = ioctl(fd, SIOCGIFINDEX, &req); - if (res < 0) { - pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname); - res = -errno; - goto error_close; - } - - spa_zero(stream->sock_addr); - stream->sock_addr.sll_family = AF_PACKET; - stream->sock_addr.sll_protocol = htons(ETH_P_TSN); - stream->sock_addr.sll_ifindex = req.ifr_ifindex; - - if (stream->direction == SPA_DIRECTION_OUTPUT) { - struct sock_txtime txtime_cfg; - - res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio, - sizeof(stream->prio)); - if (res < 0) { - pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio); - res = -errno; - goto error_close; - } - - txtime_cfg.clockid = CLOCK_TAI; - txtime_cfg.flags = 0; - res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg, - sizeof(txtime_cfg)); - if (res < 0) { - pw_log_error("setsockopt(SO_TXTIME) failed: %m"); - res = -errno; - goto error_close; - } - } else { - struct packet_mreq mreq; - - res = bind(fd, (struct sockaddr *) &stream->sock_addr, sizeof(stream->sock_addr)); - if (res < 0) { - pw_log_error("bind() failed: %m"); - res = -errno; - goto error_close; - } - - spa_zero(mreq); - mreq.mr_ifindex = req.ifr_ifindex; - mreq.mr_type = PACKET_MR_MULTICAST; - mreq.mr_alen = ETH_ALEN; - memcpy(&mreq.mr_address, stream->addr, ETH_ALEN); - res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, - &mreq, sizeof(struct packet_mreq)); - - pw_log_info("join %s", avb_utils_format_addr(buf, 128, stream->addr)); - - if (res < 0) { - pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m"); - res = -errno; - goto error_close; - } - } - return fd; - -error_close: - close(fd); - return res; + return avb_server_stream_setup_socket(stream->server, stream); } static void handle_iec61883_packet(struct stream *stream, @@ -548,3 +476,24 @@ int stream_deactivate(struct stream *stream, uint64_t now) } return 0; } + +int stream_activate_virtual(struct stream *stream, uint16_t index) +{ + struct server *server = stream->server; + int fd; + + if (stream->source == NULL) { + fd = setup_socket(stream); + if (fd < 0) + return fd; + + stream->source = pw_loop_add_io(server->impl->loop, fd, + SPA_IO_IN, true, on_socket_data, stream); + if (stream->source == NULL) { + close(fd); + return -errno; + } + } + pw_stream_set_active(stream->stream, true); + return 0; +} diff --git a/src/modules/module-avb/stream.h b/src/modules/module-avb/stream.h index f650cc216..4cc02ddd3 100644 --- a/src/modules/module-avb/stream.h +++ b/src/modules/module-avb/stream.h @@ -78,5 +78,6 @@ void stream_destroy(struct stream *stream); int stream_activate(struct stream *stream, uint16_t index, uint64_t now); int stream_deactivate(struct stream *stream, uint64_t now); +int stream_activate_virtual(struct stream *stream, uint16_t index); #endif /* AVB_STREAM_H */ diff --git a/src/modules/module-client-node/client-node.c b/src/modules/module-client-node/client-node.c index 0253591cf..14fda3a77 100644 --- a/src/modules/module-client-node/client-node.c +++ b/src/modules/module-client-node/client-node.c @@ -92,7 +92,6 @@ struct impl { struct spa_node node; - struct spa_log *log; struct spa_loop *data_loop; struct spa_system *data_system; @@ -264,7 +263,8 @@ static void clear_data(struct impl *impl, struct spa_data *d) case SPA_DATA_DmaBuf: case SPA_DATA_SyncObj: pw_log_debug("%p: close fd:%d", impl, (int)d->fd); - close(d->fd); + if (d->fd != -1) + close(d->fd); break; } } @@ -282,7 +282,7 @@ static int clear_buffers(struct impl *impl, struct mix *mix) for (i = 0; i < mix->n_buffers; i++) { struct buffer *b = &mix->buffers[i]; - spa_log_debug(impl->log, "%p: clear buffer %d", impl, i); + pw_log_debug("%p: clear buffer %d", impl, i); clear_buffer(impl, &b->buffer); pw_memblock_unref(b->mem); } @@ -299,7 +299,7 @@ static void free_mix(struct port *p, struct mix *mix) if (mix->n_buffers) { /* this shouldn't happen */ - spa_log_warn(impl->log, "%p: mix port-id:%u freeing leaked buffers", impl, mix->mix_id - 1u); + pw_log_warn("%p: mix port-id:%u freeing leaked buffers", impl, mix->mix_id - 1u); } clear_buffers(impl, mix); @@ -330,11 +330,13 @@ static int impl_node_enum_params(void *object, int seq, struct spa_pod_dynamic_builder b; struct spa_result_node_params result; uint32_t count = 0; - bool found = false; spa_return_val_if_fail(impl != NULL, -EINVAL); spa_return_val_if_fail(num != 0, -EINVAL); + if (!pw_param_info_find(impl->this.node->info.params, impl->this.node->info.n_params, id)) + return -ENOENT; + result.id = id; result.next = 0; @@ -350,8 +352,6 @@ static int impl_node_enum_params(void *object, int seq, if (param == NULL || !spa_pod_is_object_id(param, id)) continue; - found = true; - if (result.index < start) continue; @@ -366,7 +366,8 @@ static int impl_node_enum_params(void *object, int seq, if (count == num) break; } - return found ? 0 : -ENOENT; + + return 0; } static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, @@ -508,7 +509,7 @@ do_update_port(struct impl *impl, const struct spa_port_info *info) { if (change_mask & PW_CLIENT_NODE_PORT_UPDATE_PARAMS) { - spa_log_debug(impl->log, "%p: port %u update %d params", impl, port->id, n_params); + pw_log_debug("%p: port %u update %d params", impl, port->id, n_params); update_params(&port->params, n_params, params); } @@ -542,7 +543,7 @@ static int mix_clear_cb(void *item, void *data) static void clear_port(struct impl *impl, struct port *port) { - spa_log_debug(impl->log, "%p: clear port %p", impl, port); + pw_log_debug("%p: clear port %p", impl, port); do_update_port(impl, port, PW_CLIENT_NODE_PORT_UPDATE_PARAMS | @@ -598,7 +599,6 @@ node_port_enum_params(struct impl *impl, int seq, struct spa_pod_dynamic_builder b; struct spa_result_node_params result; uint32_t count = 0; - bool found = false; spa_return_val_if_fail(impl != NULL, -EINVAL); spa_return_val_if_fail(num != 0, -EINVAL); @@ -609,6 +609,9 @@ node_port_enum_params(struct impl *impl, int seq, pw_log_debug("%p: seq:%d port %d.%d id:%u start:%u num:%u n_params:%d", impl, seq, direction, port_id, id, start, num, port->params.n_params); + if (!pw_param_info_find(port->port->info.params, port->port->info.n_params, id)) + return -ENOENT; + result.id = id; result.next = 0; @@ -624,8 +627,6 @@ node_port_enum_params(struct impl *impl, int seq, if (param == NULL || !spa_pod_is_object_id(param, id)) continue; - found = true; - if (result.index < start) continue; @@ -640,7 +641,7 @@ node_port_enum_params(struct impl *impl, int seq, if (count == num) break; } - return found ? 0 : -ENOENT; + return 0; } static int @@ -781,7 +782,7 @@ do_port_use_buffers(struct impl *impl, if (n_buffers > MAX_BUFFERS) return -ENOSPC; - spa_log_debug(impl->log, "%p: %s port %d.%d use buffers %p %u flags:%08x", impl, + pw_log_debug("%p: %s port %d.%d use buffers %p %u flags:%08x", impl, direction == SPA_DIRECTION_INPUT ? "input" : "output", port_id, mix_id, buffers, n_buffers, flags); @@ -851,7 +852,7 @@ do_port_use_buffers(struct impl *impl, mb[i].mem_id = m->id; mb[i].offset = SPA_PTRDIFF(baseptr, mem->map->ptr); mb[i].size = SPA_PTRDIFF(endptr, baseptr); - spa_log_debug(impl->log, "%p: buffer %d %d %d %d", impl, i, mb[i].mem_id, + pw_log_debug("%p: buffer %d %d %d %d", impl, i, mb[i].mem_id, mb[i].offset, mb[i].size); b->buffer.n_metas = SPA_MIN(buffers[i]->n_metas, MAX_METAS); @@ -864,8 +865,11 @@ do_port_use_buffers(struct impl *impl, memcpy(&b->datas[j], d, sizeof(struct spa_data)); - if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) + if (flags & SPA_NODE_BUFFERS_FLAG_ALLOC) { + b->datas[j].fd = -1; + b->datas[j].data = SPA_UINT32_TO_PTR(SPA_ID_INVALID); continue; + } switch (d->type) { case SPA_DATA_DmaBuf: @@ -881,7 +885,7 @@ do_port_use_buffers(struct impl *impl, if (d->flags & SPA_DATA_FLAG_WRITABLE) flags |= PW_MEMBLOCK_FLAG_WRITABLE; - spa_log_debug(impl->log, "mem %d type:%d fd:%d", j, d->type, (int)d->fd); + pw_log_debug("mem %d type:%d fd:%d", j, d->type, (int)d->fd); m = pw_mempool_import(impl->client_pool, flags, d->type, d->fd); if (m == NULL) @@ -892,14 +896,14 @@ do_port_use_buffers(struct impl *impl, break; } case SPA_DATA_MemPtr: - spa_log_debug(impl->log, "mem %d %zd", j, SPA_PTRDIFF(d->data, baseptr)); + pw_log_debug("mem %d %zd", j, SPA_PTRDIFF(d->data, baseptr)); b->datas[j].data = SPA_INT_TO_PTR(SPA_PTRDIFF(d->data, baseptr)); SPA_FLAG_CLEAR(b->datas[j].flags, SPA_DATA_FLAG_MAPPABLE); break; default: b->datas[j].type = SPA_ID_INVALID; b->datas[j].data = NULL; - spa_log_error(impl->log, "invalid memory type %d", d->type); + pw_log_error("invalid memory type %d", d->type); break; } } @@ -935,7 +939,7 @@ impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id) spa_return_val_if_fail(impl != NULL, -EINVAL); spa_return_val_if_fail(CHECK_PORT(impl, SPA_DIRECTION_OUTPUT, port_id), -EINVAL); - spa_log_trace_fp(impl->log, "reuse buffer %d", buffer_id); + pw_log_trace_fp("reuse buffer %d", buffer_id); return -ENOTSUP; } @@ -948,7 +952,7 @@ static int impl_node_process(void *object) /* this should not be called, we call the exported node * directly */ - spa_log_warn(impl->log, "exported node activation"); + pw_log_warn("exported node activation"); spa_system_clock_gettime(impl->data_system, CLOCK_MONOTONIC, &ts); n->rt.target.activation->status = PW_NODE_ACTIVATION_TRIGGERED; n->rt.target.activation->signal_time = SPA_TIMESPEC_TO_NSEC(&ts); @@ -1009,7 +1013,7 @@ client_node_port_update(void *data, struct port *port; bool remove; - spa_log_debug(impl->log, "%p: got port update change:%08x params:%d", + pw_log_debug("%p: got port update change:%08x params:%d", impl, change_mask, n_params); remove = (change_mask == 0); @@ -1047,7 +1051,7 @@ client_node_port_update(void *data, static int client_node_set_active(void *data, bool active) { struct impl *impl = data; - spa_log_debug(impl->log, "%p: active:%d", impl, active); + pw_log_debug("%p: active:%d", impl, active); return pw_impl_node_set_active(impl->this.node, active); } @@ -1070,7 +1074,7 @@ static int client_node_port_buffers(void *data, struct mix *mix; uint32_t i, j; - spa_log_debug(impl->log, "%p: %s port %d.%d buffers %p %u", impl, + pw_log_debug("%p: %s port %d.%d buffers %p %u", impl, direction == SPA_DIRECTION_INPUT ? "input" : "output", port_id, mix_id, buffers, n_buffers); @@ -1098,7 +1102,7 @@ static int client_node_port_buffers(void *data, oldbuf = b->outbuf; newbuf = buffers[i]; - spa_log_debug(impl->log, "buffer %d n_datas:%d", i, newbuf->n_datas); + pw_log_debug("buffer %d n_datas:%d", i, newbuf->n_datas); for (j = 0; j < b->buffer.n_datas; j++) { struct spa_chunk *oldchunk = oldbuf->datas[j].chunk; @@ -1107,7 +1111,7 @@ static int client_node_port_buffers(void *data, if (d->type == SPA_DATA_MemFd && !SPA_FLAG_IS_SET(flags, SPA_DATA_FLAG_MAPPABLE)) { - spa_log_debug(impl->log, "buffer:%d data:%d has non mappable MemFd, " + pw_log_debug("buffer:%d data:%d has non mappable MemFd, " "fixing to ensure backwards compatibility.", i, j); flags |= SPA_DATA_FLAG_MAPPABLE; @@ -1122,7 +1126,7 @@ static int client_node_port_buffers(void *data, b->datas[j].flags = flags; b->datas[j].fd = d->fd; - spa_log_debug(impl->log, " data %d type:%d fl:%08x fd:%d, offs:%d max:%d", + pw_log_debug(" data %d type:%d fl:%08x fd:%d, offs:%d max:%d", j, d->type, flags, (int) d->fd, d->mapoffset, d->maxsize); } @@ -1149,7 +1153,7 @@ static void node_on_data_fd_events(struct spa_source *source) struct impl *impl = source->data; if (SPA_UNLIKELY(source->rmask & (SPA_IO_ERR | SPA_IO_HUP))) { - spa_log_warn(impl->log, "%p: got error", impl); + pw_log_warn("%p: got error", impl); return; } if (SPA_LIKELY(source->rmask & SPA_IO_IN)) { @@ -1166,10 +1170,10 @@ static void node_on_data_fd_events(struct spa_source *source) if (impl->resource && impl->resource->version < 5) { struct pw_node_activation *a = node->rt.target.activation; int status = a->state[0].status; - spa_log_trace_fp(impl->log, "%p: got ready %d", impl, status); + pw_log_trace_fp("%p: got ready %d", impl, status); spa_node_call_ready(&impl->callbacks, status); } else { - spa_log_trace_fp(impl->log, "%p: got complete", impl); + pw_log_trace_fp("%p: got complete", impl); pw_impl_node_rt_emit_complete(node); } } @@ -1770,7 +1774,6 @@ struct pw_impl_client_node *pw_impl_client_node_new(struct pw_resource *resource pw_log_debug("%p: new", &impl->node); impl_init(impl, NULL); - impl->log = pw_log_get(); impl->resource = resource; impl->client = client; impl->client_pool = pw_impl_client_get_mempool(client); diff --git a/src/modules/module-ffado-driver.c b/src/modules/module-ffado-driver.c index f6a76bed0..c98b0efcd 100644 --- a/src/modules/module-ffado-driver.c +++ b/src/modules/module-ffado-driver.c @@ -345,34 +345,26 @@ static void midi_to_ffado(struct port *p, float *src, uint32_t n_samples) p->event_pos = 0; 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; + uint32_t j, size = c.value.size; + const uint8_t *data = c_body; - if (c.type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_Midi) continue; if (index < c.offset) index = SPA_ROUND_UP_N(c.offset, 8); - 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++; - } + 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 - dst[index] = 0x01000000 | (uint32_t) data[j]; - index += 8; + unhandled++; } + else + dst[index] = 0x01000000 | (uint32_t) data[j]; + index += 8; } } if (unhandled > 0) @@ -497,16 +489,8 @@ static void ffado_to_midi(struct port *p, float *dst, uint32_t *src, uint32_t si continue; if (process_byte(p, i, data & 0xff, &frame, &bytes, &size)) { - uint64_t state = 0; - while (size > 0) { - uint32_t ev[4]; - int ev_size = spa_ump_from_midi(&bytes, &size, ev, sizeof(ev), 0, &state); - if (ev_size <= 0) - break; - - spa_pod_builder_control(&b, frame, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&b, ev, ev_size); - } + spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, bytes, size); } } spa_pod_builder_pop(&b, &f); @@ -1556,8 +1540,6 @@ 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_NODE_GROUP) == NULL) pw_properties_set(props, PW_KEY_NODE_GROUP, "ffado-group"); - if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL) - pw_properties_set(props, PW_KEY_NODE_LINK_GROUP, "ffado-group"); if (pw_properties_get(props, PW_KEY_NODE_PAUSE_ON_IDLE) == NULL) pw_properties_set(props, PW_KEY_NODE_PAUSE_ON_IDLE, "false"); diff --git a/src/modules/module-filter-chain.c b/src/modules/module-filter-chain.c index 64a92c5ac..c369810fe 100644 --- a/src/modules/module-filter-chain.c +++ b/src/modules/module-filter-chain.c @@ -128,7 +128,7 @@ extern struct spa_handle_factory spa_filter_graph_factory; * # an example ladspa plugin * type = ladspa * name = pitch - * plugin = "/usr/lib64/ladspa/ladspa-rubberband.so" + * plugin = "ladspa-rubberband" * label = "rubberband-r3-pitchshifter-mono" * control = { * # controls are using the ladspa port names as seen in analyseplugin @@ -398,10 +398,10 @@ extern struct spa_handle_factory spa_filter_graph_factory; * between 64 and 256. When not specified, this value is * computed automatically from the number of samples in the file. * - `tailsize` specifies the size of the tail blocks to use in the FFT. - * - `gain` the overall gain to apply to the IR file. + * - `gain` the overall gain to apply to the IR file. Default 1.0 * - `delay` The extra delay to add to the IR. A float number will be interpreted as seconds, * and integer as samples. Using the delay in seconds is independent of the graph - * and IR rate and is recommended. + * and IR rate and is recommended. Default 0 * - `filename` The IR to load or create. Possible values are: * - `/hilbert` creates a [hilbert function](https://en.wikipedia.org/wiki/Hilbert_transform) * that can be used to phase shift the signal by +/-90 degrees. The @@ -1651,7 +1651,7 @@ static const struct pw_stream_events out_stream_events = { static int setup_streams(struct impl *impl) { - int res; + int res = 0; uint32_t i, n_params, *offs, flags; struct pw_array offsets; const struct spa_pod **params = NULL; @@ -1702,6 +1702,19 @@ static int setup_streams(struct impl *impl) spa_process_latency_build(&b.b, SPA_PARAM_ProcessLatency, &impl->process_latency); + + if (impl->capture || impl->playback) { + if ((offs = pw_array_add(&offsets, sizeof(uint32_t))) != NULL) + *offs = b.b.state.offset; + + if (impl->capture) + spa_format_audio_raw_build(&b.b, + SPA_PARAM_EnumFormat, &impl->capture_info); + else + spa_format_audio_raw_build(&b.b, + SPA_PARAM_EnumFormat, &impl->playback_info); + } + n_params = pw_array_get_len(&offsets, uint32_t); if (n_params == 0) { res = -ENOMEM; @@ -1717,8 +1730,6 @@ static int setup_streams(struct impl *impl) params[i] = spa_pod_builder_deref(&b.b, offs[i]); if (impl->capture) { - params[n_params++] = spa_format_audio_raw_build(&b.b, - SPA_PARAM_EnumFormat, &impl->capture_info); flags = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS; @@ -1739,8 +1750,9 @@ static int setup_streams(struct impl *impl) spa_pod_dynamic_builder_init(&b, NULL, 0, 4096); } if (impl->playback) { - params[n_params++] = spa_format_audio_raw_build(&b.b, - SPA_PARAM_EnumFormat, &impl->playback_info); + if (n_params == 0) + params[n_params++] = spa_format_audio_raw_build(&b.b, + SPA_PARAM_EnumFormat, &impl->playback_info); flags = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | diff --git a/src/modules/module-jack-tunnel.c b/src/modules/module-jack-tunnel.c index 0c0cee034..bee37da4f 100644 --- a/src/modules/module-jack-tunnel.c +++ b/src/modules/module-jack-tunnel.c @@ -50,7 +50,6 @@ * * - `jack.library`: the libjack to load, by default libjack.so.0 is searched in * 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 @@ -243,13 +242,16 @@ static inline void do_volume(float *dst, const float *src, struct volume *vol, u } } -static inline void fix_midi_event(uint8_t *data, size_t size) +static inline bool fix_midi_event(const uint8_t *data, size_t size, uint8_t dst[3]) { /* fixup NoteOn with vel 0 */ if (size > 2 && (data[0] & 0xF0) == 0x90 && data[2] == 0x00) { - data[0] = 0x80 + (data[0] & 0x0F); - data[2] = 0x40; + dst[0] = 0x80 + (data[0] & 0x0F); + dst[1] = data[1]; + dst[2] = 0x40; + return true; } + return false; } static void midi_to_jack(struct impl *impl, float *dst, float *src, uint32_t n_samples) @@ -260,9 +262,6 @@ static void midi_to_jack(struct impl *impl, float *dst, float *src, uint32_t n_s 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) @@ -274,36 +273,20 @@ static void midi_to_jack(struct impl *impl, float *dst, float *src, uint32_t n_s return; 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; + uint32_t size = c.value.size; + const uint8_t *data = c_body; + uint8_t tmp[3]; - if (c.type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_Midi) 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(&tmp[tmp_size], size); - - 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; - } + if (impl->fix_midi && fix_midi_event(data, size, tmp)) { + data = tmp; + size = 3; } + 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)); } } @@ -319,19 +302,11 @@ static void jack_to_midi(float *dst, float *src, uint32_t size) spa_pod_builder_push_sequence(&b, &f, 0); for (i = 0; i < count; i++) { jack_midi_event_t ev; - uint64_t state = 0; jack.midi_event_get(&ev, src, i); - while (ev.size > 0) { - uint32_t ump[4]; - int ump_size = spa_ump_from_midi(&ev.buffer, &ev.size, ump, sizeof(ump), 0, &state); - if (ump_size <= 0) - break; - - spa_pod_builder_control(&b, ev.time, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&b, ump, ump_size); - } + spa_pod_builder_control(&b, ev.time, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, ev.buffer, ev.size); } spa_pod_builder_pop(&b, &f); } diff --git a/src/modules/module-jack-tunnel/weakjack.h b/src/modules/module-jack-tunnel/weakjack.h index 6d5b5503e..472adb253 100644 --- a/src/modules/module-jack-tunnel/weakjack.h +++ b/src/modules/module-jack-tunnel/weakjack.h @@ -158,34 +158,36 @@ static inline int weakjack_load_by_path(struct weakjack *jack, const char *path) static inline int weakjack_load(struct weakjack *jack, const char *lib) { int res = -ENOENT; + const char *search_dirs, *p, *state = NULL; + char path[PATH_MAX]; + size_t len; - if (lib[0] != '/') { - const char *search_dirs, *p, *state = NULL; - char path[PATH_MAX]; - size_t len; + while ((p = strstr(lib, "../")) != NULL) + lib = p + 3; - search_dirs = getenv("LIBJACK_PATH"); - if (!search_dirs) - search_dirs = PREFIX "/lib64/:" PREFIX "/lib/:" - "/usr/lib64/:/usr/lib/:" LIBDIR; + search_dirs = getenv("LIBJACK_PATH"); + if (!search_dirs) + search_dirs = PREFIX "/lib64/:" PREFIX "/lib/:" + "/usr/lib64/:/usr/lib/:" LIBDIR; - while ((p = pw_split_walk(search_dirs, ":", &len, &state))) { - int pathlen; + res = -ENAMETOOLONG; - if (len >= sizeof(path)) { - res = -ENAMETOOLONG; - continue; - } + while ((p = pw_split_walk(search_dirs, ":", &len, &state))) { + int pathlen; + + if (len == 0 || len >= sizeof(path)) + continue; + + if (strncmp(lib, p, len) == 0 && lib[len] == '/') + pathlen = snprintf(path, sizeof(path), "%s", lib); + else pathlen = snprintf(path, sizeof(path), "%.*s/%s", (int) len, p, lib); - if (pathlen < 0 || (size_t) pathlen >= sizeof(path)) { - res = -ENAMETOOLONG; - continue; - } - if ((res = weakjack_load_by_path(jack, path)) == 0) - break; - } - } else { - res = weakjack_load_by_path(jack, lib); + + if (pathlen < 0 || (size_t) pathlen >= sizeof(path)) + continue; + + if ((res = weakjack_load_by_path(jack, path)) == 0) + break; } return res; } diff --git a/src/modules/module-netjack2-manager.c b/src/modules/module-netjack2-manager.c index 0ff62d93b..a9af6a9b0 100644 --- a/src/modules/module-netjack2-manager.c +++ b/src/modules/module-netjack2-manager.c @@ -1393,8 +1393,6 @@ 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_NODE_NETWORK) == NULL) pw_properties_set(props, PW_KEY_NODE_NETWORK, "true"); - if (pw_properties_get(props, PW_KEY_NODE_LINK_GROUP) == NULL) - pw_properties_set(props, PW_KEY_NODE_LINK_GROUP, "jack-group"); if (pw_properties_get(props, PW_KEY_NODE_ALWAYS_PROCESS) == NULL) pw_properties_set(props, PW_KEY_NODE_ALWAYS_PROCESS, "true"); if (pw_properties_get(props, PW_KEY_NODE_LOCK_QUANTUM) == NULL) diff --git a/src/modules/module-netjack2/peer.c b/src/modules/module-netjack2/peer.c index eacc1c95b..7547cc5b2 100644 --- a/src/modules/module-netjack2/peer.c +++ b/src/modules/module-netjack2/peer.c @@ -273,39 +273,18 @@ static inline void *n2j_midi_buffer_reserve(struct nj2_midi_buffer *buf, } static inline void n2j_midi_buffer_write(struct nj2_midi_buffer *buf, - uint32_t offset, void *data, uint32_t size) + uint32_t offset, const void *data, uint32_t size, bool fix) { - void *ptr = n2j_midi_buffer_reserve(buf, offset, size); - if (ptr != NULL) + uint8_t *ptr = n2j_midi_buffer_reserve(buf, offset, size); + if (ptr != NULL) { memcpy(ptr, data, size); + if (fix) + fix_midi_event(ptr, 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) { @@ -314,7 +293,6 @@ static void midi_to_netjack2(struct netjack2_peer *peer, 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->params.period_size * sizeof(float); @@ -332,39 +310,17 @@ static void midi_to_netjack2(struct netjack2_peer *peer, return; while (spa_pod_parser_get_control_body(&parser, &c, &c_body) >= 0) { - int size; - uint8_t data[16]; - bool was_sysex = in_sysex; - size_t c_size = c.value.size; - uint64_t state = 0; + uint32_t size = c.value.size; + const uint8_t *data = c_body; - if (c.type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_Midi) 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) { - 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 (c.offset >= n_samples) { + buf->lost_events++; + continue; } + n2j_midi_buffer_write(buf, c.offset, data, size, peer->fix_midi); } if (buf->write_pos > 0) memmove(SPA_PTROFF(buf, sizeof(*buf) + buf->event_count * sizeof(struct nj2_midi_event), void), @@ -395,8 +351,6 @@ static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_b for (i = 0; i < buf->event_count; i++) { struct nj2_midi_event *ev = &buf->event[i]; uint8_t *data; - size_t s; - uint64_t state = 0; if (ev->size <= MIDI_INLINE_MAX) data = ev->buffer; @@ -405,17 +359,8 @@ static inline void netjack2_to_midi(float *dst, uint32_t size, struct nj2_midi_b else continue; - s = ev->size; - 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) { - 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); - } + spa_pod_builder_control(&b, ev->time, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, data, ev->size); } spa_pod_builder_pop(&b, &f); } diff --git a/src/modules/module-profiler.c b/src/modules/module-profiler.c index b49629ba9..1709ec9a5 100644 --- a/src/modules/module-profiler.c +++ b/src/modules/module-profiler.c @@ -166,7 +166,7 @@ static void do_flush_event(void *data, uint64_t count) avail = spa_ringbuffer_get_read_index(&n->buffer, &idx); - pw_log_trace("%p: avail %d", impl, avail); + pw_log_trace("%p: node:%p avail %d", n, impl, avail); if (avail > 0) { size_t size = total + avail + sizeof(struct spa_pod_struct); diff --git a/src/modules/module-protocol-native.c b/src/modules/module-protocol-native.c index 2be92a847..98a43829b 100644 --- a/src/modules/module-protocol-native.c +++ b/src/modules/module-protocol-native.c @@ -907,7 +907,7 @@ static int add_socket(struct pw_protocol *protocol, struct server *s, struct soc bool activated = false; { - int i, n = listen_fd(); + int i, n = listen_fds(); for (i = 0; i < n; ++i) { if (is_socket_unix(LISTEN_FDS_START + i, SOCK_STREAM, s->addr.sun_path) > 0) { diff --git a/src/modules/module-protocol-native/connection.c b/src/modules/module-protocol-native/connection.c index b376a0f69..15c739428 100644 --- a/src/modules/module-protocol-native/connection.c +++ b/src/modules/module-protocol-native/connection.c @@ -536,7 +536,7 @@ static int prepare_packet(struct pw_protocol_native_connection *conn, struct buf size -= impl->hdr_size; buf->msg.fds = &buf->fds[buf->fds_offset]; - if (buf->msg.n_fds + buf->fds_offset > MAX_FDS) + if (buf->msg.n_fds + buf->fds_offset > buf->n_fds) return -EPROTO; if (size < len) diff --git a/src/modules/module-protocol-native/test-connection.c b/src/modules/module-protocol-native/test-connection.c index eabcbbd67..0c82d2da8 100644 --- a/src/modules/module-protocol-native/test-connection.c +++ b/src/modules/module-protocol-native/test-connection.c @@ -3,6 +3,7 @@ /* SPDX-License-Identifier: MIT */ #include +#include #include #include @@ -165,6 +166,87 @@ static void test_reentering(struct pw_protocol_native_connection *in, } } +/* + * Test that a packet claiming more FDs in its header than were actually + * sent via SCM_RIGHTS is rejected. Without the n_fds validation this + * would cause the receiver to read uninitialised / stale FD values. + */ +static void test_spoofed_fds(struct pw_protocol_native_connection *in, + struct pw_protocol_native_connection *out) +{ + const struct pw_protocol_native_message *msg; + int res; + + /* + * First, send a valid message through the normal API so that the + * receiver's version handshake happens (it switches to HDR_SIZE=16 + * on the first message). Use a message with 0 FDs. + */ + { + struct spa_pod_builder *b; + struct pw_protocol_native_message *wmsg; + + b = pw_protocol_native_connection_begin(out, 0, 1, &wmsg); + spa_assert_se(b != NULL); + spa_pod_builder_add_struct(b, SPA_POD_Int(0)); + pw_protocol_native_connection_end(out, b); + pw_protocol_native_connection_flush(out); + + /* Consume it on the reading side */ + res = pw_protocol_native_connection_get_next(in, &msg); + spa_assert_se(res == 1); + } + + /* + * Now craft a raw packet on the wire that claims n_fds=5 in the + * header but send 0 actual FDs via SCM_RIGHTS. + * + * v3 header layout (16 bytes / 4 uint32s): + * p[0] = id + * p[1] = (opcode << 24) | (payload_size & 0xffffff) + * p[2] = seq + * p[3] = n_fds + * + * We need a minimal valid SPA pod as payload. + */ + { + /* Build a tiny SPA pod: struct { Int(0) } */ + uint8_t payload[32]; + struct spa_pod_builder pb; + + spa_pod_builder_init(&pb, payload, sizeof(payload)); + spa_pod_builder_add_struct(&pb, SPA_POD_Int(0)); + + uint32_t payload_size = pb.state.offset; + uint32_t header[4]; + + spa_assert_se(payload_size <= sizeof(payload)); + + header[0] = 1; /* id */ + header[1] = (5u << 24) | (payload_size & 0xffffff); /* opcode=5, size */ + header[2] = 0; /* seq */ + header[3] = 5; /* SPOOFED: claim 5 fds, send 0 */ + + struct iovec iov[2]; + struct msghdr mh = { 0 }; + + iov[0].iov_base = header; + iov[0].iov_len = sizeof(header); + iov[1].iov_base = payload; + iov[1].iov_len = payload_size; + mh.msg_iov = iov; + mh.msg_iovlen = 2; + /* No msg_control — 0 FDs via SCM_RIGHTS */ + + ssize_t sent = sendmsg(out->fd, &mh, MSG_NOSIGNAL); + spa_assert_se(sent == (ssize_t)(sizeof(header) + payload_size)); + } + + /* The receiver must reject this packet */ + res = pw_protocol_native_connection_get_next(in, &msg); + spa_assert_se(res == -EPROTO); +} + int main(int argc, char *argv[]) { struct pw_main_loop *loop; @@ -198,6 +280,26 @@ int main(int argc, char *argv[]) pw_protocol_native_connection_destroy(in); pw_protocol_native_connection_destroy(out); + + /* test_spoofed_fds needs its own connection pair */ + { + int fds2[2]; + struct pw_protocol_native_connection *in2, *out2; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds2) < 0) + spa_assert_not_reached(); + + in2 = pw_protocol_native_connection_new(context, fds2[0]); + spa_assert_se(in2 != NULL); + out2 = pw_protocol_native_connection_new(context, fds2[1]); + spa_assert_se(out2 != NULL); + + test_spoofed_fds(in2, out2); + + pw_protocol_native_connection_destroy(in2); + pw_protocol_native_connection_destroy(out2); + } + pw_context_destroy(context); pw_main_loop_destroy(loop); 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 d4425a3fe..ffb5aa0fe 100644 --- a/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c +++ b/src/modules/module-protocol-pulse/modules/module-zeroconf-publish.c @@ -15,14 +15,7 @@ #include "../module.h" #include "../pulse-server.h" #include "../server.h" -#include "../../module-zeroconf-discover/avahi-poll.h" - -#include -#include -#include -#include -#include -#include +#include "../../zeroconf-utils/zeroconf.h" /** \page page_pulse_module_zeroconf_publish Zeroconf Publish * @@ -52,32 +45,17 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); #define SERVICE_DATA_ID "module-zeroconf-publish.service" -enum service_subtype { - SUBTYPE_HARDWARE, - SUBTYPE_VIRTUAL, - SUBTYPE_MONITOR -}; - struct service { struct spa_list link; struct module_zeroconf_publish_data *userdata; - AvahiEntryGroup *entry_group; - AvahiStringList *txt; struct server *server; - const char *service_type; - enum service_subtype subtype; - - char *name; - bool is_sink; - struct sample_spec ss; struct channel_map cm; struct pw_properties *props; - char service_name[AVAHI_LABEL_MAX]; unsigned published:1; }; @@ -91,8 +69,8 @@ struct module_zeroconf_publish_data { struct spa_hook manager_listener; struct spa_hook impl_listener; - AvahiPoll *avahi_poll; - AvahiClient *client; + struct pw_zeroconf *zeroconf; + struct spa_hook zeroconf_listener; /* lists of services */ struct spa_list pending; @@ -116,47 +94,43 @@ static const struct pw_core_events core_events = { .error = on_core_error, }; -static void get_service_name(struct pw_manager_object *o, char *buf, size_t length) +static void unpublish_service(struct service *s) { - const char *hn, *un, *n; + const char *device; - hn = pw_get_host_name(); - un = pw_get_user_name(); - n = pw_properties_get(o->props, PW_KEY_NODE_DESCRIPTION); + spa_list_remove(&s->link); + spa_list_append(&s->userdata->pending, &s->link); + s->published = false; + s->server = NULL; - snprintf(buf, length, "%s@%s: %s", un, hn, n); + device = pw_properties_get(s->props, "device"); + + pw_log_info("unpublished service: %s", device); + + pw_zeroconf_set_announce(s->userdata->zeroconf, s, NULL); +} + +static void unpublish_all_services(struct module_zeroconf_publish_data *d) +{ + struct service *s; + spa_list_consume(s, &d->published, link) + unpublish_service(s); } static void service_free(struct service *s) { pw_log_debug("service %p: free", s); - if (s->entry_group) - avahi_entry_group_free(s->entry_group); - - if (s->name) - free(s->name); + if (s->published) + unpublish_service(s); pw_properties_free(s->props); - avahi_string_list_free(s->txt); spa_list_remove(&s->link); + /* no need to free, the service is added as custom + * data on the object */ } -static void unpublish_service(struct service *s) -{ - spa_list_remove(&s->link); - spa_list_append(&s->userdata->pending, &s->link); - s->published = false; - s->server = NULL; -} - -static void unpublish_all_services(struct module_zeroconf_publish_data *d) -{ - struct service *s; - - spa_list_consume(s, &d->published, link) - unpublish_service(s); -} +#define PA_CHANNEL_MAP_SNPRINT_MAX (CHANNELS_MAX * 32) static char* channel_map_snprint(char *s, size_t l, const struct channel_map *map) { @@ -188,6 +162,39 @@ static char* channel_map_snprint(char *s, size_t l, const struct channel_map *ma return s; } +static void txt_record_server_data(struct pw_core_info *info, struct pw_properties *props) +{ + struct utsname u; + + spa_assert(info); + + pw_properties_set(props, "server-version", PACKAGE_NAME" "PACKAGE_VERSION); + pw_properties_set(props, "user-name", pw_get_user_name()); + pw_properties_set(props, "fqdn", pw_get_host_name()); + pw_properties_setf(props, "cookie", "0x%08x", info->cookie); + if (uname(&u) >= 0) + pw_properties_setf(props, "uname", "%s %s %s", u.sysname, u.machine, u.release); +} + +static void fill_service_txt(const struct service *s, const struct pw_properties *props) +{ + static const struct mapping { + const char *pw_key, *txt_key; + } mappings[] = { + { PW_KEY_NODE_DESCRIPTION, "description" }, + { PW_KEY_DEVICE_VENDOR_NAME, "vendor-name" }, + { PW_KEY_DEVICE_PRODUCT_NAME, "product-name" }, + { PW_KEY_DEVICE_CLASS, "class" }, + { PW_KEY_DEVICE_FORM_FACTOR, "form-factor" }, + { PW_KEY_DEVICE_ICON_NAME, "icon-name" }, + }; + SPA_FOR_EACH_ELEMENT_VAR(mappings, m) { + const char *value = pw_properties_get(props, m->pw_key); + if (value != NULL) + pw_properties_set(s->props, m->txt_key, value); + } +} + static void fill_service_data(struct module_zeroconf_publish_data *d, struct service *s, struct pw_manager_object *o) { @@ -200,6 +207,10 @@ static void fill_service_data(struct module_zeroconf_publish_data *d, struct ser struct card_info card_info = CARD_INFO_INIT; struct device_info dev_info; uint32_t flags = 0; + const char *service_type, *subtype, *subtype_service[2]; + uint32_t n_subtype = 0; + char cm[PA_CHANNEL_MAP_SNPRINT_MAX]; + if (info == NULL || info->props == NULL) return; @@ -228,19 +239,49 @@ static void fill_service_data(struct module_zeroconf_publish_data *d, struct ser s->ss = dev_info.ss; s->cm = dev_info.map; - s->name = strdup(name); - s->props = pw_properties_copy(o->props); + + s->props = pw_properties_new(NULL, NULL); + + txt_record_server_data(s->userdata->manager->info, s->props); if (is_sink) { - s->is_sink = true; - s->service_type = SERVICE_TYPE_SINK; - s->subtype = flags & SINK_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL; + service_type = SERVICE_TYPE_SINK; + if (flags & SINK_HARDWARE) { + subtype = "hardware"; + subtype_service[n_subtype++] = SERVICE_SUBTYPE_SINK_HARDWARE; + } else { + subtype = "virtual"; + subtype_service[n_subtype++] = SERVICE_SUBTYPE_SINK_VIRTUAL; + } } else if (is_source) { - s->is_sink = false; - s->service_type = SERVICE_TYPE_SOURCE; - s->subtype = flags & SOURCE_HARDWARE ? SUBTYPE_HARDWARE : SUBTYPE_VIRTUAL; + service_type = SERVICE_TYPE_SOURCE; + if (flags & SOURCE_HARDWARE) { + subtype = "hardware"; + subtype_service[n_subtype++] = SERVICE_SUBTYPE_SOURCE_HARDWARE; + } else { + subtype = "virtual"; + subtype_service[n_subtype++] = SERVICE_SUBTYPE_SOURCE_VIRTUAL; + } + subtype_service[n_subtype++] = SERVICE_SUBTYPE_SOURCE_NON_MONITOR; } else spa_assert_not_reached(); + + pw_properties_set(s->props, "device", name); + pw_properties_setf(s->props, "rate", "%u", s->ss.rate); + pw_properties_setf(s->props, "channels", "%u", s->ss.channels); + pw_properties_set(s->props, "format", format_id2paname(s->ss.format)); + pw_properties_set(s->props, "channel_map", channel_map_snprint(cm, sizeof(cm), &s->cm)); + pw_properties_set(s->props, "subtype", subtype); + + pw_properties_setf(s->props, PW_KEY_ZEROCONF_NAME, "%s@%s: %s", + pw_get_user_name(), pw_get_host_name(), desc); + pw_properties_set(s->props, PW_KEY_ZEROCONF_TYPE, service_type); + pw_properties_setf(s->props, PW_KEY_ZEROCONF_SUBTYPES, "[ %s%s%s ]", + n_subtype > 0 ? subtype_service[0] : "", + n_subtype > 1 ? ", " : "", + n_subtype > 1 ? subtype_service[1] : ""); + + fill_service_txt(s, o->props); } static struct service *create_service(struct module_zeroconf_publish_data *d, struct pw_manager_object *o) @@ -252,8 +293,6 @@ static struct service *create_service(struct module_zeroconf_publish_data *d, st return NULL; s->userdata = d; - s->entry_group = NULL; - get_service_name(o, s->service_name, sizeof(s->service_name)); spa_list_append(&d->pending, &s->link); fill_service_data(d, s, o); @@ -263,127 +302,6 @@ static struct service *create_service(struct module_zeroconf_publish_data *d, st return s; } -static AvahiStringList* txt_record_server_data(struct pw_core_info *info, AvahiStringList *l) -{ - const char *t; - struct utsname u; - - spa_assert(info); - - l = avahi_string_list_add_pair(l, "server-version", PACKAGE_NAME" "PACKAGE_VERSION); - - t = pw_get_user_name(); - l = avahi_string_list_add_pair(l, "user-name", t); - - if (uname(&u) >= 0) { - char sysname[sizeof(u.sysname) + sizeof(u.machine) + sizeof(u.release)]; - - snprintf(sysname, sizeof(sysname), "%s %s %s", u.sysname, u.machine, u.release); - l = avahi_string_list_add_pair(l, "uname", sysname); - } - - t = pw_get_host_name(); - l = avahi_string_list_add_pair(l, "fqdn", t); - l = avahi_string_list_add_printf(l, "cookie=0x%08x", info->cookie); - - return l; -} - -static void clear_entry_group(struct service *s) -{ - if (s->entry_group == NULL) - return; - - avahi_entry_group_free(s->entry_group); - s->entry_group = NULL; -} - -static void publish_service(struct service *s); - -static void service_entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) -{ - struct service *s = userdata; - - spa_assert(s); - if (!s->published) { - pw_log_info("cancel unpublished service: %s", s->service_name); - clear_entry_group(s); - return; - } - - switch (state) { - case AVAHI_ENTRY_GROUP_ESTABLISHED: - pw_log_info("established service: %s", s->service_name); - break; - case AVAHI_ENTRY_GROUP_COLLISION: - { - char *t; - - t = avahi_alternative_service_name(s->service_name); - pw_log_info("service name collision: renaming '%s' to '%s'", s->service_name, t); - snprintf(s->service_name, sizeof(s->service_name), "%s", t); - avahi_free(t); - - unpublish_service(s); - publish_service(s); - break; - } - case AVAHI_ENTRY_GROUP_FAILURE: - pw_log_error("failed to establish service '%s': %s", - s->service_name, - avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g)))); - unpublish_service(s); - clear_entry_group(s); - break; - - case AVAHI_ENTRY_GROUP_UNCOMMITED: - case AVAHI_ENTRY_GROUP_REGISTERING: - break; - } -} - -#define PA_CHANNEL_MAP_SNPRINT_MAX (CHANNELS_MAX * 32) - -static AvahiStringList *get_service_txt(const struct service *s) -{ - static const char * const subtype_text[] = { - [SUBTYPE_HARDWARE] = "hardware", - [SUBTYPE_VIRTUAL] = "virtual", - [SUBTYPE_MONITOR] = "monitor" - }; - - static const struct mapping { - const char *pw_key, *txt_key; - } mappings[] = { - { PW_KEY_NODE_DESCRIPTION, "description" }, - { PW_KEY_DEVICE_VENDOR_NAME, "vendor-name" }, - { PW_KEY_DEVICE_PRODUCT_NAME, "product-name" }, - { PW_KEY_DEVICE_CLASS, "class" }, - { PW_KEY_DEVICE_FORM_FACTOR, "form-factor" }, - { PW_KEY_DEVICE_ICON_NAME, "icon-name" }, - }; - - char cm[PA_CHANNEL_MAP_SNPRINT_MAX]; - AvahiStringList *txt = NULL; - - txt = txt_record_server_data(s->userdata->manager->info, txt); - - txt = avahi_string_list_add_pair(txt, "device", s->name); - txt = avahi_string_list_add_printf(txt, "rate=%u", s->ss.rate); - txt = avahi_string_list_add_printf(txt, "channels=%u", s->ss.channels); - txt = avahi_string_list_add_pair(txt, "format", format_id2paname(s->ss.format)); - txt = avahi_string_list_add_pair(txt, "channel_map", channel_map_snprint(cm, sizeof(cm), &s->cm)); - txt = avahi_string_list_add_pair(txt, "subtype", subtype_text[s->subtype]); - - SPA_FOR_EACH_ELEMENT_VAR(mappings, m) { - const char *value = pw_properties_get(s->props, m->pw_key); - if (value != NULL) - txt = avahi_string_list_add_pair(txt, m->txt_key, value); - } - - return txt; -} - static struct server *find_server(struct service *s, int *proto, uint16_t *port) { struct module_zeroconf_publish_data *d = s->userdata; @@ -392,109 +310,47 @@ static struct server *find_server(struct service *s, int *proto, uint16_t *port) spa_list_for_each(server, &impl->servers, link) { if (server->addr.ss_family == AF_INET) { - *proto = AVAHI_PROTO_INET; + *proto = 4; *port = ntohs(((struct sockaddr_in*) &server->addr)->sin_port); return server; } else if (server->addr.ss_family == AF_INET6) { - *proto = AVAHI_PROTO_INET6; + *proto = 6; *port = ntohs(((struct sockaddr_in6*) &server->addr)->sin6_port); return server; } } - return NULL; } static void publish_service(struct service *s) { struct module_zeroconf_publish_data *d = s->userdata; - int proto; + int proto, res; uint16_t port; - struct server *server = find_server(s, &proto, &port); + const char *device; + if (!server) return; + device = pw_properties_get(s->props, "device"); + pw_log_debug("found server:%p proto:%d port:%d", server, proto, port); - if (!d->client || avahi_client_get_state(d->client) != AVAHI_CLIENT_S_RUNNING) + pw_properties_setf(s->props, PW_KEY_ZEROCONF_PROTO, "%d", proto); + pw_properties_setf(s->props, PW_KEY_ZEROCONF_PORT, "%d", port); + + if ((res = pw_zeroconf_set_announce(s->userdata->zeroconf, s, &s->props->dict)) < 0) { + pw_log_error("failed to announce service %s: %s", device, spa_strerror(res)); return; - - s->published = true; - if (!s->entry_group) { - s->entry_group = avahi_entry_group_new(d->client, service_entry_group_callback, s); - if (s->entry_group == NULL) { - pw_log_error("avahi_entry_group_new(): %s", - avahi_strerror(avahi_client_errno(d->client))); - goto error; - } - } else { - avahi_entry_group_reset(s->entry_group); - } - - if (s->txt == NULL) - s->txt = get_service_txt(s); - - if (avahi_entry_group_add_service_strlst( - s->entry_group, - AVAHI_IF_UNSPEC, proto, - 0, - s->service_name, - s->service_type, - NULL, - NULL, - port, - s->txt) < 0) { - pw_log_error("avahi_entry_group_add_service_strlst(): %s", - avahi_strerror(avahi_client_errno(d->client))); - goto error; - } - - if (avahi_entry_group_add_service_subtype( - s->entry_group, - AVAHI_IF_UNSPEC, proto, - 0, - s->service_name, - s->service_type, - NULL, - s->is_sink ? (s->subtype == SUBTYPE_HARDWARE ? SERVICE_SUBTYPE_SINK_HARDWARE : SERVICE_SUBTYPE_SINK_VIRTUAL) : - (s->subtype == SUBTYPE_HARDWARE ? SERVICE_SUBTYPE_SOURCE_HARDWARE : (s->subtype == SUBTYPE_VIRTUAL ? SERVICE_SUBTYPE_SOURCE_VIRTUAL : SERVICE_SUBTYPE_SOURCE_MONITOR))) < 0) { - - pw_log_error("avahi_entry_group_add_service_subtype(): %s", - avahi_strerror(avahi_client_errno(d->client))); - goto error; - } - - if (!s->is_sink && s->subtype != SUBTYPE_MONITOR) { - if (avahi_entry_group_add_service_subtype( - s->entry_group, - AVAHI_IF_UNSPEC, proto, - 0, - s->service_name, - SERVICE_TYPE_SOURCE, - NULL, - SERVICE_SUBTYPE_SOURCE_NON_MONITOR) < 0) { - pw_log_error("avahi_entry_group_add_service_subtype(): %s", - avahi_strerror(avahi_client_errno(d->client))); - goto error; - } - } - - if (avahi_entry_group_commit(s->entry_group) < 0) { - pw_log_error("avahi_entry_group_commit(): %s", - avahi_strerror(avahi_client_errno(d->client))); - goto error; } spa_list_remove(&s->link); spa_list_append(&d->published, &s->link); + s->published = true; s->server = server; - pw_log_info("created service: %s", s->service_name); - return; - -error: - s->published = false; + pw_log_info("published service: %s", device); return; } @@ -506,62 +362,6 @@ static void publish_pending(struct module_zeroconf_publish_data *data) publish_service(s); } -static void clear_pending_entry_groups(struct module_zeroconf_publish_data *data) -{ - struct service *s; - - spa_list_for_each(s, &data->pending, link) - clear_entry_group(s); -} - -static void client_callback(AvahiClient *c, AvahiClientState state, void *d) -{ - struct module_zeroconf_publish_data *data = d; - - spa_assert(c); - spa_assert(data); - - data->client = c; - - switch (state) { - case AVAHI_CLIENT_S_RUNNING: - pw_log_info("the avahi daemon is up and running"); - publish_pending(data); - break; - case AVAHI_CLIENT_S_COLLISION: - pw_log_error("host name collision"); - unpublish_all_services(d); - break; - case AVAHI_CLIENT_FAILURE: - { - int err = avahi_client_errno(data->client); - - pw_log_error("avahi client failure: %s", avahi_strerror(err)); - - unpublish_all_services(data); - clear_pending_entry_groups(data); - avahi_client_free(data->client); - data->client = NULL; - - if (err == AVAHI_ERR_DISCONNECTED) { - data->client = avahi_client_new(data->avahi_poll, AVAHI_CLIENT_NO_FAIL, client_callback, data, &err); - if (data->client == NULL) - pw_log_error("failed to create avahi client: %s", avahi_strerror(err)); - } - - if (data->client == NULL) - module_schedule_unload(data->module); - - break; - } - case AVAHI_CLIENT_CONNECTING: - pw_log_info("connecting to the avahi daemon..."); - break; - default: - break; - } -} - static void manager_removed(void *d, struct pw_manager_object *o) { if (!pw_manager_object_is_sink(o) && !pw_manager_object_is_source(o)) @@ -624,7 +424,6 @@ static void impl_server_stopped(void *data, struct server *server) if (s->server == server) unpublish_service(s); } - publish_pending(d); } @@ -634,10 +433,18 @@ static const struct impl_events impl_events = { .server_stopped = impl_server_stopped, }; +static void on_zeroconf_error(void *data, int err, const char *message) +{ + pw_log_error("got zeroconf error %d: %s", err, message); +} +static const struct pw_zeroconf_events zeroconf_events = { + PW_VERSION_ZEROCONF_EVENTS, + .error = on_zeroconf_error, +}; + static int module_zeroconf_publish_load(struct module *module) { struct module_zeroconf_publish_data *data = module->user_data; - int error; data->core = pw_context_connect(module->impl->context, NULL, 0); if (data->core == NULL) { @@ -649,24 +456,22 @@ static int module_zeroconf_publish_load(struct module *module) &data->core_listener, &core_events, data); - 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); - if (!data->client) { - pw_log_error("failed to create avahi client: %s", avahi_strerror(error)); - return -errno; - } - data->manager = pw_manager_new(data->core); if (data->manager == NULL) { pw_log_error("failed to create pipewire manager: %m"); return -errno; } - pw_manager_add_listener(data->manager, &data->manager_listener, &manager_events, data); + data->zeroconf = pw_zeroconf_new(module->impl->context, NULL); + if (!data->zeroconf) { + pw_log_error("failed to create zeroconf: %m"); + return -errno; + } + pw_zeroconf_add_listener(data->zeroconf, &data->zeroconf_listener, + &zeroconf_events, data); + impl_add_listener(module->impl, &data->impl_listener, &impl_events, data); return 0; @@ -684,22 +489,18 @@ static int module_zeroconf_publish_unload(struct module *module) spa_list_consume(s, &d->pending, link) service_free(s); - if (d->client) - avahi_client_free(d->client); - - if (d->avahi_poll) - pw_avahi_poll_free(d->avahi_poll); - + if (d->zeroconf) { + spa_hook_remove(&d->zeroconf_listener); + pw_zeroconf_destroy(d->zeroconf); + } if (d->manager != NULL) { spa_hook_remove(&d->manager_listener); pw_manager_destroy(d->manager); } - if (d->core != NULL) { spa_hook_remove(&d->core_listener); pw_core_disconnect(d->core); } - return 0; } diff --git a/src/modules/module-protocol-pulse/pulse-server.c b/src/modules/module-protocol-pulse/pulse-server.c index 59610ef57..2006a01cc 100644 --- a/src/modules/module-protocol-pulse/pulse-server.c +++ b/src/modules/module-protocol-pulse/pulse-server.c @@ -1621,7 +1621,7 @@ static int do_create_playback_stream(struct client *client, uint32_t command, ui struct pw_manager_object *o; bool is_monitor; - props = pw_properties_copy(client->props); + props = pw_properties_new(NULL, NULL); if (props == NULL) goto error_errno; @@ -1907,7 +1907,7 @@ static int do_create_record_stream(struct client *client, uint32_t command, uint struct pw_manager_object *o; bool is_monitor = false; - props = pw_properties_copy(client->props); + props = pw_properties_new(NULL, NULL); if (props == NULL) goto error_errno; @@ -2298,7 +2298,7 @@ static int do_create_upload_stream(struct client *client, uint32_t command, uint struct message *reply; int res; - if ((props = pw_properties_copy(client->props)) == NULL) + if ((props = pw_properties_new(NULL, NULL)) == NULL) goto error_errno; if ((res = message_get(m, @@ -4068,6 +4068,45 @@ static const char *get_media_name(struct pw_node_info *info) return media_name; } +static int fill_node_info_proplist(struct message *m, const struct spa_dict *node_props, + const struct pw_manager_object *client) +{ + struct pw_client_info *client_info = client ? client->info : NULL; + uint32_t n_items, n; + struct spa_dict dict, *client_props = NULL; + const struct spa_dict_item *it; + struct spa_dict_item *items, *it2; + + n_items = node_props->n_items; + if (client_info && client_info->props) { + client_props = client_info->props; + n_items += client_props->n_items; + } + + dict.n_items = n = 0; + dict.items = items = alloca(n_items * sizeof(struct spa_dict_item)); + + spa_dict_for_each(it, node_props) + items[n++] = *it; + dict.n_items = n; + + if (client_props) { + spa_dict_for_each(it, client_props) { + if (spa_streq(it->key, PW_KEY_OBJECT_ID) || + spa_streq(it->key, PW_KEY_OBJECT_SERIAL)) + continue; + + if ((it2 = (struct spa_dict_item*)spa_dict_lookup_item(&dict, it->key))) + it2->value = it->value; + else + items[n++] = *it; + } + dict.n_items = n; + } + message_put(m, TAG_PROPLIST, &dict, TAG_INVALID); + return 0; +} + static int fill_sink_input_info(struct client *client, struct message *m, struct pw_manager_object *o) { @@ -4128,10 +4167,16 @@ static int fill_sink_input_info(struct client *client, struct message *m, message_put(m, TAG_BOOLEAN, dev_info.volume_info.mute, /* muted */ TAG_INVALID); - if (client->version >= 13) - message_put(m, - TAG_PROPLIST, info->props, - TAG_INVALID); + if (client->version >= 13) { + int res; + struct pw_manager_object *c = NULL; + if (client_id != SPA_ID_INVALID) { + struct selector sel = { .id = client_id, .type = pw_manager_object_is_client, }; + c = select_object(manager, &sel); + } + if ((res = fill_node_info_proplist(m, info->props, c)) < 0) + return res; + } if (client->version >= 19) message_put(m, TAG_BOOLEAN, corked, /* corked */ @@ -4207,10 +4252,16 @@ static int fill_source_output_info(struct client *client, struct message *m, TAG_STRING, "PipeWire", /* resample method */ TAG_STRING, "PipeWire", /* driver */ TAG_INVALID); - if (client->version >= 13) - message_put(m, - TAG_PROPLIST, info->props, - TAG_INVALID); + if (client->version >= 13) { + int res; + struct pw_manager_object *c = NULL; + if (client_id != SPA_ID_INVALID) { + struct selector sel = { .id = client_id, .type = pw_manager_object_is_client, }; + c = select_object(manager, &sel); + } + if ((res = fill_node_info_proplist(m, info->props, c)) < 0) + return res; + } if (client->version >= 19) message_put(m, TAG_BOOLEAN, corked, /* corked */ @@ -4699,7 +4750,6 @@ static int do_set_profile(struct client *client, uint32_t command, uint32_t tag, static int do_set_default(struct client *client, uint32_t command, uint32_t tag, struct message *m) { struct pw_manager *manager = client->manager; - struct pw_manager_object *o; const char *name, *str; int res; bool sink = command == COMMAND_SET_DEFAULT_SINK; @@ -4716,10 +4766,10 @@ static int do_set_default(struct client *client, uint32_t command, uint32_t tag, if (spa_streq(name, "@NONE@")) name = NULL; - if (name != NULL && (o = find_device(client, SPA_ID_INVALID, name, sink, NULL)) == NULL) - return -ENOENT; - if (name != NULL) { + struct pw_manager_object *o; + if ((o = find_device(client, SPA_ID_INVALID, name, sink, NULL)) == NULL) + return -ENOENT; if (o->props && (str = pw_properties_get(o->props, PW_KEY_NODE_NAME)) != NULL) name = str; else if (spa_strendswith(name, ".monitor")) diff --git a/src/modules/module-protocol-pulse/server.c b/src/modules/module-protocol-pulse/server.c index aeab710b0..637757dfd 100644 --- a/src/modules/module-protocol-pulse/server.c +++ b/src/modules/module-protocol-pulse/server.c @@ -576,7 +576,7 @@ static bool is_stale_socket(int fd, const struct sockaddr_un *addr_un) static int check_socket_activation(const char *path) { - const int n = listen_fd(); + const int n = listen_fds(); for (int i = 0; i < n; i++) { const int fd = LISTEN_FDS_START + i; diff --git a/src/modules/module-raop-discover.c b/src/modules/module-raop-discover.c index 436972ac0..3675b003e 100644 --- a/src/modules/module-raop-discover.c +++ b/src/modules/module-raop-discover.c @@ -19,12 +19,8 @@ #include #include -#include -#include -#include - +#include "zeroconf-utils/zeroconf.h" #include "module-protocol-pulse/format.h" -#include "module-zeroconf-discover/avahi-poll.h" /** \page page_module_raop_discover RAOP Discover * @@ -129,29 +125,20 @@ struct impl { struct pw_properties *properties; - AvahiPoll *avahi_poll; - AvahiClient *client; - AvahiServiceBrowser *sink_browser; + struct pw_zeroconf *zeroconf; + struct spa_hook zeroconf_listener; struct spa_list tunnel_list; }; -struct tunnel_info { - const char *name; -}; - -#define TUNNEL_INFO(...) ((struct tunnel_info){ __VA_ARGS__ }) - struct tunnel { struct spa_list link; - struct tunnel_info info; + char *name; struct pw_impl_module *module; struct spa_hook module_listener; }; -static int start_client(struct impl *impl); - -static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *info) +static struct tunnel *tunnel_new(struct impl *impl, const char *name) { struct tunnel *t; @@ -159,28 +146,28 @@ static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *i if (t == NULL) return NULL; - t->info.name = strdup(info->name); + t->name = strdup(name); spa_list_append(&impl->tunnel_list, &t->link); return t; } -static struct tunnel *find_tunnel(struct impl *impl, const struct tunnel_info *info) +static struct tunnel *find_tunnel(struct impl *impl, const char *name) { struct tunnel *t; spa_list_for_each(t, &impl->tunnel_list, link) { - if (spa_streq(t->info.name, info->name)) + if (spa_streq(t->name, name)) return t; } return NULL; } -static void free_tunnel(struct tunnel *t) +static void tunnel_free(struct tunnel *t) { spa_list_remove(&t->link); if (t->module) pw_impl_module_destroy(t->module); - free((char *) t->info.name); + free(t->name); free(t); } @@ -189,14 +176,9 @@ static void impl_free(struct impl *impl) struct tunnel *t; spa_list_consume(t, &impl->tunnel_list, link) - free_tunnel(t); - - if (impl->sink_browser) - avahi_service_browser_free(impl->sink_browser); - if (impl->client) - avahi_client_free(impl->client); - if (impl->avahi_poll) - pw_avahi_poll_free(impl->avahi_poll); + tunnel_free(t); + if (impl->zeroconf) + pw_zeroconf_destroy(impl->zeroconf); pw_properties_free(impl->properties); free(impl); } @@ -224,75 +206,6 @@ static bool str_in_list(const char *haystack, const char *delimiters, const char return false; } -static void pw_properties_from_avahi_string(const char *key, const char *value, - struct pw_properties *props) -{ - if (spa_streq(key, "device")) { - pw_properties_set(props, "raop.device", value); - } - else if (spa_streq(key, "tp")) { - /* transport protocol, "UDP", "TCP", "UDP,TCP" */ - if (str_in_list(value, ",", "UDP")) - value = "udp"; - else if (str_in_list(value, ",", "TCP")) - value = "tcp"; - pw_properties_set(props, "raop.transport", value); - } else if (spa_streq(key, "et")) { - /* RAOP encryption types: - * 0 = none, - * 1 = RSA, - * 3 = FairPlay, - * 4 = MFiSAP (/auth-setup), - * 5 = FairPlay SAPv2.5 */ - 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); - } else if (spa_streq(key, "cn")) { - /* Supported audio codecs: - * 0 = PCM, - * 1 = ALAC, - * 2 = AAC, - * 3 = AAC ELD. */ - if (str_in_list(value, ",", "0")) - value = "PCM"; - else if (str_in_list(value, ",", "1")) - value = "ALAC"; - else if (str_in_list(value, ",", "2")) - value = "AAC"; - else if (str_in_list(value, ",", "3")) - value = "AAC-ELD"; - else - value = "unknown"; - pw_properties_set(props, "raop.audio.codec", value); - } else if (spa_streq(key, "ch")) { - /* Number of channels */ - pw_properties_set(props, PW_KEY_AUDIO_CHANNELS, value); - } else if (spa_streq(key, "ss")) { - /* Sample size */ - if (spa_streq(value, "16")) - value = "S16"; - else if (spa_streq(value, "24")) - value = "S24"; - else if (spa_streq(value, "32")) - value = "S32"; - else - value = "UNKNOWN"; - pw_properties_set(props, PW_KEY_AUDIO_FORMAT, value); - } else if (spa_streq(key, "sr")) { - /* Sample rate */ - pw_properties_set(props, PW_KEY_AUDIO_RATE, value); - } else if (spa_streq(key, "am")) { - /* Device model */ - pw_properties_set(props, "device.model", value); - } -} - static void submodule_destroy(void *data) { struct tunnel *t = data; @@ -364,76 +277,124 @@ static int rule_matched(void *data, const char *location, const char *action, return res; } -static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, - AvahiResolverEvent event, const char *name, const char *type, const char *domain, - const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt, - AvahiLookupResultFlags flags, void *userdata) +static void pw_properties_from_zeroconf(const char *key, const char *value, + struct pw_properties *props) { - struct impl *impl = userdata; - struct tunnel_info tinfo; - struct tunnel *t; - const char *str, *link_local_range = "169.254."; - AvahiStringList *l; - struct pw_properties *props = NULL; - char at[AVAHI_ADDRESS_STR_MAX], if_suffix[16] = ""; - - if (event != AVAHI_RESOLVER_FOUND) { - pw_log_error("Resolving of '%s' failed: %s", name, - avahi_strerror(avahi_client_errno(impl->client))); - goto done; + if (spa_streq(key, PW_KEY_ZEROCONF_IFINDEX)) { + pw_properties_set(props, "raop.ifindex", value); } + else if (spa_streq(key, PW_KEY_ZEROCONF_ADDRESS)) { + pw_properties_set(props, "raop.ip", value); + } + else if (spa_streq(key, PW_KEY_ZEROCONF_PORT)) { + pw_properties_set(props, "raop.port", value); + } + else if (spa_streq(key, PW_KEY_ZEROCONF_NAME)) { + pw_properties_set(props, "raop.name", value); + } + else if (spa_streq(key, PW_KEY_ZEROCONF_HOSTNAME)) { + pw_properties_set(props, "raop.hostname", value); + } + else if (spa_streq(key, PW_KEY_ZEROCONF_DOMAIN)) { + pw_properties_set(props, "raop.domain", value); + } + else if (spa_streq(key, "device")) { + pw_properties_set(props, "raop.device", value); + } + else if (spa_streq(key, "tp")) { + /* transport protocol, "UDP", "TCP", "UDP,TCP" */ + if (str_in_list(value, ",", "UDP")) + value = "udp"; + else if (str_in_list(value, ",", "TCP")) + value = "tcp"; + pw_properties_set(props, "raop.transport", value); + } else if (spa_streq(key, "et")) { + /* RAOP encryption types: + * 0 = none, + * 1 = RSA, + * 3 = FairPlay, + * 4 = MFiSAP (/auth-setup), + * 5 = FairPlay SAPv2.5 */ + 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); + } else if (spa_streq(key, "cn")) { + /* Supported audio codecs: + * 0 = PCM, + * 1 = ALAC, + * 2 = AAC, + * 3 = AAC ELD. */ + if (str_in_list(value, ",", "0")) + value = "PCM"; + else if (str_in_list(value, ",", "1")) + value = "ALAC"; + else if (str_in_list(value, ",", "2")) + value = "AAC"; + else if (str_in_list(value, ",", "3")) + value = "AAC-ELD"; + else + value = "unknown"; + pw_properties_set(props, "raop.audio.codec", value); + } else if (spa_streq(key, "ch")) { + /* Number of channels */ + pw_properties_set(props, PW_KEY_AUDIO_CHANNELS, value); + } else if (spa_streq(key, "ss")) { + /* Sample size */ + if (spa_streq(value, "16")) + value = "S16"; + else if (spa_streq(value, "24")) + value = "S24"; + else if (spa_streq(value, "32")) + value = "S32"; + else + value = "UNKNOWN"; + pw_properties_set(props, PW_KEY_AUDIO_FORMAT, value); + } else if (spa_streq(key, "sr")) { + /* Sample rate */ + pw_properties_set(props, PW_KEY_AUDIO_RATE, value); + } else if (spa_streq(key, "am")) { + /* Device model */ + pw_properties_set(props, "device.model", value); + } +} - avahi_address_snprint(at, sizeof(at), a); - if (spa_strstartswith(at, link_local_range)) - pw_log_info("found link-local ip address %s for '%s'", at, name); +static void on_zeroconf_added(void *data, const void *user, const struct spa_dict *info) +{ + struct impl *impl = data; + const char *name, *str; + struct tunnel *t; + const struct spa_dict_item *it; + struct pw_properties *props = NULL; - tinfo = TUNNEL_INFO(.name = name); + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); - t = find_tunnel(impl, &tinfo); - if (t == NULL) - t = make_tunnel(impl, &tinfo); + t = find_tunnel(impl, name); if (t == NULL) { - pw_log_error("Can't make tunnel: %m"); - goto done; + if ((t = tunnel_new(impl, name)) == NULL) { + pw_log_error("Can't make tunnel: %m"); + goto done; + } } if (t->module != NULL) { - pw_log_info("found duplicate mdns entry for %s on IP %s - skipping tunnel creation", name, at); + pw_log_info("found duplicate mdns entry for %s on IP %s - " + "skipping tunnel creation", name, + spa_dict_lookup(info, PW_KEY_ZEROCONF_ADDRESS)); goto done; } - props = pw_properties_new(NULL, NULL); - if (props == NULL) { + if ((props = pw_properties_new(NULL, NULL)) == NULL) { pw_log_error("Can't allocate properties: %m"); goto done; } - if (a->proto == AVAHI_PROTO_INET6 && - a->data.ipv6.address[0] == 0xfe && - (a->data.ipv6.address[1] & 0xc0) == 0x80) - snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); - - /* For IPv4 link-local, bind to the discovery interface */ - if (a->proto == AVAHI_PROTO_INET && - spa_strstartswith(at, link_local_range)) - snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); - - pw_properties_setf(props, "raop.ip", "%s%s", at, if_suffix); - pw_properties_setf(props, "raop.ifindex", "%d", interface); - pw_properties_setf(props, "raop.port", "%u", port); - pw_properties_setf(props, "raop.name", "%s", name); - pw_properties_setf(props, "raop.hostname", "%s", host_name); - pw_properties_setf(props, "raop.domain", "%s", domain); - - for (l = txt; l; l = l->next) { - char *key, *value; - - if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0) - break; - - pw_properties_from_avahi_string(key, value, props); - avahi_free(key); - avahi_free(value); - } + spa_dict_for_each(it, info) + pw_properties_from_zeroconf(it->key, it->value, props); if ((str = pw_properties_get(impl->properties, "raop.latency.ms")) != NULL) pw_properties_set(props, "raop.latency.ms", str); @@ -452,123 +413,28 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr if (!minfo.matched) pw_log_info("unmatched service found %s", str); } - done: - avahi_service_resolver_free(r); pw_properties_free(props); } - -static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, - AvahiBrowserEvent event, const char *name, const char *type, const char *domain, - AvahiLookupResultFlags flags, void *userdata) +static void on_zeroconf_removed(void *data, const void *user, const struct spa_dict *info) { - struct impl *impl = userdata; - struct tunnel_info info; + struct impl *impl = data; + const char *name; struct tunnel *t; - if ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && !impl->discover_local) + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); + + if ((t = find_tunnel(impl, name)) == NULL) return; - info = TUNNEL_INFO(.name = name); - - t = find_tunnel(impl, &info); - - switch (event) { - case AVAHI_BROWSER_NEW: - if (t != NULL) { - pw_log_info("found duplicate mdns entry - skipping tunnel creation"); - return; - } - if (!(avahi_service_resolver_new(impl->client, - interface, protocol, - name, type, domain, - AVAHI_PROTO_UNSPEC, 0, - resolver_cb, impl))) - pw_log_error("can't make service resolver: %s", - avahi_strerror(avahi_client_errno(impl->client))); - break; - case AVAHI_BROWSER_REMOVE: - if (t == NULL) - return; - free_tunnel(t); - break; - default: - break; - } -} - - -static struct AvahiServiceBrowser *make_browser(struct impl *impl, const char *service_type) -{ - struct AvahiServiceBrowser *s; - - s = avahi_service_browser_new(impl->client, - AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, - service_type, NULL, 0, - browser_cb, impl); - if (s == NULL) { - pw_log_error("can't make browser for %s: %s", service_type, - avahi_strerror(avahi_client_errno(impl->client))); - } - return s; -} - -static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata) -{ - struct impl *impl = userdata; - - impl->client = c; - - switch (state) { - case AVAHI_CLIENT_S_REGISTERING: - case AVAHI_CLIENT_S_RUNNING: - case AVAHI_CLIENT_S_COLLISION: - if (impl->sink_browser == NULL) - impl->sink_browser = make_browser(impl, SERVICE_TYPE_SINK); - if (impl->sink_browser == NULL) - goto error; - break; - case AVAHI_CLIENT_FAILURE: - if (avahi_client_errno(c) == AVAHI_ERR_DISCONNECTED) - start_client(impl); - - SPA_FALLTHROUGH; - case AVAHI_CLIENT_CONNECTING: - if (impl->sink_browser) { - avahi_service_browser_free(impl->sink_browser); - impl->sink_browser = NULL; - } - break; - default: - break; - } - return; -error: - pw_impl_module_schedule_destroy(impl->module); -} - -static int start_client(struct impl *impl) -{ - int res; - if ((impl->client = avahi_client_new(impl->avahi_poll, - AVAHI_CLIENT_NO_FAIL, - client_callback, impl, - &res)) == NULL) { - pw_log_error("can't create client: %s", avahi_strerror(res)); - pw_impl_module_schedule_destroy(impl->module); - return -EIO; - } - return 0; -} - -static int start_avahi(struct impl *impl) -{ - - impl->avahi_poll = pw_avahi_poll_new(impl->context); - - return start_client(impl); + tunnel_free(t); } +static const struct pw_zeroconf_events zeroconf_events = { + PW_VERSION_ZEROCONF_EVENTS, + .added = on_zeroconf_added, + .removed = on_zeroconf_removed, +}; SPA_EXPORT int pipewire__module_init(struct pw_impl_module *module, const char *args) @@ -576,6 +442,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) struct pw_context *context = pw_impl_module_get_context(module); struct pw_properties *props; struct impl *impl; + const char *local; int res; PW_LOG_TOPIC_INIT(mod_topic); @@ -599,15 +466,24 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->context = context; impl->properties = props; - impl->discover_local = pw_properties_get_bool(impl->properties, - "raop.discover-local", false); + if ((local = pw_properties_get(impl->properties, "raop.discover-local")) == NULL) + local = "false"; + pw_properties_set(impl->properties, PW_KEY_ZEROCONF_DISCOVER_LOCAL, local); pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); - start_avahi(impl); + if ((impl->zeroconf = pw_zeroconf_new(context, &props->dict)) == NULL) { + pw_log_error("can't create zeroconf: %m"); + goto error_errno; + } + pw_zeroconf_add_listener(impl->zeroconf, &impl->zeroconf_listener, + &zeroconf_events, impl); + pw_zeroconf_set_browse(impl->zeroconf, NULL, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, SERVICE_TYPE_SINK))); return 0; error_errno: diff --git a/src/modules/module-raop-sink.c b/src/modules/module-raop-sink.c index e217ff5b2..5e25b3089 100644 --- a/src/modules/module-raop-sink.c +++ b/src/modules/module-raop-sink.c @@ -45,6 +45,7 @@ #include "network-utils.h" #include "module-raop/rtsp-client.h" +#include "module-raop/base64.h" #include "module-rtp/rtp.h" #include "module-rtp/stream.h" @@ -130,11 +131,6 @@ PW_LOG_TOPIC(mod_topic, "mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic -#define BUFFER_SIZE (1u<<22) -#define BUFFER_MASK (BUFFER_SIZE-1) -#define BUFFER_SIZE2 (BUFFER_SIZE>>1) -#define BUFFER_MASK2 (BUFFER_SIZE2-1) - #define FRAMES_PER_TCP_PACKET 4096 #define FRAMES_PER_UDP_PACKET 352 @@ -273,13 +269,6 @@ struct impl { bool mute; float volume; - - struct spa_ringbuffer ring; - uint8_t buffer[BUFFER_SIZE]; - - struct spa_io_position *io_position; - - uint32_t filled; }; static inline void bit_writer(uint8_t **p, int *pos, uint8_t data, int len) @@ -357,7 +346,7 @@ static int send_udp_sync_packet(struct impl *impl, uint32_t rtptime, unsigned in res = sendmsg(impl->control_fd, &msg, MSG_NOSIGNAL); if (res < 0) { res = -errno; - pw_log_warn("error sending control packet: %d", res); + pw_log_warn("error sending control packet: %d (%m)", res); } pw_log_debug("raop control sync: first:%d latency:%u now:%"PRIx64" rtptime:%u", @@ -703,49 +692,6 @@ on_control_source_io(void *data, int fd, uint32_t mask) } } -static void base64_encode(const uint8_t *data, size_t len, char *enc, char pad) -{ - static const char tab[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - size_t i; - for (i = 0; i < len; i += 3) { - uint32_t v; - v = data[i+0] << 16; - v |= (i+1 < len ? data[i+1] : 0) << 8; - v |= (i+2 < len ? data[i+2] : 0); - *enc++ = tab[(v >> (3*6)) & 0x3f]; - *enc++ = tab[(v >> (2*6)) & 0x3f]; - *enc++ = i+1 < len ? tab[(v >> (1*6)) & 0x3f] : pad; - *enc++ = i+2 < len ? tab[(v >> (0*6)) & 0x3f] : pad; - } - *enc = '\0'; -} - -static size_t base64_decode(const char *data, size_t len, uint8_t *dec) -{ - uint8_t tab[] = { - 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, - 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, - -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, - 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, - -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }; - size_t i, j; - for (i = 0, j = 0; i < len; i += 4) { - uint32_t v; - v = tab[data[i+0]-43] << (3*6); - v |= tab[data[i+1]-43] << (2*6); - v |= (data[i+2] == '=' ? 0 : tab[data[i+2]-43]) << (1*6); - v |= (data[i+3] == '=' ? 0 : tab[data[i+3]-43]); - dec[j++] = (v >> 16) & 0xff; - if (data[i+2] != '=') dec[j++] = (v >> 8) & 0xff; - if (data[i+3] != '=') dec[j++] = v & 0xff; - } - return j; -} - SPA_PRINTF_FUNC(2,3) static int MD5_hash(char hash[MD5_HASH_LENGTH+1], const char *fmt, ...) { @@ -778,7 +724,7 @@ static int rtsp_add_raop_auth_header(struct impl *impl, const char *method) char buf[256]; char enc[512]; spa_scnprintf(buf, sizeof(buf), "%s:%s", RAOP_AUTH_USER_NAME, impl->password); - base64_encode((uint8_t*)buf, strlen(buf), enc, '='); + pw_base64_encode((uint8_t*)buf, strlen(buf), enc, '='); spa_scnprintf(auth, sizeof(auth), "Basic %s", enc); } else if (spa_streq(impl->auth_method, "Digest")) { @@ -1136,8 +1082,8 @@ static int rsa_encrypt(uint8_t *data, int len, uint8_t *enc) "imNVvYFZeCXg/IdTQ+x4IRdiXNv5hEew=="; char e[] = "AQAB"; - msize = base64_decode(n, strlen(n), modulus); - esize = base64_decode(e, strlen(e), exponent); + msize = pw_base64_decode(n, strlen(n), modulus); + esize = pw_base64_decode(e, strlen(e), exponent); #if OPENSSL_API_LEVEL >= 30000 EVP_PKEY *pkey = NULL; @@ -1263,15 +1209,15 @@ static int rtsp_do_announce(struct impl *impl) (res = pw_getrandom(impl->aes_iv, sizeof(impl->aes_iv), 0)) < 0) return res; - base64_encode(rac, sizeof(rac), sac, '\0'); + pw_base64_encode(rac, sizeof(rac), sac, '\0'); pw_properties_set(impl->headers, "Apple-Challenge", sac); rsa_len = rsa_encrypt(impl->aes_key, 16, rsakey); if (rsa_len < 0) return -rsa_len; - base64_encode(rsakey, rsa_len, key, '='); - base64_encode(impl->aes_iv, 16, iv, '='); + pw_base64_encode(rsakey, rsa_len, key, '='); + pw_base64_encode(impl->aes_iv, 16, iv, '='); sdp = spa_aprintf("v=0\r\n" "o=iTunes %s 0 IN IP%d %s\r\n" diff --git a/src/modules/module-raop/base64.h b/src/modules/module-raop/base64.h new file mode 100644 index 000000000..d8906c287 --- /dev/null +++ b/src/modules/module-raop/base64.h @@ -0,0 +1,62 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_BASE64_H +#define PIPEWIRE_BASE64_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +static inline void pw_base64_encode(const uint8_t *data, size_t len, char *enc, char pad) +{ + static const char tab[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t i; + for (i = 0; i < len; i += 3) { + uint32_t v; + v = data[i+0] << 16; + v |= (i+1 < len ? data[i+1] : 0) << 8; + v |= (i+2 < len ? data[i+2] : 0); + *enc++ = tab[(v >> (3*6)) & 0x3f]; + *enc++ = tab[(v >> (2*6)) & 0x3f]; + *enc++ = i+1 < len ? tab[(v >> (1*6)) & 0x3f] : pad; + *enc++ = i+2 < len ? tab[(v >> (0*6)) & 0x3f] : pad; + } + *enc = '\0'; +} + +static inline size_t pw_base64_decode(const char *data, size_t len, uint8_t *dec) +{ + uint8_t tab[] = { + 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, + -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, + -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }; + size_t i, j; + for (i = 0, j = 0; i < len; i += 4) { + uint32_t v; + v = tab[data[i+0]-43] << (3*6); + v |= tab[data[i+1]-43] << (2*6); + v |= (data[i+2] == '=' ? 0 : tab[data[i+2]-43]) << (1*6); + v |= (data[i+3] == '=' ? 0 : tab[data[i+3]-43]); + dec[j++] = (v >> 16) & 0xff; + if (data[i+2] != '=') dec[j++] = (v >> 8) & 0xff; + if (data[i+3] != '=') dec[j++] = v & 0xff; + } + return j; +} + + +#ifdef __cplusplus +} +#endif + +#endif /* PIPEWIRE_BASE64_H */ diff --git a/src/modules/module-raop/rtsp-client.c b/src/modules/module-raop/rtsp-client.c index fae71977c..4bcff8b88 100644 --- a/src/modules/module-raop/rtsp-client.c +++ b/src/modules/module-raop/rtsp-client.c @@ -445,7 +445,10 @@ on_source_io(void *data, int fd, uint32_t mask) int res; if (mask & (SPA_IO_ERR | SPA_IO_HUP)) { - res = -EPIPE; + socklen_t len = sizeof(res); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &len) < 0) + res = errno; + res = -res; goto error; } if (mask & SPA_IO_IN) { @@ -478,7 +481,7 @@ int pw_rtsp_client_connect(struct pw_rtsp_client *client, { struct addrinfo hints; struct addrinfo *result, *rp; - int res, fd; + int res, fd = -1; char port_str[12]; if (client->source != NULL) diff --git a/src/modules/module-roc-sink.c b/src/modules/module-roc-sink.c index 39ca2bce1..66a2c716b 100644 --- a/src/modules/module-roc-sink.c +++ b/src/modules/module-roc-sink.c @@ -120,8 +120,6 @@ struct module_roc_sink_data { roc_endpoint *remote_control_addr; int remote_control_port; - - roc_log_level loglevel; }; static void stream_destroy(void *d) @@ -391,8 +389,7 @@ static const struct spa_dict_item module_roc_sink_info[] = { "( remote.repair.port= ) " "( remote.control.port= ) " "( audio.position= ) " - "( sink.props= { key=val ... } ) " - "( log.level=|DEFAULT|NONE|RROR|INFO|DEBUG|TRACE ) " }, + "( sink.props= { key=val ... } ) " }, { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; @@ -513,14 +510,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) pw_log_error("can't connect: %m"); goto out; } - if ((str = pw_properties_get(props, "log.level")) != NULL) { - const struct spa_log *log_conf = pw_log_get(); - const roc_log_level default_level = pw_roc_log_level_pw_2_roc(log_conf->level); - if (pw_roc_parse_log_level(&data->loglevel, str, default_level)) { - pw_log_error("Invalid log level %s, using default", str); - data->loglevel = default_level; - } - } pw_proxy_add_listener((struct pw_proxy*)data->core, &data->core_proxy_listener, diff --git a/src/modules/module-roc-source.c b/src/modules/module-roc-source.c index 2173c6af1..a46189e5d 100644 --- a/src/modules/module-roc-source.c +++ b/src/modules/module-roc-source.c @@ -140,8 +140,6 @@ struct module_roc_source_data { roc_endpoint *local_control_addr; int local_control_port; - - roc_log_level loglevel; }; static void stream_destroy(void *d) @@ -430,8 +428,7 @@ static const struct spa_dict_item module_roc_source_info[] = { "( local.repair.port= ) " "( local.control.port= ) " "( audio.position= ) " - "( source.props= { key=value ... } ) " - "( log.level=|DEFAULT|NONE|RROR|INFO|DEBUG|TRACE ) " }, + "( source.props= { key=value ... } ) " }, { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; @@ -567,14 +564,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) } else { data->fec_code = ROC_FEC_ENCODING_DEFAULT; } - if ((str = pw_properties_get(props, "log.level")) != NULL) { - const struct spa_log *log_conf = pw_log_get(); - const roc_log_level default_level = pw_roc_log_level_pw_2_roc(log_conf->level); - if (pw_roc_parse_log_level(&data->loglevel, str, default_level)) { - pw_log_error("Invalid log level %s, using default", str); - data->loglevel = default_level; - } - } data->core = pw_context_get_object(data->module_context, PW_TYPE_INTERFACE_Core); if (data->core == NULL) { diff --git a/src/modules/module-roc/common.c b/src/modules/module-roc/common.c index 244c203dd..475c5f40f 100644 --- a/src/modules/module-roc/common.c +++ b/src/modules/module-roc/common.c @@ -5,17 +5,54 @@ PW_LOG_TOPIC(roc_log_topic, "mod.roc.lib"); +static inline roc_log_level pw_roc_log_level_pw_2_roc(const enum spa_log_level pw_log_level) +{ + switch (pw_log_level) { + case SPA_LOG_LEVEL_NONE: + return ROC_LOG_NONE; + case SPA_LOG_LEVEL_ERROR: + return ROC_LOG_ERROR; + case SPA_LOG_LEVEL_WARN: + return ROC_LOG_ERROR; + case SPA_LOG_LEVEL_INFO: + return ROC_LOG_INFO; + case SPA_LOG_LEVEL_DEBUG: + return ROC_LOG_DEBUG; + case SPA_LOG_LEVEL_TRACE: + return ROC_LOG_TRACE; + default: + return ROC_LOG_NONE; + } +} + +static inline enum spa_log_level pw_roc_log_level_roc_2_pw(const roc_log_level roc_log_level) +{ + switch (roc_log_level) { + case ROC_LOG_NONE: + return SPA_LOG_LEVEL_NONE; + case ROC_LOG_ERROR: + return SPA_LOG_LEVEL_ERROR; + case ROC_LOG_INFO: + return SPA_LOG_LEVEL_INFO; + case ROC_LOG_DEBUG: + return SPA_LOG_LEVEL_DEBUG; + case ROC_LOG_TRACE: + return SPA_LOG_LEVEL_TRACE; + default: + return SPA_LOG_LEVEL_NONE; + } +} + +static void pw_roc_log_handler(const roc_log_message *message, void *argument) +{ + const enum spa_log_level log_level = pw_roc_log_level_roc_2_pw(message->level); + if (SPA_UNLIKELY(pw_log_topic_enabled(log_level, roc_log_topic))) { + pw_log_logt(log_level, roc_log_topic, message->file, message->line, message->module, "%s", message->text); + } +} + void pw_roc_log_init(void) { roc_log_set_handler(pw_roc_log_handler, NULL); roc_log_set_level(pw_roc_log_level_pw_2_roc(roc_log_topic->has_custom_level ? roc_log_topic->level : pw_log_level)); } - -void pw_roc_log_handler(const roc_log_message *message, void *argument) -{ - const enum spa_log_level log_level = pw_roc_log_level_roc_2_pw(message->level); - if (SPA_UNLIKELY(pw_log_topic_enabled(log_level, roc_log_topic))) { - pw_log_logt(log_level, roc_log_topic, message->file, message->line, message->module, message->text, ""); - } -} - diff --git a/src/modules/module-roc/common.h b/src/modules/module-roc/common.h index c94ac69a8..d49e392fe 100644 --- a/src/modules/module-roc/common.h +++ b/src/modules/module-roc/common.h @@ -3,7 +3,6 @@ #include #include -#include #include #include @@ -21,7 +20,6 @@ #define PW_ROC_STEREO_POSITIONS "[ FL FR ]" void pw_roc_log_init(void); -void pw_roc_log_handler(const roc_log_message *message, void *argument); static inline int pw_roc_parse_fec_encoding(roc_fec_encoding *out, const char *str) { @@ -137,62 +135,4 @@ static inline void pw_roc_fec_encoding_to_proto(roc_fec_encoding fec_code, roc_p } } -static inline roc_log_level pw_roc_log_level_pw_2_roc(const enum spa_log_level pw_log_level) -{ - switch (pw_log_level) { - case SPA_LOG_LEVEL_NONE: - return ROC_LOG_NONE; - case SPA_LOG_LEVEL_ERROR: - return ROC_LOG_ERROR; - case SPA_LOG_LEVEL_WARN: - return ROC_LOG_ERROR; - case SPA_LOG_LEVEL_INFO: - return ROC_LOG_INFO; - case SPA_LOG_LEVEL_DEBUG: - return ROC_LOG_DEBUG; - case SPA_LOG_LEVEL_TRACE: - return ROC_LOG_TRACE; - default: - return ROC_LOG_NONE; - } -} - -static inline enum spa_log_level pw_roc_log_level_roc_2_pw(const roc_log_level roc_log_level) -{ - switch (roc_log_level) { - case ROC_LOG_NONE: - return SPA_LOG_LEVEL_NONE; - case ROC_LOG_ERROR: - return SPA_LOG_LEVEL_ERROR; - case ROC_LOG_INFO: - return SPA_LOG_LEVEL_INFO; - case ROC_LOG_DEBUG: - return SPA_LOG_LEVEL_DEBUG; - case ROC_LOG_TRACE: - return SPA_LOG_LEVEL_TRACE; - default: - return SPA_LOG_LEVEL_NONE; - } -} - -static inline int pw_roc_parse_log_level(roc_log_level *loglevel, const char *str, - roc_log_level default_level) -{ - if (spa_streq(str, "DEFAULT")) - *loglevel = default_level; - else if (spa_streq(str, "NONE")) - *loglevel = ROC_LOG_NONE; - else if (spa_streq(str, "ERROR")) - *loglevel = ROC_LOG_ERROR; - else if (spa_streq(str, "INFO")) - *loglevel = ROC_LOG_INFO; - else if (spa_streq(str, "DEBUG")) - *loglevel = ROC_LOG_DEBUG; - else if (spa_streq(str, "TRACE")) - *loglevel = ROC_LOG_TRACE; - else - return -EINVAL; - return 0; -} - #endif /* MODULE_ROC_COMMON_H */ diff --git a/src/modules/module-rtp-sap.c b/src/modules/module-rtp-sap.c index c734758c3..cfbcbf419 100644 --- a/src/modules/module-rtp-sap.c +++ b/src/modules/module-rtp-sap.c @@ -1235,8 +1235,11 @@ static struct session *session_new_announce(struct impl *impl, struct node *node sess->has_sdp = true; } + pw_log_debug("sending out initial SAP"); send_sap(impl, sess, 0); + pw_log_debug("new announcement session up and running"); + return sess; error_free: @@ -1835,6 +1838,7 @@ static int start_sap(struct impl *impl) 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); + pw_log_info("starting SAP retry timer"); /* It is important to return 0 in this case. Otherwise, the nonzero return * value will later be propagated through the core as an error. */ @@ -2005,8 +2009,10 @@ static void impl_destroy(struct impl *impl) 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) + if (impl->sap_source) { + pw_log_info("destroying SAP source"); pw_loop_destroy_source(impl->loop, impl->sap_source); + } if (impl->sap_fd != -1) close(impl->sap_fd); diff --git a/src/modules/module-rtp-session.c b/src/modules/module-rtp-session.c index 63470c539..2dca681d6 100644 --- a/src/modules/module-rtp-session.c +++ b/src/modules/module-rtp-session.c @@ -27,12 +27,7 @@ #include #include -#include -#include -#include -#include - -#include "module-zeroconf-discover/avahi-poll.h" +#include "zeroconf-utils/zeroconf.h" #include #include @@ -160,31 +155,21 @@ static const struct spa_dict_item module_info[] = { }; struct service_info { - AvahiIfIndex interface; - AvahiProtocol protocol; + int ifindex; + int protocol; const char *name; const char *type; const char *domain; - const char *host_name; - AvahiAddress address; - uint16_t port; }; #define SERVICE_INFO(...) ((struct service_info){ __VA_ARGS__ }) -struct service { - struct service_info info; - - struct spa_list link; - struct impl *impl; - - struct session *sess; -}; - struct session { struct impl *impl; struct spa_list link; + struct service_info info; + struct sockaddr_storage ctrl_addr; socklen_t ctrl_len; struct sockaddr_storage data_addr; @@ -229,11 +214,8 @@ struct impl { struct pw_properties *props; bool discover_local; - AvahiPoll *avahi_poll; - AvahiClient *client; - AvahiServiceBrowser *browser; - AvahiEntryGroup *group; - struct spa_list service_list; + struct pw_zeroconf *zeroconf; + struct spa_hook zeroconf_listener; struct pw_properties *stream_props; @@ -595,6 +577,9 @@ static void free_session(struct session *sess) if (sess->recv) rtp_stream_destroy(sess->recv); free(sess->name); + free((char *) sess->info.name); + free((char *) sess->info.type); + free((char *) sess->info.domain); free(sess); } @@ -612,7 +597,8 @@ static bool cmp_ip(const struct sockaddr_storage *sa, const struct sockaddr_stor return false; } -static struct session *make_session(struct impl *impl, struct pw_properties *props) +static struct session *make_session(struct impl *impl, struct service_info *info, + struct pw_properties *props) { struct session *sess; const char *str; @@ -627,6 +613,11 @@ static struct session *make_session(struct impl *impl, struct pw_properties *pro sess->impl = impl; sess->ssrc = pw_rand32(); + sess->info.ifindex = info->ifindex; + sess->info.protocol = info->protocol; + sess->info.name = strdup(info->name); + sess->info.type = strdup(info->type); + sess->info.domain = strdup(info->domain); str = pw_properties_get(props, "sess.name"); sess->name = str ? strdup(str) : strdup("RTP Session"); @@ -669,6 +660,21 @@ error: return NULL; } +static struct session *find_session_by_info(struct impl *impl, + const struct service_info *info) +{ + struct session *s; + spa_list_for_each(s, &impl->sessions, link) { + if (s->info.ifindex == info->ifindex && + s->info.protocol == info->protocol && + spa_streq(s->info.name, info->name) && + spa_streq(s->info.type, info->type) && + spa_streq(s->info.domain, info->domain)) + return s; + } + return NULL; +} + static struct session *find_session_by_addr_name(struct impl *impl, const struct sockaddr_storage *sa, const char *name) { @@ -1220,8 +1226,8 @@ static void impl_destroy(struct impl *impl) if (impl->data_source) pw_loop_destroy_source(impl->data_loop, impl->data_source); - if (impl->client) - avahi_client_free(impl->client); + if (impl->zeroconf) + pw_zeroconf_destroy(impl->zeroconf); if (impl->data_loop) pw_context_release_loop(impl->context, impl->data_loop); @@ -1263,21 +1269,7 @@ static const struct pw_core_events core_events = { .error = on_core_error, }; -static void free_service(struct service *s) -{ - spa_list_remove(&s->link); - - if (s->sess) - free_session(s->sess); - - free((char *) s->info.name); - free((char *) s->info.type); - free((char *) s->info.domain); - free((char *) s->info.host_name); - free(s); -} - -static const char *get_service_name(struct impl *impl) +static const char *get_service_type(struct impl *impl) { const char *str; str = pw_properties_get(impl->props, "sess.media"); @@ -1288,21 +1280,36 @@ static const char *get_service_name(struct impl *impl) return NULL; } -static struct service *make_service(struct impl *impl, const struct service_info *info, - AvahiStringList *txt) +static void on_zeroconf_added(void *data, const void *user, const struct spa_dict *info) { - struct service *s = NULL; - char at[AVAHI_ADDRESS_STR_MAX], if_suffix[16] = ""; + struct impl *impl = data; + const char *str, *service_type, *address, *hostname; + struct service_info sinfo; struct session *sess; - int res, ipv; + int ifindex = -1, protocol = 0, res, port = 0; struct pw_properties *props = NULL; - const char *service_name, *str; - AvahiStringList *l; bool compatible = true; + if ((str = spa_dict_lookup(info, PW_KEY_ZEROCONF_IFINDEX))) + ifindex = atoi(str); + if ((str = spa_dict_lookup(info, PW_KEY_ZEROCONF_PROTO))) + protocol = atoi(str); + if ((str = spa_dict_lookup(info, PW_KEY_ZEROCONF_PORT))) + port = atoi(str); + + sinfo = SERVICE_INFO(.ifindex = ifindex, + .protocol = protocol, + .name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME), + .type = spa_dict_lookup(info, PW_KEY_ZEROCONF_TYPE), + .domain = spa_dict_lookup(info, PW_KEY_ZEROCONF_DOMAIN)); + + sess = find_session_by_info(impl, &sinfo); + if (sess != NULL) + return; + /* check for compatible session */ - service_name = get_service_name(impl); - compatible = spa_streq(service_name, info->type); + service_type = get_service_type(impl); + compatible = spa_streq(service_type, sinfo.type); props = pw_properties_copy(impl->stream_props); if (props == NULL) { @@ -1310,55 +1317,53 @@ static struct service *make_service(struct impl *impl, const struct service_info goto error; } - if (spa_streq(service_name, "_pipewire-audio._udp")) { + if (spa_streq(service_type, "_pipewire-audio._udp")) { uint32_t mask = 0; - for (l = txt; l && compatible; l = l->next) { + const struct spa_dict_item *it; + spa_dict_for_each(it, info) { const char *k = NULL; - char *key, *value; - if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0) + if (!compatible) break; - if (spa_streq(key, "subtype")) { + if (spa_streq(it->key, "subtype")) { k = "sess.media"; mask |= 1<<0; - } else if (spa_streq(key, "format")) { + } else if (spa_streq(it->key, "format")) { k = PW_KEY_AUDIO_FORMAT; mask |= 1<<1; - } else if (spa_streq(key, "rate")) { + } else if (spa_streq(it->key, "rate")) { k = PW_KEY_AUDIO_RATE; mask |= 1<<2; - } else if (spa_streq(key, "channels")) { + } else if (spa_streq(it->key, "channels")) { k = PW_KEY_AUDIO_CHANNELS; mask |= 1<<3; - } else if (spa_streq(key, "position")) { + } else if (spa_streq(it->key, "position")) { pw_properties_set(props, - SPA_KEY_AUDIO_POSITION, value); - } else if (spa_streq(key, "layout")) { + SPA_KEY_AUDIO_POSITION, it->value); + } else if (spa_streq(it->key, "layout")) { pw_properties_set(props, - SPA_KEY_AUDIO_LAYOUT, value); - } else if (spa_streq(key, "channelnames")) { + SPA_KEY_AUDIO_LAYOUT, it->value); + } else if (spa_streq(it->key, "channelnames")) { pw_properties_set(props, - PW_KEY_NODE_CHANNELNAMES, value); - } else if (spa_streq(key, "ts-refclk")) { + PW_KEY_NODE_CHANNELNAMES, it->value); + } else if (spa_streq(it->key, "ts-refclk")) { pw_properties_set(props, - "sess.ts-refclk", value); - if (spa_streq(value, impl->ts_refclk)) + "sess.ts-refclk", it->value); + if (spa_streq(it->value, impl->ts_refclk)) pw_properties_set(props, "sess.ts-direct", "true"); - } else if (spa_streq(key, "ts-offset")) { + } else if (spa_streq(it->key, "ts-offset")) { uint32_t v; - if (spa_atou32(value, &v, 0)) + if (spa_atou32(it->value, &v, 0)) pw_properties_setf(props, "rtp.receiver-ts-offset", "%u", v); } if (k != NULL) { str = pw_properties_get(props, k); - if (str == NULL || !spa_streq(str, value)) + if (str == NULL || !spa_streq(str, it->value)) compatible = false; } - avahi_free(key); - avahi_free(value); } str = pw_properties_get(props, "sess.media"); if (spa_streq(str, "opus") && mask != 0xd) @@ -1368,281 +1373,147 @@ static struct service *make_service(struct impl *impl, const struct service_info } if (!compatible) { pw_log_info("found incompatible session IP%d:%s", - info->protocol == AVAHI_PROTO_INET ? 4 : 6, - info->name); + sinfo.protocol, sinfo.name); res = 0; goto error; } - s = calloc(1, sizeof(*s)); - if (s == NULL) { - res = -errno; - goto error; - } + address = spa_dict_lookup(info, PW_KEY_ZEROCONF_ADDRESS); + hostname = spa_dict_lookup(info, PW_KEY_ZEROCONF_HOSTNAME); - s->impl = impl; - spa_list_append(&impl->service_list, &s->link); + pw_log_info("create session: %s %s:%u %s", sinfo.name, address, port, sinfo.type); - s->info.interface = info->interface; - s->info.protocol = info->protocol; - s->info.name = strdup(info->name); - s->info.type = strdup(info->type); - s->info.domain = strdup(info->domain); - s->info.host_name = strdup(info->host_name); - s->info.address = info->address; - s->info.port = info->port; - - avahi_address_snprint(at, sizeof(at), &s->info.address); - pw_log_info("create session: %s %s:%u %s", s->info.name, at, s->info.port, s->info.type); - - if (s->info.protocol == AVAHI_PROTO_INET6 && - s->info.address.data.ipv6.address[0] == 0xfe && - (s->info.address.data.ipv6.address[1] & 0xc0) == 0x80) - snprintf(if_suffix, sizeof(if_suffix), "%%%d", s->info.interface); - - ipv = s->info.protocol == AVAHI_PROTO_INET ? 4 : 6; - pw_properties_set(props, "sess.name", s->info.name); - pw_properties_setf(props, "destination.ip", "%s%s", at, if_suffix); - pw_properties_setf(props, "destination.ifindex", "%u", s->info.interface); - pw_properties_setf(props, "destination.port", "%u", s->info.port); + pw_properties_set(props, "sess.name", sinfo.name); + pw_properties_set(props, "destination.ip", address); + pw_properties_setf(props, "destination.ifindex", "%u", sinfo.ifindex); + pw_properties_setf(props, "destination.port", "%u", port); if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL) pw_properties_setf(props, PW_KEY_NODE_NAME, "rtp_session.%s.%s.ipv%d", - s->info.name, s->info.host_name, ipv); + sinfo.name, hostname, sinfo.protocol); if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "%s (IPv%d)", - s->info.name, ipv); + sinfo.name, sinfo.protocol); if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL) pw_properties_setf(props, PW_KEY_MEDIA_NAME, "RTP Session with %s (IPv%d)", - s->info.name, ipv); + sinfo.name, sinfo.protocol); - sess = make_session(impl, props); - props = NULL; + sess = make_session(impl, &sinfo, spa_steal_ptr(props)); if (sess == NULL) { res = -errno; pw_log_error("can't create session: %m"); goto error; } - s->sess = sess; - if ((res = pw_net_parse_address(at, s->info.port, &sess->ctrl_addr, &sess->ctrl_len)) < 0) { - pw_log_error("invalid address %s: %s", at, spa_strerror(res)); + if ((res = pw_net_parse_address(address, port, &sess->ctrl_addr, &sess->ctrl_len)) < 0) { + pw_log_error("invalid address %s: %s", address, spa_strerror(res)); } - if ((res = pw_net_parse_address(at, s->info.port+1, &sess->data_addr, &sess->data_len)) < 0) { - pw_log_error("invalid address %s: %s", at, spa_strerror(res)); + if ((res = pw_net_parse_address(address, port+1, &sess->data_addr, &sess->data_len)) < 0) { + pw_log_error("invalid address %s: %s", address, spa_strerror(res)); } - return s; + return; error: pw_properties_free(props); - if (s != NULL) - free_service(s); - errno = -res; - return NULL; + return; } -static struct service *find_service(struct impl *impl, const struct service_info *info) +static void on_zeroconf_removed(void *data, const void *user, const struct spa_dict *info) { - struct service *s; - spa_list_for_each(s, &impl->service_list, link) { - if (s->info.interface == info->interface && - s->info.protocol == info->protocol && - spa_streq(s->info.name, info->name) && - spa_streq(s->info.type, info->type) && - spa_streq(s->info.domain, info->domain)) - return s; - } - return NULL; -} - -static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, - AvahiResolverEvent event, const char *name, const char *type, const char *domain, - const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt, - AvahiLookupResultFlags flags, void *userdata) -{ - struct impl *impl = userdata; + struct impl *impl = data; struct service_info sinfo; + struct session *sess; + const char *str; + int ifindex = -1, protocol = 0; - if (event != AVAHI_RESOLVER_FOUND) { - pw_log_error("Resolving of '%s' failed: %s", name, - avahi_strerror(avahi_client_errno(impl->client))); - goto done; - } + if ((str = spa_dict_lookup(info, PW_KEY_ZEROCONF_IFINDEX))) + ifindex = atoi(str); + if ((str = spa_dict_lookup(info, PW_KEY_ZEROCONF_PROTO))) + protocol = atoi(str); - sinfo = SERVICE_INFO(.interface = interface, + sinfo = SERVICE_INFO(.ifindex = ifindex, .protocol = protocol, - .name = name, - .type = type, - .domain = domain, - .host_name = host_name, - .address = *a, - .port = port); + .name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME), + .type = spa_dict_lookup(info, PW_KEY_ZEROCONF_TYPE), + .domain = spa_dict_lookup(info, PW_KEY_ZEROCONF_DOMAIN)); - make_service(impl, &sinfo, txt); -done: - avahi_service_resolver_free(r); -} - -static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, - AvahiBrowserEvent event, const char *name, const char *type, const char *domain, - AvahiLookupResultFlags flags, void *userdata) -{ - struct impl *impl = userdata; - struct service_info info; - struct service *s; - - if ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && !impl->discover_local) + sess = find_session_by_info(impl, &sinfo); + if (sess == NULL) return; - info = SERVICE_INFO(.interface = interface, - .protocol = protocol, - .name = name, - .type = type, - .domain = domain); - - s = find_service(impl, &info); - - switch (event) { - case AVAHI_BROWSER_NEW: - if (s != NULL) - return; - if (!(avahi_service_resolver_new(impl->client, - interface, protocol, - name, type, domain, - AVAHI_PROTO_UNSPEC, 0, - resolver_cb, impl))) - pw_log_error("can't make service resolver: %s", - avahi_strerror(avahi_client_errno(impl->client))); - break; - case AVAHI_BROWSER_REMOVE: - if (s == NULL) - return; - free_service(s); - break; - default: - break; - } + free_session(sess); } static int make_browser(struct impl *impl) { - const char *service_name; + const char *service_type; + int res; - service_name = get_service_name(impl); - if (service_name == NULL) + service_type = get_service_type(impl); + if (service_type == NULL) return -EINVAL; - if (impl->browser == NULL) { - impl->browser = avahi_service_browser_new(impl->client, - AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, - service_name, NULL, 0, - browser_cb, impl); - } - if (impl->browser == NULL) { - pw_log_error("can't make browser: %s", - avahi_strerror(avahi_client_errno(impl->client))); - return -EIO; + if ((res = pw_zeroconf_set_browse(impl->zeroconf, impl, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, service_type)))) < 0) { + pw_log_error("can't make browser for %s: %s", + service_type, spa_strerror(res)); + return res; } return 0; } -static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) -{ - switch (state) { - case AVAHI_ENTRY_GROUP_ESTABLISHED: - pw_log_info("Service successfully established"); - break; - case AVAHI_ENTRY_GROUP_COLLISION: - pw_log_error("Service name collision"); - break; - case AVAHI_ENTRY_GROUP_FAILURE: - pw_log_error("Entry group failure: %s", - avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g)))); - break; - case AVAHI_ENTRY_GROUP_UNCOMMITED: - case AVAHI_ENTRY_GROUP_REGISTERING:; - break; - } -} - static int make_announce(struct impl *impl) { int res; - const char *service_name, *str; - AvahiStringList *txt = NULL; + const char *service_type, *str; + struct pw_properties *props; - if ((service_name = get_service_name(impl)) == NULL) + props = pw_properties_new(NULL, NULL); + + if ((service_type = get_service_type(impl)) == NULL) return -ENOTSUP; - if (impl->group == NULL) { - impl->group = avahi_entry_group_new(impl->client, - entry_group_callback, impl); - } - if (impl->group == NULL) { - pw_log_error("can't make group: %s", - avahi_strerror(avahi_client_errno(impl->client))); - return -EIO; - } - avahi_entry_group_reset(impl->group); - - if (spa_streq(service_name, "_pipewire-audio._udp")) { + if (spa_streq(service_type, "_pipewire-audio._udp")) { str = pw_properties_get(impl->props, "sess.media"); - txt = avahi_string_list_add_pair(txt, "subtype", str); + pw_properties_set(props, "subtype", str); if ((str = pw_properties_get(impl->stream_props, PW_KEY_AUDIO_FORMAT)) != NULL) - txt = avahi_string_list_add_pair(txt, "format", str); + pw_properties_set(props, "format", str); if ((str = pw_properties_get(impl->stream_props, PW_KEY_AUDIO_RATE)) != NULL) - txt = avahi_string_list_add_pair(txt, "rate", str); + pw_properties_set(props, "rate", str); if ((str = pw_properties_get(impl->stream_props, PW_KEY_AUDIO_CHANNELS)) != NULL) - txt = avahi_string_list_add_pair(txt, "channels", str); + pw_properties_set(props, "channels", str); if ((str = pw_properties_get(impl->stream_props, SPA_KEY_AUDIO_POSITION)) != NULL) - txt = avahi_string_list_add_pair(txt, "position", str); + pw_properties_set(props, "position", str); if ((str = pw_properties_get(impl->stream_props, SPA_KEY_AUDIO_LAYOUT)) != NULL) - txt = avahi_string_list_add_pair(txt, "layout", str); + pw_properties_set(props, "layout", str); if ((str = pw_properties_get(impl->stream_props, PW_KEY_NODE_CHANNELNAMES)) != NULL) - txt = avahi_string_list_add_pair(txt, "channelnames", str); + pw_properties_set(props, "channelnames", str); if (impl->ts_refclk != NULL) { - txt = avahi_string_list_add_pair(txt, "ts-refclk", impl->ts_refclk); - txt = avahi_string_list_add_printf(txt, "ts-offset=%u", impl->ts_offset); + pw_properties_set(props, "ts-refclk", impl->ts_refclk); + pw_properties_setf(props, "ts-offset", "%u", impl->ts_offset); } } - res = avahi_entry_group_add_service_strlst(impl->group, - AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, - (AvahiPublishFlags)0, impl->session_name, - service_name, NULL, NULL, - impl->ctrl_port, txt); - avahi_string_list_free(txt); + pw_properties_set(props, PW_KEY_ZEROCONF_NAME, impl->session_name); + pw_properties_set(props, PW_KEY_ZEROCONF_TYPE, service_type); + pw_properties_setf(props, PW_KEY_ZEROCONF_PORT, "%u", impl->ctrl_port); + + res = pw_zeroconf_set_announce(impl->zeroconf, impl, &props->dict); + + pw_properties_free(props); if (res < 0) { - pw_log_error("can't add service: %s", - avahi_strerror(avahi_client_errno(impl->client))); - return -EIO; - } - if ((res = avahi_entry_group_commit(impl->group)) < 0) { - pw_log_error("can't commit group: %s", - avahi_strerror(avahi_client_errno(impl->client))); - return -EIO; + pw_log_error("can't add service: %s", spa_strerror(res)); + return res; } return 0; } -static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata) -{ - struct impl *impl = userdata; - impl->client = c; - switch (state) { - case AVAHI_CLIENT_S_REGISTERING: - case AVAHI_CLIENT_S_RUNNING: - case AVAHI_CLIENT_S_COLLISION: - make_browser(impl); - make_announce(impl); - break; - case AVAHI_CLIENT_FAILURE: - case AVAHI_CLIENT_CONNECTING: - break; - default: - break; - } -} +static const struct pw_zeroconf_events zeroconf_events = { + PW_VERSION_ZEROCONF_EVENTS, + .added = on_zeroconf_added, + .removed = on_zeroconf_removed, +}; static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) { @@ -1673,7 +1544,6 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) args = ""; spa_list_init(&impl->sessions); - spa_list_init(&impl->service_list); props = pw_properties_new_string(args); if (props == NULL) { @@ -1685,6 +1555,8 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->discover_local = pw_properties_get_bool(impl->props, "sess.discover-local", false); + pw_properties_set(impl->props, PW_KEY_ZEROCONF_DISCOVER_LOCAL, + impl->discover_local ? "true" : "false"); stream_props = pw_properties_new(NULL, NULL); if (stream_props == NULL) { @@ -1804,14 +1676,17 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) if ((res = setup_apple_session(impl)) < 0) goto out; - 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, - &res)) == NULL) { - pw_log_error("can't create avahi client: %s", avahi_strerror(res)); + impl->zeroconf = pw_zeroconf_new(impl->context, &impl->props->dict); + if (impl->zeroconf == NULL) { + res = -errno; + pw_log_error("can't create zeroconf: %m"); goto out; } + pw_zeroconf_add_listener(impl->zeroconf, &impl->zeroconf_listener, + &zeroconf_events, impl); + + make_browser(impl); + make_announce(impl); pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c index 8f911c62a..b0b52ed50 100644 --- a/src/modules/module-rtp-source.c +++ b/src/modules/module-rtp-source.c @@ -231,7 +231,7 @@ struct impl { /* 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; + SPA_ALIGNED(8) uint64_t last_packet_time; struct pw_timer standby_timer; /* This timer is used when the first stream_start() call fails because @@ -246,6 +246,9 @@ struct impl { socklen_t src_len; struct spa_source *source; + bool is_multicast; + bool filter_by_address; + uint8_t *buffer; size_t buffer_size; @@ -300,14 +303,41 @@ on_rtp_io(void *data, int fd, uint32_t mask) ssize_t len; int suppressed; uint64_t current_time; + struct sockaddr_storage recvaddr; + socklen_t recvaddr_len = sizeof(recvaddr); current_time = get_time_ns(impl); if (mask & SPA_IO_IN) { - - if ((len = recv(fd, impl->buffer, impl->buffer_size, 0)) < 0) + if ((len = recvfrom(fd, impl->buffer, impl->buffer_size, 0, (struct sockaddr *)(&recvaddr), &recvaddr_len)) < 0) goto receive_error; + /* Filter the packets to exclude those with source addresses + * that do not match the expected one. Only used with unicast. + * (The bind() call in make_socket takes care of only + * receiving packets that target the specified port.) */ + if (impl->filter_by_address && !pw_net_are_addresses_equal(&recvaddr, &(impl->src_addr), false)) { + /* In the IPv6 case, pw_net_get_ip() produces output formatted + * as "%". Both constants + * INET6_ADDRSTRLEN and IFNAMSIZ include the null terminator + * in their respective length values. This works out well for + * the formatted output, since this ensures there is one extra + * character for the % delimiter and another extra character + * for the null terminator of the entire string. + * + * (In the IPv4 case, pw_net_get_ip() just outputs the address.) */ + char address_str[INET6_ADDRSTRLEN + IFNAMSIZ]; + int res; + + res = pw_net_get_ip(&recvaddr, address_str, sizeof(address_str), NULL, NULL); + if (SPA_LIKELY(res == 0)) + pw_log_trace("Filtering out packet with mismatching address %s", address_str); + else + pw_log_warn("Filtering out packet with unrecognized address"); + + return; + } + if (len < 12) goto short_packet; @@ -474,12 +504,12 @@ finish: } static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname, - struct igmp_recovery *igmp_recovery) + struct igmp_recovery *igmp_recovery, bool *is_multicast, + bool *filter_by_address) { int af, fd, val, res; struct ifreq req; struct sockaddr_storage ba = *(struct sockaddr_storage *)sa; - bool do_connect = false; char addr[128]; af = sa->sa_family; @@ -522,12 +552,16 @@ static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname, pw_net_get_ip((struct sockaddr_storage*)sa, addr, sizeof(addr), NULL, NULL); pw_log_info("join IPv4 group: %s", addr); res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mr4, sizeof(mr4)); + *filter_by_address = false; + *is_multicast = true; } else { struct sockaddr_in *ba4 = (struct sockaddr_in*)&ba; - if (ba4->sin_addr.s_addr != INADDR_ANY) { - ba4->sin_addr.s_addr = INADDR_ANY; - do_connect = true; - } + *filter_by_address = (ba4->sin_addr.s_addr != INADDR_ANY); + *is_multicast = false; + /* Make sure the ANY address is always used. This is important + * for the bind() call below - with unicast, it shall only filter + * by port number (address filtering is done by recvfrom()). */ + ba4->sin_addr.s_addr = INADDR_ANY; } } else if (af == AF_INET6) { struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)sa; @@ -539,8 +573,15 @@ static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname, pw_net_get_ip((struct sockaddr_storage*)sa, addr, sizeof(addr), NULL, NULL); pw_log_info("join IPv6 group: %s", addr); res = setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mr6, sizeof(mr6)); + *filter_by_address = false; + *is_multicast = true; } else { struct sockaddr_in6 *ba6 = (struct sockaddr_in6*)&ba; + *filter_by_address = !IN6_IS_ADDR_UNSPECIFIED(&(ba6->sin6_addr.s6_addr)); + *is_multicast = false; + /* Make sure the ANY address is always used. This is important + * for the bind() call below - with unicast, it shall only filter + * by port number (address filtering is done by recvfrom()). */ ba6->sin6_addr = in6addr_any; } } else { @@ -569,13 +610,6 @@ static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname, pw_log_error("bind() failed: %m"); goto error; } - if (do_connect) { - if (connect(fd, sa, salen) < 0) { - res = -errno; - pw_log_error("connect() failed: %m"); - goto error; - } - } return fd; error: close(fd); @@ -613,7 +647,9 @@ static void stream_open_connection(void *data, int *result) if ((fd = make_socket((const struct sockaddr *)&impl->src_addr, impl->src_len, impl->ifname, - &(impl->igmp_recovery))) < 0) { + &(impl->igmp_recovery), + &(impl->is_multicast), + &(impl->filter_by_address))) < 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 @@ -663,11 +699,13 @@ static void stream_open_connection(void *data, int *result) goto finish; } - 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; + if (impl->is_multicast) { + 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: diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index d20e9a37c..085f3bae8 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -546,7 +546,7 @@ static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets, uin else header.m = 0; - rtp_timestamp = impl->ts_offset + (set_timestamp ? set_timestamp : timestamp); + rtp_timestamp = impl->ts_offset + impl->ts_align + (set_timestamp ? set_timestamp : timestamp); header.sequence_number = htons(impl->seq); header.timestamp = htonl(rtp_timestamp); @@ -556,12 +556,12 @@ static void rtp_audio_flush_packets(struct impl *impl, uint32_t num_packets, uin ((uint64_t)timestamp * stride) % impl->actual_max_buffer_size, &iov[1], tosend * stride); - pw_log_trace("sending %d packet:%d ts_offset:%d timestamp:%u (%f s)", + pw_log_trace_fp("sending %d packet:%d ts_offset:%d timestamp:%u (%f s)", tosend, num_packets, impl->ts_offset, timestamp, (double)timestamp * impl->io_position->clock.rate.num / impl->io_position->clock.rate.denom); - rtp_stream_emit_send_packet(impl, iov, 3); + rtp_stream_call_send_packet(impl, iov, 3); impl->seq++; impl->first = false; @@ -606,7 +606,7 @@ static void rtp_audio_stop_timer(struct impl *impl) static void rtp_audio_flush_timeout(struct impl *impl, uint64_t expirations) { if (expirations > 1) - pw_log_warn("missing timeout %"PRIu64, expirations); + pw_log_trace("missing timeout %"PRIu64, expirations); rtp_audio_flush_packets(impl, expirations, 0); } @@ -705,8 +705,10 @@ static void rtp_audio_process_capture(void *data) * that resynchronization is needed, then this will be done immediately below. */ if (!impl->have_sync) { - pw_log_info("(re)sync to timestamp:%u seq:%u ts_offset:%u SSRC:%u", - actual_timestamp, impl->seq, impl->ts_offset, impl->ssrc); + if (!impl->direct_timestamp) + impl->ts_align = actual_timestamp - impl->ring.readindex; + pw_log_info("(re)sync to timestamp:%u seq:%u ts_offset:%u ts_align:%u SSRC:%u", + actual_timestamp, impl->seq, impl->ts_offset, impl->ts_align, impl->ssrc); spa_ringbuffer_read_update(&impl->ring, actual_timestamp); spa_ringbuffer_write_update(&impl->ring, actual_timestamp); memset(impl->buffer, 0, BUFFER_SIZE); diff --git a/src/modules/module-rtp/midi.c b/src/modules/module-rtp/midi.c index 5fbdf3b63..1237f66c6 100644 --- a/src/modules/module-rtp/midi.c +++ b/src/modules/module-rtp/midi.c @@ -151,7 +151,7 @@ static int parse_journal(struct impl *impl, uint8_t *packet, uint16_t seq, uint3 return -EINVAL; j = (struct rtp_midi_journal*)packet; uint16_t seqnum = ntohs(j->checkpoint_seqnum); - rtp_stream_emit_send_feedback(impl, seqnum); + rtp_stream_call_send_feedback(impl, seqnum); return 0; } @@ -271,9 +271,6 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti while (offs < end) { uint32_t delta; int size; - uint64_t state = 0; - uint8_t *d; - size_t s; if (first && !hdr.z) delta = 0; @@ -294,17 +291,9 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti return -EINVAL; } - d = &packet[offs]; - s = size; - while (s > 0) { - uint32_t ump[4]; - int ump_size = spa_ump_from_midi(&d, &s, ump, sizeof(ump), 0, &state); - if (ump_size <= 0) - break; + spa_pod_builder_control(&b, timestamp, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, &packet[offs], size); - spa_pod_builder_control(&b, timestamp, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&b, ump, ump_size); - } offs += size; first = false; } @@ -378,7 +367,7 @@ unexpected_ssrc: return -EINVAL; } -static int write_event(uint8_t *p, uint32_t buffer_size, uint32_t value, void *ev, uint32_t size) +static int write_event(uint8_t *p, uint32_t buffer_size, uint32_t value, const void *ev, uint32_t size) { uint64_t buffer; uint8_t b; @@ -437,62 +426,54 @@ static void rtp_midi_flush_packets(struct impl *impl, while (spa_pod_parser_get_control_body(parser, &c, &c_body) >= 0) { uint32_t delta, offset; - uint8_t event[16]; - int size; - size_t c_size = c.value.size; - uint64_t state = 0; + uint32_t size = c.value.size; + const uint8_t *data = c_body; - if (c.type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_Midi) 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); - } - 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; - } - 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); - - memcpy(&impl->buffer[len], event, size); - len += size; + 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 { - delta = offset - prev_offset; - prev_offset = offset; - len += write_event(&impl->buffer[len], BUFFER_SIZE - len, delta, event, size); + 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_call_send_packet(impl, iov, 3); + + impl->seq++; + len = 0; + } + 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); + + memcpy(&impl->buffer[len], data, size); + len += size; + } else { + delta = offset - prev_offset; + prev_offset = offset; + len += write_event(&impl->buffer[len], BUFFER_SIZE - len, delta, data, size); } } if (len > 0) { @@ -510,7 +491,7 @@ static void rtp_midi_flush_packets(struct impl *impl, iov[2].iov_len = len; pw_log_trace("sending %d timestamp:%d", len, base); - rtp_stream_emit_send_packet(impl, iov, 3); + rtp_stream_call_send_packet(impl, iov, 3); impl->seq++; } } diff --git a/src/modules/module-rtp/opus.c b/src/modules/module-rtp/opus.c index d13a4efaf..9175d4a31 100644 --- a/src/modules/module-rtp/opus.c +++ b/src/modules/module-rtp/opus.c @@ -252,7 +252,7 @@ static void rtp_opus_flush_packets(struct impl *impl) pw_log_trace("sending %d len:%d timestamp:%d", tosend, res, timestamp); iov[1].iov_len = res; - rtp_stream_emit_send_packet(impl, iov, 2); + rtp_stream_call_send_packet(impl, iov, 2); impl->seq++; timestamp += tosend; diff --git a/src/modules/module-rtp/stream.c b/src/modules/module-rtp/stream.c index d69b16524..11bba4f98 100644 --- a/src/modules/module-rtp/stream.c +++ b/src/modules/module-rtp/stream.c @@ -48,8 +48,11 @@ PW_LOG_TOPIC_EXTERN(mod_topic); #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) + +#define rtp_stream_call(s,m,v,...) spa_callbacks_call_fast(&s->rtp_callbacks, \ + struct rtp_stream_events, m, v, ##__VA_ARGS__) +#define rtp_stream_call_send_packet(s,i,l) rtp_stream_call(s, send_packet,0,i,l) +#define rtp_stream_call_send_feedback(s,seq) rtp_stream_call(s, send_feedback,0,seq) enum rtp_stream_internal_state { /* The state when the stream is idle / stopped. The background @@ -85,6 +88,8 @@ struct impl { struct spa_hook stream_listener; struct pw_stream_events stream_events; + struct spa_callbacks rtp_callbacks; + struct spa_hook_list listener_list; struct spa_hook listener; @@ -109,6 +114,7 @@ struct impl { uint32_t mtu; uint32_t header_size; uint32_t payload_size; + uint32_t ts_align; struct spa_ringbuffer ring; uint8_t buffer[BUFFER_SIZE]; @@ -426,7 +432,7 @@ static int stream_stop(struct impl *impl) * 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; + int res = 0; pw_log_info("closing connection as part of stopping the stream"); rtp_stream_emit_close_connection(impl, &res); if (res > 0) { @@ -1002,6 +1008,7 @@ struct rtp_stream *rtp_stream_new(struct pw_core *core, (res = stream_start(impl)) < 0) goto out; + impl->rtp_callbacks = SPA_CALLBACKS_INIT(events, data); spa_hook_list_append(&impl->listener_list, &impl->listener, events, data); return (struct rtp_stream*)impl; diff --git a/src/modules/module-scheduler-v1.c b/src/modules/module-scheduler-v1.c new file mode 100644 index 000000000..0bfe43a4a --- /dev/null +++ b/src/modules/module-scheduler-v1.c @@ -0,0 +1,1038 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2018 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_SYS_VFS_H +#include +#endif +#ifdef HAVE_SYS_MOUNT_H +#include +#endif + +#include +#include +#include +#include + +#include +#include + +/** \page page_module_scheduler_v1 SchedulerV1 + * + * + * ## Module Name + * + * `libpipewire-module-scheduler-v1` + * + * ## Module Options + * + * Options specific to the behavior of this module + * + * ## General options + * + * Options with well-known behavior: + * + * ## Config override + * + * A `module.scheduler-v1.args` config section can be added + * to override the module arguments. + * + *\code{.unparsed} + * # ~/.config/pipewire/pipewire.conf.d/my-scheduler-v1-args.conf + * + * module.scheduler-v1.args = { + * } + *\endcode + * + * ## Example configuration + * + *\code{.unparsed} + * context.modules = [ + * { name = libpipewire-module-scheduler-v1 + * args = { + * } + * } + *] + *\endcode + * + * Since: 1.7.0 + */ + +#define NAME "scheduler-v1" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define MODULE_USAGE "" + +static const struct spa_dict_item module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { PW_KEY_MODULE_DESCRIPTION, "Implement the Scheduler V1" }, + { PW_KEY_MODULE_USAGE, MODULE_USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +#define MAX_HOPS 64 +#define MAX_SYNC 4u + +struct impl { + struct pw_context *context; + + struct pw_properties *props; + + struct spa_hook context_listener; + struct spa_hook module_listener; +}; + +static int ensure_state(struct pw_impl_node *node, bool running) +{ + enum pw_node_state state = node->info.state; + if (node->active && node->runnable && + !SPA_FLAG_IS_SET(node->spa_flags, SPA_NODE_FLAG_NEED_CONFIGURE) && running) + state = PW_NODE_STATE_RUNNING; + else if (state > PW_NODE_STATE_IDLE) + state = PW_NODE_STATE_IDLE; + return pw_impl_node_set_state(node, state); +} + +/* Make a node runnable. Peer nodes are also made runnable when the passive_mode + * of the peer port is !TRUE. + * + * A (*) -> B if A running -> B set to running + * A (*) -> (f) B if A running -> B set to running + * A (*) -> (fs) B if A running -> B set to running + * A (*) -> (p) B if A running -> B no change + */ +static inline bool makes_runnable(struct pw_impl_port *a, struct pw_impl_port *b) +{ + return b->passive_mode != PASSIVE_MODE_TRUE; +} +static void make_runnable(struct pw_context *context, struct pw_impl_node *node) +{ + struct pw_impl_port *p; + struct pw_impl_link *l; + struct pw_impl_node *n; + + if (!node->runnable) { + pw_log_debug("%s is runnable", node->name); + node->runnable = true; + } + + spa_list_for_each(p, &node->output_ports, link) { + spa_list_for_each(l, &p->links, output_link) { + n = l->input->node; + pw_log_trace(" out-port %p: link %p passive:%d prepared:%d active:%d runn:%d", p, + l, l->input->passive_mode, l->prepared, n->active, n->runnable); + if (!n->active || !makes_runnable(p, l->input)) + continue; + pw_impl_link_prepare(l); + if (!l->prepared) + continue; + if (!n->runnable) + make_runnable(context, n); + } + } + spa_list_for_each(p, &node->input_ports, link) { + spa_list_for_each(l, &p->links, input_link) { + n = l->output->node; + pw_log_trace(" in-port %p: link %p passive:%d prepared:%d active:%d runn:%d", p, + l, l->output->passive_mode, l->prepared, n->active, n->runnable); + if (!n->active || !makes_runnable(p, l->output)) + continue; + pw_impl_link_prepare(l); + if (!l->prepared) + continue; + if (!n->runnable) + make_runnable(context, n); + } + } + /* now go through all the nodes that share groups and link_groups + * that are not yet runnable. We don't include sync-groups because they + * are only used to group the node with a driver, not to determine the + * runnable state of a node. */ + if (node->groups != NULL || node->link_groups != NULL) { + spa_list_for_each(n, &context->node_list, link) { + if (n->exported || !n->active || n->runnable) + continue; + /* the other node will be scheduled with this one if it's in + * the same group or link group */ + if (pw_strv_find_common(n->groups, node->groups) < 0 && + pw_strv_find_common(n->link_groups, node->link_groups) < 0) + continue; + + make_runnable(context, n); + } + } +} + +/* check if a node and its peer can run. + * + * Only consider ports that have a PASSIVE_MODE_FALSE link. + * All other port modes don't make A and B runnable. + * + * A -> B A + B both set to running + * A -> (p) B A + B both set to running + * A -> (f) B A + B both set to running + * A (fs) -> (fs) B A + B both set to running + * A (p) -> (*) B A + B no change + * A (f) -> (*) B A + B no change + * A (fs) -> (*) B A + B no change + * + * There is a special case for FOLLOW_SUSPEND<->FOLLOW_SUSPEND to make + * it possible to manually link a source to a sink + */ +static inline bool runnable_pair(struct pw_impl_port *a, struct pw_impl_port *b) +{ + bool res = false; + if (a->passive_mode == PASSIVE_MODE_FALSE) + res = true; + if (a->passive_mode == PASSIVE_MODE_FOLLOW_SUSPEND && + b->passive_mode == PASSIVE_MODE_FOLLOW_SUSPEND) + res = true; + pw_log_trace(" port %p <-> %p: %s <> %s -> %d", a, b, + passive_mode_to_string(a->passive_mode), + passive_mode_to_string(b->passive_mode), res); + return res; +} +static void check_runnable(struct pw_context *context, struct pw_impl_node *node) +{ + struct pw_impl_port *p; + struct pw_impl_link *l; + struct pw_impl_node *n; + + pw_log_trace("node %p: '%s' always-process:%d runnable:%u active:%d", node, + node->name, node->always_process, node->runnable, node->active); + + if (node->always_process && !node->runnable) + make_runnable(context, node); + + spa_list_for_each(p, &node->output_ports, link) { + spa_list_for_each(l, &p->links, output_link) { + n = l->input->node; + /* the peer needs to be active and we are linked to it + * with a non-passive link */ + pw_log_trace(" out-port %p: link %p prepared:%d active:%d", p, + l, l->prepared, n->active); + if (!n->active || !runnable_pair(p, l->input)) + continue; + /* explicitly prepare the link in case it was suspended */ + pw_impl_link_prepare(l); + if (!l->prepared) + continue; + make_runnable(context, node); + make_runnable(context, n); + } + } + spa_list_for_each(p, &node->input_ports, link) { + spa_list_for_each(l, &p->links, input_link) { + n = l->output->node; + pw_log_trace(" in-port %p: link %p prepared:%d active:%d", p, + l, l->prepared, n->active); + if (!n->active || !runnable_pair(p, l->output)) + continue; + pw_impl_link_prepare(l); + if (!l->prepared) + continue; + make_runnable(context, node); + make_runnable(context, n); + } + } +} + +/* Follow all links and groups from node. + * + * After this is done, we end up with a list of nodes in collect that are all + * linked to node. + * + * We don't need to care about active nodes or links, we just follow and group everything. + * The inactive nodes or links will simply not be runnable but will already be grouped + * correctly when they do become active and prepared. + */ +static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, struct spa_list *collect) +{ + struct spa_list queue; + struct pw_impl_node *n, *t; + struct pw_impl_port *p; + struct pw_impl_link *l; + uint32_t n_sync; + char *sync[MAX_SYNC+1]; + + pw_log_debug("node %p: '%s'", node, node->name); + + /* start with node in the queue */ + spa_list_init(&queue); + spa_list_append(&queue, &node->sort_link); + node->visited = true; + + n_sync = 0; + sync[0] = NULL; + + /* now follow all the links from the nodes in the queue + * and add the peers to the queue. */ + spa_list_consume(n, &queue, sort_link) { + spa_list_remove(&n->sort_link); + spa_list_append(collect, &n->sort_link); + + pw_log_debug(" next node %p: '%s' runnable:%u active:%d", + n, n->name, n->runnable, n->active); + + if (n->sync) { + for (uint32_t i = 0; n->sync_groups[i]; i++) { + if (n_sync >= MAX_SYNC) + break; + if (pw_strv_find(sync, n->sync_groups[i]) >= 0) + continue; + sync[n_sync++] = n->sync_groups[i]; + sync[n_sync] = NULL; + } + } + + spa_list_for_each(p, &n->input_ports, link) { + spa_list_for_each(l, &p->links, input_link) { + t = l->output->node; + if (!t->visited) { + t->visited = true; + spa_list_append(&queue, &t->sort_link); + } + } + } + spa_list_for_each(p, &n->output_ports, link) { + spa_list_for_each(l, &p->links, output_link) { + t = l->input->node; + if (!t->visited) { + t->visited = true; + spa_list_append(&queue, &t->sort_link); + } + } + } + /* now go through all the nodes that have the same groups and + * that are not yet visited */ + if (n->groups != NULL || n->link_groups != NULL || sync[0] != NULL) { + spa_list_for_each(t, &context->node_list, link) { + if (t->exported || t->visited) + continue; + /* the other node will be scheduled with this one if it's in + * the same group, link group or sync group */ + if (pw_strv_find_common(t->groups, n->groups) < 0 && + pw_strv_find_common(t->link_groups, n->link_groups) < 0 && + pw_strv_find_common(t->sync_groups, sync) < 0) + continue; + + pw_log_debug("%p: %s join group of %s", + t, t->name, n->name); + t->visited = true; + spa_list_append(&queue, &t->sort_link); + } + } + pw_log_debug(" next node %p: '%s' runnable:%u %p %p %p", n, n->name, n->runnable, + n->groups, n->link_groups, sync); + } + return 0; +} + +static void move_to_driver(struct pw_context *context, struct spa_list *nodes, + struct pw_impl_node *driver) +{ + struct pw_impl_node *n; + pw_log_debug("driver: %p %s runnable:%u", driver, driver->name, driver->runnable); + spa_list_consume(n, nodes, sort_link) { + spa_list_remove(&n->sort_link); + + driver->runnable |= n->runnable; + + pw_log_debug(" follower: %p %s runnable:%u driver-runnable:%u", n, n->name, + n->runnable, driver->runnable); + pw_impl_node_set_driver(n, driver); + } +} +static void remove_from_driver(struct pw_context *context, struct spa_list *nodes) +{ + struct pw_impl_node *n; + spa_list_consume(n, nodes, sort_link) { + spa_list_remove(&n->sort_link); + pw_impl_node_set_driver(n, NULL); + ensure_state(n, false); + } +} + +static inline void get_quantums(struct pw_context *context, uint32_t *def, + uint32_t *min, uint32_t *max, uint32_t *rate, uint32_t *floor, uint32_t *ceil) +{ + struct settings *s = &context->settings; + if (s->clock_force_quantum != 0) { + *def = *min = *max = s->clock_force_quantum; + *rate = 0; + } else { + *def = s->clock_quantum; + *min = s->clock_min_quantum; + *max = s->clock_max_quantum; + *rate = s->clock_rate; + } + *floor = s->clock_quantum_floor; + *ceil = s->clock_quantum_limit; +} + +static inline const uint32_t *get_rates(struct pw_context *context, uint32_t *def, uint32_t *n_rates, + bool *force) +{ + struct settings *s = &context->settings; + if (s->clock_force_rate != 0) { + *force = true; + *n_rates = 1; + *def = s->clock_force_rate; + return &s->clock_force_rate; + } else { + *force = false; + *n_rates = s->n_clock_rates; + *def = s->clock_rate; + return s->clock_rates; + } +} +static void reconfigure_driver(struct pw_context *context, struct pw_impl_node *n) +{ + struct pw_impl_node *s; + + spa_list_for_each(s, &n->follower_list, follower_link) { + if (s == n) + continue; + pw_log_debug("%p: follower %p: '%s' suspend", + context, s, s->name); + pw_impl_node_set_state(s, PW_NODE_STATE_SUSPENDED); + } + pw_log_debug("%p: driver %p: '%s' suspend", + context, n, n->name); + + if (n->info.state >= PW_NODE_STATE_IDLE) + n->need_resume = !n->pause_on_idle; + pw_impl_node_set_state(n, PW_NODE_STATE_SUSPENDED); +} + +/* find smaller power of 2 */ +static uint32_t flp2(uint32_t x) +{ + x = x | (x >> 1); + x = x | (x >> 2); + x = x | (x >> 4); + x = x | (x >> 8); + x = x | (x >> 16); + return x - (x >> 1); +} + +/* cmp fractions, avoiding overflows */ +static int fraction_compare(const struct spa_fraction *a, const struct spa_fraction *b) +{ + uint64_t fa = (uint64_t)a->num * (uint64_t)b->denom; + uint64_t fb = (uint64_t)b->num * (uint64_t)a->denom; + return fa < fb ? -1 : (fa > fb ? 1 : 0); +} + +static inline uint32_t calc_gcd(uint32_t a, uint32_t b) +{ + while (b != 0) { + uint32_t temp = a; + a = b; + b = temp % b; + } + return a; +} + +struct rate_info { + uint32_t rate; + uint32_t gcd; + uint32_t diff; +}; + +static inline void update_highest_rate(struct rate_info *best, struct rate_info *current) +{ + /* find highest rate */ + if (best->rate == 0 || best->rate < current->rate) + *best = *current; +} + +static inline void update_nearest_gcd(struct rate_info *best, struct rate_info *current) +{ + /* find nearest GCD */ + if (best->rate == 0 || + (best->gcd < current->gcd) || + (best->gcd == current->gcd && best->diff > current->diff)) + *best = *current; +} +static inline void update_nearest_rate(struct rate_info *best, struct rate_info *current) +{ + /* find nearest rate */ + if (best->rate == 0 || best->diff > current->diff) + *best = *current; +} + +static uint32_t find_best_rate(const uint32_t *rates, uint32_t n_rates, uint32_t rate, uint32_t def) +{ + uint32_t i, limit; + struct rate_info best; + struct rate_info info[n_rates]; + + for (i = 0; i < n_rates; i++) { + info[i].rate = rates[i]; + info[i].gcd = calc_gcd(rate, rates[i]); + info[i].diff = SPA_ABS((int32_t)rate - (int32_t)rates[i]); + } + + /* first find higher nearest GCD. This tries to find next bigest rate that + * requires the least amount of resample filter banks. Usually these are + * rates that are multiples of each other or multiples of a common rate. + * + * 44100 and [ 32000 56000 88200 96000 ] -> 88200 + * 48000 and [ 32000 56000 88200 96000 ] -> 96000 + * 88200 and [ 44100 48000 96000 192000 ] -> 96000 + * 32000 and [ 44100 192000 ] -> 44100 + * 8000 and [ 44100 48000 ] -> 48000 + * 8000 and [ 44100 192000 ] -> 44100 + * 11025 and [ 44100 48000 ] -> 44100 + * 44100 and [ 48000 176400 ] -> 48000 + * 144 and [ 44100 48000 88200 96000] -> 48000 + */ + spa_zero(best); + /* Don't try to do excessive upsampling by limiting the max rate + * for desired < default to default*2. For other rates allow + * a x3 upsample rate max. For values lower than half of the default, + * limit to the default. */ + limit = rate < def/2 ? def : rate < def ? def*2 : rate*3; + for (i = 0; i < n_rates; i++) { + if (info[i].rate >= rate && info[i].rate <= limit) + update_nearest_gcd(&best, &info[i]); + } + if (best.rate != 0) + return best.rate; + + /* we would need excessive upsampling, pick a nearest higher rate */ + spa_zero(best); + for (i = 0; i < n_rates; i++) { + if (info[i].rate >= rate) + update_nearest_rate(&best, &info[i]); + } + if (best.rate != 0) + return best.rate; + + /* There is nothing above the rate, we need to downsample. Try to downsample + * but only to something that is from a common rate family. Also don't + * try to downsample to something that will sound worse (< 44100). + * + * 88200 and [ 22050 44100 48000 ] -> 44100 + * 88200 and [ 22050 48000 ] -> 48000 + */ + spa_zero(best); + for (i = 0; i < n_rates; i++) { + if (info[i].rate >= 44100) + update_nearest_gcd(&best, &info[i]); + } + if (best.rate != 0) + return best.rate; + + /* There is nothing to downsample above our threshold. Downsample to whatever + * is the highest rate then. */ + spa_zero(best); + for (i = 0; i < n_rates; i++) + update_highest_rate(&best, &info[i]); + if (best.rate != 0) + return best.rate; + + return def; +} + +/* here we evaluate the complete state of the graph. + * + * It roughly operates in 4 stages: + * + * 1. go over all nodes and check if they should be scheduled (runnable) or not. + * + * 2. go over all drivers and collect the nodes that need to be scheduled with the + * driver. This include all nodes that have an active link with the driver or + * with a node already scheduled with the driver. + * + * 3. go over all nodes that are not assigned to a driver. The ones that require + * a driver are moved to some random active driver found in step 2. + * + * 4. go over all drivers again, collect the quantum/rate of all followers, select + * the desired final value and activate the followers and then the driver. + * + * A complete graph evaluation is performed for each change that is made to the + * graph, such as making/destroying links, adding/removing nodes, property changes such + * as quantum/rate changes or metadata changes. + */ +static void context_recalc_graph(void *data) +{ + struct impl *impl = data; + struct pw_context *context = impl->context; + struct settings *settings = &context->settings; + struct pw_impl_node *n, *s, *target, *fallback; + const uint32_t *rates; + uint32_t max_quantum, min_quantum, def_quantum, rate_quantum, floor_quantum, ceil_quantum; + uint32_t n_rates, def_rate, transport; + bool freewheel, global_force_rate, global_force_quantum; + struct spa_list collect; + +again: + freewheel = false; + + /* clean up the flags first */ + spa_list_for_each(n, &context->node_list, link) { + n->visited = false; + n->checked = 0; + n->runnable = false; + } + + get_quantums(context, &def_quantum, &min_quantum, &max_quantum, &rate_quantum, + &floor_quantum, &ceil_quantum); + rates = get_rates(context, &def_rate, &n_rates, &global_force_rate); + + global_force_quantum = rate_quantum == 0; + + /* first look at all nodes and decide which one should be runnable */ + spa_list_for_each(n, &context->node_list, link) { + if (n->exported || !n->active) + continue; + check_runnable(context, n); + } + + /* start from all drivers and group all nodes that are linked + * to it. Some nodes are not (yet) linked to anything and they + * will end up 'unassigned' to a driver. Other nodes are drivers + * and if they have active followers, we can use them to schedule + * the unassigned nodes. */ + target = fallback = NULL; + spa_list_for_each(n, &context->driver_list, driver_link) { + if (n->exported) + continue; + + if (!n->visited) { + spa_list_init(&collect); + collect_nodes(context, n, &collect); + move_to_driver(context, &collect, n); + } + /* from now on we are only interested in active driving nodes + * with a driver_priority. We're going to see if there are + * active followers. */ + if (!n->driving || !n->active || n->priority_driver <= 0) + continue; + + /* first active driving node is fallback */ + if (fallback == NULL) + fallback = n; + + if (!n->runnable) + continue; + + spa_list_for_each(s, &n->follower_list, follower_link) { + pw_log_debug("%p: driver %p: follower %p %s: active:%d", + context, n, s, s->name, s->active); + if (s != n && s->active) { + /* if the driving node has active followers, it + * is a target for our unassigned nodes */ + if (target == NULL) + target = n; + if (n->freewheel) + freewheel = true; + break; + } + } + } + /* no active node, use fallback driving node */ + if (target == NULL) + target = fallback; + + /* update the freewheel status */ + pw_context_set_freewheel(context, freewheel); + + /* now go through all available nodes. The ones we didn't visit + * in collect_nodes() are not linked to any driver. We assign them + * to either an active driver or the first driver if they are in a + * group that needs a driver. Else we remove them from a driver + * and stop them. */ + spa_list_for_each(n, &context->node_list, link) { + struct pw_impl_node *t, *driver; + + if (n->exported || n->visited) + continue; + + pw_log_debug("%p: unassigned node %p: '%s' active:%d want_driver:%d target:%p", + context, n, n->name, n->active, n->want_driver, target); + + /* collect all nodes in this group */ + spa_list_init(&collect); + collect_nodes(context, n, &collect); + + driver = NULL; + spa_list_for_each(t, &collect, sort_link) { + /* is any active and want a driver */ + if ((t->want_driver && t->active && t->runnable) || + t->always_process) { + driver = target; + break; + } + } + if (driver != NULL) { + driver->runnable = true; + /* driver needed for this group */ + move_to_driver(context, &collect, driver); + } else { + /* no driver, make sure the nodes stop */ + remove_from_driver(context, &collect); + } + } + + /* assign final quantum and set state for followers and drivers */ + spa_list_for_each(n, &context->driver_list, driver_link) { + bool running = false, lock_quantum = false, lock_rate = false; + struct spa_fraction latency = SPA_FRACTION(0, 0); + struct spa_fraction max_latency = SPA_FRACTION(0, 0); + struct spa_fraction rate = SPA_FRACTION(0, 0); + uint32_t target_quantum, target_rate, current_rate, current_quantum; + uint64_t quantum_stamp = 0, rate_stamp = 0; + bool force_rate, force_quantum, restore_rate = false, restore_quantum = false; + bool do_reconfigure = false, need_resume, was_target_pending; + bool have_request = false; + const uint32_t *node_rates; + uint32_t node_n_rates, node_def_rate; + uint32_t node_max_quantum, node_min_quantum, node_def_quantum, node_rate_quantum; + + if (!n->driving || n->exported) + continue; + + node_def_quantum = def_quantum; + node_min_quantum = min_quantum; + node_max_quantum = max_quantum; + node_rate_quantum = rate_quantum; + force_quantum = global_force_quantum; + + node_def_rate = def_rate; + node_n_rates = n_rates; + node_rates = rates; + force_rate = global_force_rate; + + /* collect quantum and rate */ + spa_list_for_each(s, &n->follower_list, follower_link) { + + if (!s->moved) { + /* We only try to enforce the lock flags for nodes that + * are not recently moved between drivers. The nodes that + * are moved should try to enforce their quantum on the + * new driver. */ + lock_quantum |= s->lock_quantum; + lock_rate |= s->lock_rate; + } + if (!global_force_quantum && s->force_quantum > 0 && + s->stamp > quantum_stamp) { + node_def_quantum = node_min_quantum = node_max_quantum = s->force_quantum; + node_rate_quantum = 0; + quantum_stamp = s->stamp; + force_quantum = true; + } + if (!global_force_rate && s->force_rate > 0 && + s->stamp > rate_stamp) { + node_def_rate = s->force_rate; + node_n_rates = 1; + node_rates = &s->force_rate; + force_rate = true; + rate_stamp = s->stamp; + } + + /* smallest latencies */ + if (latency.denom == 0 || + (s->latency.denom > 0 && + fraction_compare(&s->latency, &latency) < 0)) + latency = s->latency; + if (max_latency.denom == 0 || + (s->max_latency.denom > 0 && + fraction_compare(&s->max_latency, &max_latency) < 0)) + max_latency = s->max_latency; + + /* largest rate, which is in fact the smallest fraction */ + if (rate.denom == 0 || + (s->rate.denom > 0 && + fraction_compare(&s->rate, &rate) < 0)) + rate = s->rate; + + if (s->active) + running = n->runnable; + + pw_log_debug("%p: follower %p running:%d runnable:%d rate:%u/%u latency %u/%u '%s'", + context, s, running, s->runnable, rate.num, rate.denom, + latency.num, latency.denom, s->name); + + if (running && s != n && s->supports_request > 0) + have_request = true; + + s->moved = false; + } + + if (n->forced_rate && !force_rate && n->runnable) { + /* A node that was forced to a rate but is no longer being + * forced can restore its rate */ + pw_log_info("(%s-%u) restore rate", n->name, n->info.id); + restore_rate = true; + } + if (n->forced_quantum && !force_quantum && n->runnable) { + /* A node that was forced to a quantum but is no longer being + * forced can restore its quantum */ + pw_log_info("(%s-%u) restore quantum", n->name, n->info.id); + restore_quantum = true; + } + + if (force_quantum) + lock_quantum = false; + if (force_rate) + lock_rate = false; + + need_resume = n->need_resume; + if (need_resume) { + running = true; + n->need_resume = false; + } + + current_rate = n->target_rate.denom; + if (!restore_rate && + (lock_rate || need_resume || !running || + (!force_rate && (n->info.state > PW_NODE_STATE_IDLE)))) { + pw_log_debug("%p: keep rate:1/%u restore:%u lock:%u resume:%u " + "running:%u force:%u state:%s", context, + current_rate, restore_rate, lock_rate, need_resume, + running, force_rate, + pw_node_state_as_string(n->info.state)); + + /* when we don't need to restore or rate and + * when someone wants us to lock the rate of this driver or + * when we are in the process of reconfiguring the driver or + * when we are not running any followers or + * when the driver is busy and we don't need to force a rate, + * keep the current rate */ + target_rate = current_rate; + } + else { + /* Here we are allowed to change the rate of the driver. + * Start with the default rate. If the desired rate is + * allowed, switch to it */ + if (rate.denom != 0 && rate.num == 1) + target_rate = rate.denom; + else + target_rate = node_def_rate; + + target_rate = find_best_rate(node_rates, node_n_rates, + target_rate, node_def_rate); + + pw_log_debug("%p: def_rate:%d target_rate:%d rate:%d/%d", context, + node_def_rate, target_rate, rate.num, rate.denom); + } + + was_target_pending = n->target_pending; + + if (target_rate != current_rate) { + /* we doing a rate switch */ + pw_log_info("(%s-%u) state:%s new rate:%u/(%u)->%u", + n->name, n->info.id, + pw_node_state_as_string(n->info.state), + n->target_rate.denom, current_rate, + target_rate); + + if (force_rate) { + if (settings->clock_rate_update_mode == CLOCK_RATE_UPDATE_MODE_HARD) + do_reconfigure |= !was_target_pending; + } else { + if (n->info.state >= PW_NODE_STATE_SUSPENDED) + do_reconfigure |= !was_target_pending; + } + /* we're setting the pending rate. This will become the new + * current rate in the next iteration of the graph. */ + n->target_rate = SPA_FRACTION(1, target_rate); + n->forced_rate = force_rate; + n->target_pending = true; + current_rate = target_rate; + } + + if (node_rate_quantum != 0 && current_rate != node_rate_quantum) { + /* the quantum values are scaled with the current rate */ + node_def_quantum = SPA_SCALE32(node_def_quantum, current_rate, node_rate_quantum); + node_min_quantum = SPA_SCALE32(node_min_quantum, current_rate, node_rate_quantum); + node_max_quantum = SPA_SCALE32(node_max_quantum, current_rate, node_rate_quantum); + } + + /* calculate desired quantum. Don't limit to the max_latency when we are + * going to force a quantum or rate and reconfigure the nodes. */ + if (max_latency.denom != 0 && !force_quantum && !force_rate) { + uint32_t tmp = SPA_SCALE32(max_latency.num, current_rate, max_latency.denom); + if (tmp < node_max_quantum) + node_max_quantum = tmp; + } + + current_quantum = n->target_quantum; + if (!restore_quantum && (lock_quantum || need_resume || !running)) { + pw_log_debug("%p: keep quantum:%u restore:%u lock:%u resume:%u " + "running:%u force:%u state:%s", context, + current_quantum, restore_quantum, lock_quantum, need_resume, + running, force_quantum, + pw_node_state_as_string(n->info.state)); + target_quantum = current_quantum; + } + else { + target_quantum = node_def_quantum; + if (latency.denom != 0) + target_quantum = SPA_SCALE32(latency.num, current_rate, latency.denom); + target_quantum = SPA_CLAMP(target_quantum, node_min_quantum, node_max_quantum); + target_quantum = SPA_CLAMP(target_quantum, floor_quantum, ceil_quantum); + + if (settings->clock_power_of_two_quantum && !force_quantum) + target_quantum = flp2(target_quantum); + } + + if (target_quantum != current_quantum) { + pw_log_info("(%s-%u) new quantum:%"PRIu64"->%u", + n->name, n->info.id, + n->target_quantum, + target_quantum); + /* this is the new pending quantum */ + n->target_quantum = target_quantum; + n->forced_quantum = force_quantum; + n->target_pending = true; + + if (force_quantum) + do_reconfigure |= !was_target_pending; + } + + if (n->target_pending) { + if (do_reconfigure) { + reconfigure_driver(context, n); + /* we might be suspended now and the links need to be prepared again */ + goto again; + } + /* we have a pending change. We place the new values in the + * pending fields so that they are picked up by the driver in + * the next cycle */ + pw_log_debug("%p: apply duration:%"PRIu64" rate:%u/%u", context, + n->target_quantum, n->target_rate.num, + n->target_rate.denom); + SPA_SEQ_WRITE(n->rt.position->clock.target_seq); + n->rt.position->clock.target_duration = n->target_quantum; + n->rt.position->clock.target_rate = n->target_rate; + SPA_SEQ_WRITE(n->rt.position->clock.target_seq); + + if (n->info.state < PW_NODE_STATE_RUNNING) { + n->rt.position->clock.duration = n->target_quantum; + n->rt.position->clock.rate = n->target_rate; + } + n->target_pending = false; + } else { + n->target_quantum = n->rt.position->clock.target_duration; + n->target_rate = n->rt.position->clock.target_rate; + } + + if (n->info.state < PW_NODE_STATE_RUNNING) + n->rt.position->clock.nsec = get_time_ns(n->rt.target.system); + + SPA_FLAG_UPDATE(n->rt.position->clock.flags, + SPA_IO_CLOCK_FLAG_LAZY, have_request && n->supports_lazy > 0); + + pw_log_debug("%p: driver %p running:%d runnable:%d quantum:%u rate:%u (%"PRIu64"/%u)'%s'", + context, n, running, n->runnable, target_quantum, target_rate, + n->rt.position->clock.target_duration, + n->rt.position->clock.target_rate.denom, n->name); + + transport = PW_NODE_ACTIVATION_COMMAND_NONE; + + /* first change the node states of the followers to the new target */ + spa_list_for_each(s, &n->follower_list, follower_link) { + if (s->transport != PW_NODE_ACTIVATION_COMMAND_NONE) { + transport = s->transport; + s->transport = PW_NODE_ACTIVATION_COMMAND_NONE; + } + if (s == n) + continue; + pw_log_debug("%p: follower %p: active:%d '%s'", + context, s, s->active, s->name); + ensure_state(s, running); + } + + if (transport != PW_NODE_ACTIVATION_COMMAND_NONE) { + pw_log_info("%s: transport %d", n->name, transport); + SPA_ATOMIC_STORE(n->rt.target.activation->command, transport); + } + + /* now that all the followers are ready, start the driver */ + ensure_state(n, running); + } +} + +static const struct pw_context_events context_events = { + PW_VERSION_CONTEXT_EVENTS, + .recalc_graph = context_recalc_graph, +}; + +static void module_destroy(void *data) +{ + struct impl *impl = data; + + if (impl->context) { + spa_hook_remove(&impl->context_listener); + spa_hook_remove(&impl->module_listener); + } + + pw_properties_free(impl->props); + + free(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +SPA_EXPORT +int pipewire__module_init(struct pw_impl_module *module, const char *args_str) +{ + struct pw_context *context = pw_impl_module_get_context(module); + struct pw_properties *args; + struct impl *impl; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + return -errno; + + pw_log_debug("module %p: new %s", impl, args_str); + + if (args_str) + args = pw_properties_new_string(args_str); + else + args = pw_properties_new(NULL, NULL); + + if (!args) { + res = -errno; + goto error; + } + + pw_context_conf_update_props(context, "module."NAME".args", args); + + impl->props = args; + impl->context = context; + + pw_context_add_listener(context, &impl->context_listener, &context_events, impl); + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); + + return 0; + +error: + module_destroy(impl); + return res; +} diff --git a/src/modules/module-sendspin-recv.c b/src/modules/module-sendspin-recv.c new file mode 100644 index 000000000..fe6525767 --- /dev/null +++ b/src/modules/module-sendspin-recv.c @@ -0,0 +1,1401 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 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 +#include +#include +#include +#include +#include + +#include +#include + +#include "module-sendspin/sendspin.h" +#include "module-sendspin/websocket.h" +#include "module-sendspin/regress.h" +#include "network-utils.h" + +#ifdef HAVE_AVAHI +#include "zeroconf-utils/zeroconf.h" +#endif + +/** \page page_module_sendspin_recv sendspin receiver + * + * The `sendspin-recv` module creates a PipeWire source that receives audio + * packets using the sendspin protocol. + * + * The receive will listen on a specific port (8928) and create a stream for the + * data on the port. + * + * ## Module Name + * + * `libpipewire-module-sendspin-recv` + * + * ## Module Options + * + * Options specific to the behavior of this module + * + * - `local.ifname = `: interface name to use + * - `source.ip = `: the source ip address to listen on, default 127.0.0.1 + * - `source.port = `: the source port to listen on, default 8928 + * - `source.path = `: the path to listen on, default "/sendspin" + * - `sendspin.ip`: the IP address of the sendspin server + * - `sendspin.port`: the port of the sendspin server, default 8927 + * - `sendspin.path`: the path on the sendspin server, default "/sendspin" + * - `sendspin.client-id`: the client id, default "pipewire-$(hostname)" + * - `sendspin.client-name`: the client name, default "$(hostname)" + * - `sendspin.autoconnect`: Use zeroconf to connect to an available server, default false. + * - `sendspin.announce`: Use zeroconf to announce the client, default true unless + * sendspin.autoconnect or sendspin.ip is given. + * - `sendspin.single-server`: Allow only a single server to connect, default true + * - `node.always-process = `: true to receive even when not running + * - `stream.props = {}`: properties to be passed to all the stream + * + * ## General options + * + * 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 + * - \ref PW_KEY_NODE_NAME + * - \ref PW_KEY_NODE_DESCRIPTION + * - \ref PW_KEY_NODE_GROUP + * - \ref PW_KEY_NODE_LATENCY + * - \ref PW_KEY_NODE_VIRTUAL + * + * ## Example configuration + *\code{.unparsed} + * # ~/.config/pipewire/pipewire.conf.d/my-sendspin-recv.conf + * + * context.modules = [ + * { name = libpipewire-module-sendspin-recv + * args = { + * #local.ifname = eth0 + * #source.ip = 127.0.0.1 + * #source.port = 8928 + * #source.path = "/sendspin" + * #sendspin.ip = 127.0.0.1 + * #sendspin.port = 8927 + * #sendspin.path = "/sendspin" + * #sendspin.client-id = "pipewire-test" + * #sendspin.client-name = "PipeWire Test" + * #sendspin.autoconnect = false + * #sendspin.announce = true + * #sendspin.single-server = true + * #node.always-process = false + * #audio.position = [ FL FR ] + * stream.props = { + * #media.class = "Audio/Source" + * #node.name = "sendspin-receiver" + * } + * } + * } + * ] + *\endcode + * + * \since 1.6.0 + */ + +#define NAME "sendspin-recv" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define DEFAULT_SOURCE_IP "127.0.0.1" +#define DEFAULT_SOURCE_PORT PW_SENDSPIN_DEFAULT_CLIENT_PORT +#define DEFAULT_SOURCE_PATH PW_SENDSPIN_DEFAULT_PATH + +#define DEFAULT_SERVER_PORT PW_SENDSPIN_DEFAULT_SERVER_PORT +#define DEFAULT_SENDSPIN_PATH PW_SENDSPIN_DEFAULT_PATH + +#define DEFAULT_CREATE_RULES \ + "[ { matches = [ { sendspin.ip = \"~.*\" } ] actions = { create-stream = { } } } ] " + +#define DEFAULT_POSITION "[ FL FR ]" + +#define USAGE "( local.ifname= ) " \ + "( source.ip= ) " \ + "( source.port= " \ + "( audio.position= ) " \ + "( stream.props= { key=value ... } ) " + +static const struct spa_dict_item module_info[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { PW_KEY_MODULE_DESCRIPTION, "sendspin Receiver" }, + { PW_KEY_MODULE_USAGE, USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +struct client { + struct impl *impl; + struct spa_list link; + + char *name; + struct pw_properties *props; + struct pw_websocket_connection *conn; + struct spa_hook conn_listener; + + struct spa_audio_info info; + struct pw_stream *stream; + struct spa_hook stream_listener; + + struct pw_timer timer; + int timeout_count; + + uint32_t stride; + struct spa_ringbuffer ring; + void *buffer; + uint32_t buffer_size; + +#define ROLE_PLAYER (1<<0) +#define ROLE_METADATA (1<<1) + uint32_t active_roles; +#define REASON_DISCOVERY (0) +#define REASON_PLAYBACK (1) + uint32_t connection_reason; + + struct spa_regress regress_index; + struct spa_regress regress_time; + + bool resync; + struct spa_dll dll; +}; + +struct impl { + struct pw_impl_module *module; + struct spa_hook module_listener; + struct pw_properties *props; + struct pw_context *context; + + 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; + +#ifdef HAVE_AVAHI + struct pw_zeroconf *zeroconf; + struct spa_hook zeroconf_listener; +#endif + + bool always_process; + bool single_server; + + struct pw_properties *stream_props; + + struct pw_websocket *websocket; + struct spa_hook websocket_listener; + + struct spa_list clients; +}; + +static void on_stream_destroy(void *d) +{ + struct client *client = d; + spa_hook_remove(&client->stream_listener); + client->stream = NULL; +} + +static void on_stream_state_changed(void *d, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct client *client = d; + switch (state) { + case PW_STREAM_STATE_ERROR: + case PW_STREAM_STATE_UNCONNECTED: + pw_impl_module_schedule_destroy(client->impl->module); + break; + case PW_STREAM_STATE_PAUSED: + case PW_STREAM_STATE_STREAMING: + break; + default: + break; + } +} + +static void on_capture_stream_process(void *d) +{ + struct client *client = d; + struct pw_buffer *b; + struct spa_buffer *buf; + uint8_t *p; + uint32_t index = 0, n_frames, n_bytes; + int32_t avail, stride; + struct pw_time ts; + double err, corr, target, current_time; + + if ((b = pw_stream_dequeue_buffer(client->stream)) == NULL) { + pw_log_debug("out of buffers: %m"); + return; + } + + buf = b->buffer; + if ((p = buf->datas[0].data) == NULL) + return; + + stride = client->stride; + n_frames = buf->datas[0].maxsize / stride; + if (b->requested) + n_frames = SPA_MIN(b->requested, n_frames); + n_bytes = n_frames * stride; + + avail = spa_ringbuffer_get_read_index(&client->ring, &index); + + if (client->timeout_count > 4) { + pw_stream_get_time_n(client->stream, &ts, sizeof(ts)); + + /* index to server time */ + target = spa_regress_calc_y(&client->regress_index, index); + /* server time to client time */ + target = spa_regress_calc_y(&client->regress_time, target); + + current_time = ts.now / 1000.0; + current_time -= (ts.buffered * 1000000.0 / client->info.info.raw.rate) + + ((ts.delay) * 1000000.0 * ts.rate.num / ts.rate.denom); + err = target - (double)current_time; + + if (client->resync) { + if (target < current_time) { + target = spa_regress_calc_x(&client->regress_time, current_time); + index = (uint32_t)spa_regress_calc_x(&client->regress_index, target); + index = SPA_ROUND_DOWN(index, stride); + + pw_log_info("resync %u %f %f %f", index, target, + current_time, target - current_time); + + spa_ringbuffer_read_update(&client->ring, index); + avail = spa_ringbuffer_get_read_index(&client->ring, &index); + + err = 0.0; + client->resync = false; + } else { + avail = 0; + } + } + + corr = spa_dll_update(&client->dll, SPA_CLAMPD(err, -1000, 1000)); + + pw_stream_set_rate(client->stream, 1.0 / corr); + + pw_log_trace("%u %f %f %f %f", index, current_time, target, err, corr); + } else { + avail = 0; + } + if (avail < (int32_t)n_bytes) { + avail = 0; + client->resync = true; + } + else if (avail > (int32_t)client->buffer_size) { + index += avail - client->buffer_size; + avail = client->buffer_size; + client->resync = true; + } + if (avail > 0) { + n_bytes = SPA_MIN(n_bytes, (uint32_t)avail); + + spa_ringbuffer_read_data(&client->ring, + client->buffer, client->buffer_size, + index % client->buffer_size, + p, n_bytes); + spa_ringbuffer_read_update(&client->ring, index + n_bytes); + } else { + memset(p, 0, n_bytes); + } + + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->stride = stride; + buf->datas[0].chunk->size = n_bytes; + + pw_stream_queue_buffer(client->stream, b); +} + +static const struct pw_stream_events capture_stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = on_stream_destroy, + .state_changed = on_stream_state_changed, + .process = on_capture_stream_process +}; + +static int create_stream(struct client *client) +{ + struct impl *impl = client->impl; + int res; + uint32_t n_params; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + const char *server_id, *ip, *port, *server_name; + struct pw_properties *props = pw_properties_copy(client->props); + + ip = pw_properties_get(props, "sendspin.ip"); + port = pw_properties_get(props, "sendspin.port"); + server_id = pw_properties_get(props, "sendspin.server-id"); + server_name = pw_properties_get(props, "sendspin.server-name"); + + if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL) + pw_properties_setf(props, PW_KEY_NODE_NAME, "sendspin.%s.%s.%s", server_id, ip, port); + if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) + pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "Sendspin from %s", server_name); + if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL) + pw_properties_setf(props, PW_KEY_MEDIA_NAME, "Sendspin from %s", server_name); + + client->stream = pw_stream_new(impl->core, "sendspin receiver", props); + if (client->stream == NULL) + return -errno; + + spa_ringbuffer_init(&client->ring); + client->buffer_size = 1024 * 1024; + client->buffer = calloc(1, client->buffer_size * client->stride); + + pw_stream_add_listener(client->stream, + &client->stream_listener, + &capture_stream_events, client); + + n_params = 0; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[n_params++] = spa_format_audio_build(&b, + SPA_PARAM_EnumFormat, &client->info); + + if ((res = pw_stream_connect(client->stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, n_params)) < 0) + return res; + + return 0; +} + +static void add_format(struct spa_json_builder *b, const char *codec, int channels, int rate, int depth) +{ + spa_json_builder_array_push(b, "{"); + spa_json_builder_object_string(b, "codec", codec); + spa_json_builder_object_int(b, "channels", channels); + spa_json_builder_object_int(b, "sample_rate", rate); + spa_json_builder_object_int(b, "bit_depth", depth); + spa_json_builder_pop(b, "}"); +} +static void add_playerv1_support(struct client *client, struct spa_json_builder *b) +{ + spa_json_builder_object_push(b, "player@v1_support", "{"); + spa_json_builder_object_push(b, "supported_formats", "["); + add_format(b, "pcm", 2, 48000, 16); + add_format(b, "pcm", 1, 48000, 16); + spa_json_builder_pop(b, "]"); + spa_json_builder_object_int(b, "buffer_capacity", 32000000); + spa_json_builder_object_push(b, "supported_commands", "["); + spa_json_builder_array_string(b, "volume"); + spa_json_builder_array_string(b, "mute"); + spa_json_builder_pop(b, "]"); + spa_json_builder_pop(b, "}"); +} +static int send_client_hello(struct client *client) +{ + struct impl *impl = client->impl; + struct spa_json_builder b; + int res; + char *mem; + size_t size; + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "client/hello"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_string(&b, "client_id", pw_properties_get(impl->props, "sendspin.client-id")); + spa_json_builder_object_string(&b, "name", pw_properties_get(impl->props, "sendspin.client-name")); + spa_json_builder_object_int(&b, "version", 1); + spa_json_builder_object_push(&b, "supported_roles", "["); + spa_json_builder_array_string(&b, "player@v1"); + spa_json_builder_array_string(&b, "metadata@v1"); + spa_json_builder_pop(&b, "]"); + spa_json_builder_object_push(&b, "device_info", "{"); + spa_json_builder_object_string(&b, "product_name", "Linux"); /* Use os-release */ + spa_json_builder_object_stringf(&b, "software_version", "PipeWire %s", pw_get_library_version()); + spa_json_builder_pop(&b, "}"); + add_playerv1_support(client, &b); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(client->conn, mem, size); + free(mem); + + return res; +} + +static int send_client_state(struct client *client) +{ + struct spa_json_builder b; + int res; + char *mem; + size_t size; + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "client/state"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_push(&b, "player", "{"); + spa_json_builder_object_string(&b, "state", "synchronized"); + spa_json_builder_object_int(&b, "volume", 100); + spa_json_builder_object_bool(&b, "muted", false); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(client->conn, mem, size); + free(mem); + return res; +} + +static uint64_t get_time_us(struct client *client) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return 0; + return SPA_TIMESPEC_TO_USEC(&now); +} + +static int send_client_time(struct client *client) +{ + struct spa_json_builder b; + int res; + uint64_t now; + char *mem; + size_t size; + + now = get_time_us(client); + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "client/time"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_uint(&b, "client_transmitted", now); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(client->conn, mem, size); + free(mem); + return res; +} + +static void do_client_timer(void *data) +{ + struct client *client = data; + send_client_time(client); +} + +#if 0 +static int send_client_command(struct client *client) +{ + return 0; +} +#endif +static int send_client_goodbye(struct client *client, const char *reason) +{ + struct spa_json_builder b; + int res; + char *mem; + size_t size; + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "client/goodbye"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_string(&b, "reason", reason); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(client->conn, mem, size); + pw_websocket_connection_disconnect(client->conn, true); + free(mem); + return res; +} + +#if 0 +static int send_stream_request_format(struct client *client) +{ + return 0; +} +#endif + +static int handle_server_hello(struct client *client, struct spa_json *payload) +{ + struct impl *impl = client->impl; + struct spa_json it[1]; + char key[256], *t; + const char *v; + int l, version = 0; + struct client *c, *ct; + + while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "server_id")) { + t = alloca(l+1); + spa_json_parse_stringn(v, l, t, l+1); + pw_properties_set(client->props, "sendspin.server-id", t); + } + else if (spa_streq(key, "name")) { + t = alloca(l+1); + spa_json_parse_stringn(v, l, t, l+1); + pw_properties_set(client->props, "sendspin.server-name", t); + } + else if (spa_streq(key, "version")) { + if (spa_json_parse_int(v, l, &version) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "active_roles")) { + if (!spa_json_is_array(v, l)) + return -EPROTO; + + spa_json_enter(payload, &it[0]); + while ((l = spa_json_next(&it[0], &v)) > 0) { + t = alloca(l+1); + spa_json_parse_stringn(v, l, t, l+1); + + if (spa_streq(t, "player@v1")) + client->active_roles |= ROLE_PLAYER; + else if (spa_streq(t, "metadata@v1")) + client->active_roles |= ROLE_METADATA; + } + } + else if (spa_streq(key, "connection_reason")) { + t = alloca(l+1); + spa_json_parse_stringn(v, l, t, l+1); + + if (spa_streq(t, "discovery")) + client->connection_reason = REASON_DISCOVERY; + else if (spa_streq(t, "playback")) + client->connection_reason = REASON_PLAYBACK; + + pw_properties_set(client->props, "sendspin.connection-reason", t); + } + } + if (version != 1) + return -ENOTSUP; + + if (impl->single_server) { + if (client->connection_reason == REASON_PLAYBACK) { + /* keep this server, destroy others */ + spa_list_for_each_safe(c, ct, &impl->clients, link) { + if (c == client) + continue; + send_client_goodbye(c, "another_server"); + } + } else { + /* keep other servers, destroy this one */ + spa_list_for_each_safe(c, ct, &impl->clients, link) { + if (c == client) + continue; + return send_client_goodbye(client, "another_server"); + } + } + } + return send_client_state(client); +} + +static int handle_server_state(struct client *client, struct spa_json *payload) +{ + return 0; +} + +static int parse_uint64(const char *val, int len, uint64_t *result) +{ + char buf[64]; + char *end; + + if (len <= 0 || len >= (int)sizeof(buf)) + return 0; + + memcpy(buf, val, len); + buf[len] = '\0'; + + *result = strtoull(buf, &end, 0); + return len > 0 && end == buf + len; +} + +static int handle_server_time(struct client *client, struct spa_json *payload) +{ + struct impl *impl = client->impl; + char key[256]; + const char *v; + int l; + uint64_t t1 = 0, t2 = 0, t3 = 0, t4 = 0, timeout; + + t4 = get_time_us(client); + + while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "client_transmitted")) { + if (parse_uint64(v, l, &t1) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "server_received")) { + if (parse_uint64(v, l, &t2) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "server_transmitted")) { + if (parse_uint64(v, l, &t3) <= 0) + return -EINVAL; + } + } + + spa_regress_update(&client->regress_time, (t2+t3)/2, (t1+t4)/2); + + if (client->timeout_count < 4) + timeout = 200 * SPA_MSEC_PER_SEC; + else if (client->timeout_count < 10) + timeout = SPA_NSEC_PER_SEC; + else if (client->timeout_count < 20) + timeout = 2 * SPA_NSEC_PER_SEC; + else + timeout = 5 * SPA_NSEC_PER_SEC; + + client->timeout_count++; + pw_timer_queue_add(impl->timer_queue, &client->timer, + &client->timer.timeout, timeout, + do_client_timer, client); + return 0; +} + +static int handle_server_command(struct client *client, struct spa_json *payload) +{ + return 0; +} + +/* {"codec":"pcm","sample_rate":44100,"channels":2,"bit_depth":16} */ +static int parse_player(struct client *client, struct spa_json *player) +{ + char key[256], codec[64] = ""; + const char *v; + int l, sample_rate = 0, channels = 0, bit_depth = 0; + + spa_zero(client->info); + client->info.media_type = SPA_MEDIA_TYPE_audio; + while ((l = spa_json_object_next(player, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "codec")) { + if (spa_json_parse_stringn(v, l, codec, sizeof(codec)) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "sample_rate")) { + if (spa_json_parse_int(v, l, &sample_rate) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "channels")) { + if (spa_json_parse_int(v, l, &channels) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "bit_depth")) { + if (spa_json_parse_int(v, l, &bit_depth) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "codec_header")) { + } + } + if (sample_rate == 0 || channels == 0) + return -EINVAL; + + if (spa_streq(codec, "pcm")) { + client->info.media_subtype = SPA_MEDIA_SUBTYPE_raw; + client->info.info.raw.rate = sample_rate; + client->info.info.raw.channels = channels; + switch (bit_depth) { + case 16: + client->info.info.raw.format = SPA_AUDIO_FORMAT_S16_LE; + client->stride = 2 * channels; + break; + case 24: + client->info.info.raw.format = SPA_AUDIO_FORMAT_S24_LE; + client->stride = 3 * channels; + break; + default: + return -EINVAL; + } + } + else if (spa_streq(codec, "opus")) { + client->info.media_subtype = SPA_MEDIA_SUBTYPE_opus; + client->info.info.opus.rate = sample_rate; + client->info.info.opus.channels = channels; + } + else if (spa_streq(codec, "flac")) { + client->info.media_subtype = SPA_MEDIA_SUBTYPE_flac; + client->info.info.flac.rate = sample_rate; + client->info.info.flac.channels = channels; + } + else + return -EINVAL; + + spa_dll_set_bw(&client->dll, SPA_DLL_BW_MIN, 1000, sample_rate); + + return 0; +} + +/* {"player":{}} */ +static int handle_stream_start(struct client *client, struct spa_json *payload) +{ + struct impl *impl = client->impl; + struct spa_json it[1]; + char key[256]; + const char *v; + int l; + + while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "player")) { + if (!spa_json_is_object(v, l)) + return -EPROTO; + spa_json_enter(payload, &it[0]); + parse_player(client, &it[0]); + } + } + + if (client->stream == NULL) { + create_stream(client); + + pw_timer_queue_cancel(&client->timer); + pw_timer_queue_add(impl->timer_queue, &client->timer, + NULL, 0, do_client_timer, client); + } else { + } + + return 0; +} + +static void stream_clear(struct client *client) +{ + spa_ringbuffer_init(&client->ring); + memset(client->buffer, 0, client->buffer_size); +} + +static int handle_stream_clear(struct client *client, struct spa_json *payload) +{ + stream_clear(client); + return 0; +} +static int handle_stream_end(struct client *client, struct spa_json *payload) +{ + if (client->stream != NULL) { + pw_stream_destroy(client->stream); + client->stream = NULL; + stream_clear(client); + } + return 0; +} + +static int handle_group_update(struct client *client, struct spa_json *payload) +{ + return 0; +} + +/* { "type":... "payload":{...} } */ +static int do_parse_text(struct client *client, const char *content, int size) +{ + struct spa_json it[2], *payload = NULL; + char key[256], type[256] = ""; + const char *v; + int res, l; + + pw_log_info("received text %.*s", size, content); + + if (spa_json_begin_object(&it[0], content, size) <= 0) + return -EINVAL; + + while ((l = spa_json_object_next(&it[0], key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "payload")) { + if (!spa_json_is_object(v, l)) + return -EPROTO; + + spa_json_enter(&it[0], &it[1]); + payload = &it[1]; + } + else if (spa_streq(key, "type")) { + if (spa_json_parse_stringn(v, l, type, sizeof(type)) <= 0) + continue; + } + } + if (spa_streq(type, "server/hello")) + res = handle_server_hello(client, payload); + else if (spa_streq(type, "server/state")) + res = handle_server_state(client, payload); + else if (spa_streq(type, "server/time")) + res = handle_server_time(client, payload); + else if (spa_streq(type, "server/command")) + res = handle_server_command(client, payload); + else if (spa_streq(type, "stream/start")) + res = handle_stream_start(client, payload); + else if (spa_streq(type, "stream/end")) + res = handle_stream_end(client, payload); + else if (spa_streq(type, "stream/clear")) + res = handle_stream_clear(client, payload); + else if (spa_streq(type, "group/update")) + res = handle_group_update(client, payload); + else + res = 0; + + return res; +} + +static int do_handle_binary(struct client *client, const uint8_t *payload, int size) +{ + struct impl *impl = client->impl; + int32_t filled; + uint32_t index, length = size - 9; + uint64_t timestamp; + + if (payload[0] != 4 || client->stream == NULL) + return 0; + + timestamp = ((uint64_t)payload[1]) << 56; + timestamp |= ((uint64_t)payload[2]) << 48; + timestamp |= ((uint64_t)payload[3]) << 40; + timestamp |= ((uint64_t)payload[4]) << 32; + timestamp |= ((uint64_t)payload[5]) << 24; + timestamp |= ((uint64_t)payload[6]) << 16; + timestamp |= ((uint64_t)payload[7]) << 8; + timestamp |= ((uint64_t)payload[8]); + + filled = spa_ringbuffer_get_write_index(&client->ring, &index); + if (filled < 0) { + pw_log_warn("%p: underrun write:%u filled:%d", + client, index, filled); + } else if (filled + length > client->buffer_size) { + pw_log_debug("%p: overrun write:%u filled:%d", + client, index, filled); + } + + spa_ringbuffer_write_data(&client->ring, + client->buffer, client->buffer_size, + index % client->buffer_size, + &payload[9], length); + + spa_ringbuffer_write_update(&client->ring, index + length); + + pw_loop_lock(impl->data_loop); + spa_regress_update(&client->regress_index, index, timestamp); + pw_loop_unlock(impl->data_loop); + + return 0; +} + +static void on_connection_message(void *data, int opcode, void *payload, size_t size) +{ + struct client *client = data; + if (opcode == PW_WEBSOCKET_OPCODE_TEXT) { + do_parse_text(client, payload, size); + } else if (opcode == PW_WEBSOCKET_OPCODE_BINARY) { + do_handle_binary(client, payload, size); + } else { + pw_log_warn("%02x unknown %08x", opcode, (int)size); + } +} + +static void client_free(struct client *client) +{ + struct impl *impl = client->impl; + + spa_list_remove(&client->link); + + handle_stream_end(client, NULL); + if (client->conn) { + spa_hook_remove(&client->conn_listener); + pw_websocket_connection_destroy(client->conn); + } else { + pw_websocket_cancel(impl->websocket, client); + } + pw_timer_queue_cancel(&client->timer); + + pw_properties_free(client->props); + free(client->buffer); + free(client->name); + free(client); +} + +static void on_connection_destroy(void *data) +{ + struct client *client = data; + client->conn = NULL; + pw_log_info("connection %p destroy", client); +} +static void on_connection_error(void *data, int res, const char *reason) +{ + struct client *client = data; + pw_log_error("connection %p error %d %s", client, res, reason); +} + +static void on_connection_disconnected(void *data) +{ + struct client *client = data; + client_free(client); +} + +static const struct pw_websocket_connection_events websocket_connection_events = { + PW_VERSION_WEBSOCKET_CONNECTION_EVENTS, + .destroy = on_connection_destroy, + .error = on_connection_error, + .disconnected = on_connection_disconnected, + .message = on_connection_message, +}; + +static struct client *client_new(struct impl *impl, const char *name, struct pw_properties *props) +{ + struct client *client; + + client = calloc(1, sizeof(*client)); + if (client == NULL) + goto error; + + client->impl = impl; + spa_list_append(&impl->clients, &client->link); + + client->name = name ? strdup(name) : NULL; + client->props = props; + spa_regress_init(&client->regress_index, 5); + spa_regress_init(&client->regress_time, 5); + + spa_dll_init(&client->dll); + client->resync = true; + + return client; +error: + pw_properties_free(props); + return NULL; +} + +static int client_connect(struct client *c) +{ + struct impl *impl = c->impl; + const char *addr, *port, *path; + addr = pw_properties_get(c->props, "sendspin.ip"); + port = pw_properties_get(c->props, "sendspin.port"); + path = pw_properties_get(c->props, "sendspin.path"); + return pw_websocket_connect(impl->websocket, c, addr, port, path); +} + +static void client_connected(struct client *c, struct pw_websocket_connection *conn) +{ + if (c->conn) { + spa_hook_remove(&c->conn_listener); + pw_websocket_connection_destroy(c->conn); + } + c->conn = conn; + if (conn) + pw_websocket_connection_add_listener(c->conn, &c->conn_listener, + &websocket_connection_events, c); +} + +struct match_info { + struct impl *impl; + const char *name; + struct pw_properties *props; + struct pw_websocket_connection *conn; + bool matched; +}; + +static int rule_matched(void *data, const char *location, const char *action, + const char *str, size_t len) +{ + struct match_info *i = data; + struct impl *impl = i->impl; + int res = 0; + + i->matched = true; + if (spa_streq(action, "create-stream")) { + struct client *c; + + pw_properties_update_string(i->props, str, len); + if ((c = client_new(impl, i->name, spa_steal_ptr(i->props))) == NULL) + return -errno; + if (i->conn) + client_connected(c, i->conn); + else + client_connect(c); + } + return res; +} + +static inline int match_client(struct impl *impl, const char *name, struct pw_properties *props, + struct pw_websocket_connection *conn) +{ + const char *str; + struct match_info minfo = { + .impl = impl, + .name = name, + .props = props, + .conn = conn, + }; + + if ((str = pw_properties_get(impl->props, "stream.rules")) == NULL) + str = DEFAULT_CREATE_RULES; + + pw_conf_match_rules(str, strlen(str), NAME, &props->dict, + rule_matched, &minfo); + + if (!minfo.matched) { + pw_log_info("unmatched client found %s", str); + if (conn) + pw_websocket_connection_destroy(conn); + pw_properties_free(props); + } + return minfo.matched; +} + +static void on_websocket_connected(void *data, void *user, + struct pw_websocket_connection *conn, const char *path) +{ + struct impl *impl = data; + struct client *c = user; + + pw_log_info("connected to %s", path); + if (c == NULL) { + struct sockaddr_storage addr; + char ip[128]; + uint16_t port = 0; + bool ipv4; + struct pw_properties *props; + + pw_websocket_connection_address(conn, + (struct sockaddr*)&addr, sizeof(addr)); + + props = pw_properties_copy(impl->stream_props); + if (pw_net_get_ip(&addr, ip, sizeof(ip), &ipv4, &port) >= 0) { + pw_properties_set(props, "sendspin.ip", ip); + pw_properties_setf(props, "sendspin.port", "%u", port); + } + pw_properties_set(props, "sendspin.path", path); + + if ((c = client_new(impl, "", props)) == NULL) { + pw_log_error("can't create new client: %m"); + return; + } + } + client_connected(c, conn); + send_client_hello(c); +} + +static const struct pw_websocket_events websocket_events = { + PW_VERSION_WEBSOCKET_EVENTS, + .connected = on_websocket_connected, +}; + +#ifdef HAVE_AVAHI +static struct client *client_find(struct impl *impl, const char *name) +{ + struct client *c; + spa_list_for_each(c, &impl->clients, link) { + if (spa_streq(c->name, name)) + return c; + } + return NULL; +} + +static void on_zeroconf_added(void *data, const void *user, const struct spa_dict *info) +{ + struct impl *impl = data; + const char *name, *addr, *port, *path; + struct client *c; + struct pw_properties *props; + + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); + + if (impl->single_server && !spa_list_is_empty(&impl->clients)) + return; + + if ((c = client_find(impl, name)) != NULL) + return; + + props = pw_properties_copy(impl->stream_props); + pw_properties_update(props, info); + + addr = spa_dict_lookup(info, PW_KEY_ZEROCONF_ADDRESS); + port = spa_dict_lookup(info, PW_KEY_ZEROCONF_PORT); + path = spa_dict_lookup(info, "path"); + + pw_properties_set(props, "sendspin.ip", addr); + pw_properties_set(props, "sendspin.port", port); + pw_properties_set(props, "sendspin.path", path); + + match_client(impl, name, props, NULL); +} + +static void on_zeroconf_removed(void *data, const void *user, const struct spa_dict *info) +{ + struct impl *impl = data; + const char *name; + struct client *c; + + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); + + if ((c = client_find(impl, name)) == NULL) + return; + + client_free(c); +} + +static const struct pw_zeroconf_events zeroconf_events = { + PW_VERSION_ZEROCONF_EVENTS, + .added = on_zeroconf_added, + .removed = on_zeroconf_removed, +}; +#endif + +static void core_destroy(void *d) +{ + struct impl *impl = d; + spa_hook_remove(&impl->core_listener); + impl->core = NULL; + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_proxy_events core_proxy_events = { + .destroy = core_destroy, +}; + +static void impl_destroy(struct impl *impl) +{ + struct client *c; + + spa_list_consume(c, &impl->clients, link) + client_free(c); + + if (impl->core && impl->do_disconnect) + pw_core_disconnect(impl->core); + + if (impl->data_loop) + pw_context_release_loop(impl->context, impl->data_loop); + + pw_properties_free(impl->stream_props); + pw_properties_free(impl->props); + + free(impl); +} + +static void module_destroy(void *d) +{ + struct impl *impl = d; + spa_hook_remove(&impl->module_listener); + impl_destroy(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message) +{ + struct impl *impl = d; + + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = on_core_error, +}; + +static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) +{ + const char *str; + if ((str = pw_properties_get(props, key)) != NULL) { + if (pw_properties_get(impl->stream_props, key) == NULL) + pw_properties_set(impl->stream_props, key, str); + } +} + +SPA_EXPORT +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, *hostname, *port, *path; + struct pw_properties *props, *stream_props; + int res = 0; + bool autoconnect; + + PW_LOG_TOPIC_INIT(mod_topic); + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + return -errno; + + if (args == NULL) + args = ""; + + props = impl->props = pw_properties_new_string(args); + stream_props = impl->stream_props = pw_properties_new(NULL, NULL); + if (props == NULL || stream_props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto out; + } + + impl->module = module; + impl->context = context; + impl->main_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); + spa_list_init(&impl->clients); + + pw_properties_set(props, PW_KEY_NODE_LOOP_NAME, impl->data_loop->name); + + if ((str = pw_properties_get(props, "stream.props")) != NULL) + 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); + copy_props(impl, props, PW_KEY_NODE_GROUP); + copy_props(impl, props, PW_KEY_NODE_LATENCY); + copy_props(impl, props, PW_KEY_NODE_VIRTUAL); + copy_props(impl, props, PW_KEY_NODE_CHANNELNAMES); + copy_props(impl, props, PW_KEY_MEDIA_NAME); + copy_props(impl, props, PW_KEY_MEDIA_CLASS); + + impl->always_process = pw_properties_get_bool(stream_props, + PW_KEY_NODE_ALWAYS_PROCESS, true); + + autoconnect = pw_properties_get_bool(props, "sendspin.autoconnect", false); + impl->single_server = pw_properties_get_bool(props, + "sendspin.single-server", true); + + if ((str = pw_properties_get(props, "sendspin.client-name")) == NULL) + pw_properties_set(props, "sendspin.client-name", pw_get_host_name()); + if ((str = pw_properties_get(props, "sendspin.client-id")) == NULL) + pw_properties_setf(props, "sendspin.client-id", "pipewire-%s", pw_get_host_name()); + + 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); + impl->core = pw_context_connect(impl->context, + pw_properties_new( + PW_KEY_REMOTE_NAME, str, + NULL), + 0); + impl->do_disconnect = true; + } + if (impl->core == NULL) { + res = -errno; + pw_log_error("can't connect: %m"); + goto out; + } + + pw_proxy_add_listener((struct pw_proxy*)impl->core, + &impl->core_proxy_listener, + &core_proxy_events, impl); + pw_core_add_listener(impl->core, + &impl->core_listener, + &core_events, impl); + + impl->websocket = pw_websocket_new(impl->main_loop, &props->dict); + pw_websocket_add_listener(impl->websocket, &impl->websocket_listener, + &websocket_events, impl); + +#ifdef HAVE_AVAHI + if ((impl->zeroconf = pw_zeroconf_new(context, NULL)) != NULL) { + pw_zeroconf_add_listener(impl->zeroconf, &impl->zeroconf_listener, + &zeroconf_events, impl); + } +#endif + + hostname = pw_properties_get(props, "sendspin.ip"); + /* a client should either connect itself or advertize itself and listen + * for connections, not both */ + if (!autoconnect && hostname == NULL){ + /* listen for server connection */ + if ((hostname = pw_properties_get(props, "source.ip")) == NULL) + hostname = DEFAULT_SOURCE_IP; + if ((port = pw_properties_get(props, "source.port")) == NULL) + port = SPA_STRINGIFY(DEFAULT_SOURCE_PORT); + if ((path = pw_properties_get(props, "source.path")) == NULL) + path = DEFAULT_SOURCE_PATH; + + pw_websocket_listen(impl->websocket, NULL, hostname, port, path); + +#ifdef HAVE_AVAHI + bool announce = pw_properties_get_bool(props, "sendspin.announce", true); + if (impl->zeroconf && announce) { + /* optionally announce ourselves */ + str = pw_properties_get(props, "sendspin.client-id"); + pw_zeroconf_set_announce(impl->zeroconf, NULL, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, PW_SENDSPIN_CLIENT_SERVICE_TYPE), + SPA_DICT_ITEM(PW_KEY_ZEROCONF_NAME, str), + SPA_DICT_ITEM(PW_KEY_ZEROCONF_PORT, port), + SPA_DICT_ITEM("path", path))); + } +#endif + } + else { + if (hostname != NULL) { + struct client *c; + struct pw_properties *p; + + /* connect to hardcoded server */ + port = pw_properties_get(props, "sendspin.port"); + if (port == NULL) + port = SPA_STRINGIFY(DEFAULT_SERVER_PORT); + if ((path = pw_properties_get(props, "sendspin.path")) == NULL) + path = DEFAULT_SENDSPIN_PATH; + + p = pw_properties_copy(impl->stream_props); + pw_properties_set(p, "sendspin.ip", hostname); + pw_properties_set(p, "sendspin.port", port); + pw_properties_set(p, "sendspin.path", path); + + if ((c = client_new(impl, "", p)) != NULL) + client_connect(c); + } + /* connect to zeroconf server if we can */ +#ifdef HAVE_AVAHI + if (impl->zeroconf) { + pw_zeroconf_set_browse(impl->zeroconf, NULL, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, PW_SENDSPIN_SERVER_SERVICE_TYPE))); + } +#endif + } + + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info)); + + pw_log_info("Successfully loaded module-sendspin-recv"); + + return 0; +out: + impl_destroy(impl); + return res; +} diff --git a/src/modules/module-sendspin-send.c b/src/modules/module-sendspin-send.c new file mode 100644 index 000000000..2c9f6df22 --- /dev/null +++ b/src/modules/module-sendspin-send.c @@ -0,0 +1,1451 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 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 +#include +#include +#include +#include +#include + +#include +#include + +#include "module-sendspin/sendspin.h" +#include "module-sendspin/websocket.h" +#include "network-utils.h" + +#ifdef HAVE_AVAHI +#include "zeroconf-utils/zeroconf.h" +#endif + +/** \page page_module_sendspin_send sendspin sender + * + * The `sendspin-send` module creates a PipeWire sink that sends audio + * packets using the sendspin protocol to a client. + * + * The sender will listen on a specific port (8927) and create a stream for + * each connection. + * + * In combination with a virtual sink, each of the client streams can be sent + * the same data in the client specific format. + * + * ## Module Name + * + * `libpipewire-module-sendspin-send` + * + * ## Module Options + * + * Options specific to the behavior of this module + * + * - `local.ifname = `: interface name to use + * - `local.ifaddress = `: interface address to use + * - `source.ip = `: the source ip address to listen on, default "127.0.0.1" + * - `source.port = `: the source port to listen on, default 8927 + * - `source.path = `: comma separated list of paths to listen on, + * default "/sendspin" + * - `sendspin.ip`: an array of IP addresses of sendspin clients to connect to + * - `sendspin.port`: the port of the sendspin client to connect to, default 8928 + * - `sendspin.path`: the path of the sendspin client to connect to, default "/sendspin" + * - `sendspin.group-id`: the group-id of the server, default random + * - `sendspin.group-name`: the group-name of the server, default "PipeWire" + * - `sendspin.delay`: the delay to add to clients in seconds. Default 5.0 + * - `node.always-process = `: true to send silence even when not connected. + * - `stream.props = {}`: properties to be passed to all the stream + * - `stream.rules` = : match rules, use the create-stream action to + * make a stream for the client. + * + * ## General options + * + * 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 + * - \ref PW_KEY_NODE_NAME + * - \ref PW_KEY_NODE_DESCRIPTION + * - \ref PW_KEY_NODE_GROUP + * - \ref PW_KEY_NODE_LATENCY + * - \ref PW_KEY_NODE_VIRTUAL + * + * ## Example configuration + *\code{.unparsed} + * # ~/.config/pipewire/pipewire.conf.d/my-sendspin-send.conf + * + * context.modules = [ + * { name = libpipewire-module-sendspin-send + * args = { + * #local.ifname = eth0 + * #source.ip = 127.0.0.1 + * #source.port = 8927 + * #source.path = "/sendspin" + * #sendspin.ip = [ 127.0.0.1 ] + * #sendspin.port = 8928 + * #sendspin.path = "/sendspin" + * #sendspin.group-id = "abcded" + * #sendspin.group-name = "PipeWire" + * #sendspin.delay = 5.0 + * #node.always-process = false + * #audio.position = [ FL FR ] + * stream.props = { + * #media.class = "Audio/sink" + * #node.name = "sendspin-send" + * } + * stream.rules = [ + * { matches = [ + * { sendspin.ip = "~.*" + * #sendspin.port = 8928 + * #sendspin.path = "/sendspin" + * #zeroconf.ifindex = 0 + * #zeroconf.name = "" + * #zeroconf.type = "_sendspin._tcp" + * #zeroconf.domain = "local" + * #zeroconf.hostname = "" + * } + * ] + * actions = { + * create-stream = { + * stream.props = { + * #target.object = "" + * #media.class = "Audio/Sink" + * } + * } + * } + * } + * ] + * } + * } + * ] + *\endcode + * + * \since 1.6.0 + */ + +#define NAME "sendspin-send" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +#define DEFAULT_SOURCE_IP "127.0.0.1" +#define DEFAULT_SOURCE_PORT PW_SENDSPIN_DEFAULT_SERVER_PORT +#define DEFAULT_SOURCE_PATH PW_SENDSPIN_DEFAULT_PATH + +#define DEFAULT_CLIENT_PORT PW_SENDSPIN_DEFAULT_CLIENT_PORT +#define DEFAULT_SENDSPIN_PATH PW_SENDSPIN_DEFAULT_PATH + +#define DEFAULT_SENDSPIN_DELAY 5.0 + +#define DEFAULT_POSITION "[ FL FR ]" + +#define DEFAULT_CREATE_RULES \ + "[ { matches = [ { sendspin.ip = \"~.*\" } ] actions = { create-stream = { } } } ] " + +#define USAGE "( local.ifname= ) " \ + "( source.ip= ) " \ + "( source.port= " \ + "( audio.position= ) " \ + "( stream.props= { key=value ... } ) " + +static const struct spa_dict_item module_info[] = { + { PW_KEY_MODULE_AUTHOR, "Wim Taymans " }, + { PW_KEY_MODULE_DESCRIPTION, "Sendspin sender" }, + { PW_KEY_MODULE_USAGE, USAGE }, + { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, +}; + +struct client { + struct impl *impl; + struct spa_list link; + + char *name; + struct pw_properties *props; + + struct pw_websocket_connection *conn; + struct spa_hook conn_listener; + + struct spa_audio_info info; + struct pw_stream *stream; + struct spa_hook stream_listener; + + struct spa_io_position *io_position; + struct pw_timer timer; + + uint64_t delay_usec; + uint32_t stride; + + int buffer_capacity; +#define ROLE_PLAYER (1<<0) +#define ROLE_METADATA (1<<1) + uint32_t supported_roles; +#define COMMAND_VOLUME (1<<0) +#define COMMAND_MUTE (1<<1) + uint32_t supported_commands; +#define STATE_UNKNOWN 0 +#define STATE_SYNCHRONIZED 1 +#define STATE_ERROR 2 + uint32_t state; + + int volume; + bool mute; + + bool playing; +}; + +struct impl { + struct pw_impl_module *module; + struct spa_hook module_listener; + struct pw_properties *props; + struct pw_context *context; + + 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; + +#ifdef HAVE_AVAHI + struct pw_zeroconf *zeroconf; + struct spa_hook zeroconf_listener; +#endif + + float delay; + bool always_process; + + struct pw_properties *stream_props; + + struct pw_websocket *websocket; + struct spa_hook websocket_listener; + + struct spa_list clients; + +}; + +static int send_group_update(struct client *c, bool playing); +static int send_stream_start(struct client *c); +static int send_server_state(struct client *c); + +static void on_stream_destroy(void *d) +{ + struct client *c = d; + spa_hook_remove(&c->stream_listener); + c->stream = NULL; +} + +static void on_stream_state_changed(void *d, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + struct client *c = d; + switch (state) { + case PW_STREAM_STATE_ERROR: + case PW_STREAM_STATE_UNCONNECTED: + //pw_impl_module_schedule_destroy(c->impl->module); + break; + case PW_STREAM_STATE_PAUSED: + send_group_update(c, false); + break; + case PW_STREAM_STATE_STREAMING: + send_group_update(c, true); + break; + default: + break; + } +} + +static uint64_t get_time_us(struct client *c) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return 0; + return SPA_TIMESPEC_TO_USEC(&now); +} + +static void on_playback_stream_process(void *d) +{ + struct client *c = d; + struct pw_buffer *b; + struct spa_buffer *buf; + uint8_t *p; + struct iovec iov[2]; + uint8_t header[9]; + uint64_t timestamp; + + if ((b = pw_stream_dequeue_buffer(c->stream)) == NULL) { + pw_log_debug("out of buffers: %m"); + return; + } + + if (c->playing) { + buf = b->buffer; + if ((p = buf->datas[0].data) == NULL) + return; + + timestamp = c->io_position ? + c->io_position->clock.nsec / 1000 : + get_time_us(c); + timestamp += c->delay_usec; + + header[0] = 4; + header[1] = (timestamp >> 56) & 0xff; + header[2] = (timestamp >> 48) & 0xff; + header[3] = (timestamp >> 40) & 0xff; + header[4] = (timestamp >> 32) & 0xff; + header[5] = (timestamp >> 24) & 0xff; + header[6] = (timestamp >> 16) & 0xff; + header[7] = (timestamp >> 8) & 0xff; + header[8] = (timestamp ) & 0xff; + + iov[0].iov_base = header; + iov[0].iov_len = sizeof(header); + iov[1].iov_base = p; + iov[1].iov_len = buf->datas[0].chunk->size; + + pw_websocket_connection_send(c->conn, + PW_WEBSOCKET_OPCODE_BINARY, iov, 2); + } + pw_stream_queue_buffer(c->stream, b); +} + +static void +on_stream_param_changed(void *d, uint32_t id, const struct spa_pod *param) +{ + struct client *c = d; + + if (param == NULL) + return; + + switch (id) { + case SPA_PARAM_Format: + if (spa_format_audio_parse(param, &c->info) < 0) + return; + send_stream_start(c); + break; + case SPA_PARAM_Tag: + send_server_state(c); + break; + } +} + +static void on_stream_io_changed(void *d, uint32_t id, void *area, uint32_t size) +{ + struct client *c = d; + switch (id) { + case SPA_IO_Position: + c->io_position = area; + break; + } +} + +static const struct pw_stream_events playback_stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = on_stream_destroy, + .io_changed = on_stream_io_changed, + .state_changed = on_stream_state_changed, + .param_changed = on_stream_param_changed, + .process = on_playback_stream_process +}; + +static int create_stream(struct client *c) +{ + struct impl *impl = c->impl; + int res; + uint32_t n_params; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b; + const char *client_id, *ip, *port, *client_name; + struct pw_properties *props = pw_properties_copy(c->props); + + ip = pw_properties_get(props, "sendspin.ip"); + port = pw_properties_get(props, "sendspin.port"); + client_id = pw_properties_get(props, "sendspin.client-id"); + client_name = pw_properties_get(props, "sendspin.client-name"); + + if (pw_properties_get(props, PW_KEY_NODE_NAME) == NULL) + pw_properties_setf(props, PW_KEY_NODE_NAME, "sendspin.%s.%s.%s", client_id, ip, port); + if (pw_properties_get(props, PW_KEY_NODE_DESCRIPTION) == NULL) + pw_properties_setf(props, PW_KEY_NODE_DESCRIPTION, "Sendspin to %s", client_name); + if (pw_properties_get(props, PW_KEY_MEDIA_NAME) == NULL) + pw_properties_setf(props, PW_KEY_MEDIA_NAME, "Sendspin to %s", client_name); + + + c->stream = pw_stream_new(impl->core, "sendspin sender", props); + if (c->stream == NULL) + return -errno; + + pw_stream_add_listener(c->stream, + &c->stream_listener, + &playback_stream_events, c); + + n_params = 0; + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[n_params++] = spa_format_audio_build(&b, + SPA_PARAM_EnumFormat, &c->info); + + if ((res = pw_stream_connect(c->stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, n_params)) < 0) + return res; + + return 0; +} + +static int send_server_hello(struct client *c) +{ + struct impl *impl = c->impl; + struct spa_json_builder b; + int res; + size_t size; + char *mem; + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "server/hello"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_string(&b, "server_id", pw_properties_get(impl->props, "sendspin.server-id")); + spa_json_builder_object_string(&b, "name", pw_properties_get(impl->props, "sendspin.server-name")); + spa_json_builder_object_int(&b, "version", 1); + spa_json_builder_object_push(&b, "active_roles", "["); + if (c->supported_roles & ROLE_PLAYER) + spa_json_builder_array_string(&b, "player@v1"); + if (c->supported_roles & ROLE_METADATA) + spa_json_builder_array_string(&b, "metadata@v1"); + spa_json_builder_pop(&b, "]"); + spa_json_builder_object_string(&b, "connection_reason", "discovery"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(c->conn, mem, size); + free(mem); + + return res; +} + +static int send_server_state(struct client *c) +{ + struct spa_json_builder b; + int res; + size_t size; + char *mem; + + if (!SPA_FLAG_IS_SET(c->supported_roles, ROLE_METADATA)) + return 0; + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "server/state"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_push(&b, "metadata", "{"); + spa_json_builder_object_uint(&b, "timestamp", get_time_us(c)); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(c->conn, mem, size); + free(mem); + return res; +} + +static int send_server_time(struct client *c, uint64_t t1, uint64_t t2) +{ + struct spa_json_builder b; + int res; + uint64_t t3; + size_t size; + char *mem; + + t3 = get_time_us(c); + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "server/time"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_uint(&b, "client_transmitted", t1); + spa_json_builder_object_uint(&b, "server_received", t2); + spa_json_builder_object_uint(&b, "server_transmitted", t3); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(c->conn, mem, size); + free(mem); + return res; +} + +#if 0 +static int send_server_command(struct client *c, int command, int value) +{ + struct spa_json_builder b; + size_t size; + char *mem; + int res; + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "server/command"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_push(&b, "player", "{"); + if (command == COMMAND_VOLUME) { + spa_json_builder_object_string(&b, "command", "volume"); + spa_json_builder_object_int(&b, "volume", value); + } else { + spa_json_builder_object_string(&b, "command", "mute"); + spa_json_builder_object_int(&b, "mute", value ? true : false); + } + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(c->conn, mem, size); + free(mem); + return res; +} +#endif + +static int send_stream_start(struct client *c) +{ + struct spa_json_builder b; + int res, channels, rate, depth = 0; + const char *codec; + size_t size; + char *mem; + + switch (c->info.media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + codec = "pcm"; + channels = c->info.info.raw.channels; + rate = c->info.info.raw.rate; + switch (c->info.info.raw.format) { + case SPA_AUDIO_FORMAT_S16_LE: + depth = 16; + break; + case SPA_AUDIO_FORMAT_S24_LE: + depth = 24; + break; + default: + return -ENOTSUP; + } + break; + case SPA_MEDIA_SUBTYPE_opus: + codec = "opus"; + channels = c->info.info.opus.channels; + rate = c->info.info.opus.rate; + break; + case SPA_MEDIA_SUBTYPE_flac: + codec = "flac"; + channels = c->info.info.flac.channels; + rate = c->info.info.flac.rate; + break; + default: + return -ENOTSUP; + } + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "stream/start"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_push(&b, "player", "{"); + spa_json_builder_object_string(&b, "codec", codec); + spa_json_builder_object_int(&b, "channels", channels); + spa_json_builder_object_int(&b, "sample_rate", rate); + if (depth) + spa_json_builder_object_int(&b, "bit_depth", depth); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(c->conn, mem, size); + free(mem); + return res; +} + +#if 0 +static int send_stream_end(struct client *c) +{ + struct spa_json_builder b; + int res; + size_t size; + char *mem; + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "stream/end"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_push(&b, "roles", "["); + spa_json_builder_array_string(&b, "player"); + spa_json_builder_array_string(&b, "metadata"); + spa_json_builder_pop(&b, "]"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + res = pw_websocket_connection_send_text(c->conn, mem, size); + free(mem); + return res; +} +#endif + +static int send_group_update(struct client *c, bool playing) +{ + struct impl *impl = c->impl; + struct spa_json_builder b; + int res; + char *mem; + size_t size; + + spa_json_builder_memstream(&b, &mem, &size, 0); + spa_json_builder_array_push(&b, "{"); + spa_json_builder_object_string(&b, "type", "group/update"); + spa_json_builder_object_push(&b, "payload", "{"); + spa_json_builder_object_string(&b, "playback_state", playing ? "playing" : "stopped"); + spa_json_builder_object_string(&b, "group_id", pw_properties_get(impl->props, "sendspin.group-id")); + spa_json_builder_object_string(&b, "group_name", pw_properties_get(impl->props, "sendspin.group-name")); + spa_json_builder_pop(&b, "}"); + spa_json_builder_pop(&b, "}"); + spa_json_builder_close(&b); + + c->playing = playing; + + res = pw_websocket_connection_send_text(c->conn, mem, size); + free(mem); + return res; +} + +/* {"codec":"pcm","sample_rate":44100,"channels":2,"bit_depth":16} */ +static int parse_codec(struct client *c, struct spa_json *object, struct spa_audio_info *info) +{ + char key[256], codec[64] = ""; + const char *v; + int l, sample_rate = 0, channels = 0, bit_depth = 0; + + spa_zero(*info); + info->media_type = SPA_MEDIA_TYPE_audio; + while ((l = spa_json_object_next(object, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "codec")) { + if (spa_json_parse_stringn(v, l, codec, sizeof(codec)) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "sample_rate")) { + if (spa_json_parse_int(v, l, &sample_rate) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "channels")) { + if (spa_json_parse_int(v, l, &channels) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "bit_depth")) { + if (spa_json_parse_int(v, l, &bit_depth) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "codec_header")) { + } + } + if (sample_rate == 0 || channels == 0) + return -EINVAL; + + if (spa_streq(codec, "pcm")) { + info->media_subtype = SPA_MEDIA_SUBTYPE_raw; + info->info.raw.rate = sample_rate; + info->info.raw.channels = channels; + switch (bit_depth) { + case 16: + info->info.raw.format = SPA_AUDIO_FORMAT_S16_LE; + break; + case 24: + info->info.raw.format = SPA_AUDIO_FORMAT_S24_LE; + break; + default: + return -EINVAL; + } + } + else if (spa_streq(codec, "opus")) { + info->media_subtype = SPA_MEDIA_SUBTYPE_opus; + info->info.opus.rate = sample_rate; + info->info.opus.channels = channels; + } + else if (spa_streq(codec, "flac")) { + info->media_subtype = SPA_MEDIA_SUBTYPE_flac; + info->info.flac.rate = sample_rate; + info->info.flac.channels = channels; + } + else + return -EINVAL; + + return 0; +} + +static int parse_player_v1_support(struct client *c, struct spa_json *payload) +{ + struct spa_json it[2]; + char key[256], *t; + const char *v; + int l, res; + + while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "supported_formats")) { + int count = 0; + + if (!spa_json_is_array(v, l)) + return -EPROTO; + spa_json_enter(payload, &it[0]); + + while ((l = spa_json_next(&it[0], &v)) > 0) { + struct spa_audio_info info; + if (!spa_json_is_object(v, l)) + return -EPROTO; + + spa_json_enter(&it[0], &it[1]); + if ((res = parse_codec(c, &it[1], &info)) < 0) + return res; + + if (count == 0 && info.media_subtype == SPA_MEDIA_SUBTYPE_raw) { + c->info = info; + count++; + } + } + } + else if (spa_streq(key, "buffer_capacity")) { + if (spa_json_parse_int(v, l, &c->buffer_capacity) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "supported_commands")) { + if (!spa_json_is_array(v, l)) + return -EPROTO; + spa_json_enter(payload, &it[0]); + + while ((l = spa_json_next(&it[0], &v)) > 0) { + t = alloca(l+1); + spa_json_parse_stringn(v, l, t, l+1); + if (spa_streq(t, "volume")) + c->supported_commands |= COMMAND_VOLUME; + else if (spa_streq(t, "mute")) + c->supported_commands |= COMMAND_MUTE; + } + } + } + return 0; +} + +static int handle_client_hello(struct client *c, struct spa_json *payload) +{ + struct spa_json it[1]; + char key[256], *t; + const char *v; + int res, l, version = 0; + + while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "client_id")) { + t = alloca(l+1); + spa_json_parse_stringn(v, l, t, l+1); + pw_properties_set(c->props, "sendspin.client-id", t); + } + else if (spa_streq(key, "name")) { + t = alloca(l+1); + spa_json_parse_stringn(v, l, t, l+1); + pw_properties_set(c->props, "sendspin.client-name", t); + } + else if (spa_streq(key, "version")) { + if (spa_json_parse_int(v, l, &version) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "supported_roles")) { + if (!spa_json_is_array(v, l)) + return -EPROTO; + + spa_json_enter(payload, &it[0]); + while ((l = spa_json_next(&it[0], &v)) > 0) { + t = alloca(l+1); + spa_json_parse_stringn(v, l, t, l+1); + + if (spa_streq(t, "player@v1")) + c->supported_roles |= ROLE_PLAYER; + else if (spa_streq(t, "metadata@v1")) + c->supported_roles |= ROLE_METADATA; + } + } + else if (spa_streq(key, "player_support") || + spa_streq(key, "player@v1_support")) { + if (!spa_json_is_object(v, l)) + return -EPROTO; + spa_json_enter(payload, &it[0]); + if ((res = parse_player_v1_support(c, &it[0])) < 0) + return res; + } + } + if (version != 1) + return -ENOTSUP; + + return send_server_hello(c); +} + +static int handle_client_state(struct client *c, struct spa_json *payload) +{ + struct spa_json it[1]; + char key[256], val[128]; + const char *v; + int l; + + while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "player")) { + if (!spa_json_is_object(v, l)) + return -EPROTO; + spa_json_enter(payload, &it[0]); + while ((l = spa_json_object_next(&it[0], key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "state")) { + spa_json_parse_stringn(v, l, val, sizeof(val)); + if (spa_streq(val, "synchronized")) + c->state = STATE_SYNCHRONIZED; + else if (spa_streq(val, "error")) + c->state = STATE_ERROR; + else + c->state = STATE_UNKNOWN; + } + else if (spa_streq(key, "volume")) { + if (spa_json_parse_int(v, l, &c->volume) <= 0) + return -EINVAL; + } + else if (spa_streq(key, "mute")) { + if (spa_json_parse_bool(v, l, &c->mute) <= 0) + return -EINVAL; + } + } + } + } + if (c->stream == NULL) + create_stream(c); + return 0; +} + +static int parse_uint64(const char *val, int len, uint64_t *result) +{ + char buf[64]; + char *end; + + if (len <= 0 || len >= (int)sizeof(buf)) + return 0; + + memcpy(buf, val, len); + buf[len] = '\0'; + + *result = strtoull(buf, &end, 0); + return len > 0 && end == buf + len; +} + +static int handle_client_time(struct client *c, struct spa_json *payload) +{ + char key[256]; + const char *v; + int l; + uint64_t t1 = 0,t2; + + t2 = get_time_us(c); + + while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "client_transmitted")) { + if (parse_uint64(v, l, &t1) <= 0) + return -EINVAL; + } + } + if (t1 == 0) + return -EPROTO; + + return send_server_time(c, t1, t2); +} + +static int handle_client_command(struct client *c, struct spa_json *payload) +{ + return 0; +} + +/* {"player":{}} */ +static int handle_stream_request_format(struct client *c, struct spa_json *payload) +{ + struct spa_json it[1]; + char key[256]; + const char *v; + int l; + + while ((l = spa_json_object_next(payload, key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "player")) { + if (!spa_json_is_object(v, l)) + return -EPROTO; + spa_json_enter(payload, &it[0]); + parse_codec(c, &it[0], &c->info); + } + } + return 0; +} + +static int handle_client_goodbye(struct client *c, struct spa_json *payload) +{ + if (c->stream != NULL) { + pw_stream_destroy(c->stream); + c->stream = NULL; + } + return 0; +} + +/* { "type":... "payload":{...} } */ +static int do_parse_text(struct client *c, const char *content, int size) +{ + struct spa_json it[2], *payload = NULL; + char key[256], type[256] = ""; + const char *v; + int res, l; + + pw_log_info("received text %.*s", size, content); + + if (spa_json_begin_object(&it[0], content, size) <= 0) + return -EINVAL; + + while ((l = spa_json_object_next(&it[0], key, sizeof(key), &v)) > 0) { + if (spa_streq(key, "payload")) { + if (!spa_json_is_object(v, l)) + return -EPROTO; + + spa_json_enter(&it[0], &it[1]); + payload = &it[1]; + } + else if (spa_streq(key, "type")) { + if (spa_json_parse_stringn(v, l, type, sizeof(type)) <= 0) + continue; + } + } + if (spa_streq(type, "client/hello")) + res = handle_client_hello(c, payload); + else if (spa_streq(type, "client/state")) + res = handle_client_state(c, payload); + else if (spa_streq(type, "client/time")) + res = handle_client_time(c, payload); + else if (spa_streq(type, "client/command")) + res = handle_client_command(c, payload); + else if (spa_streq(type, "client/goodbye")) + res = handle_client_goodbye(c, payload); + else if (spa_streq(type, "stream/request-format")) + res = handle_stream_request_format(c, payload); + else + res = 0; + + return res; +} + +static void on_connection_message(void *data, int opcode, void *payload, size_t size) +{ + struct client *c = data; + if (opcode == PW_WEBSOCKET_OPCODE_TEXT) { + do_parse_text(c, payload, size); + } else { + pw_log_warn("%02x unknown %08x", opcode, (int)size); + } +} + +static void client_free(struct client *c) +{ + struct impl *impl = c->impl; + + spa_list_remove(&c->link); + + handle_client_goodbye(c, NULL); + if (c->conn) { + spa_hook_remove(&c->conn_listener); + pw_websocket_connection_destroy(c->conn); + } else { + pw_websocket_cancel(impl->websocket, c); + } + pw_timer_queue_cancel(&c->timer); + pw_properties_free(c->props); + free(c->name); + free(c); +} + +static void on_connection_destroy(void *data) +{ + struct client *c = data; + c->conn = NULL; + pw_log_info("connection %p destroy", c); +} + +static void on_connection_error(void *data, int res, const char *reason) +{ + struct client *c = data; + pw_log_error("connection %p error %d %s", c, res, reason); +} + +static void on_connection_disconnected(void *data) +{ + struct client *c = data; + client_free(c); +} + +static const struct pw_websocket_connection_events websocket_connection_events = { + PW_VERSION_WEBSOCKET_CONNECTION_EVENTS, + .destroy = on_connection_destroy, + .error = on_connection_error, + .disconnected = on_connection_disconnected, + .message = on_connection_message, +}; + +static struct client *client_new(struct impl *impl, const char *name, struct pw_properties *props) +{ + struct client *c; + + if ((c = calloc(1, sizeof(*c))) == NULL) + goto error; + + c->impl = impl; + spa_list_append(&impl->clients, &c->link); + + c->props = props; + c->name = name ? strdup(name) : NULL; + c->delay_usec = (uint64_t)(impl->delay * SPA_USEC_PER_SEC); + + return c; +error: + pw_properties_free(props); + return NULL; +} + +static int client_connect(struct client *c) +{ + struct impl *impl = c->impl; + const char *addr, *port, *path; + addr = pw_properties_get(c->props, "sendspin.ip"); + port = pw_properties_get(c->props, "sendspin.port"); + path = pw_properties_get(c->props, "sendspin.path"); + return pw_websocket_connect(impl->websocket, c, addr, port, path); +} + +static void client_connected(struct client *c, struct pw_websocket_connection *conn) +{ + if (c->conn) { + spa_hook_remove(&c->conn_listener); + pw_websocket_connection_destroy(c->conn); + } + c->conn = conn; + if (conn) + pw_websocket_connection_add_listener(c->conn, &c->conn_listener, + &websocket_connection_events, c); +} + +struct match_info { + struct impl *impl; + const char *name; + struct pw_properties *props; + struct pw_websocket_connection *conn; + bool matched; +}; + +static int rule_matched(void *data, const char *location, const char *action, + const char *str, size_t len) +{ + struct match_info *i = data; + struct impl *impl = i->impl; + int res = 0; + + i->matched = true; + if (spa_streq(action, "create-stream")) { + struct client *c; + + pw_properties_update_string(i->props, str, len); + if ((c = client_new(impl, i->name, spa_steal_ptr(i->props))) == NULL) + return -errno; + if (i->conn) + client_connected(c, i->conn); + else + client_connect(c); + } + return res; +} + +static int match_client(struct impl *impl, const char *name, struct pw_properties *props, + struct pw_websocket_connection *conn) +{ + const char *str; + struct match_info minfo = { + .impl = impl, + .name = name, + .props = props, + .conn = conn, + }; + + if ((str = pw_properties_get(impl->props, "stream.rules")) == NULL) + str = DEFAULT_CREATE_RULES; + + pw_conf_match_rules(str, strlen(str), NAME, &props->dict, + rule_matched, &minfo); + + if (!minfo.matched) { + pw_log_info("unmatched client found %s", str); + if (conn) + pw_websocket_connection_destroy(conn); + pw_properties_free(props); + } + return minfo.matched; +} + +static void on_websocket_connected(void *data, void *user, + struct pw_websocket_connection *conn, const char *path) +{ + struct impl *impl = data; + struct client *c = user; + + pw_log_info("connected to %s", path); + if (c == NULL) { + struct sockaddr_storage addr; + char ip[128]; + uint16_t port = 0; + bool ipv4; + struct pw_properties *props; + + pw_websocket_connection_address(conn, + (struct sockaddr*)&addr, sizeof(addr)); + + props = pw_properties_copy(impl->stream_props); + if (pw_net_get_ip(&addr, ip, sizeof(ip), &ipv4, &port) >= 0) { + pw_properties_set(props, "sendspin.ip", ip); + pw_properties_setf(props, "sendspin.port", "%u", port); + } + pw_properties_set(props, "sendspin.path", path); + + match_client(impl, "", props, conn); + } else { + client_connected(c, conn); + } +} + +static const struct pw_websocket_events websocket_events = { + PW_VERSION_WEBSOCKET_EVENTS, + .connected = on_websocket_connected, +}; + +#ifdef HAVE_AVAHI +static struct client *client_find(struct impl *impl, const char *name) +{ + struct client *c; + spa_list_for_each(c, &impl->clients, link) { + if (spa_streq(c->name, name)) + return c; + } + return NULL; +} + +static void on_zeroconf_added(void *data, const void *user, const struct spa_dict *info) +{ + struct impl *impl = data; + const char *name, *addr, *port, *path; + struct client *c; + struct pw_properties *props; + + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); + + if ((c = client_find(impl, name)) != NULL) + return; + + props = pw_properties_copy(impl->stream_props); + pw_properties_update(props, info); + + addr = spa_dict_lookup(info, PW_KEY_ZEROCONF_ADDRESS); + port = spa_dict_lookup(info, PW_KEY_ZEROCONF_PORT); + path = spa_dict_lookup(info, "path"); + + pw_properties_set(props, "sendspin.ip", addr); + pw_properties_set(props, "sendspin.port", port); + pw_properties_set(props, "sendspin.path", path); + + match_client(impl, name, props, NULL); +} + +static void on_zeroconf_removed(void *data, const void *user, const struct spa_dict *info) +{ + struct impl *impl = data; + const char *name; + struct client *c; + + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); + + if ((c = client_find(impl, name)) == NULL) + return; + + client_free(c); +} + +static const struct pw_zeroconf_events zeroconf_events = { + PW_VERSION_ZEROCONF_EVENTS, + .added = on_zeroconf_added, + .removed = on_zeroconf_removed, +}; +#endif + +static void core_destroy(void *d) +{ + struct impl *impl = d; + spa_hook_remove(&impl->core_listener); + impl->core = NULL; + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_proxy_events core_proxy_events = { + .destroy = core_destroy, +}; + +static void impl_destroy(struct impl *impl) +{ + struct client *c; + spa_list_consume(c, &impl->clients, link) + client_free(c); + + if (impl->core && impl->do_disconnect) + pw_core_disconnect(impl->core); + + if (impl->data_loop) + pw_context_release_loop(impl->context, impl->data_loop); + +#ifdef HAVE_AVAHI + if (impl->zeroconf) + pw_zeroconf_destroy(impl->zeroconf); +#endif + + pw_properties_free(impl->stream_props); + pw_properties_free(impl->props); + + free(impl); +} + +static void module_destroy(void *d) +{ + struct impl *impl = d; + spa_hook_remove(&impl->module_listener); + impl_destroy(impl); +} + +static const struct pw_impl_module_events module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = module_destroy, +}; + +static void on_core_error(void *d, uint32_t id, int seq, int res, const char *message) +{ + struct impl *impl = d; + + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) + pw_impl_module_schedule_destroy(impl->module); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = on_core_error, +}; + +static void copy_props(struct impl *impl, struct pw_properties *props, const char *key) +{ + const char *str; + if ((str = pw_properties_get(props, key)) != NULL) { + if (pw_properties_get(impl->stream_props, key) == NULL) + pw_properties_set(impl->stream_props, key, str); + } +} + +SPA_EXPORT +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, *hostname, *port, *path; + struct pw_properties *props, *stream_props; + int res = 0; + + PW_LOG_TOPIC_INIT(mod_topic); + + impl = calloc(1, sizeof(struct impl)); + if (impl == NULL) + return -errno; + + if (args == NULL) + args = ""; + + props = impl->props = pw_properties_new_string(args); + stream_props = impl->stream_props = pw_properties_new(NULL, NULL); + if (props == NULL || stream_props == NULL) { + res = -errno; + pw_log_error( "can't create properties: %m"); + goto out; + } + + impl->module = module; + impl->context = context; + impl->main_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); + spa_list_init(&impl->clients); + + pw_properties_set(props, PW_KEY_NODE_LOOP_NAME, impl->data_loop->name); + + if ((str = pw_properties_get(props, "stream.props")) != NULL) + 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); + copy_props(impl, props, PW_KEY_NODE_GROUP); + copy_props(impl, props, PW_KEY_NODE_LATENCY); + copy_props(impl, props, PW_KEY_NODE_VIRTUAL); + copy_props(impl, props, PW_KEY_NODE_CHANNELNAMES); + copy_props(impl, props, PW_KEY_MEDIA_NAME); + copy_props(impl, props, PW_KEY_MEDIA_CLASS); + + impl->always_process = pw_properties_get_bool(stream_props, + PW_KEY_NODE_ALWAYS_PROCESS, true); + + if ((str = pw_properties_get(props, "sendspin.group-id")) == NULL) { + uint64_t group_id; + pw_random(&group_id, sizeof(group_id)); + pw_properties_setf(props, "sendspin.group-id", "%016"PRIx64, group_id); + } + if ((str = pw_properties_get(props, "sendspin.group-name")) == NULL) + pw_properties_set(props, "sendspin.group-name", "PipeWire"); + if ((str = pw_properties_get(props, "sendspin.server-name")) == NULL) + pw_properties_set(props, "sendspin.server-name", pw_get_host_name()); + if ((str = pw_properties_get(props, "sendspin.server-id")) == NULL) + pw_properties_setf(props, "sendspin.server-id", "pipewire-%s", pw_get_host_name()); + + 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); + impl->core = pw_context_connect(impl->context, + pw_properties_new( + PW_KEY_REMOTE_NAME, str, + NULL), + 0); + impl->do_disconnect = true; + } + if (impl->core == NULL) { + res = -errno; + pw_log_error("can't connect: %m"); + goto out; + } + + pw_proxy_add_listener((struct pw_proxy*)impl->core, + &impl->core_proxy_listener, + &core_proxy_events, impl); + pw_core_add_listener(impl->core, + &impl->core_listener, + &core_events, impl); + + impl->websocket = pw_websocket_new(impl->main_loop, &props->dict); + pw_websocket_add_listener(impl->websocket, &impl->websocket_listener, + &websocket_events, impl); + + if ((str = pw_properties_get(props, "sendspin.delay")) == NULL) + str = SPA_STRINGIFY(DEFAULT_SENDSPIN_DELAY); + impl->delay = pw_properties_parse_float(str); + +#ifdef HAVE_AVAHI + if ((impl->zeroconf = pw_zeroconf_new(context, NULL)) != NULL) { + pw_zeroconf_add_listener(impl->zeroconf, &impl->zeroconf_listener, + &zeroconf_events, impl); + } +#endif + + hostname = pw_properties_get(props, "sendspin.ip"); + if (hostname != NULL) { + struct spa_json iter; + char v[256]; + + port = pw_properties_get(props, "sendspin.port"); + if (port == NULL) + port = SPA_STRINGIFY(DEFAULT_CLIENT_PORT); + if ((path = pw_properties_get(props, "sendspin.path")) == NULL) + path = DEFAULT_SENDSPIN_PATH; + + if (spa_json_begin_array_relax(&iter, hostname, strlen(hostname)) <= 0) { + res = -EINVAL; + pw_log_error("can't parse sendspin.ip %s", hostname); + goto out; + } + while (spa_json_get_string(&iter, v, sizeof(v)) > 0) { + struct client *c; + struct pw_properties *p = pw_properties_copy(impl->stream_props); + + pw_properties_set(p, "sendspin.ip", v); + pw_properties_set(p, "sendspin.port", port); + pw_properties_set(p, "sendspin.path", path); + + if ((c = client_new(impl, "", p)) != NULL) + client_connect(c); + } + } else { + if ((hostname = pw_properties_get(props, "source.ip")) == NULL) + hostname = DEFAULT_SOURCE_IP; + if ((port = pw_properties_get(props, "source.port")) == NULL) + port = SPA_STRINGIFY(DEFAULT_SOURCE_PORT); + if ((path = pw_properties_get(props, "source.path")) == NULL) + path = DEFAULT_SOURCE_PATH; + + pw_websocket_listen(impl->websocket, NULL, hostname, port, path); + +#ifdef HAVE_AVAHI + if (impl->zeroconf) { + str = pw_properties_get(props, "sendspin.group-name"); + pw_zeroconf_set_announce(impl->zeroconf, NULL, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, PW_SENDSPIN_SERVER_SERVICE_TYPE), + SPA_DICT_ITEM(PW_KEY_ZEROCONF_NAME, str), + SPA_DICT_ITEM(PW_KEY_ZEROCONF_PORT, port), + SPA_DICT_ITEM("path", path))); + } +#endif + } +#ifdef HAVE_AVAHI + if (impl->zeroconf) { + pw_zeroconf_set_browse(impl->zeroconf, NULL, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, PW_SENDSPIN_CLIENT_SERVICE_TYPE))); + } +#endif + pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); + + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_info)); + + pw_log_info("Successfully loaded module-sendspin-send"); + + return 0; +out: + impl_destroy(impl); + return res; +} diff --git a/src/modules/module-sendspin/regress.h b/src/modules/module-sendspin/regress.h new file mode 100644 index 000000000..1b6919906 --- /dev/null +++ b/src/modules/module-sendspin/regress.h @@ -0,0 +1,58 @@ + +struct spa_regress { + double meanX; + double meanY; + double varX; + double covXY; + uint32_t n; + uint32_t m; + double a; +}; + +static inline void spa_regress_init(struct spa_regress *r, uint32_t m) +{ + memset(r, 0, sizeof(*r)); + r->m = m; + r->a = 1.0/m; +} +static inline void spa_regress_update(struct spa_regress *r, double x, double y) +{ + double a, dx, dy; + + if (r->n == 0) { + r->meanX = x; + r->meanY = y; + r->n++; + a = 1.0; + } else if (r->n < r->m) { + a = 1.0/r->n; + r->n++; + } else { + a = r->a; + } + dx = x - r->meanX; + dy = y - r->meanY; + + r->varX += ((1.0 - a) * dx * dx - r->varX) * a; + r->covXY += ((1.0 - a) * dx * dy - r->covXY) * a; + r->meanX += dx * a; + r->meanY += dy * a; +} +static inline void spa_regress_get(struct spa_regress *r, double *a, double *b) +{ + *a = r->covXY/r->varX; + *b = r->meanY - *a * r->meanX; +} +static inline double spa_regress_calc_y(struct spa_regress *r, double x) +{ + double a, b; + spa_regress_get(r, &a, &b); + return x * a + b; +} +static inline double spa_regress_calc_x(struct spa_regress *r, double y) +{ + double a, b; + spa_regress_get(r, &a, &b); + return (y - b) / a; +} + diff --git a/src/modules/module-sendspin/sendspin.h b/src/modules/module-sendspin/sendspin.h new file mode 100644 index 000000000..dab9d36f3 --- /dev/null +++ b/src/modules/module-sendspin/sendspin.h @@ -0,0 +1,27 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_SENDSPIN_H +#define PIPEWIRE_SENDSPIN_H + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define PW_SENDSPIN_SERVER_SERVICE_TYPE "_sendspin-server._tcp" +#define PW_SENDSPIN_CLIENT_SERVICE_TYPE "_sendspin._tcp" + +#define PW_SENDSPIN_DEFAULT_SERVER_PORT 8927 +#define PW_SENDSPIN_DEFAULT_CLIENT_PORT 8928 +#define PW_SENDSPIN_DEFAULT_PATH "/sendspin" + +#ifdef __cplusplus +} +#endif + +#endif /* PIPEWIRE_SENDSPIN_H */ diff --git a/src/modules/module-sendspin/teeny-sha1.c b/src/modules/module-sendspin/teeny-sha1.c new file mode 100644 index 000000000..fa5a56753 --- /dev/null +++ b/src/modules/module-sendspin/teeny-sha1.c @@ -0,0 +1,201 @@ +/******************************************************************************* + * Teeny SHA-1 + * + * The below sha1digest() calculates a SHA-1 hash value for a + * specified data buffer and generates a hex representation of the + * result. This implementation is a re-forming of the SHA-1 code at + * https://github.com/jinqiangshou/EncryptionLibrary. + * + * Copyright (c) 2017 CTrabant + * + * License: MIT, see included LICENSE file for details. + * + * To use the sha1digest() function either copy it into an existing + * project source code file or include this file in a project and put + * the declaration (example below) in the sources files where needed. + ******************************************************************************/ + +#include +#include +#include +#include + +/* Declaration: +extern int sha1digest(uint8_t *digest, char *hexdigest, const uint8_t *data, size_t databytes); +*/ + +/******************************************************************************* + * sha1digest: https://github.com/CTrabant/teeny-sha1 + * + * Calculate the SHA-1 value for supplied data buffer and generate a + * text representation in hexadecimal. + * + * Based on https://github.com/jinqiangshou/EncryptionLibrary, credit + * goes to @jinqiangshou, all new bugs are mine. + * + * @input: + * data -- data to be hashed + * databytes -- bytes in data buffer to be hashed + * + * @output: + * digest -- the result, MUST be at least 20 bytes + * hexdigest -- the result in hex, MUST be at least 41 bytes + * + * At least one of the output buffers must be supplied. The other, if not + * desired, may be set to NULL. + * + * @return: 0 on success and non-zero on error. + ******************************************************************************/ +static inline int +sha1digest(uint8_t *digest, char *hexdigest, const uint8_t *data, size_t databytes) +{ +#define SHA1ROTATELEFT(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) + + uint32_t W[80]; + uint32_t H[] = {0x67452301, + 0xEFCDAB89, + 0x98BADCFE, + 0x10325476, + 0xC3D2E1F0}; + uint32_t a; + uint32_t b; + uint32_t c; + uint32_t d; + uint32_t e; + uint32_t f = 0; + uint32_t k = 0; + + uint32_t idx; + uint32_t lidx; + uint32_t widx; + uint32_t didx = 0; + + int32_t wcount; + uint32_t temp; + uint64_t databits = ((uint64_t)databytes) * 8; + uint32_t loopcount = (databytes + 8) / 64 + 1; + uint32_t tailbytes = 64 * loopcount - databytes; + uint8_t datatail[128] = {0}; + + if (!digest && !hexdigest) + return -1; + + if (!data) + return -1; + + /* Pre-processing of data tail (includes padding to fill out 512-bit chunk): + Add bit '1' to end of message (big-endian) + Add 64-bit message length in bits at very end (big-endian) */ + datatail[0] = 0x80; + datatail[tailbytes - 8] = (uint8_t) (databits >> 56 & 0xFF); + datatail[tailbytes - 7] = (uint8_t) (databits >> 48 & 0xFF); + datatail[tailbytes - 6] = (uint8_t) (databits >> 40 & 0xFF); + datatail[tailbytes - 5] = (uint8_t) (databits >> 32 & 0xFF); + datatail[tailbytes - 4] = (uint8_t) (databits >> 24 & 0xFF); + datatail[tailbytes - 3] = (uint8_t) (databits >> 16 & 0xFF); + datatail[tailbytes - 2] = (uint8_t) (databits >> 8 & 0xFF); + datatail[tailbytes - 1] = (uint8_t) (databits >> 0 & 0xFF); + + /* Process each 512-bit chunk */ + for (lidx = 0; lidx < loopcount; lidx++) + { + /* Compute all elements in W */ + memset (W, 0, 80 * sizeof (uint32_t)); + + /* Break 512-bit chunk into sixteen 32-bit, big endian words */ + for (widx = 0; widx <= 15; widx++) + { + wcount = 24; + + /* Copy byte-per byte from specified buffer */ + while (didx < databytes && wcount >= 0) + { + W[widx] += (((uint32_t)data[didx]) << wcount); + didx++; + wcount -= 8; + } + /* Fill out W with padding as needed */ + while (wcount >= 0) + { + W[widx] += (((uint32_t)datatail[didx - databytes]) << wcount); + didx++; + wcount -= 8; + } + } + + /* Extend the sixteen 32-bit words into eighty 32-bit words, with potential optimization from: + "Improving the Performance of the Secure Hash Algorithm (SHA-1)" by Max Locktyukhin */ + for (widx = 16; widx <= 31; widx++) + { + W[widx] = SHA1ROTATELEFT ((W[widx - 3] ^ W[widx - 8] ^ W[widx - 14] ^ W[widx - 16]), 1); + } + for (widx = 32; widx <= 79; widx++) + { + W[widx] = SHA1ROTATELEFT ((W[widx - 6] ^ W[widx - 16] ^ W[widx - 28] ^ W[widx - 32]), 2); + } + + /* Main loop */ + a = H[0]; + b = H[1]; + c = H[2]; + d = H[3]; + e = H[4]; + + for (idx = 0; idx <= 79; idx++) + { + if (idx <= 19) + { + f = (b & c) | ((~b) & d); + k = 0x5A827999; + } + else if (idx >= 20 && idx <= 39) + { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } + else if (idx >= 40 && idx <= 59) + { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } + else if (idx >= 60 && idx <= 79) + { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + temp = SHA1ROTATELEFT (a, 5) + f + e + k + W[idx]; + e = d; + d = c; + c = SHA1ROTATELEFT (b, 30); + b = a; + a = temp; + } + + H[0] += a; + H[1] += b; + H[2] += c; + H[3] += d; + H[4] += e; + } + + /* Store binary digest in supplied buffer */ + if (digest) + { + for (idx = 0; idx < 5; idx++) + { + digest[idx * 4 + 0] = (uint8_t) (H[idx] >> 24); + digest[idx * 4 + 1] = (uint8_t) (H[idx] >> 16); + digest[idx * 4 + 2] = (uint8_t) (H[idx] >> 8); + digest[idx * 4 + 3] = (uint8_t) (H[idx]); + } + } + + /* Store hex version of digest in supplied buffer */ + if (hexdigest) + { + snprintf (hexdigest, 41, "%08x%08x%08x%08x%08x", + H[0],H[1],H[2],H[3],H[4]); + } + + return 0; +} /* End of sha1digest() */ diff --git a/src/modules/module-sendspin/websocket.c b/src/modules/module-sendspin/websocket.c new file mode 100644 index 000000000..5730a5c87 --- /dev/null +++ b/src/modules/module-sendspin/websocket.c @@ -0,0 +1,1072 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "config.h" + +#include "websocket.h" +#include "teeny-sha1.c" +#include "../network-utils.h" +#include "../module-raop/base64.h" + +#define pw_websocket_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_websocket_events, m, v, ##__VA_ARGS__) +#define pw_websocket_emit_destroy(w) pw_websocket_emit(w, destroy, 0) +#define pw_websocket_emit_connected(w,u,c,p) pw_websocket_emit(w, connected, 0, u, c, p) + +#define pw_websocket_connection_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_websocket_connection_events, m, v, ##__VA_ARGS__) +#define pw_websocket_connection_emit_destroy(w) pw_websocket_connection_emit(w, destroy, 0) +#define pw_websocket_connection_emit_error(w,r,m) pw_websocket_connection_emit(w, error, 0, r, m) +#define pw_websocket_connection_emit_disconnected(w) pw_websocket_connection_emit(w, disconnected, 0) +#define pw_websocket_connection_emit_drained(w) pw_websocket_connection_emit(w, drained, 0) +#define pw_websocket_connection_emit_message(w,...) pw_websocket_connection_emit(w, message, 0, __VA_ARGS__) + +#define MAX_CONNECTIONS 64 + +struct message { + struct spa_list link; + size_t len; + size_t offset; + uint32_t seq; + int (*reply) (void *user_data, int status); + void *user_data; + unsigned char data[]; +}; + +struct server { + struct pw_websocket *ws; + struct spa_list link; + + struct sockaddr_storage addr; + struct spa_source *source; + + void *user; + char **paths; + + struct spa_list connections; + uint32_t n_connections; +}; + +struct pw_websocket_connection { + struct pw_websocket *ws; + struct spa_list link; + + int refcount; + + void *user; + struct server *server; + + struct spa_hook_list listener_list; + + struct spa_source *source; + unsigned int connecting:1; + unsigned int need_flush:1; + + char *host; + char *path; + char name[128]; + bool ipv4; + uint16_t port; + + struct sockaddr_storage addr; + + uint8_t maskbit; + + int status; + char message[128]; + char key[25]; + size_t content_length; + + uint32_t send_seq; + uint32_t recv_seq; + bool draining; + + struct spa_list messages; + struct spa_list pending; + + struct pw_array data; + size_t data_wanted; + size_t data_cursor; + size_t data_state; + int (*have_data) (struct pw_websocket_connection *conn, + void *data, size_t size, size_t current); +}; + +struct pw_websocket { + struct pw_loop *loop; + + struct spa_hook_list listener_list; + + struct spa_source *source; + + char *ifname; + char *ifaddress; + char *user_agent; + char *server_name; + + struct spa_list connections; + struct spa_list servers; +}; + +void pw_websocket_connection_disconnect(struct pw_websocket_connection *conn, bool drain) +{ + struct message *msg; + + if (drain && !spa_list_is_empty(&conn->messages)) { + conn->draining = true; + return; + } + + if (conn->source != NULL) { + pw_loop_destroy_source(conn->ws->loop, conn->source); + conn->source = NULL; + } + spa_list_insert_list(&conn->messages, &conn->pending); + spa_list_consume(msg, &conn->messages, link) { + spa_list_remove(&msg->link); + free(msg); + } + if (conn->server) { + conn->server->n_connections--; + conn->server = NULL; + } + pw_websocket_connection_emit_disconnected(conn); +} + +static void websocket_connection_unref(struct pw_websocket_connection *conn) +{ + if (--conn->refcount > 0) + return; + pw_array_clear(&conn->data); + free(conn->host); + free(conn->path); + free(conn); +} + +void pw_websocket_connection_destroy(struct pw_websocket_connection *conn) +{ + pw_log_debug("destroy connection %p", conn); + spa_list_remove(&conn->link); + + pw_websocket_connection_emit_destroy(conn); + + pw_websocket_connection_disconnect(conn, false); + spa_hook_list_clean(&conn->listener_list); + + websocket_connection_unref(conn); +} + +void pw_websocket_connection_add_listener(struct pw_websocket_connection *conn, + struct spa_hook *listener, + const struct pw_websocket_connection_events *events, void *data) +{ + spa_hook_list_append(&conn->listener_list, listener, events, data); +} + +struct pw_websocket *pw_websocket_new(struct pw_loop *main_loop, struct spa_dict *props) +{ + struct pw_websocket *ws; + uint32_t i; + + if ((ws = calloc(1, sizeof(*ws))) == NULL) + return NULL; + + 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, "local.ifname")) + ws->ifname = s ? strdup(s) : NULL; + if (spa_streq(k, "local.ifaddress")) + ws->ifaddress = s ? strdup(s) : NULL; + if (spa_streq(k, "http.user-agent")) + ws->user_agent = s ? strdup(s) : NULL; + if (spa_streq(k, "http.server-name")) + ws->server_name = s ? strdup(s) : NULL; + } + if (ws->user_agent == NULL) + ws->user_agent = spa_aprintf("PipeWire/%s", PACKAGE_VERSION); + if (ws->server_name == NULL) + ws->server_name = spa_aprintf("PipeWire/%s", PACKAGE_VERSION); + + ws->loop = main_loop; + spa_hook_list_init(&ws->listener_list); + + spa_list_init(&ws->connections); + spa_list_init(&ws->servers); + return ws; +} + +static void server_free(struct server *server) +{ + struct pw_websocket *ws = server->ws; + struct pw_websocket_connection *conn; + + pw_log_debug("%p: free server %p", ws, server); + + spa_list_remove(&server->link); + spa_list_consume(conn, &server->connections, link) + pw_websocket_connection_destroy(conn); + if (server->source) + pw_loop_destroy_source(ws->loop, server->source); + pw_free_strv(server->paths); + free(server); +} + +void pw_websocket_destroy(struct pw_websocket *ws) +{ + struct server *server; + struct pw_websocket_connection *conn; + + pw_log_info("destroy sebsocket %p", ws); + pw_websocket_emit_destroy(ws); + + spa_list_consume(server, &ws->servers, link) + server_free(server); + spa_list_consume(conn, &ws->connections, link) + pw_websocket_connection_destroy(conn); + + spa_hook_list_clean(&ws->listener_list); + free(ws->ifname); + free(ws->ifaddress); + free(ws->user_agent); + free(ws->server_name); + free(ws); +} + +void pw_websocket_add_listener(struct pw_websocket *ws, + struct spa_hook *listener, + const struct pw_websocket_events *events, void *data) +{ + spa_hook_list_append(&ws->listener_list, listener, events, data); +} + +static int update_io(struct pw_websocket_connection *conn, int io, bool active) +{ + if (conn->source) { + uint32_t mask = conn->source->mask; + SPA_FLAG_UPDATE(mask, io, active); + if (mask != conn->source->mask) + pw_loop_update_io(conn->ws->loop, conn->source, mask); + } + return 0; +} + +static int receiver_expect(struct pw_websocket_connection *conn, size_t wanted, + int (*have_data) (struct pw_websocket_connection *conn, + void *data, size_t size, size_t current)) +{ + pw_array_reset(&conn->data); + conn->data_wanted = wanted; + conn->data_cursor = 0; + conn->data_state = 0; + conn->have_data = have_data; + return update_io(conn, SPA_IO_IN, wanted); +} + +static int queue_message(struct pw_websocket_connection *conn, struct message *msg) +{ + spa_list_append(&conn->messages, &msg->link); + conn->need_flush = true; + return update_io(conn, SPA_IO_OUT, true); +} + +static int receive_websocket(struct pw_websocket_connection *conn, + void *data, size_t size, size_t current) +{ + uint8_t *d = data; + int need = 0, header = 0, i; + if (conn->data_state == 0) { + /* header done */ + conn->status = d[0] & 0xf; + if (d[1] & 0x80) + header += 4; + if ((d[1] & 0x7f) == 126) + header += 2; + else if ((d[1] & 0x7f) == 127) + header += 8; + else + need += d[1] & 0x7f; + conn->data_cursor = 2 + header; + need += header; + conn->data_state++; + } + else if (conn->data_state == 1) { + /* extra length and mask */ + size_t payload_len = 0; + if ((d[1] & 0x7f) == 126) + header = 2; + else if ((d[1] & 0x7f) == 127) + header = 8; + for (i = 0; i < header; i++) + payload_len = (payload_len << 8) | d[i + 2]; + if (payload_len > (size_t)(INT_MAX - need)) + return -EMSGSIZE; + need += (int)payload_len; + conn->data_state++; + } + if (need == 0) { + uint8_t *payload = &d[conn->data_cursor]; + size_t i, payload_size = conn->data.size - conn->data_cursor; + struct iovec iov[1] = {{ payload, payload_size }}; + + if (d[1] & 0x80) { + uint8_t *mask = &d[conn->data_cursor - 4]; + for (i = 0; i < payload_size; i++) + payload[i] ^= mask[i & 3]; + } + + switch (conn->status) { + case PW_WEBSOCKET_OPCODE_PING: + pw_log_info("received ping"); + pw_websocket_connection_send(conn, PW_WEBSOCKET_OPCODE_PONG, iov, 1); + break; + case PW_WEBSOCKET_OPCODE_CLOSE: + pw_log_info("received close"); + pw_websocket_connection_send(conn, PW_WEBSOCKET_OPCODE_CLOSE, iov, 1); + pw_websocket_connection_disconnect(conn, true); + break; + default: + pw_log_debug("received message %02x", conn->status); + pw_websocket_connection_emit_message(conn, conn->status, + payload, payload_size); + } + receiver_expect(conn, 2, receive_websocket); + } + return need; +} + +static int connection_upgrade_failed(struct pw_websocket_connection *conn, + int status, const char *message) +{ + FILE *f; + size_t len; + struct message *msg; + + if ((f = open_memstream((char**)&msg, &len)) == NULL) + return -errno; + + fseek(f, offsetof(struct message, data), SEEK_SET); + fprintf(f, "HTTP/1.1 %d %s\r\n", status, message); + fprintf(f, "Transfer-Encoding: chunked\r\n"); + fprintf(f, "Content-Type: application/octet-stream\r\n"); + fprintf(f, "Server: %s\r\n", conn->ws->server_name); + fprintf(f, "\r\n"); + fclose(f); + + msg->len = len - offsetof(struct message, data); + pw_log_info("send error %d %s", status, message); + return queue_message(conn, msg); +} + +static void make_accept(const char *key, char *accept) +{ + static const char *str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + uint8_t tmp[24 + 36], sha1[20]; + memcpy(&tmp[ 0], key, 24); + memcpy(&tmp[24], str, 36); + sha1digest(sha1, NULL, tmp, sizeof(tmp)); + pw_base64_encode(sha1, sizeof(sha1), accept, '='); +} + +static int connection_upgraded_send(struct pw_websocket_connection *conn) +{ + FILE *f; + size_t len; + struct message *msg; + char accept[29]; + + if ((f = open_memstream((char**)&msg, &len)) == NULL) + return -errno; + + make_accept(conn->key, accept); + + fseek(f, offsetof(struct message, data), SEEK_SET); + fprintf(f, "HTTP/1.1 101 Switching Protocols\r\n"); + fprintf(f, "Upgrade: websocket\r\n"); + fprintf(f, "Connection: Upgrade\r\n"); + fprintf(f, "Sec-WebSocket-Accept: %s\r\n", accept); + fprintf(f, "\r\n"); + fclose(f); + + msg->len = len - offsetof(struct message, data); + pw_log_info("send upgrade %s", msg->data); + return queue_message(conn, msg); +} + +static int complete_upgrade(struct pw_websocket_connection *conn) +{ + pw_websocket_emit_connected(conn->ws, conn->user, conn, conn->path); + return receiver_expect(conn, 2, receive_websocket); +} + +static int header_key_val(char *buf, char **key, char **val) +{ + char *v; + *key = buf; + if ((v = strstr(buf, ":")) == NULL) + return -EPROTO; + *v++ = '\0'; + *val = pw_strip(v, " "); + return 0; +} + +static int receive_http_request(struct pw_websocket_connection *conn, + void *data, size_t size, size_t current) +{ + char *d = data, *l; + char c = d[current]; + int need = 1; + + if (conn->data_state == 0) { + if (c == '\n') { + int v1, v2; + d[current] = '\0'; + l = pw_strip(&d[conn->data_cursor], "\n\r "); + conn->data_cursor = current+1; + if (sscanf(l, "GET %ms HTTP/%d.%d", &conn->path, &v1, &v2) != 3) + return -EPROTO; + conn->data_state++; + } + } + else if (conn->data_state == 1) { + if (c == '\n') { + char *key, *val; + d[current] = '\0'; + l = pw_strip(&d[conn->data_cursor], "\n\r "); + if (strlen(l) > 0) { + conn->data_cursor = current+1; + if (header_key_val(l, &key, &val) < 0) + return -EPROTO; + if (spa_streq(key, "Sec-WebSocket-Key")) + strncpy(conn->key, val, sizeof(conn->key)-1); + } else { + conn->data_state++; + need = 0; + } + } + } + if (need == 0) { + if (conn->server && conn->server->paths && + pw_strv_find(conn->server->paths, conn->path) < 0) { + connection_upgrade_failed(conn, 404, "Not Found"); + } else { + connection_upgraded_send(conn); + complete_upgrade(conn); + } + } + return need; +} + +static struct message *find_pending(struct pw_websocket_connection *conn, uint32_t seq) +{ + struct message *msg; + spa_list_for_each(msg, &conn->pending, link) { + if (msg->seq == seq) + return msg; + } + return NULL; +} + +static int receive_http_reply(struct pw_websocket_connection *conn, + void *data, size_t size, size_t current) +{ + char *d = data, *l; + char c = d[current]; + int need = 1; + + if (conn->data_state == 0) { + if (c == '\n') { + int v1, v2, status, message; + /* status complete */ + d[current] = '\0'; + l = pw_strip(&d[conn->data_cursor], "\n\r "); + conn->data_cursor = current+1; + if (sscanf(l, "HTTP/%d.%d %n%d", &v1, &v2, &message, &status) != 3) + return -EPROTO; + conn->status = status; + snprintf(conn->message, sizeof(conn->message), "%s", &l[message]); + conn->content_length = 0; + conn->data_state++; + } + } + else if (conn->data_state == 1) { + if (c == '\n') { + /* header line complete */ + d[current] = '\0'; + l = pw_strip(&d[conn->data_cursor], "\n\r "); + conn->data_cursor = current+1; + if (strlen(l) > 0) { + char *key, *value; + if (header_key_val(l, &key, &value) < 0) + return -EPROTO; + if (spa_streq(key, "Sec-WebSocket-Accept")) { + char accept[29]; + make_accept(conn->key, accept); + if (!spa_streq(value, accept)) { + pw_log_error("got Accept:%s expected:%s", value, accept); + return -EPROTO; + } + } + else if (spa_streq(key, "Content-Length")) + conn->content_length = atoi(value); + } else { + conn->data_state++; + need = conn->content_length; + } + } + } + if (need == 0) { + /* message completed */ + uint32_t seq; + int res; + struct message *msg; + + seq = conn->recv_seq++; + + pw_log_info("received reply to request with seq:%" PRIu32, seq); + + if ((msg = find_pending(conn, seq)) != NULL) { + res = msg->reply(msg->user_data, conn->status); + spa_list_remove(&msg->link); + free(msg); + + if (res < 0) + pw_websocket_connection_emit_error(conn, res, conn->message); + } + } + return need; +} + +static int on_upgrade_reply(void *user_data, int status) +{ + struct pw_websocket_connection *conn = user_data; + if (status != 101) + return -EPROTO; + return complete_upgrade(conn); +} + +static int handle_connect(struct pw_websocket_connection *conn, int fd) +{ + int res = 0; + socklen_t res_len; + FILE *f; + size_t len; + struct message *msg; + uint8_t key[16]; + + len = sizeof(res); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &res_len) < 0) { + pw_log_error("getsockopt: %m"); + return -errno; + } + if (res != 0) + return -res; + + pw_log_info("connected to %s:%u", conn->name, conn->port); + + conn->connecting = false; + conn->status = 0; + + if ((f = open_memstream((char**)&msg, &len)) == NULL) + return -errno; + + fseek(f, offsetof(struct message, data), SEEK_SET); + + /* make a key */ + pw_random(key, sizeof(key)); + pw_base64_encode(key, sizeof(key), conn->key, '='); + + fprintf(f, "GET %s HTTP/1.1\r\n", conn->path); + fprintf(f, "Host: %s\r\n", conn->host); + fprintf(f, "Upgrade: websocket\r\n"); + fprintf(f, "Connection: Upgrade\r\n"); + fprintf(f, "Sec-WebSocket-Version: 13\r\n"); + fprintf(f, "Sec-WebSocket-Key: %s\r\n", conn->key); + fprintf(f, "Accept: */*\r\n"); + fprintf(f, "User-Agent: %s\r\n", conn->ws->user_agent); + fprintf(f, "\r\n"); + fclose(f); + + msg->len = len - offsetof(struct message, data); + msg->reply = on_upgrade_reply; + msg->user_data = conn; + msg->seq = conn->send_seq++; + + pw_log_info("%s", msg->data); + + receiver_expect(conn, 1, receive_http_reply); + + return queue_message(conn, msg); +} + +static int handle_input(struct pw_websocket_connection *conn) +{ + int res; + + while (conn->data.size < conn->data_wanted) { + size_t current = conn->data.size; + size_t pending = conn->data_wanted - current; + void *b; + + if (conn->source == NULL) + return -EPIPE; + + if ((res = pw_array_ensure_size(&conn->data, pending)) < 0) + return res; + b = SPA_PTROFF(conn->data.data, current, void); + + res = read(conn->source->fd, b, pending); + if (res == 0) + return 0; + if (res < 0) { + res = -errno; + if (res == -EINTR) + continue; + if (res != -EAGAIN && res != -EWOULDBLOCK) + return res; + return -EAGAIN; + } + conn->data.size += res; + if (conn->data.size == conn->data_wanted) { + if ((res = conn->have_data(conn, + conn->data.data, + conn->data.size, + current)) < 0) + return res; + + if (conn->data_wanted > SIZE_MAX - res) + return -EOVERFLOW; + conn->data_wanted += res; + } + } + return 0; +} + +static int flush_output(struct pw_websocket_connection *conn) +{ + int res; + + conn->need_flush = false; + + if (conn->source == NULL) + return -EPIPE; + + while (true) { + struct message *msg; + void *data; + size_t size; + + if (spa_list_is_empty(&conn->messages)) { + if (conn->draining) + pw_websocket_connection_disconnect(conn, false); + break; + } + msg = spa_list_first(&conn->messages, struct message, link); + + if (msg->offset < msg->len) { + data = SPA_PTROFF(msg->data, msg->offset, void); + size = msg->len - msg->offset; + } else { + spa_list_remove(&msg->link); + if (msg->reply != NULL) + spa_list_append(&conn->pending, &msg->link); + else + free(msg); + continue; + } + + while (true) { + res = send(conn->source->fd, data, size, MSG_NOSIGNAL | MSG_DONTWAIT); + if (res < 0) { + res = -errno; + if (res == -EINTR) + continue; + if (res != -EAGAIN && res != -EWOULDBLOCK) + pw_log_warn("conn %p: send %zu, error %d: %m", + conn, size, res); + return res; + } + msg->offset += res; + break; + } + } + return 0; +} + +static void +on_source_io(void *data, int fd, uint32_t mask) +{ + struct pw_websocket_connection *conn = data; + int res; + + conn->refcount++; + + if (mask & (SPA_IO_ERR | SPA_IO_HUP)) { + socklen_t len = sizeof(res); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &len) < 0) + res = errno; + res = -res; + goto error; + } + if (mask & SPA_IO_IN) { + if ((res = handle_input(conn)) != -EAGAIN) + goto error; + } + if (mask & SPA_IO_OUT || conn->need_flush) { + if (conn->connecting) { + if ((res = handle_connect(conn, fd)) < 0) + goto error; + } + res = flush_output(conn); + if (res >= 0) { + if (conn->source) + pw_loop_update_io(conn->ws->loop, conn->source, + conn->source->mask & ~SPA_IO_OUT); + } else if (res != -EAGAIN) + goto error; + } +done: + websocket_connection_unref(conn); + return; +error: + if (res < 0) { + pw_log_error("%p: %s got connection error %d (%s)", conn, + conn->name, res, spa_strerror(res)); + snprintf(conn->message, sizeof(conn->message), "%s", spa_strerror(res)); + pw_websocket_connection_emit_error(conn, res, conn->message); + } else { + pw_log_info("%p: %s connection closed", conn, conn->name); + } + pw_websocket_connection_disconnect(conn, false); + goto done; +} + +int pw_websocket_connection_address(struct pw_websocket_connection *conn, + struct sockaddr *addr, socklen_t addr_len) +{ + memcpy(addr, &conn->addr, SPA_MIN(addr_len, sizeof(conn->addr))); + return 0; +} + +static struct pw_websocket_connection *connection_new(struct pw_websocket *ws, void *user, + struct sockaddr *addr, socklen_t addr_len, int fd, struct server *server) +{ + struct pw_websocket_connection *conn; + + if ((conn = calloc(1, sizeof(*conn))) == NULL) + goto error; + + if ((conn->source = pw_loop_add_io(ws->loop, spa_steal_fd(fd), + SPA_IO_ERR | SPA_IO_HUP | SPA_IO_OUT, + true, on_source_io, conn)) == NULL) + goto error; + + memcpy(&conn->addr, addr, SPA_MIN(addr_len, sizeof(conn->addr))); + conn->ws = ws; + conn->server = server; + conn->user = user; + if (server) + spa_list_append(&server->connections, &conn->link); + else + spa_list_append(&ws->connections, &conn->link); + + conn->refcount = 1; + if (pw_net_get_ip(&conn->addr, conn->name, sizeof(conn->name), + &conn->ipv4, &conn->port) < 0) + snprintf(conn->name, sizeof(conn->name), "connection %p", conn); + + spa_list_init(&conn->messages); + spa_list_init(&conn->pending); + spa_hook_list_init(&conn->listener_list); + pw_array_init(&conn->data, 4096); + + pw_log_debug("new websocket %p connection %p %s:%u", ws, + conn, conn->name, conn->port); + + return conn; +error: + if (fd != -1) + close(fd); + free(conn); + return NULL; +} + +static int make_tcp_socket(struct server *server, const char *name, uint16_t port, const char *ifname, + const char *ifaddress) +{ + struct sockaddr_storage addr; + int res, on; + socklen_t len = 0; + spa_autoclose int fd = -1; + + if ((res = pw_net_parse_address_port(name, ifaddress, port, &addr, &len)) < 0) { + pw_log_error("%p: can't parse address %s: %s", server, + name, spa_strerror(res)); + goto error; + } + + if ((fd = socket(addr.ss_family, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) { + res = -errno; + pw_log_error("%p: socket() failed: %m", server); + goto error; + } +#ifdef SO_BINDTODEVICE + if (ifname && setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname)) < 0) { + res = -errno; + pw_log_error("%p: setsockopt(SO_BINDTODEVICE) failed: %m", server); + goto error; + } +#endif + on = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void *) &on, sizeof(on)) < 0) + pw_log_warn("%p: setsockopt(): %m", server); + + if (bind(fd, (struct sockaddr *) &addr, len) < 0) { + res = -errno; + pw_log_error("%p: bind() failed: %m", server); + goto error; + } + if (listen(fd, 5) < 0) { + res = -errno; + pw_log_error("%p: listen() failed: %m", server); + goto error; + } + if (getsockname(fd, (struct sockaddr *)&addr, &len) < 0) { + res = -errno; + pw_log_error("%p: getsockname() failed: %m", server); + goto error; + } + + server->addr = addr; + + return spa_steal_fd(fd); +error: + return res; +} + +static void +on_server_connect(void *data, int fd, uint32_t mask) +{ + struct server *server = data; + struct pw_websocket *ws = server->ws; + struct sockaddr_storage addr; + socklen_t addrlen; + spa_autoclose int conn_fd = -1; + int val; + struct pw_websocket_connection *conn = NULL; + + addrlen = sizeof(addr); + if ((conn_fd = accept4(fd, (struct sockaddr*)&addr, &addrlen, + SOCK_NONBLOCK | SOCK_CLOEXEC)) < 0) + goto error; + + if (server->n_connections >= MAX_CONNECTIONS) + goto error_refused; + + if ((conn = connection_new(ws, server->user, (struct sockaddr*)&addr, sizeof(addr), + spa_steal_fd(conn_fd), server)) == NULL) + goto error; + + server->n_connections++; + + pw_log_info("%p: connection:%p %s:%u connected", ws, + conn, conn->name, conn->port); + + val = 1; + if (setsockopt(conn->source->fd, IPPROTO_TCP, TCP_NODELAY, + (const void *) &val, sizeof(val)) < 0) + pw_log_warn("TCP_NODELAY failed: %m"); + + val = IPTOS_LOWDELAY; + if (setsockopt(conn->source->fd, IPPROTO_IP, IP_TOS, + (const void *) &val, sizeof(val)) < 0) + pw_log_warn("IP_TOS failed: %m"); + + receiver_expect(conn, 1, receive_http_request); + return; + +error_refused: + errno = ECONNREFUSED; +error: + pw_log_error("%p: failed to create connection: %m", ws); + return; +} + +int pw_websocket_listen(struct pw_websocket *ws, void *user, + const char *hostname, const char *service, const char *paths) +{ + int res; + struct server *server; + uint16_t port = atoi(service); + + if ((server = calloc(1, sizeof(struct server))) == NULL) + return -errno; + + server->ws = ws; + spa_list_append(&ws->servers, &server->link); + + server->user = user; + spa_list_init(&server->connections); + + if ((res = make_tcp_socket(server, hostname, port, ws->ifname, ws->ifaddress)) < 0) + goto error; + + if ((server->source = pw_loop_add_io(ws->loop, res, SPA_IO_IN, + true, on_server_connect, server)) == NULL) { + res = -errno; + goto error; + } + if (paths) + server->paths = pw_strv_parse(paths, strlen(paths), INT_MAX, NULL); + + pw_log_info("%p: listen %s:%u %s", ws, hostname, port, paths); + return 0; +error: + pw_log_error("%p: can't create server: %s", ws, spa_strerror(res)); + server_free(server); + return res; +} + +int pw_websocket_cancel(struct pw_websocket *ws, void *user) +{ + struct server *s, *ts; + struct pw_websocket_connection *c, *tc; + int count = 0; + + spa_list_for_each_safe(s, ts, &ws->servers, link) { + if (s->user == user) { + server_free(s); + count++; + } + } + spa_list_for_each_safe(c, tc, &ws->connections, link) { + if (c->user == user) { + pw_websocket_connection_destroy(c); + count++; + } + } + return count; +} + +int pw_websocket_connect(struct pw_websocket *ws, void *user, + const char *hostname, const char *service, const char *path) +{ + struct addrinfo hints; + struct addrinfo *result, *rp; + int res, fd = -1; + struct pw_websocket_connection *conn = NULL; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = 0; + hints.ai_protocol = 0; + + if ((res = getaddrinfo(hostname, service, &hints, &result)) != 0) { + pw_log_error("getaddrinfo: %s", gai_strerror(res)); + return -EINVAL; + } + res = -ENOENT; + for (rp = result; rp != NULL; rp = rp->ai_next) { + if ((fd = socket(rp->ai_family, + rp->ai_socktype | SOCK_CLOEXEC | SOCK_NONBLOCK, + rp->ai_protocol)) == -1) + continue; + + res = connect(fd, rp->ai_addr, rp->ai_addrlen); + if (res == 0 || (res < 0 && errno == EINPROGRESS)) + break; + + res = -errno; + close(fd); + } + if (rp == NULL) { + pw_log_error("Could not connect to %s:%s: %s", hostname, service, + spa_strerror(res)); + } else { + if ((conn = connection_new(ws, user, rp->ai_addr, rp->ai_addrlen, fd, NULL)) == NULL) + res = -errno; + } + freeaddrinfo(result); + if (conn == NULL) + return res; + + conn->connecting = true; + conn->maskbit = 0x80; + conn->path = strdup(path); + asprintf(&conn->host, "%s:%s", hostname, service); + + pw_log_info("%p: connecting to %s:%u path:%s", conn, + conn->name, conn->port, path); + return 0; +} + +int pw_websocket_connection_send(struct pw_websocket_connection *conn, uint8_t opcode, + const struct iovec *iov, size_t iov_len) +{ + struct message *msg; + size_t len = 2, i, j, k; + uint8_t *d, *mask = NULL, maskbit = conn->maskbit; + size_t payload_length = 0; + + for (i = 0; i < iov_len; i++) { + if (payload_length > SIZE_MAX - iov[i].iov_len) + return -EOVERFLOW; + payload_length += iov[i].iov_len; + } + if (payload_length > SIZE_MAX - sizeof(*msg) - 14) + return -EOVERFLOW; + + if ((msg = calloc(1, sizeof(*msg) + 14 + payload_length)) == NULL) + return -errno; + + d = msg->data; + d[0] = 0x80 | opcode; + + if (payload_length < 126) + k = 0; + else if (payload_length < 65536) + k = 2; + else + k = 8; + + d[1] = maskbit | (k == 0 ? payload_length : (k == 2 ? 126 : 127)); + for (i = 0, j = (k-1)*8 ; i < k; i++, j -= 8) + d[len++] = (payload_length >> j) & 0xff; + + if (maskbit) { + mask = &d[len]; + pw_random(mask, 4); + len += 4; + } + for (i = 0, k = 0; i < iov_len; i++) { + if (maskbit) + for (j = 0; j < iov[i].iov_len; j++, k++) + d[len+j] = ((uint8_t*)iov[i].iov_base)[j] ^ mask[k & 3]; + else + memcpy(&d[len], iov[i].iov_base, iov[i].iov_len); + + len += iov[i].iov_len; + } + msg->len = len; + + return queue_message(conn, msg); +} + +int pw_websocket_connection_send_text(struct pw_websocket_connection *conn, + const char *payload, size_t payload_len) +{ + struct iovec iov[1] = {{ (void*)payload, payload_len }}; + pw_log_info("send text %.*s", (int)payload_len, payload); + return pw_websocket_connection_send(conn, PW_WEBSOCKET_OPCODE_TEXT, iov, 1); +} diff --git a/src/modules/module-sendspin/websocket.h b/src/modules/module-sendspin/websocket.h new file mode 100644 index 000000000..0f0da36e7 --- /dev/null +++ b/src/modules/module-sendspin/websocket.h @@ -0,0 +1,85 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_WEBSOCKET_H +#define PIPEWIRE_WEBSOCKET_H + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct pw_websocket; +struct pw_websocket_connection; + +#define PW_WEBSOCKET_OPCODE_TEXT 0x1 +#define PW_WEBSOCKET_OPCODE_BINARY 0x2 +#define PW_WEBSOCKET_OPCODE_CLOSE 0x8 +#define PW_WEBSOCKET_OPCODE_PING 0x9 +#define PW_WEBSOCKET_OPCODE_PONG 0xa + +struct pw_websocket_connection_events { +#define PW_VERSION_WEBSOCKET_CONNECTION_EVENTS 0 + uint32_t version; + + void (*destroy) (void *data); + void (*error) (void *data, int res, const char *reason); + void (*disconnected) (void *data); + + void (*message) (void *data, + int opcode, void *payload, size_t size); +}; + +void pw_websocket_connection_add_listener(struct pw_websocket_connection *conn, + struct spa_hook *listener, + const struct pw_websocket_connection_events *events, void *data); + +void pw_websocket_connection_destroy(struct pw_websocket_connection *conn); +void pw_websocket_connection_disconnect(struct pw_websocket_connection *conn, bool drain); + +int pw_websocket_connection_address(struct pw_websocket_connection *conn, + struct sockaddr *addr, socklen_t addr_len); + +int pw_websocket_connection_send(struct pw_websocket_connection *conn, + uint8_t opcode, const struct iovec *iov, size_t iov_len); + +int pw_websocket_connection_send_text(struct pw_websocket_connection *conn, + const char *payload, size_t payload_len); + + +struct pw_websocket_events { +#define PW_VERSION_WEBSOCKET_EVENTS 0 + uint32_t version; + + void (*destroy) (void *data); + + void (*connected) (void *data, void *user, + struct pw_websocket_connection *conn, const char *path); +}; + +struct pw_websocket * pw_websocket_new(struct pw_loop *main_loop, + struct spa_dict *props); + +void pw_websocket_destroy(struct pw_websocket *ws); + +void pw_websocket_add_listener(struct pw_websocket *ws, + struct spa_hook *listener, + const struct pw_websocket_events *events, void *data); + +int pw_websocket_connect(struct pw_websocket *ws, void *user, + const char *hostname, const char *service, const char *path); + +int pw_websocket_listen(struct pw_websocket *ws, void *user, + const char *hostname, const char *service, const char *paths); + +int pw_websocket_cancel(struct pw_websocket *ws, void *user); + +#ifdef __cplusplus +} +#endif + +#endif /* PIPEWIRE_WEBSOCKET_H */ diff --git a/src/modules/module-snapcast-discover.c b/src/modules/module-snapcast-discover.c index e929be9c0..5c7896bcf 100644 --- a/src/modules/module-snapcast-discover.c +++ b/src/modules/module-snapcast-discover.c @@ -29,12 +29,8 @@ #include #include -#include -#include -#include - #include "module-protocol-pulse/format.h" -#include "module-zeroconf-discover/avahi-poll.h" +#include "zeroconf-utils/zeroconf.h" #include "network-utils.h" @@ -175,10 +171,8 @@ struct impl { bool discover_local; struct pw_loop *loop; - AvahiPoll *avahi_poll; - AvahiClient *client; - AvahiServiceBrowser *jsonrpc_browser; - AvahiServiceBrowser *ctrl_browser; + struct pw_zeroconf *zeroconf; + struct spa_hook zeroconf_listener; struct spa_list tunnel_list; uint32_t id; @@ -206,8 +200,6 @@ struct tunnel { bool need_flush; }; -static int start_client(struct impl *impl); - static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *info) { struct tunnel *t; @@ -254,14 +246,8 @@ static void impl_free(struct impl *impl) spa_list_consume(t, &impl->tunnel_list, link) free_tunnel(t); - if (impl->jsonrpc_browser) - avahi_service_browser_free(impl->jsonrpc_browser); - if (impl->ctrl_browser) - avahi_service_browser_free(impl->ctrl_browser); - if (impl->client) - avahi_client_free(impl->client); - if (impl->avahi_poll) - pw_avahi_poll_free(impl->avahi_poll); + if (impl->zeroconf) + pw_zeroconf_destroy(impl->zeroconf); pw_properties_free(impl->properties); free(impl); } @@ -278,7 +264,7 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void pw_properties_from_avahi_string(const char *key, const char *value, +static void pw_properties_from_zeroconf(const char *key, const char *value, struct pw_properties *props) { } @@ -395,7 +381,10 @@ on_source_io(void *data, int fd, uint32_t mask) int res; if (mask & (SPA_IO_ERR | SPA_IO_HUP)) { - res = -EPIPE; + socklen_t len = sizeof(res); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &len) < 0) + res = errno; + res = -res; goto error; } if (mask & SPA_IO_IN) { @@ -427,7 +416,7 @@ static int snapcast_connect(struct tunnel *t) { struct addrinfo hints; struct addrinfo *result, *rp; - int res, fd; + int res, fd = -1; char port_str[12]; if (t->server_address == NULL) @@ -616,34 +605,26 @@ static int rule_matched(void *data, const char *location, const char *action, return res; } -static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, - AvahiResolverEvent event, const char *name, const char *type, const char *domain, - const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt, - AvahiLookupResultFlags flags, void *userdata) +static void on_zeroconf_added(void *data, const void *user, const struct spa_dict *info) { - struct impl *impl = userdata; + struct impl *impl = data; struct tunnel_info tinfo; struct tunnel *t; - const char *str, *link_local_range = "169.254."; - AvahiStringList *l; + const char *name, *address, *str; struct pw_properties *props = NULL; - char at[AVAHI_ADDRESS_STR_MAX]; char hbuf[NI_MAXHOST]; - char if_suffix[16] = ""; struct ifreq ifreq; - int res, family; + int res, family, port = 0, ifindex = 0, protocol = 4; + const struct spa_dict_item *it; - if (event != AVAHI_RESOLVER_FOUND) { - pw_log_error("Resolving of '%s' failed: %s", name, - avahi_strerror(avahi_client_errno(impl->client))); - goto done; - } - - avahi_address_snprint(at, sizeof(at), a); - if (spa_strstartswith(at, link_local_range)) - pw_log_info("found link-local ip address %s for '%s'", at, name); - - pw_log_info("%s %s", name, at); + if ((str = spa_dict_lookup(info, PW_KEY_ZEROCONF_IFINDEX))) + ifindex = atoi(str); + if ((str = spa_dict_lookup(info, PW_KEY_ZEROCONF_PROTO))) + protocol = atoi(str); + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); + address = spa_dict_lookup(info, PW_KEY_ZEROCONF_ADDRESS); + if ((str = spa_dict_lookup(info, PW_KEY_ZEROCONF_PORT))) + port = atoi(str); tinfo = TUNNEL_INFO(.name = name, .port = port); @@ -655,7 +636,8 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr goto done; } if (t->module != NULL) { - pw_log_info("found duplicate mdns entry for %s on IP %s - skipping tunnel creation", name, at); + pw_log_info("found duplicate mdns entry for %s on IP %s - skipping tunnel creation", + name, address); goto done; } @@ -665,33 +647,23 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr goto done; } - if (a->proto == AVAHI_PROTO_INET6 && - a->data.ipv6.address[0] == 0xfe && - (a->data.ipv6.address[1] & 0xc0) == 0x80) - snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); - - /* For IPv4 link-local, bind to the discovery interface */ - if (a->proto == AVAHI_PROTO_INET && - spa_strstartswith(at, link_local_range)) - snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); - - pw_properties_setf(props, "snapcast.ip", "%s%s", at, if_suffix); - pw_properties_setf(props, "snapcast.ifindex", "%d", interface); + pw_properties_set(props, "snapcast.ip", address); + pw_properties_setf(props, "snapcast.ifindex", "%u", ifindex); pw_properties_setf(props, "snapcast.port", "%u", port); - pw_properties_setf(props, "snapcast.name", "%s", name); - pw_properties_setf(props, "snapcast.hostname", "%s", host_name); - pw_properties_setf(props, "snapcast.domain", "%s", domain); + pw_properties_set(props, "snapcast.name", name); + pw_properties_set(props, "snapcast.hostname", spa_dict_lookup(info, PW_KEY_ZEROCONF_HOSTNAME)); + pw_properties_set(props, "snapcast.domain", spa_dict_lookup(info, PW_KEY_ZEROCONF_DOMAIN)); free((char*)t->info.host); t->info.host = strdup(pw_properties_get(props, "snapcast.ip")); - family = protocol == AVAHI_PROTO_INET ? AF_INET : AF_INET6; + family = protocol == 4 ? AF_INET : AF_INET6; spa_zero(ifreq); - ifreq.ifr_ifindex = interface; - if_indextoname(interface, ifreq.ifr_name); - pw_properties_setf(props, "snapcast.ifname", "%s", ifreq.ifr_name); - pw_properties_setf(props, "local.ifname", "%s", ifreq.ifr_name); + ifreq.ifr_ifindex = ifindex; + if_indextoname(ifindex, ifreq.ifr_name); + pw_properties_set(props, "snapcast.ifname", ifreq.ifr_name); + pw_properties_set(props, "local.ifname", ifreq.ifr_name); struct ifaddrs *if_addr, *ifp; if (getifaddrs(&if_addr) < 0) @@ -725,16 +697,8 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr } freeifaddrs(if_addr); - for (l = txt; l; l = l->next) { - char *key, *value; - - if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0) - break; - - pw_properties_from_avahi_string(key, value, props); - avahi_free(key); - avahi_free(value); - } + spa_dict_for_each(it, info) + pw_properties_from_zeroconf(it->key, it->value, props); if ((str = pw_properties_get(impl->properties, "stream.rules")) == NULL) str = DEFAULT_CREATE_RULES; @@ -752,133 +716,46 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr } done: - avahi_service_resolver_free(r); pw_properties_free(props); } -static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, - AvahiBrowserEvent event, const char *name, const char *type, const char *domain, - AvahiLookupResultFlags flags, void *userdata) +static void on_zeroconf_removed(void *data, const void *user, const struct spa_dict *info) { - struct impl *impl = userdata; - struct tunnel_info info; + struct impl *impl = data; struct tunnel *t; + struct tunnel_info tinfo; + const char *name; - if ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && !impl->discover_local) + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); + + tinfo = TUNNEL_INFO(.name = name); + + t = find_tunnel(impl, &tinfo); + if (t == NULL) return; - /* snapcast does not seem to work well with IPv6 */ - if (protocol == AVAHI_PROTO_INET6) - return; - - info = TUNNEL_INFO(.name = name); - - t = find_tunnel(impl, &info); - - switch (event) { - case AVAHI_BROWSER_NEW: - if (t != NULL) { - pw_log_info("found duplicate mdns entry - skipping tunnel creation"); - return; - } - if (!(avahi_service_resolver_new(impl->client, - interface, protocol, - name, type, domain, - AVAHI_PROTO_UNSPEC, 0, - resolver_cb, impl))) - pw_log_error("can't make service resolver: %s", - avahi_strerror(avahi_client_errno(impl->client))); - break; - case AVAHI_BROWSER_REMOVE: - if (t == NULL) - return; - free_tunnel(t); - break; - default: - break; - } + free_tunnel(t); } - -static struct AvahiServiceBrowser *make_browser(struct impl *impl, const char *service_type) -{ - struct AvahiServiceBrowser *s; - - s = avahi_service_browser_new(impl->client, - AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, - service_type, NULL, 0, - browser_cb, impl); - if (s == NULL) { - pw_log_error("can't make browser for %s: %s", service_type, - avahi_strerror(avahi_client_errno(impl->client))); - } - return s; -} - -static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata) -{ - struct impl *impl = userdata; - - impl->client = c; - - switch (state) { - case AVAHI_CLIENT_S_REGISTERING: - case AVAHI_CLIENT_S_RUNNING: - case AVAHI_CLIENT_S_COLLISION: - if (impl->ctrl_browser == NULL) - impl->ctrl_browser = make_browser(impl, SERVICE_TYPE_CONTROL); - if (impl->ctrl_browser == NULL) - goto error; - if (impl->jsonrpc_browser == NULL) - impl->jsonrpc_browser = make_browser(impl, SERVICE_TYPE_JSONRPC); - if (impl->jsonrpc_browser == NULL) - goto error; - break; - case AVAHI_CLIENT_FAILURE: - if (avahi_client_errno(c) == AVAHI_ERR_DISCONNECTED) - start_client(impl); - - SPA_FALLTHROUGH; - case AVAHI_CLIENT_CONNECTING: - if (impl->ctrl_browser) { - avahi_service_browser_free(impl->ctrl_browser); - impl->ctrl_browser = NULL; - } - if (impl->jsonrpc_browser) { - avahi_service_browser_free(impl->jsonrpc_browser); - impl->jsonrpc_browser = NULL; - } - break; - default: - break; - } - return; -error: - pw_impl_module_schedule_destroy(impl->module); -} - -static int start_client(struct impl *impl) +static int make_browser(struct impl *impl, const char *service_type) { int res; - if ((impl->client = avahi_client_new(impl->avahi_poll, - AVAHI_CLIENT_NO_FAIL, - client_callback, impl, - &res)) == NULL) { - pw_log_error("can't create client: %s", avahi_strerror(res)); - pw_impl_module_schedule_destroy(impl->module); - return -EIO; + if ((res = pw_zeroconf_set_browse(impl->zeroconf, service_type, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, service_type)))) < 0) { + pw_log_error("can't make browser for %s: %s", + service_type, spa_strerror(res)); + return res; } return 0; } -static int start_avahi(struct impl *impl) -{ - - impl->avahi_poll = pw_avahi_poll_new(impl->context); - - return start_client(impl); -} +static const struct pw_zeroconf_events zeroconf_events = { + PW_VERSION_ZEROCONF_EVENTS, + .added = on_zeroconf_added, + .removed = on_zeroconf_removed, +}; SPA_EXPORT int pipewire__module_init(struct pw_impl_module *module, const char *args) @@ -912,13 +789,24 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->discover_local = pw_properties_get_bool(impl->properties, "snapcast.discover-local", false); + pw_properties_set(props, PW_KEY_ZEROCONF_DISCOVER_LOCAL, + impl->discover_local ? "true" : "false"); + + impl->zeroconf = pw_zeroconf_new(impl->context, &props->dict); + if (impl->zeroconf == NULL) { + pw_log_error("can't create zeroconf: %m"); + goto error_errno; + } + pw_zeroconf_add_listener(impl->zeroconf, &impl->zeroconf_listener, + &zeroconf_events, impl); + + make_browser(impl, SERVICE_TYPE_CONTROL); + make_browser(impl, SERVICE_TYPE_JSONRPC); pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); - start_avahi(impl); - return 0; error_errno: diff --git a/src/modules/module-vban/audio.c b/src/modules/module-vban/audio.c index 099246895..40a6415dc 100644 --- a/src/modules/module-vban/audio.c +++ b/src/modules/module-vban/audio.c @@ -98,38 +98,43 @@ static int vban_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len) samples = SPA_MIN(hdr->format_nbs+1, plen / stride); n_frames = hdr->n_frames; - if (impl->have_sync && impl->n_frames != n_frames) { - pw_log_info("unexpected frame (%d != %d)", - n_frames, impl->n_frames); + + if (impl->samples_per_frame == 0) { + impl->samples_per_frame = samples; + } else if (samples != impl->samples_per_frame) { + pw_log_warn("samples_per_frame changed (%u != %u)", + samples, impl->samples_per_frame); + impl->samples_per_frame = samples; impl->have_sync = false; } + + if (impl->have_sync && impl->n_frames != n_frames) { + pw_log_info("unexpected frame (%u != %u)", + n_frames, impl->n_frames); + } impl->n_frames = n_frames + 1; - timestamp = impl->timestamp; - impl->timestamp += samples; + /* derive write position from frame counter, like module-rtp */ + timestamp = n_frames * impl->samples_per_frame; + write = timestamp + impl->target_buffer; filled = spa_ringbuffer_get_write_index(&impl->ring, &expected_write); - /* we always write to timestamp + delay */ - write = timestamp + impl->target_buffer; - if (!impl->have_sync) { - pw_log_info("sync to timestamp:%u target:%u", - timestamp, impl->target_buffer); + pw_log_info("sync to n_frames:%u timestamp:%u target:%u", + n_frames, timestamp, impl->target_buffer); /* we read from timestamp, keeping target_buffer of data * in the ringbuffer. */ impl->ring.readindex = timestamp; impl->ring.writeindex = write; filled = impl->target_buffer; + expected_write = write; spa_dll_init(&impl->dll); spa_dll_set_bw(&impl->dll, SPA_DLL_BW_MAX, 128, impl->rate); memset(impl->buffer, 0, BUFFER_SIZE); impl->have_sync = true; - } else if (expected_write != write) { - pw_log_debug("unexpected write (%u != %u)", - write, expected_write); } if (filled + samples > BUFFER_SIZE / stride) { @@ -137,14 +142,17 @@ static int vban_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len) BUFFER_SIZE / stride); impl->have_sync = false; } else { - pw_log_trace("got samples:%u", samples); + pw_log_trace("got n_frames:%u samples:%u", n_frames, samples); spa_ringbuffer_write_data(&impl->ring, impl->buffer, BUFFER_SIZE, (write * stride) & BUFFER_MASK, &buffer[hlen], (samples * stride)); + + /* only advance writeindex if this extends the frontier */ write += samples; - spa_ringbuffer_write_update(&impl->ring, write); + if ((int32_t)(write - expected_write) > 0) + spa_ringbuffer_write_update(&impl->ring, write); } return 0; } @@ -262,5 +270,6 @@ static int vban_audio_init(struct impl *impl, enum spa_direction direction) else impl->stream_events.process = vban_audio_process_playback; impl->receive_vban = vban_audio_receive; + impl->samples_per_frame = 0; return 0; } diff --git a/src/modules/module-vban/midi.c b/src/modules/module-vban/midi.c index f460bd274..484902448 100644 --- a/src/modules/module-vban/midi.c +++ b/src/modules/module-vban/midi.c @@ -163,9 +163,6 @@ static int vban_midi_receive_midi(struct impl *impl, uint8_t *packet, while (offs < plen) { int size; - uint8_t *midi_data; - size_t midi_size; - uint64_t midi_state = 0; size = get_midi_size(&packet[offs], plen - offs); if (size <= 0 || offs + size > plen) { @@ -174,18 +171,9 @@ static int vban_midi_receive_midi(struct impl *impl, uint8_t *packet, break; } - midi_data = &packet[offs]; - midi_size = size; - while (midi_size > 0) { - uint32_t ump[4]; - int ump_size = spa_ump_from_midi(&midi_data, &midi_size, - ump, sizeof(ump), 0, &midi_state); - if (ump_size <= 0) - break; + spa_pod_builder_control(&b, timestamp, SPA_CONTROL_Midi); + spa_pod_builder_bytes(&b, &packet[offs], size); - spa_pod_builder_control(&b, timestamp, SPA_CONTROL_UMP); - spa_pod_builder_bytes(&b, ump, ump_size); - } offs += size; } spa_pod_builder_pop(&b, &f[0]); @@ -237,34 +225,25 @@ static void vban_midi_flush_packets(struct impl *impl, len = 0; 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; + uint32_t size = c.value.size; + const void *data = c_body; - if (c.type != SPA_CONTROL_UMP) + if (c.type != SPA_CONTROL_Midi) 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; - } - memcpy(&impl->buffer[len], event, size); - len += size; + pw_log_debug("sending %d", len); + vban_stream_emit_send_packet(impl, iov, 2); + len = 0; } + memcpy(&impl->buffer[len], data, size); + len += size; } if (len > 0) { /* flush last packet */ diff --git a/src/modules/module-vban/stream.c b/src/modules/module-vban/stream.c index da3469583..168ecc837 100644 --- a/src/modules/module-vban/stream.c +++ b/src/modules/module-vban/stream.c @@ -61,6 +61,7 @@ struct impl { struct vban_header header; uint32_t timestamp; uint32_t n_frames; + uint32_t samples_per_frame; struct spa_ringbuffer ring; uint8_t buffer[BUFFER_SIZE]; diff --git a/src/modules/module-zeroconf-discover.c b/src/modules/module-zeroconf-discover.c index 177556637..61108d424 100644 --- a/src/modules/module-zeroconf-discover.c +++ b/src/modules/module-zeroconf-discover.c @@ -20,12 +20,8 @@ #include #include -#include -#include -#include - #include "module-protocol-pulse/format.h" -#include "module-zeroconf-discover/avahi-poll.h" +#include "zeroconf-utils/zeroconf.h" /** \page page_module_zeroconf_discover Zeroconf Discover * @@ -84,11 +80,8 @@ struct impl { struct pw_properties *properties; - bool discover_local; - AvahiPoll *avahi_poll; - AvahiClient *client; - AvahiServiceBrowser *sink_browser; - AvahiServiceBrowser *source_browser; + struct pw_zeroconf *zeroconf; + struct spa_hook zeroconf_listener; struct spa_list tunnel_list; }; @@ -107,9 +100,7 @@ struct tunnel { struct spa_hook module_listener; }; -static int start_client(struct impl *impl); - -static struct tunnel *make_tunnel(struct impl *impl, const struct tunnel_info *info) +static struct tunnel *tunnel_new(struct impl *impl, const struct tunnel_info *info) { struct tunnel *t; @@ -135,7 +126,7 @@ static struct tunnel *find_tunnel(struct impl *impl, const struct tunnel_info *i return NULL; } -static void free_tunnel(struct tunnel *t) +static void tunnel_free(struct tunnel *t) { spa_list_remove(&t->link); if (t->module) @@ -151,16 +142,10 @@ static void impl_free(struct impl *impl) struct tunnel *t; spa_list_consume(t, &impl->tunnel_list, link) - free_tunnel(t); + tunnel_free(t); - if (impl->sink_browser) - avahi_service_browser_free(impl->sink_browser); - if (impl->source_browser) - avahi_service_browser_free(impl->source_browser); - if (impl->client) - avahi_client_free(impl->client); - if (impl->avahi_poll) - pw_avahi_poll_free(impl->avahi_poll); + if (impl->zeroconf) + pw_zeroconf_destroy(impl->zeroconf); pw_properties_free(impl->properties); free(impl); } @@ -177,7 +162,7 @@ static const struct pw_impl_module_events module_events = { .destroy = module_destroy, }; -static void pw_properties_from_avahi_string(const char *key, const char *value, +static void pw_properties_from_zeroconf(const char *key, const char *value, struct pw_properties *props) { if (spa_streq(key, "device")) { @@ -241,38 +226,28 @@ static const struct pw_impl_module_events submodule_events = { .destroy = submodule_destroy, }; -static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, - AvahiResolverEvent event, const char *name, const char *type, const char *domain, - const char *host_name, const AvahiAddress *a, uint16_t port, AvahiStringList *txt, - AvahiLookupResultFlags flags, void *userdata) +static void on_zeroconf_added(void *data, const void *user_data, const struct spa_dict *info) { - struct impl *impl = userdata; + struct impl *impl = data; + const char *name, *type, *mode, *device, *host_name, *desc, *fqdn, *user, *str; struct tunnel *t; struct tunnel_info tinfo; - const char *str, *device, *desc, *fqdn, *user, *mode; - char if_suffix[16] = ""; - char at[AVAHI_ADDRESS_STR_MAX]; - AvahiStringList *l; + const struct spa_dict_item *it; FILE *f; char *args; size_t size; struct pw_impl_module *mod; struct pw_properties *props = NULL; - - if (event != AVAHI_RESOLVER_FOUND) { - pw_log_error("Resolving of '%s' failed: %s", name, - avahi_strerror(avahi_client_errno(impl->client))); - goto done; - } - + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); + type = spa_dict_lookup(info, PW_KEY_ZEROCONF_TYPE); mode = strstr(type, "sink") ? "sink" : "source"; tinfo = TUNNEL_INFO(.name = name, .mode = mode); t = find_tunnel(impl, &tinfo); if (t == NULL) - t = make_tunnel(impl, &tinfo); + t = tunnel_new(impl, &tinfo); if (t == NULL) { pw_log_error("Can't make tunnel: %m"); goto done; @@ -288,16 +263,10 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr goto done; } - for (l = txt; l; l = l->next) { - char *key, *value; + spa_dict_for_each(it, info) + pw_properties_from_zeroconf(it->key, it->value, props); - if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0) - break; - - pw_properties_from_avahi_string(key, value, props); - avahi_free(key); - avahi_free(value); - } + host_name = spa_dict_lookup(info, PW_KEY_ZEROCONF_HOSTNAME); if ((device = pw_properties_get(props, PW_KEY_TARGET_OBJECT)) != NULL) pw_properties_setf(props, PW_KEY_NODE_NAME, @@ -308,14 +277,9 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr pw_properties_set(props, "tunnel.mode", mode); - if (a->proto == AVAHI_PROTO_INET6 && - a->data.ipv6.address[0] == 0xfe && - (a->data.ipv6.address[1] & 0xc0) == 0x80) - snprintf(if_suffix, sizeof(if_suffix), "%%%d", interface); - - pw_properties_setf(props, "pulse.server.address", " [%s%s]:%u", - avahi_address_snprint(at, sizeof(at), a), - if_suffix, port); + pw_properties_setf(props, "pulse.server.address", " [%s]:%s", + spa_dict_lookup(info, PW_KEY_ZEROCONF_ADDRESS), + spa_dict_lookup(info, PW_KEY_ZEROCONF_PORT)); desc = pw_properties_get(props, "tunnel.remote.description"); if (desc == NULL) @@ -373,130 +337,33 @@ static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiPr t->module = mod; done: - avahi_service_resolver_free(r); pw_properties_free(props); } - -static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, - AvahiBrowserEvent event, const char *name, const char *type, const char *domain, - AvahiLookupResultFlags flags, void *userdata) +static void on_zeroconf_removed(void *data, const void *user, const struct spa_dict *info) { - struct impl *impl = userdata; - struct tunnel_info info; + struct impl *impl = data; + const char *name, *type, *mode; struct tunnel *t; + struct tunnel_info tinfo; - if ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && !impl->discover_local) + name = spa_dict_lookup(info, PW_KEY_ZEROCONF_NAME); + type = spa_dict_lookup(info, PW_KEY_ZEROCONF_TYPE); + mode = strstr(type, "sink") ? "sink" : "source"; + + tinfo = TUNNEL_INFO(.name = name, .mode = mode); + + if ((t = find_tunnel(impl, &tinfo)) == NULL) return; - info = TUNNEL_INFO(.name = name); - - t = find_tunnel(impl, &info); - - switch (event) { - case AVAHI_BROWSER_NEW: - if (t != NULL) { - pw_log_info("found duplicate mdns entry - skipping tunnel creation"); - return; - } - if (!(avahi_service_resolver_new(impl->client, - interface, protocol, - name, type, domain, - AVAHI_PROTO_UNSPEC, 0, - resolver_cb, impl))) - pw_log_error("can't make service resolver: %s", - avahi_strerror(avahi_client_errno(impl->client))); - break; - case AVAHI_BROWSER_REMOVE: - if (t == NULL) - return; - free_tunnel(t); - break; - default: - break; - } + tunnel_free(t); } - -static struct AvahiServiceBrowser *make_browser(struct impl *impl, const char *service_type) -{ - struct AvahiServiceBrowser *s; - - s = avahi_service_browser_new(impl->client, - AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, - service_type, NULL, 0, - browser_cb, impl); - if (s == NULL) { - pw_log_error("can't make browser for %s: %s", service_type, - avahi_strerror(avahi_client_errno(impl->client))); - } - return s; -} - -static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata) -{ - struct impl *impl = userdata; - - impl->client = c; - - switch (state) { - case AVAHI_CLIENT_S_REGISTERING: - case AVAHI_CLIENT_S_RUNNING: - case AVAHI_CLIENT_S_COLLISION: - if (impl->sink_browser == NULL) - impl->sink_browser = make_browser(impl, SERVICE_TYPE_SINK); - if (impl->sink_browser == NULL) - goto error; - - if (impl->source_browser == NULL) - impl->source_browser = make_browser(impl, SERVICE_TYPE_SOURCE); - if (impl->source_browser == NULL) - goto error; - - break; - case AVAHI_CLIENT_FAILURE: - if (avahi_client_errno(c) == AVAHI_ERR_DISCONNECTED) - start_client(impl); - - SPA_FALLTHROUGH; - case AVAHI_CLIENT_CONNECTING: - if (impl->sink_browser) { - avahi_service_browser_free(impl->sink_browser); - impl->sink_browser = NULL; - } - if (impl->source_browser) { - avahi_service_browser_free(impl->source_browser); - impl->source_browser = NULL; - } - break; - default: - break; - } - return; -error: - pw_impl_module_schedule_destroy(impl->module); -} - -static int start_client(struct impl *impl) -{ - int res; - if ((impl->client = avahi_client_new(impl->avahi_poll, - AVAHI_CLIENT_NO_FAIL, - client_callback, impl, - &res)) == NULL) { - pw_log_error("can't create client: %s", avahi_strerror(res)); - pw_impl_module_schedule_destroy(impl->module); - return -EIO; - } - return 0; -} - -static int start_avahi(struct impl *impl) -{ - impl->avahi_poll = pw_avahi_poll_new(impl->context); - - return start_client(impl); -} +static const struct pw_zeroconf_events zeroconf_events = { + PW_VERSION_ZEROCONF_EVENTS, + .added = on_zeroconf_added, + .removed = on_zeroconf_removed, +}; SPA_EXPORT int pipewire__module_init(struct pw_impl_module *module, const char *args) @@ -504,6 +371,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) struct pw_context *context = pw_impl_module_get_context(module); struct pw_properties *props; struct impl *impl; + bool discover_local; int res; PW_LOG_TOPIC_INIT(mod_topic); @@ -527,14 +395,29 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->context = context; impl->properties = props; - impl->discover_local = pw_properties_get_bool(impl->properties, + discover_local = pw_properties_get_bool(impl->properties, "pulse.discover-local", false); + pw_properties_setf(impl->properties, PW_KEY_ZEROCONF_DISCOVER_LOCAL, + discover_local ? "true" : "false"); pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl); pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props)); - start_avahi(impl); + if ((impl->zeroconf = pw_zeroconf_new(context, &props->dict)) == NULL) { + pw_log_error("can't create zeroconf: %m"); + goto error_errno; + } + pw_zeroconf_add_listener(impl->zeroconf, &impl->zeroconf_listener, + &zeroconf_events, impl); + + pw_zeroconf_set_browse(impl->zeroconf, SERVICE_TYPE_SINK, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, SERVICE_TYPE_SINK))); + + pw_zeroconf_set_browse(impl->zeroconf, SERVICE_TYPE_SOURCE, + &SPA_DICT_ITEMS( + SPA_DICT_ITEM(PW_KEY_ZEROCONF_TYPE, SERVICE_TYPE_SOURCE))); return 0; diff --git a/src/modules/network-utils.h b/src/modules/network-utils.h index a89b7d3bd..568e9cb19 100644 --- a/src/modules/network-utils.h +++ b/src/modules/network-utils.h @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -86,6 +87,64 @@ static inline int pw_net_parse_address_port(const char *address, return pw_net_parse_address(n, port, addr, len); } +static inline bool pw_net_are_addresses_equal(const struct sockaddr_storage *addr1, + const struct sockaddr_storage *addr2, + bool compare_ports) +{ + /* IPv6 addresses might actually be mapped IPv4 ones. In cases where + * such mapped IPv4 addresses are compared against plain IPv4 ones + * (that is, ss_family == AF_INET), special handling is required. */ + bool addr1_is_mapped_ipv4 = (addr1->ss_family == AF_INET6) && + IN6_IS_ADDR_V4MAPPED(&((struct sockaddr_in6*)addr1)->sin6_addr); + bool addr2_is_mapped_ipv4 = (addr2->ss_family == AF_INET6) && + IN6_IS_ADDR_V4MAPPED(&((struct sockaddr_in6*)addr2)->sin6_addr); + + if ((addr1->ss_family == AF_INET) && (addr2->ss_family == AF_INET)) { + /* Both addresses are plain IPv4 addresses. */ + + const struct sockaddr_in *addr1_in = (const struct sockaddr_in *)addr1; + const struct sockaddr_in *addr2_in = (const struct sockaddr_in *)addr2; + return (!compare_ports || (addr1_in->sin_port == addr2_in->sin_port)) && + (addr1_in->sin_addr.s_addr == addr2_in->sin_addr.s_addr); + } else if ((addr1->ss_family == AF_INET6) && (addr2->ss_family == AF_INET6)) { + /* Both addresses are IPv6 addresses. (Note that this logic here + * works correctly even if both are actually mapped IPv4 addresses.) */ + + const struct sockaddr_in6 *addr1_in6 = (const struct sockaddr_in6 *)addr1; + const struct sockaddr_in6 *addr2_in6 = (const struct sockaddr_in6 *)addr2; + return (!compare_ports || (addr1_in6->sin6_port == addr2_in6->sin6_port)) && + (addr1_in6->sin6_scope_id == addr2_in6->sin6_scope_id) && + (memcmp(&addr1_in6->sin6_addr, &addr2_in6->sin6_addr, sizeof(struct in6_addr)) == 0); + } else if ((addr1->ss_family == AF_INET) && addr2_is_mapped_ipv4) { + /* addr1 is a plain IPv4 address, addr2 is a mapped IPv4 address. + * Extract the IPv4 portion of addr2 to form a plain IPv4 address + * out of it, then compare the two plain IPv4 addresses. */ + + struct in_addr addr2_as_ipv4; + const struct sockaddr_in *addr1_in = (const struct sockaddr_in *)addr1; + const struct sockaddr_in6 *addr2_in6 = (const struct sockaddr_in6 *)addr2; + + memcpy(&addr2_as_ipv4, &(addr2_in6->sin6_addr.s6_addr[12]), 4); + + return (!compare_ports || (addr1_in->sin_port == addr2_in6->sin6_port)) && + (addr1_in->sin_addr.s_addr == addr2_as_ipv4.s_addr); + } else if (addr1_is_mapped_ipv4 && (addr2->ss_family == AF_INET)) { + /* addr2 is a plain IPv4 address, addr1 is a mapped IPv4 address. + * Extract the IPv4 portion of addr1 to form a plain IPv4 address + * out of it, then compare the two plain IPv4 addresses. */ + + struct in_addr addr1_as_ipv4; + const struct sockaddr_in6 *addr1_in6 = (const struct sockaddr_in6 *)addr1; + const struct sockaddr_in *addr2_in = (const struct sockaddr_in *)addr2; + + memcpy(&addr1_as_ipv4, &(addr1_in6->sin6_addr.s6_addr[12]), 4); + + return (!compare_ports || (addr1_in6->sin6_port == addr2_in->sin_port)) && + (addr1_as_ipv4.s_addr == addr2_in->sin_addr.s_addr); + } else + return false; +} + static inline int pw_net_get_ip(const struct sockaddr_storage *sa, char *ip, size_t len, bool *ip4, uint16_t *port) { if (ip4) @@ -143,7 +202,7 @@ static inline bool pw_net_addr_is_any(struct sockaddr_storage *addr) /* Returns the number of file descriptors passed for socket activation. * Returns 0 if none, -1 on error. */ -static inline int listen_fd(void) +static inline int listen_fds(void) { uint32_t n; int i, flags; @@ -161,8 +220,6 @@ static inline int listen_fd(void) return -1; } - unsetenv("LISTEN_FDS"); - return (int)n; } @@ -192,12 +249,10 @@ static inline int is_socket_unix(int fd, int type, const char *path) if (addr.sun_family != AF_UNIX) return 0; size_t length = strlen(path); - if (length > 0) { - if (len < offsetof(struct sockaddr_un, sun_path) + length) - return 0; - if (memcmp(addr.sun_path, path, length) != 0) - return 0; - } + if (len < offsetof(struct sockaddr_un, sun_path) + length + 1) + return 0; + if (memcmp(addr.sun_path, path, length + 1) != 0) + return 0; } return 1; diff --git a/src/modules/module-zeroconf-discover/avahi-poll.c b/src/modules/zeroconf-utils/avahi-poll.c similarity index 100% rename from src/modules/module-zeroconf-discover/avahi-poll.c rename to src/modules/zeroconf-utils/avahi-poll.c diff --git a/src/modules/module-zeroconf-discover/avahi-poll.h b/src/modules/zeroconf-utils/avahi-poll.h similarity index 100% rename from src/modules/module-zeroconf-discover/avahi-poll.h rename to src/modules/zeroconf-utils/avahi-poll.h diff --git a/src/modules/zeroconf-utils/zeroconf.c b/src/modules/zeroconf-utils/zeroconf.c new file mode 100644 index 000000000..a3ec5f4a8 --- /dev/null +++ b/src/modules/zeroconf-utils/zeroconf.c @@ -0,0 +1,622 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2021 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#include +#include + +#include +#include +#include +#include + +#include "avahi-poll.h" +#include "zeroconf.h" + +#define pw_zeroconf_emit(o,m,v,...) spa_hook_list_call(&o->listener_list, struct pw_zeroconf_events, m, v, ##__VA_ARGS__) +#define pw_zeroconf_emit_destroy(c) pw_zeroconf_emit(c, destroy, 0) +#define pw_zeroconf_emit_error(c,e,m) pw_zeroconf_emit(c, error, 0, e, m) +#define pw_zeroconf_emit_added(c,id,i) pw_zeroconf_emit(c, added, 0, id, i) +#define pw_zeroconf_emit_removed(c,id,i) pw_zeroconf_emit(c, removed, 0, id, i) + +struct service_info { + AvahiIfIndex interface; + AvahiProtocol protocol; + const char *name; + const char *type; + const char *domain; + const char *host_name; + AvahiAddress address; + uint16_t port; +}; + +#define SERVICE_INFO(...) ((struct service_info){ __VA_ARGS__ }) + +#define STR_TO_PROTO(s) (atoi(s) == 6 ? AVAHI_PROTO_INET6 : AVAHI_PROTO_INET) + +struct entry { + struct pw_zeroconf *zc; + struct spa_list link; + +#define TYPE_ANNOUNCE 0 +#define TYPE_BROWSE 1 + uint32_t type; + const void *user; + + struct pw_properties *props; + + AvahiEntryGroup *group; + AvahiServiceBrowser *browser; + + struct spa_list services; +}; + +struct service { + struct entry *e; + struct spa_list link; + + struct service_info info; + + struct pw_properties *props; +}; + +struct pw_zeroconf { + int refcount; + struct pw_context *context; + + struct pw_properties *props; + + struct spa_hook_list listener_list; + + AvahiPoll *poll; + AvahiClient *client; + AvahiClientState state; + + struct spa_list entries; + + bool discover_local; +}; + +static struct service *service_find(struct entry *e, const struct service_info *info) +{ + struct service *s; + spa_list_for_each(s, &e->services, link) { + if (s->info.interface == info->interface && + s->info.protocol == info->protocol && + spa_streq(s->info.name, info->name) && + spa_streq(s->info.type, info->type) && + spa_streq(s->info.domain, info->domain)) + return s; + } + return NULL; +} + +static void service_free(struct service *s) +{ + spa_list_remove(&s->link); + free((void*)s->info.name); + free((void*)s->info.type); + free((void*)s->info.domain); + free((void*)s->info.host_name); + pw_properties_free(s->props); + free(s); +} + +struct entry *entry_find(struct pw_zeroconf *zc, uint32_t type, const void *user) +{ + struct entry *e; + spa_list_for_each(e, &zc->entries, link) + if (e->type == type && e->user == user) + return e; + return NULL; +} + +static void entry_free(struct entry *e) +{ + struct service *s; + + spa_list_remove(&e->link); + if (e->group) + avahi_entry_group_free(e->group); + spa_list_consume(s, &e->services, link) + service_free(s); + pw_properties_free(e->props); + free(e); +} + +static void zeroconf_free(struct pw_zeroconf *zc) +{ + struct entry *a; + + spa_list_consume(a, &zc->entries, link) + entry_free(a); + + if (zc->client) + avahi_client_free(zc->client); + if (zc->poll) + pw_avahi_poll_free(zc->poll); + pw_properties_free(zc->props); + free(zc); +} + +static void zeroconf_unref(struct pw_zeroconf *zc) +{ + if (--zc->refcount == 0) + zeroconf_free(zc); +} + +void pw_zeroconf_destroy(struct pw_zeroconf *zc) +{ + pw_zeroconf_emit_destroy(zc); + + zeroconf_unref(zc); +} + +static struct service *service_new(struct entry *e, + const struct service_info *info, AvahiStringList *txt) +{ + struct service *s; + struct pw_zeroconf *zc = e->zc; + const AvahiAddress *a = &info->address; + static const char *link_local_range = "169.254."; + AvahiStringList *l; + char at[AVAHI_ADDRESS_STR_MAX], if_suffix[16] = ""; + + if ((s = calloc(1, sizeof(*s))) == NULL) + goto error; + + s->e = e; + spa_list_append(&e->services, &s->link); + + s->info.interface = info->interface; + s->info.protocol = info->protocol; + s->info.name = strdup(info->name); + s->info.type = strdup(info->type); + s->info.domain = strdup(info->domain); + s->info.host_name = strdup(info->host_name); + s->info.address = info->address; + s->info.port = info->port; + + if ((s->props = pw_properties_new(NULL, NULL)) == NULL) + goto error; + + if (a->proto == AVAHI_PROTO_INET6 && info->interface != AVAHI_IF_UNSPEC && + a->data.ipv6.address[0] == 0xfe && + (a->data.ipv6.address[1] & 0xc0) == 0x80) + snprintf(if_suffix, sizeof(if_suffix), "%%%d", info->interface); + + avahi_address_snprint(at, sizeof(at), a); + if (a->proto == AVAHI_PROTO_INET && info->interface != AVAHI_IF_UNSPEC && + spa_strstartswith(at, link_local_range)) + snprintf(if_suffix, sizeof(if_suffix), "%%%d", info->interface); + + if (info->interface != AVAHI_IF_UNSPEC) + pw_properties_setf(s->props, PW_KEY_ZEROCONF_IFINDEX, "%d", info->interface); + if (a->proto != AVAHI_PROTO_UNSPEC) + pw_properties_set(s->props, PW_KEY_ZEROCONF_PROTO, + a->proto == AVAHI_PROTO_INET ? "4" : "6"); + + pw_properties_set(s->props, PW_KEY_ZEROCONF_NAME, info->name); + pw_properties_set(s->props, PW_KEY_ZEROCONF_TYPE, info->type); + pw_properties_set(s->props, PW_KEY_ZEROCONF_DOMAIN, info->domain); + pw_properties_set(s->props, PW_KEY_ZEROCONF_HOSTNAME, info->host_name); + pw_properties_setf(s->props, PW_KEY_ZEROCONF_ADDRESS, "%s%s", at, if_suffix); + pw_properties_setf(s->props, PW_KEY_ZEROCONF_PORT, "%u", info->port); + + for (l = txt; l; l = l->next) { + char *key, *value; + + if (avahi_string_list_get_pair(l, &key, &value, NULL) != 0) + break; + + pw_properties_set(s->props, key, value); + avahi_free(key); + avahi_free(value); + } + + pw_log_info("new %s %s %s %s", info->name, info->type, info->domain, info->host_name); + pw_zeroconf_emit_added(zc, e->user, &s->props->dict); + + return s; + +error: + if (s) + service_free(s); + return NULL; +} + +static void resolver_cb(AvahiServiceResolver *r, AvahiIfIndex interface, + AvahiProtocol protocol, AvahiResolverEvent event, + const char *name, const char *type, const char *domain, + const char *host_name, const AvahiAddress *a, uint16_t port, + AvahiStringList *txt, AvahiLookupResultFlags flags, + void *userdata) +{ + struct entry *e = userdata; + struct pw_zeroconf *zc = e->zc; + struct service_info info; + + if (event != AVAHI_RESOLVER_FOUND) { + pw_log_error("Resolving of '%s' failed: %s", name, + avahi_strerror(avahi_client_errno(zc->client))); + goto done; + } + + info = SERVICE_INFO(.interface = interface, + .protocol = protocol, + .name = name, + .type = type, + .domain = domain, + .host_name = host_name, + .address = *a, + .port = port); + + service_new(e, &info, txt); +done: + avahi_service_resolver_free(r); +} + +static void browser_cb(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, + AvahiBrowserEvent event, const char *name, const char *type, const char *domain, + AvahiLookupResultFlags flags, void *userdata) +{ + struct entry *e = userdata; + struct pw_zeroconf *zc = e->zc; + struct service_info info; + struct service *s; + int aproto = AVAHI_PROTO_UNSPEC; + const char *str; + + if ((flags & AVAHI_LOOKUP_RESULT_LOCAL) && !zc->discover_local) + return; + + info = SERVICE_INFO(.interface = interface, + .protocol = protocol, + .name = name, + .type = type, + .domain = domain); + + s = service_find(e, &info); + + switch (event) { + case AVAHI_BROWSER_NEW: + if (s != NULL) + return; + + if ((str = pw_properties_get(e->props, PW_KEY_ZEROCONF_RESOLVE_PROTO))) + aproto = STR_TO_PROTO(str); + + if (!(avahi_service_resolver_new(zc->client, + interface, protocol, + name, type, domain, + aproto, 0, + resolver_cb, e))) { + int res = avahi_client_errno(zc->client); + pw_log_error("can't make service resolver: %s", avahi_strerror(res)); + pw_zeroconf_emit_error(zc, res, avahi_strerror(res)); + } + break; + case AVAHI_BROWSER_REMOVE: + if (s == NULL) + return; + pw_log_info("removed %s %s %s", name, type, domain); + pw_zeroconf_emit_removed(zc, e->user, &s->props->dict); + service_free(s); + break; + default: + break; + } +} + +static int do_browse(struct pw_zeroconf *zc, struct entry *e) +{ + const struct spa_dict_item *it; + const char *type = NULL, *domain = NULL; + int res, ifindex = AVAHI_IF_UNSPEC, proto = AVAHI_PROTO_UNSPEC; + + if (e->browser == NULL) { + spa_dict_for_each(it, &e->props->dict) { + if (spa_streq(it->key, PW_KEY_ZEROCONF_IFINDEX)) + ifindex = atoi(it->value); + else if (spa_streq(it->key, PW_KEY_ZEROCONF_PROTO)) + proto = STR_TO_PROTO(it->value); + else if (spa_streq(it->key, PW_KEY_ZEROCONF_TYPE)) + type = it->value; + else if (spa_streq(it->key, PW_KEY_ZEROCONF_DOMAIN)) + domain = it->value; + } + if (type == NULL) { + res = -EINVAL; + pw_log_error("can't make browser: no "PW_KEY_ZEROCONF_TYPE" provided"); + pw_zeroconf_emit_error(zc, res, spa_strerror(res)); + return res; + } + e->browser = avahi_service_browser_new(zc->client, + ifindex, proto, type, domain, 0, + browser_cb, e); + if (e->browser == NULL) { + res = avahi_client_errno(zc->client); + pw_log_error("can't make browser: %s", avahi_strerror(res)); + pw_zeroconf_emit_error(zc, res, avahi_strerror(res)); + return -EIO; + } + } + return 0; +} + +static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) +{ + struct entry *e = userdata; + struct pw_zeroconf *zc = e->zc; + const char *name; + int res; + + zc->refcount++; + + name = pw_properties_get(e->props, PW_KEY_ZEROCONF_NAME); + + switch (state) { + case AVAHI_ENTRY_GROUP_ESTABLISHED: + pw_log_debug("Entry \"%s\" added", name); + break; + case AVAHI_ENTRY_GROUP_COLLISION: + pw_log_error("Entry \"%s\" name collision", name); + break; + case AVAHI_ENTRY_GROUP_FAILURE: + res = avahi_client_errno(zc->client); + pw_log_error("Entry \"%s\" failure: %s", name, avahi_strerror(res)); + pw_zeroconf_emit_error(zc, res, avahi_strerror(res)); + break; + case AVAHI_ENTRY_GROUP_UNCOMMITED: + case AVAHI_ENTRY_GROUP_REGISTERING: + break; + } + zeroconf_unref(zc); +} + +static int do_announce(struct pw_zeroconf *zc, struct entry *e) +{ + AvahiStringList *txt = NULL; + int res, ifindex = AVAHI_IF_UNSPEC, proto = AVAHI_PROTO_UNSPEC; + const struct spa_dict_item *it; + const char *name = "unnamed", *type = NULL, *subtypes = NULL; + const char *domain = NULL, *host = NULL; + uint16_t port = 0; + + if (e->group == NULL) { + e->group = avahi_entry_group_new(zc->client, + entry_group_callback, e); + if (e->group == NULL) { + res = avahi_client_errno(zc->client); + pw_log_error("can't make group: %s", avahi_strerror(res)); + pw_zeroconf_emit_error(zc, res, avahi_strerror(res)); + return -EIO; + } + } + avahi_entry_group_reset(e->group); + + spa_dict_for_each(it, &e->props->dict) { + if (spa_streq(it->key, PW_KEY_ZEROCONF_IFINDEX)) + ifindex = atoi(it->value); + else if (spa_streq(it->key, PW_KEY_ZEROCONF_PROTO)) + proto = STR_TO_PROTO(it->value); + else if (spa_streq(it->key, PW_KEY_ZEROCONF_NAME)) + name = it->value; + else if (spa_streq(it->key, PW_KEY_ZEROCONF_TYPE)) + type = it->value; + else if (spa_streq(it->key, PW_KEY_ZEROCONF_DOMAIN)) + domain = it->value; + else if (spa_streq(it->key, PW_KEY_ZEROCONF_HOST)) + host = it->value; + else if (spa_streq(it->key, PW_KEY_ZEROCONF_PORT)) + port = atoi(it->value); + else if (spa_streq(it->key, PW_KEY_ZEROCONF_SUBTYPES)) + subtypes = it->value; + else if (!spa_strstartswith(it->key, "zeroconf.")) + txt = avahi_string_list_add_pair(txt, it->key, it->value); + } + if (type == NULL) { + res = -EINVAL; + pw_log_error("can't announce: no "PW_KEY_ZEROCONF_TYPE" provided"); + pw_zeroconf_emit_error(zc, res, spa_strerror(res)); + avahi_string_list_free(txt); + return res; + } + res = avahi_entry_group_add_service_strlst(e->group, + ifindex, proto, + (AvahiPublishFlags)0, name, + type, domain, host, port, txt); + avahi_string_list_free(txt); + + if (res < 0) { + res = avahi_client_errno(zc->client); + pw_log_error("can't add service: %s", avahi_strerror(res)); + pw_zeroconf_emit_error(zc, res, avahi_strerror(res)); + return -EIO; + } + + if (subtypes) { + struct spa_json iter; + char v[512]; + + if (spa_json_begin_array_relax(&iter, subtypes, strlen(subtypes)) <= 0) { + res = -EINVAL; + pw_log_error("invalid subtypes: %s", subtypes); + pw_zeroconf_emit_error(zc, res, spa_strerror(res)); + return res; + } + while (spa_json_get_string(&iter, v, sizeof(v)) > 0) { + res = avahi_entry_group_add_service_subtype(e->group, + ifindex, proto, + (AvahiPublishFlags)0, name, + type, domain, v); + if (res < 0) { + res = avahi_client_errno(zc->client); + pw_log_error("can't add subtype %s: %s", v, avahi_strerror(res)); + pw_zeroconf_emit_error(zc, res, avahi_strerror(res)); + return -EIO; + } + } + } + if ((res = avahi_entry_group_commit(e->group)) < 0) { + res = avahi_client_errno(zc->client); + pw_log_error("can't commit group: %s", avahi_strerror(res)); + pw_zeroconf_emit_error(zc, res, avahi_strerror(res)); + return -EIO; + } + return 0; +} + +static int entry_start(struct pw_zeroconf *zc, struct entry *e) +{ + if (zc->state != AVAHI_CLIENT_S_REGISTERING && + zc->state != AVAHI_CLIENT_S_RUNNING && + zc->state != AVAHI_CLIENT_S_COLLISION) + return 0; + + if (e->type == TYPE_ANNOUNCE) + return do_announce(zc, e); + else + return do_browse(zc, e); +} + +static void client_callback(AvahiClient *c, AvahiClientState state, void *d) +{ + struct pw_zeroconf *zc = d; + struct entry *e; + + zc->client = c; + zc->refcount++; + zc->state = state; + + switch (state) { + case AVAHI_CLIENT_S_REGISTERING: + case AVAHI_CLIENT_S_RUNNING: + case AVAHI_CLIENT_S_COLLISION: + spa_list_for_each(e, &zc->entries, link) + entry_start(zc, e); + break; + case AVAHI_CLIENT_FAILURE: + { + int err = avahi_client_errno(c); + pw_zeroconf_emit_error(zc, err, avahi_strerror(err)); + break; + } + case AVAHI_CLIENT_CONNECTING: + default: + break; + } + zeroconf_unref(zc); +} + +static struct entry *entry_new(struct pw_zeroconf *zc, uint32_t type, const void *user, const struct spa_dict *info) +{ + struct entry *e; + + if ((e = calloc(1, sizeof(*e))) == NULL) + return NULL; + + e->zc = zc; + e->type = type; + e->user = user; + e->props = pw_properties_new_dict(info); + spa_list_append(&zc->entries, &e->link); + spa_list_init(&e->services); + + if (type == TYPE_ANNOUNCE) + pw_log_debug("created announce for \"%s\"", + pw_properties_get(e->props, PW_KEY_ZEROCONF_NAME)); + else + pw_log_debug("created browse for \"%s\"", + pw_properties_get(e->props, PW_KEY_ZEROCONF_TYPE)); + return e; +} + +static int set_entry(struct pw_zeroconf *zc, uint32_t type, const void *user, const struct spa_dict *info) +{ + struct entry *e; + int res = 0; + + e = entry_find(zc, type, user); + if (e == NULL) { + if (info == NULL) + return 0; + if ((e = entry_new(zc, type, user, info)) == NULL) + return -errno; + res = entry_start(zc, e); + } else { + if (info == NULL) + entry_free(e); + else { + pw_properties_update(e->props, info); + res = entry_start(zc, e); + } + } + return res; +} +int pw_zeroconf_set_announce(struct pw_zeroconf *zc, const void *user, const struct spa_dict *info) +{ + return set_entry(zc, TYPE_ANNOUNCE, user, info); +} +int pw_zeroconf_set_browse(struct pw_zeroconf *zc, const void *user, const struct spa_dict *info) +{ + return set_entry(zc, TYPE_BROWSE, user, info); +} + +struct pw_zeroconf * pw_zeroconf_new(struct pw_context *context, + struct spa_dict *props) +{ + struct pw_zeroconf *zc; + uint32_t i; + int res; + + if ((zc = calloc(1, sizeof(*zc))) == NULL) + return NULL; + + zc->refcount = 1; + zc->context = context; + spa_hook_list_init(&zc->listener_list); + spa_list_init(&zc->entries); + zc->props = props ? pw_properties_new_dict(props) : pw_properties_new(NULL, NULL); + zc->discover_local = true; + + for (i = 0; props && i < props->n_items; i++) { + const char *k = props->items[i].key; + const char *v = props->items[i].value; + + if (spa_streq(k, PW_KEY_ZEROCONF_DISCOVER_LOCAL) && v) + zc->discover_local = spa_atob(v); + } + + zc->poll = pw_avahi_poll_new(context); + if (zc->poll == NULL) + goto error; + + zc->client = avahi_client_new(zc->poll, AVAHI_CLIENT_NO_FAIL, + client_callback, zc, &res); + if (!zc->client) { + pw_log_error("failed to create avahi client: %s", avahi_strerror(res)); + goto error; + } + return zc; +error: + zeroconf_free(zc); + return NULL; +} + +void pw_zeroconf_add_listener(struct pw_zeroconf *zc, + struct spa_hook *listener, + const struct pw_zeroconf_events *events, void *data) +{ + spa_hook_list_append(&zc->listener_list, listener, events, data); +} diff --git a/src/modules/zeroconf-utils/zeroconf.h b/src/modules/zeroconf-utils/zeroconf.h new file mode 100644 index 000000000..3fbd0bde8 --- /dev/null +++ b/src/modules/zeroconf-utils/zeroconf.h @@ -0,0 +1,59 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 Wim Taymans */ +/* SPDX-License-Identifier: MIT */ + +#ifndef PIPEWIRE_ZEROCONF_H +#define PIPEWIRE_ZEROCONF_H + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define PW_KEY_ZEROCONF_DISCOVER_LOCAL "zeroconf.discover-local" /* discover local services, true by default */ + +#define PW_KEY_ZEROCONF_IFINDEX "zeroconf.ifindex" /* interface index */ +#define PW_KEY_ZEROCONF_PROTO "zeroconf.proto" /* protocol version, "4" ot "6" */ +#define PW_KEY_ZEROCONF_NAME "zeroconf.name" /* session name */ +#define PW_KEY_ZEROCONF_TYPE "zeroconf.type" /* service type, like "_http._tcp", not NULL */ +#define PW_KEY_ZEROCONF_DOMAIN "zeroconf.domain" /* domain to register in, recommended NULL */ +#define PW_KEY_ZEROCONF_HOST "zeroconf.host" /* host to register on, recommended NULL */ +#define PW_KEY_ZEROCONF_SUBTYPES "zeroconf.subtypes" /* subtypes to register, array of strings */ +#define PW_KEY_ZEROCONF_RESOLVE_PROTO "zeroconf.resolve-proto" /* protocol to resolve to, "4" or "6" */ +#define PW_KEY_ZEROCONF_HOSTNAME "zeroconf.hostname" /* hostname of resolved service */ +#define PW_KEY_ZEROCONF_PORT "zeroconf.port" /* port of resolved service */ +#define PW_KEY_ZEROCONF_ADDRESS "zeroconf.address" /* address of resolved service */ + +struct pw_zeroconf; + +struct pw_zeroconf_events { +#define PW_VERSION_ZEROCONF_EVENTS 0 + uint32_t version; + + void (*destroy) (void *data); + void (*error) (void *data, int err, const char *message); + + void (*added) (void *data, const void *user, const struct spa_dict *info); + void (*removed) (void *data, const void *user, const struct spa_dict *info); +}; + +struct pw_zeroconf * pw_zeroconf_new(struct pw_context *context, + struct spa_dict *props); + +void pw_zeroconf_destroy(struct pw_zeroconf *zc); + +int pw_zeroconf_set_announce(struct pw_zeroconf *zc, const void *user, const struct spa_dict *info); +int pw_zeroconf_set_browse(struct pw_zeroconf *zc, const void *user, const struct spa_dict *info); + +void pw_zeroconf_add_listener(struct pw_zeroconf *zc, + struct spa_hook *listener, + const struct pw_zeroconf_events *events, void *data); + +#ifdef __cplusplus +} +#endif + +#endif /* PIPEWIRE_ZEROCONF_H */ diff --git a/src/pipewire/buffers.c b/src/pipewire/buffers.c index db1a01551..ac3911f33 100644 --- a/src/pipewire/buffers.c +++ b/src/pipewire/buffers.c @@ -146,8 +146,11 @@ param_filter(struct pw_buffers *this, if (in_res < 1) { /* in_res == -ENOENT : unknown parameter, assume NULL and we will * exit the loop below. - * in_res < 1 : some error or no data, exit now + * in_res == 0 : no data, assume NULL + * in_res < 0 : some error, exit now */ + if (in_res == 0) + in_res = -ENOENT; if (in_res == -ENOENT) iparam = NULL; else @@ -163,6 +166,8 @@ param_filter(struct pw_buffers *this, id, &oidx, iparam, &oparam, result); /* out_res < 1 : no value or error, exit now */ + if (out_res == 0) + out_res = -ENOENT; if (out_res < 1) break; diff --git a/src/pipewire/capabilities.h b/src/pipewire/capabilities.h index d3040b761..3bb6c7b20 100644 --- a/src/pipewire/capabilities.h +++ b/src/pipewire/capabilities.h @@ -21,15 +21,16 @@ extern "C" { * \{ */ -/**< Link capable of device ID negotiation. The value is either "true" or "false" */ +/**< Link capable of device ID negotiation. The value is to the version of the + * API specification. */ #define PW_CAPABILITY_DEVICE_ID_NEGOTIATION "pipewire.device-id-negotiation" /**< Link with device ID negotition capability supports negotiating with - * provided list of devices. The value consists of a JSON encoded string array - * of base64 encoded dev_t values. */ + * a specific set of devices. The value of API version 1 consists of a JSON + * object containing a single key "available-devices" that contain a list of + * hexadecimal encoded `dev_t` device IDs. + */ #define PW_CAPABILITY_DEVICE_IDS "pipewire.device-ids" -#define PW_CAPABILITY_DEVICE_ID "pipewire.device-id" /**< Link capable of device Id negotation */ - /** \} */ diff --git a/src/pipewire/context.c b/src/pipewire/context.c index 1c30be70a..e3f65ad41 100644 --- a/src/pipewire/context.c +++ b/src/pipewire/context.c @@ -36,8 +36,6 @@ PW_LOG_TOPIC_EXTERN(log_context); #define PW_LOG_TOPIC_DEFAULT log_context -#define MAX_HOPS 64 -#define MAX_SYNC 4u #define MAX_LOOPS 64u #define DEFAULT_DATA_LOOPS 1 @@ -112,13 +110,17 @@ static void fill_core_properties(struct pw_context *context) pw_properties_set(properties, PW_KEY_CORE_NAME, context->core->info.name); } -static int context_set_freewheel(struct pw_context *context, bool freewheel) +SPA_EXPORT +int pw_context_set_freewheel(struct pw_context *context, bool freewheel) { struct impl *impl = SPA_CONTAINER_OF(context, struct impl, this); struct spa_thread *thr; uint32_t i; int res = 0; + if (context->freewheeling == freewheel) + return 0; + for (i = 0; i < impl->n_data_loops; i++) { if (impl->data_loops[i].impl == NULL || (thr = pw_data_loop_get_thread(impl->data_loops[i].impl)) == NULL) @@ -351,17 +353,19 @@ static int adjust_rlimits(const struct spa_dict *dict) [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_STACK] = "stack", +#ifdef __linux__ + [RLIMIT_LOCKS] = "locks", + [RLIMIT_MSGQUEUE] = "msgqueue", + [RLIMIT_NICE] = "nice", [RLIMIT_RTPRIO] = "rtprio", [RLIMIT_RTTIME] = "rttime", [RLIMIT_SIGPENDING] = "sigpending", - [RLIMIT_STACK] = "stack", +#endif }; int res; spa_dict_for_each(it, dict) { @@ -982,468 +986,9 @@ SPA_PRINTF_FUNC(7, 8) int pw_context_debug_port_params(struct pw_context *this, return 0; } -static int ensure_state(struct pw_impl_node *node, bool running) -{ - enum pw_node_state state = node->info.state; - if (node->active && node->runnable && - !SPA_FLAG_IS_SET(node->spa_flags, SPA_NODE_FLAG_NEED_CONFIGURE) && running) - state = PW_NODE_STATE_RUNNING; - else if (state > PW_NODE_STATE_IDLE) - state = PW_NODE_STATE_IDLE; - return pw_impl_node_set_state(node, state); -} - -/* From a node (that is runnable) follow all prepared links in the given direction - * and groups to active nodes and make them recursively runnable as well. - */ -static inline int run_nodes(struct pw_context *context, struct pw_impl_node *node, - struct spa_list *nodes, enum pw_direction direction, int hop) -{ - struct pw_impl_node *t; - struct pw_impl_port *p; - struct pw_impl_link *l; - - if (hop == MAX_HOPS) { - pw_log_warn("exceeded hops (%d)", hop); - return -EIO; - } - - pw_log_debug("node %p: '%s' direction:%s", node, node->name, - pw_direction_as_string(direction)); - - SPA_FLAG_SET(node->checked, 1u<input_ports, link) { - spa_list_for_each(l, &p->links, input_link) { - t = l->output->node; - - if (!t->active || !l->prepared || - (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<driving && p->node == t) - continue; - - pw_log_debug(" peer %p: '%s'", t, t->name); - t->runnable = true; - run_nodes(context, t, nodes, direction, hop + 1); - } - } - } else { - spa_list_for_each(p, &node->output_ports, link) { - spa_list_for_each(l, &p->links, output_link) { - t = l->input->node; - - if (!t->active || !l->prepared || - (!t->driving && SPA_FLAG_IS_SET(t->checked, 1u<driving && p->node == t) - continue; - - pw_log_debug(" peer %p: '%s'", t, t->name); - t->runnable = true; - run_nodes(context, t, nodes, direction, hop + 1); - } - } - } - /* now go through all the nodes that have the same link group and - * that are not yet visited. Note how nodes with the same group - * don't get included here. They were added to the same driver but - * need to otherwise stay idle unless some non-passive link activates - * them. */ - if (node->link_groups != NULL) { - spa_list_for_each(t, nodes, sort_link) { - if (t->exported || !t->active || - SPA_FLAG_IS_SET(t->checked, 1u<link_groups, node->link_groups) < 0) - continue; - - pw_log_debug(" group %p: '%s'", t, t->name); - t->runnable = true; - if (!t->driving) - run_nodes(context, t, nodes, direction, hop + 1); - } - } - return 0; -} - -/* Follow all prepared links and groups from node, activate the links. - * If a non-passive link is found, we set the peer runnable flag. - * - * After this is done, we end up with a list of nodes in collect that are all - * linked to node. - * Some of the nodes have the runnable flag set. We then start from those nodes - * and make all linked nodes and groups runnable as well. (see run_nodes). - * - * This ensures that we only activate the paths from the runnable nodes to the - * driver nodes and leave the other nodes idle. - */ -static int collect_nodes(struct pw_context *context, struct pw_impl_node *node, struct spa_list *collect) -{ - struct spa_list queue; - struct pw_impl_node *n, *t; - struct pw_impl_port *p; - struct pw_impl_link *l; - uint32_t n_sync; - char *sync[MAX_SYNC+1]; - - pw_log_debug("node %p: '%s'", node, node->name); - - /* start with node in the queue */ - spa_list_init(&queue); - spa_list_append(&queue, &node->sort_link); - node->visited = true; - - n_sync = 0; - sync[0] = NULL; - - /* now follow all the links from the nodes in the queue - * and add the peers to the queue. */ - spa_list_consume(n, &queue, sort_link) { - spa_list_remove(&n->sort_link); - spa_list_append(collect, &n->sort_link); - - pw_log_debug(" next node %p: '%s' runnable:%u active:%d", - n, n->name, n->runnable, n->active); - - if (!n->active) - continue; - - if (n->sync) { - for (uint32_t i = 0; n->sync_groups[i]; i++) { - if (n_sync >= MAX_SYNC) - break; - if (pw_strv_find(sync, n->sync_groups[i]) >= 0) - continue; - sync[n_sync++] = n->sync_groups[i]; - sync[n_sync] = NULL; - } - } - - spa_list_for_each(p, &n->input_ports, link) { - spa_list_for_each(l, &p->links, input_link) { - t = l->output->node; - - if (!t->active) - continue; - - pw_impl_link_prepare(l); - - if (!l->prepared) - continue; - - if (!l->passive) - t->runnable = true; - - if (!t->visited) { - t->visited = true; - spa_list_append(&queue, &t->sort_link); - } - } - } - spa_list_for_each(p, &n->output_ports, link) { - spa_list_for_each(l, &p->links, output_link) { - t = l->input->node; - - if (!t->active) - continue; - - pw_impl_link_prepare(l); - - if (!l->prepared) - continue; - - if (!l->passive) - t->runnable = true; - - if (!t->visited) { - t->visited = true; - spa_list_append(&queue, &t->sort_link); - } - } - } - /* now go through all the nodes that have the same group and - * that are not yet visited */ - if (n->groups != NULL || n->link_groups != NULL || sync[0] != NULL) { - spa_list_for_each(t, &context->node_list, link) { - if (t->exported || !t->active || t->visited) - continue; - /* the other node will be scheduled with this one if it's in - * the same group or link group */ - if (pw_strv_find_common(t->groups, n->groups) < 0 && - pw_strv_find_common(t->link_groups, n->link_groups) < 0 && - pw_strv_find_common(t->sync_groups, sync) < 0) - continue; - - pw_log_debug("%p: %s join group of %s", - t, t->name, n->name); - t->visited = true; - spa_list_append(&queue, &t->sort_link); - } - } - pw_log_debug(" next node %p: '%s' runnable:%u %p %p %p", n, n->name, n->runnable, - n->groups, n->link_groups, sync); - } - /* 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; -} - -static void move_to_driver(struct pw_context *context, struct spa_list *nodes, - struct pw_impl_node *driver) -{ - struct pw_impl_node *n; - pw_log_debug("driver: %p %s runnable:%u", driver, driver->name, driver->runnable); - spa_list_consume(n, nodes, sort_link) { - spa_list_remove(&n->sort_link); - - driver->runnable |= n->runnable; - - pw_log_debug(" follower: %p %s runnable:%u driver-runnable:%u", n, n->name, - n->runnable, driver->runnable); - pw_impl_node_set_driver(n, driver); - } -} -static void remove_from_driver(struct pw_context *context, struct spa_list *nodes) -{ - struct pw_impl_node *n; - spa_list_consume(n, nodes, sort_link) { - spa_list_remove(&n->sort_link); - pw_impl_node_set_driver(n, NULL); - ensure_state(n, false); - } -} - -static inline void get_quantums(struct pw_context *context, uint32_t *def, - uint32_t *min, uint32_t *max, uint32_t *rate, uint32_t *floor, uint32_t *ceil) -{ - struct settings *s = &context->settings; - if (s->clock_force_quantum != 0) { - *def = *min = *max = s->clock_force_quantum; - *rate = 0; - } else { - *def = s->clock_quantum; - *min = s->clock_min_quantum; - *max = s->clock_max_quantum; - *rate = s->clock_rate; - } - *floor = s->clock_quantum_floor; - *ceil = s->clock_quantum_limit; -} - -static inline const uint32_t *get_rates(struct pw_context *context, uint32_t *def, uint32_t *n_rates, - bool *force) -{ - struct settings *s = &context->settings; - if (s->clock_force_rate != 0) { - *force = true; - *n_rates = 1; - *def = s->clock_force_rate; - return &s->clock_force_rate; - } else { - *force = false; - *n_rates = s->n_clock_rates; - *def = s->clock_rate; - return s->clock_rates; - } -} -static void reconfigure_driver(struct pw_context *context, struct pw_impl_node *n) -{ - struct pw_impl_node *s; - - spa_list_for_each(s, &n->follower_list, follower_link) { - if (s == n) - continue; - pw_log_debug("%p: follower %p: '%s' suspend", - context, s, s->name); - pw_impl_node_set_state(s, PW_NODE_STATE_SUSPENDED); - } - pw_log_debug("%p: driver %p: '%s' suspend", - context, n, n->name); - - if (n->info.state >= PW_NODE_STATE_IDLE) - n->need_resume = !n->pause_on_idle; - pw_impl_node_set_state(n, PW_NODE_STATE_SUSPENDED); -} - -/* find smaller power of 2 */ -static uint32_t flp2(uint32_t x) -{ - x = x | (x >> 1); - x = x | (x >> 2); - x = x | (x >> 4); - x = x | (x >> 8); - x = x | (x >> 16); - return x - (x >> 1); -} - -/* cmp fractions, avoiding overflows */ -static int fraction_compare(const struct spa_fraction *a, const struct spa_fraction *b) -{ - uint64_t fa = (uint64_t)a->num * (uint64_t)b->denom; - uint64_t fb = (uint64_t)b->num * (uint64_t)a->denom; - return fa < fb ? -1 : (fa > fb ? 1 : 0); -} - -static inline uint32_t calc_gcd(uint32_t a, uint32_t b) -{ - while (b != 0) { - uint32_t temp = a; - a = b; - b = temp % b; - } - return a; -} - -struct rate_info { - uint32_t rate; - uint32_t gcd; - uint32_t diff; -}; - -static inline void update_highest_rate(struct rate_info *best, struct rate_info *current) -{ - /* find highest rate */ - if (best->rate == 0 || best->rate < current->rate) - *best = *current; -} - -static inline void update_nearest_gcd(struct rate_info *best, struct rate_info *current) -{ - /* find nearest GCD */ - if (best->rate == 0 || - (best->gcd < current->gcd) || - (best->gcd == current->gcd && best->diff > current->diff)) - *best = *current; -} -static inline void update_nearest_rate(struct rate_info *best, struct rate_info *current) -{ - /* find nearest rate */ - if (best->rate == 0 || best->diff > current->diff) - *best = *current; -} - -static uint32_t find_best_rate(const uint32_t *rates, uint32_t n_rates, uint32_t rate, uint32_t def) -{ - uint32_t i, limit; - struct rate_info best; - struct rate_info info[n_rates]; - - for (i = 0; i < n_rates; i++) { - info[i].rate = rates[i]; - info[i].gcd = calc_gcd(rate, rates[i]); - info[i].diff = SPA_ABS((int32_t)rate - (int32_t)rates[i]); - } - - /* first find higher nearest GCD. This tries to find next bigest rate that - * requires the least amount of resample filter banks. Usually these are - * rates that are multiples of each other or multiples of a common rate. - * - * 44100 and [ 32000 56000 88200 96000 ] -> 88200 - * 48000 and [ 32000 56000 88200 96000 ] -> 96000 - * 88200 and [ 44100 48000 96000 192000 ] -> 96000 - * 32000 and [ 44100 192000 ] -> 44100 - * 8000 and [ 44100 48000 ] -> 48000 - * 8000 and [ 44100 192000 ] -> 44100 - * 11025 and [ 44100 48000 ] -> 44100 - * 44100 and [ 48000 176400 ] -> 48000 - * 144 and [ 44100 48000 88200 96000] -> 48000 - */ - spa_zero(best); - /* Don't try to do excessive upsampling by limiting the max rate - * for desired < default to default*2. For other rates allow - * a x3 upsample rate max. For values lower than half of the default, - * limit to the default. */ - limit = rate < def/2 ? def : rate < def ? def*2 : rate*3; - for (i = 0; i < n_rates; i++) { - if (info[i].rate >= rate && info[i].rate <= limit) - update_nearest_gcd(&best, &info[i]); - } - if (best.rate != 0) - return best.rate; - - /* we would need excessive upsampling, pick a nearest higher rate */ - spa_zero(best); - for (i = 0; i < n_rates; i++) { - if (info[i].rate >= rate) - update_nearest_rate(&best, &info[i]); - } - if (best.rate != 0) - return best.rate; - - /* There is nothing above the rate, we need to downsample. Try to downsample - * but only to something that is from a common rate family. Also don't - * try to downsample to something that will sound worse (< 44100). - * - * 88200 and [ 22050 44100 48000 ] -> 44100 - * 88200 and [ 22050 48000 ] -> 48000 - */ - spa_zero(best); - for (i = 0; i < n_rates; i++) { - if (info[i].rate >= 44100) - update_nearest_gcd(&best, &info[i]); - } - if (best.rate != 0) - return best.rate; - - /* There is nothing to downsample above our threshold. Downsample to whatever - * is the highest rate then. */ - spa_zero(best); - for (i = 0; i < n_rates; i++) - update_highest_rate(&best, &info[i]); - if (best.rate != 0) - return best.rate; - - return def; -} - -/* here we evaluate the complete state of the graph. - * - * It roughly operates in 3 stages: - * - * 1. go over all drivers and collect the nodes that need to be scheduled with the - * driver. This include all nodes that have an active link with the driver or - * with a node already scheduled with the driver. - * - * 2. go over all nodes that are not assigned to a driver. The ones that require - * a driver are moved to some random active driver found in step 1. - * - * 3. go over all drivers again, collect the quantum/rate of all followers, select - * the desired final value and activate the followers and then the driver. - * - * A complete graph evaluation is performed for each change that is made to the - * graph, such as making/destroying links, adding/removing nodes, property changes such - * as quantum/rate changes or metadata changes. - */ int pw_context_recalc_graph(struct pw_context *context, const char *reason) { struct impl *impl = SPA_CONTAINER_OF(context, struct impl, this); - struct settings *settings = &context->settings; - struct pw_impl_node *n, *s, *target, *fallback; - const uint32_t *rates; - uint32_t max_quantum, min_quantum, def_quantum, rate_quantum, floor_quantum, ceil_quantum; - uint32_t n_rates, def_rate, transport; - bool freewheel, global_force_rate, global_force_quantum; - struct spa_list collect; pw_log_info("%p: busy:%d reason:%s", context, impl->recalc, reason); @@ -1454,392 +999,14 @@ int pw_context_recalc_graph(struct pw_context *context, const char *reason) again: impl->recalc = true; - freewheel = false; - /* clean up the flags first */ - spa_list_for_each(n, &context->node_list, link) { - n->visited = false; - n->checked = 0; - n->runnable = n->always_process && n->active; - } + pw_context_emit_recalc_graph(context); - get_quantums(context, &def_quantum, &min_quantum, &max_quantum, &rate_quantum, - &floor_quantum, &ceil_quantum); - rates = get_rates(context, &def_rate, &n_rates, &global_force_rate); - - global_force_quantum = rate_quantum == 0; - - /* start from all drivers and group all nodes that are linked - * to it. Some nodes are not (yet) linked to anything and they - * will end up 'unassigned' to a driver. Other nodes are drivers - * and if they have active followers, we can use them to schedule - * the unassigned nodes. */ - target = fallback = NULL; - spa_list_for_each(n, &context->driver_list, driver_link) { - if (n->exported) - continue; - - if (!n->visited) { - spa_list_init(&collect); - collect_nodes(context, n, &collect); - move_to_driver(context, &collect, n); - } - /* from now on we are only interested in active driving nodes - * with a driver_priority. We're going to see if there are - * active followers. */ - if (!n->driving || !n->active || n->priority_driver <= 0) - continue; - - /* first active driving node is fallback */ - if (fallback == NULL) - fallback = n; - - if (!n->runnable) - continue; - - spa_list_for_each(s, &n->follower_list, follower_link) { - pw_log_debug("%p: driver %p: follower %p %s: active:%d", - context, n, s, s->name, s->active); - if (s != n && s->active) { - /* if the driving node has active followers, it - * is a target for our unassigned nodes */ - if (target == NULL) - target = n; - if (n->freewheel) - freewheel = true; - break; - } - } - } - /* no active node, use fallback driving node */ - if (target == NULL) - target = fallback; - - /* update the freewheel status */ - if (context->freewheeling != freewheel) - context_set_freewheel(context, freewheel); - - /* now go through all available nodes. The ones we didn't visit - * in collect_nodes() are not linked to any driver. We assign them - * to either an active driver or the first driver if they are in a - * group that needs a driver. Else we remove them from a driver - * and stop them. */ - spa_list_for_each(n, &context->node_list, link) { - struct pw_impl_node *t, *driver; - - if (n->exported || n->visited) - continue; - - pw_log_debug("%p: unassigned node %p: '%s' active:%d want_driver:%d target:%p", - context, n, n->name, n->active, n->want_driver, target); - - /* collect all nodes in this group */ - spa_list_init(&collect); - collect_nodes(context, n, &collect); - - driver = NULL; - spa_list_for_each(t, &collect, sort_link) { - /* is any active and want a driver */ - if ((t->want_driver && t->active && t->runnable) || - t->always_process) { - driver = target; - break; - } - } - if (driver != NULL) { - driver->runnable = true; - /* driver needed for this group */ - move_to_driver(context, &collect, driver); - } else { - /* no driver, make sure the nodes stop */ - remove_from_driver(context, &collect); - } - } - - /* assign final quantum and set state for followers and drivers */ - spa_list_for_each(n, &context->driver_list, driver_link) { - bool running = false, lock_quantum = false, lock_rate = false; - struct spa_fraction latency = SPA_FRACTION(0, 0); - struct spa_fraction max_latency = SPA_FRACTION(0, 0); - struct spa_fraction rate = SPA_FRACTION(0, 0); - uint32_t target_quantum, target_rate, current_rate, current_quantum; - uint64_t quantum_stamp = 0, rate_stamp = 0; - bool force_rate, force_quantum, restore_rate = false, restore_quantum = false; - bool do_reconfigure = false, need_resume, was_target_pending; - bool have_request = false; - const uint32_t *node_rates; - uint32_t node_n_rates, node_def_rate; - uint32_t node_max_quantum, node_min_quantum, node_def_quantum, node_rate_quantum; - - if (!n->driving || n->exported) - continue; - - node_def_quantum = def_quantum; - node_min_quantum = min_quantum; - node_max_quantum = max_quantum; - node_rate_quantum = rate_quantum; - force_quantum = global_force_quantum; - - node_def_rate = def_rate; - node_n_rates = n_rates; - node_rates = rates; - force_rate = global_force_rate; - - /* collect quantum and rate */ - spa_list_for_each(s, &n->follower_list, follower_link) { - - if (!s->moved) { - /* We only try to enforce the lock flags for nodes that - * are not recently moved between drivers. The nodes that - * are moved should try to enforce their quantum on the - * new driver. */ - lock_quantum |= s->lock_quantum; - lock_rate |= s->lock_rate; - } - if (!global_force_quantum && s->force_quantum > 0 && - s->stamp > quantum_stamp) { - node_def_quantum = node_min_quantum = node_max_quantum = s->force_quantum; - node_rate_quantum = 0; - quantum_stamp = s->stamp; - force_quantum = true; - } - if (!global_force_rate && s->force_rate > 0 && - s->stamp > rate_stamp) { - node_def_rate = s->force_rate; - node_n_rates = 1; - node_rates = &s->force_rate; - force_rate = true; - rate_stamp = s->stamp; - } - - /* smallest latencies */ - if (latency.denom == 0 || - (s->latency.denom > 0 && - fraction_compare(&s->latency, &latency) < 0)) - latency = s->latency; - if (max_latency.denom == 0 || - (s->max_latency.denom > 0 && - fraction_compare(&s->max_latency, &max_latency) < 0)) - max_latency = s->max_latency; - - /* largest rate, which is in fact the smallest fraction */ - if (rate.denom == 0 || - (s->rate.denom > 0 && - fraction_compare(&s->rate, &rate) < 0)) - rate = s->rate; - - if (s->active) - running = n->runnable; - - pw_log_debug("%p: follower %p running:%d runnable:%d rate:%u/%u latency %u/%u '%s'", - context, s, running, s->runnable, rate.num, rate.denom, - latency.num, latency.denom, s->name); - - if (running && s != n && s->supports_request > 0) - have_request = true; - - s->moved = false; - } - - if (n->forced_rate && !force_rate && n->runnable) { - /* A node that was forced to a rate but is no longer being - * forced can restore its rate */ - pw_log_info("(%s-%u) restore rate", n->name, n->info.id); - restore_rate = true; - } - if (n->forced_quantum && !force_quantum && n->runnable) { - /* A node that was forced to a quantum but is no longer being - * forced can restore its quantum */ - pw_log_info("(%s-%u) restore quantum", n->name, n->info.id); - restore_quantum = true; - } - - if (force_quantum) - lock_quantum = false; - if (force_rate) - lock_rate = false; - - need_resume = n->need_resume; - if (need_resume) { - running = true; - n->need_resume = false; - } - - current_rate = n->target_rate.denom; - if (!restore_rate && - (lock_rate || need_resume || !running || - (!force_rate && (n->info.state > PW_NODE_STATE_IDLE)))) { - pw_log_debug("%p: keep rate:1/%u restore:%u lock:%u resume:%u " - "running:%u force:%u state:%s", context, - current_rate, restore_rate, lock_rate, need_resume, - running, force_rate, - pw_node_state_as_string(n->info.state)); - - /* when we don't need to restore or rate and - * when someone wants us to lock the rate of this driver or - * when we are in the process of reconfiguring the driver or - * when we are not running any followers or - * when the driver is busy and we don't need to force a rate, - * keep the current rate */ - target_rate = current_rate; - } - else { - /* Here we are allowed to change the rate of the driver. - * Start with the default rate. If the desired rate is - * allowed, switch to it */ - if (rate.denom != 0 && rate.num == 1) - target_rate = rate.denom; - else - target_rate = node_def_rate; - - target_rate = find_best_rate(node_rates, node_n_rates, - target_rate, node_def_rate); - - pw_log_debug("%p: def_rate:%d target_rate:%d rate:%d/%d", context, - node_def_rate, target_rate, rate.num, rate.denom); - } - - was_target_pending = n->target_pending; - - if (target_rate != current_rate) { - /* we doing a rate switch */ - pw_log_info("(%s-%u) state:%s new rate:%u/(%u)->%u", - n->name, n->info.id, - pw_node_state_as_string(n->info.state), - n->target_rate.denom, current_rate, - target_rate); - - if (force_rate) { - if (settings->clock_rate_update_mode == CLOCK_RATE_UPDATE_MODE_HARD) - do_reconfigure |= !was_target_pending; - } else { - if (n->info.state >= PW_NODE_STATE_SUSPENDED) - do_reconfigure |= !was_target_pending; - } - /* we're setting the pending rate. This will become the new - * current rate in the next iteration of the graph. */ - n->target_rate = SPA_FRACTION(1, target_rate); - n->forced_rate = force_rate; - n->target_pending = true; - current_rate = target_rate; - } - - if (node_rate_quantum != 0 && current_rate != node_rate_quantum) { - /* the quantum values are scaled with the current rate */ - node_def_quantum = SPA_SCALE32(node_def_quantum, current_rate, node_rate_quantum); - node_min_quantum = SPA_SCALE32(node_min_quantum, current_rate, node_rate_quantum); - node_max_quantum = SPA_SCALE32(node_max_quantum, current_rate, node_rate_quantum); - } - - /* calculate desired quantum. Don't limit to the max_latency when we are - * going to force a quantum or rate and reconfigure the nodes. */ - if (max_latency.denom != 0 && !force_quantum && !force_rate) { - uint32_t tmp = SPA_SCALE32(max_latency.num, current_rate, max_latency.denom); - if (tmp < node_max_quantum) - node_max_quantum = tmp; - } - - current_quantum = n->target_quantum; - if (!restore_quantum && (lock_quantum || need_resume || !running)) { - pw_log_debug("%p: keep quantum:%u restore:%u lock:%u resume:%u " - "running:%u force:%u state:%s", context, - current_quantum, restore_quantum, lock_quantum, need_resume, - running, force_quantum, - pw_node_state_as_string(n->info.state)); - target_quantum = current_quantum; - } - else { - target_quantum = node_def_quantum; - if (latency.denom != 0) - target_quantum = SPA_SCALE32(latency.num, current_rate, latency.denom); - target_quantum = SPA_CLAMP(target_quantum, node_min_quantum, node_max_quantum); - target_quantum = SPA_CLAMP(target_quantum, floor_quantum, ceil_quantum); - - if (settings->clock_power_of_two_quantum && !force_quantum) - target_quantum = flp2(target_quantum); - } - - if (target_quantum != current_quantum) { - pw_log_info("(%s-%u) new quantum:%"PRIu64"->%u", - n->name, n->info.id, - n->target_quantum, - target_quantum); - /* this is the new pending quantum */ - n->target_quantum = target_quantum; - n->forced_quantum = force_quantum; - n->target_pending = true; - - if (force_quantum) - do_reconfigure |= !was_target_pending; - } - - if (n->target_pending) { - if (do_reconfigure) { - reconfigure_driver(context, n); - /* we might be suspended now and the links need to be prepared again */ - goto again; - } - /* we have a pending change. We place the new values in the - * pending fields so that they are picked up by the driver in - * the next cycle */ - pw_log_debug("%p: apply duration:%"PRIu64" rate:%u/%u", context, - n->target_quantum, n->target_rate.num, - n->target_rate.denom); - SPA_SEQ_WRITE(n->rt.position->clock.target_seq); - n->rt.position->clock.target_duration = n->target_quantum; - n->rt.position->clock.target_rate = n->target_rate; - SPA_SEQ_WRITE(n->rt.position->clock.target_seq); - - if (n->info.state < PW_NODE_STATE_RUNNING) { - n->rt.position->clock.duration = n->target_quantum; - n->rt.position->clock.rate = n->target_rate; - } - n->target_pending = false; - } else { - n->target_quantum = n->rt.position->clock.target_duration; - n->target_rate = n->rt.position->clock.target_rate; - } - - if (n->info.state < PW_NODE_STATE_RUNNING) - n->rt.position->clock.nsec = get_time_ns(n->rt.target.system); - - SPA_FLAG_UPDATE(n->rt.position->clock.flags, - SPA_IO_CLOCK_FLAG_LAZY, have_request && n->supports_lazy > 0); - - pw_log_debug("%p: driver %p running:%d runnable:%d quantum:%u rate:%u (%"PRIu64"/%u)'%s'", - context, n, running, n->runnable, target_quantum, target_rate, - n->rt.position->clock.target_duration, - n->rt.position->clock.target_rate.denom, n->name); - - transport = PW_NODE_ACTIVATION_COMMAND_NONE; - - /* first change the node states of the followers to the new target */ - spa_list_for_each(s, &n->follower_list, follower_link) { - if (s->transport != PW_NODE_ACTIVATION_COMMAND_NONE) { - transport = s->transport; - s->transport = PW_NODE_ACTIVATION_COMMAND_NONE; - } - if (s == n) - continue; - pw_log_debug("%p: follower %p: active:%d '%s'", - context, s, s->active, s->name); - ensure_state(s, running); - } - - if (transport != PW_NODE_ACTIVATION_COMMAND_NONE) { - pw_log_info("%s: transport %d", n->name, transport); - SPA_ATOMIC_STORE(n->rt.target.activation->command, transport); - } - - /* now that all the followers are ready, start the driver */ - ensure_state(n, running); - } impl->recalc = false; if (impl->recalc_pending) { impl->recalc_pending = false; goto again; } - return 0; } diff --git a/src/pipewire/context.h b/src/pipewire/context.h index 61c6662c4..5eaa8de30 100644 --- a/src/pipewire/context.h +++ b/src/pipewire/context.h @@ -51,7 +51,7 @@ struct pw_impl_node; /** context events emitted by the context object added with \ref pw_context_add_listener */ struct pw_context_events { -#define PW_VERSION_CONTEXT_EVENTS 1 +#define PW_VERSION_CONTEXT_EVENTS 2 uint32_t version; /** The context is being destroyed */ @@ -69,6 +69,9 @@ struct pw_context_events { void (*driver_added) (void *data, struct pw_impl_node *node); /** a driver was removed, since 0.3.75 version:1 */ void (*driver_removed) (void *data, struct pw_impl_node *node); + + /** recalculate the graph state, since 1.7.0 version:2 */ + void (*recalc_graph) (void *data); }; /** Make a new context object for a given main_loop. Ownership of the properties is taken, even diff --git a/src/pipewire/filter.c b/src/pipewire/filter.c index 85d16a6b1..e88a5db0a 100644 --- a/src/pipewire/filter.c +++ b/src/pipewire/filter.c @@ -692,7 +692,10 @@ static int map_data(struct filter *impl, struct spa_data *data, int prot) void *ptr; struct pw_map_range range; - pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize); + if (pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize) < 0) { + pw_log_error("%p: invalid buffer map range", impl); + return -EOVERFLOW; + } ptr = mmap(NULL, range.size, prot, MAP_SHARED, data->fd, range.offset); if (ptr == MAP_FAILED) { @@ -721,7 +724,8 @@ static int unmap_data(struct filter *impl, struct spa_data *data) { struct pw_map_range range; - pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize); + if (pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize) < 0) + return -EOVERFLOW; if (munmap(SPA_PTROFF(data->data, -range.start, void), range.size) < 0) pw_log_warn("%p: failed to unmap: %m", impl); @@ -1793,11 +1797,13 @@ static void add_control_dsp_port_params(struct filter *impl, struct port *port, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control), 0); +#if 0 if (types != 0) { spa_pod_builder_add(&b, SPA_FORMAT_CONTROL_types, SPA_POD_CHOICE_FLAGS_Int(types), 0); } +#endif add_param(impl, port, SPA_PARAM_EnumFormat, PARAM_FLAG_LOCKED, spa_pod_builder_pop(&b, &f[0])); } @@ -1857,10 +1863,13 @@ void *pw_filter_add_port(struct pw_filter *filter, add_video_dsp_port_params(impl, p); else if (spa_streq(str, "8 bit raw midi")) add_control_dsp_port_params(impl, p, 1u << SPA_CONTROL_Midi); - else if (spa_streq(str, "8 bit raw control")) + else if (spa_streq(str, "8 bit raw control")) { add_control_dsp_port_params(impl, p, 0); + pw_properties_set(props, "control.ump", "false"); + } else if (spa_streq(str, "32 bit raw UMP")) { add_control_dsp_port_params(impl, p, 1u << SPA_CONTROL_UMP); + pw_properties_set(props, "control.ump", "true"); pw_properties_set(props, PW_KEY_FORMAT_DSP, "8 bit raw midi"); } } diff --git a/src/pipewire/impl-link.c b/src/pipewire/impl-link.c index a77dcf35f..6eae5e6c7 100644 --- a/src/pipewire/impl-link.c +++ b/src/pipewire/impl-link.c @@ -701,7 +701,10 @@ static int do_allocation(struct pw_impl_link *this) /* always enable async mode */ alloc_flags = PW_BUFFERS_FLAG_ASYNC; - if (output->node->remote || input->node->remote) + /* shared mem can only be used if both nodes are in the same process + * and we are sure that the buffers are never going to be shared + * because of the exclusive flag */ + if (output->node->remote || input->node->remote || !output->exclusive) alloc_flags |= PW_BUFFERS_FLAG_SHARED; if (output->node->driver) @@ -969,16 +972,14 @@ static void output_remove(struct pw_impl_link *this) this->output = NULL; } +SPA_EXPORT int pw_impl_link_prepare(struct pw_impl_link *this) { struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); - pw_log_debug("%p: prepared:%d preparing:%d in_active:%d out_active:%d passive:%u", + pw_log_debug("%p: prepared:%d preparing:%d in_active:%d out_active:%d", this, this->prepared, this->preparing, - impl->input.node->active, impl->output.node->active, this->passive); - - if (!impl->input.node->active || !impl->output.node->active) - return 0; + impl->input.node->active, impl->output.node->active); if (this->destroyed || this->preparing || this->prepared) return 0; @@ -1053,13 +1054,13 @@ static void port_state_changed(struct pw_impl_link *this, struct pw_impl_port *p case PW_IMPL_PORT_STATE_INIT: case PW_IMPL_PORT_STATE_CONFIGURE: if (this->prepared || state < old) { - this->prepared = false; + this->prepared = this->preparing = false; link_update_state(this, PW_LINK_STATE_INIT, 0, NULL); } break; case PW_IMPL_PORT_STATE_READY: if (this->prepared || state < old) { - this->prepared = false; + this->prepared = this->preparing = false; link_update_state(this, PW_LINK_STATE_NEGOTIATING, 0, NULL); } break; @@ -1219,12 +1220,6 @@ static void output_node_result(void *data, int seq, int res, uint32_t type, cons node_result(impl, &impl->output, seq, res, type, result); } -static void node_active_changed(void *data, bool active) -{ - struct impl *impl = data; - 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; @@ -1239,14 +1234,12 @@ static void node_driver_changed(void *data, struct pw_impl_node *old, struct pw_ 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, }; @@ -1476,7 +1469,6 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context, struct impl *impl; struct pw_impl_link *this; struct pw_impl_node *input_node, *output_node; - const char *str; int res; if (output == input) @@ -1526,15 +1518,6 @@ struct pw_impl_link *pw_context_create_link(struct pw_context *context, this->output = output; this->input = input; - /* passive means that this link does not make the nodes active */ - str = pw_properties_get(properties, PW_KEY_LINK_PASSIVE); - this->passive = str ? spa_atob(str) : - (output->passive && input_node->can_suspend) || - (input->passive && output_node->can_suspend) || - (input->passive && output->passive); - if (this->passive && str == NULL) - pw_properties_set(properties, PW_KEY_LINK_PASSIVE, "true"); - 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); diff --git a/src/pipewire/impl-module.c b/src/pipewire/impl-module.c index 22c8e91fa..54db8f868 100644 --- a/src/pipewire/impl-module.c +++ b/src/pipewire/impl-module.c @@ -141,7 +141,7 @@ pw_context_load_module(struct pw_context *context, { struct pw_impl_module *this; struct impl *impl; - void *hnd; + void *hnd = NULL; char *filename = NULL; const char *module_dir; int res; @@ -154,6 +154,9 @@ pw_context_load_module(struct pw_context *context, NULL }; + while ((p = strstr(name, "../")) != NULL) + name = p + 3; + pw_log_info("%p: name:%s args:%s", context, name, args); module_dir = getenv("PIPEWIRE_MODULE_DIR"); diff --git a/src/pipewire/impl-node.c b/src/pipewire/impl-node.c index 85bf452b0..2cfb0c2c8 100644 --- a/src/pipewire/impl-node.c +++ b/src/pipewire/impl-node.c @@ -528,30 +528,17 @@ static void node_update_state(struct pw_impl_node *node, enum pw_node_state stat static int suspend_node(struct pw_impl_node *this) { + struct impl *impl = SPA_CONTAINER_OF(this, struct impl, this); int res = 0; struct pw_impl_port *p; pw_log_debug("%p: suspend node state:%s", this, pw_node_state_as_string(this->info.state)); - if (this->info.state > 0 && this->info.state <= PW_NODE_STATE_SUSPENDED) + if ((this->info.state > 0 && this->info.state < PW_NODE_STATE_SUSPENDED) || + (this->info.state == PW_NODE_STATE_SUSPENDED && impl->pending_state == PW_NODE_STATE_SUSPENDED)) return 0; - spa_list_for_each(p, &this->input_ports, link) { - if (p->busy_count > 0) { - pw_log_debug("%p: can't suspend, input port %d busy:%d", - this, p->port_id, p->busy_count); - return -EBUSY; - } - } - spa_list_for_each(p, &this->output_ports, link) { - if (p->busy_count > 0) { - pw_log_debug("%p: can't suspend, output port %d busy:%d", - this, p->port_id, p->busy_count); - return -EBUSY; - } - } - node_deactivate(this); pw_log_debug("%p: suspend node driving:%d driver:%d prepared:%d", this, @@ -1136,11 +1123,12 @@ static void check_properties(struct pw_impl_node *node) { struct impl *impl = SPA_CONTAINER_OF(node, struct impl, this); struct pw_context *context = node->context; - const char *str, *recalc_reason = NULL; + const char *str, *recalc_reason = NULL, *state = NULL, *s; struct spa_fraction frac; uint32_t value; bool driver, trigger, sync, async; struct match match; + size_t len; match = MATCH_INIT(node); pw_context_conf_section_match_rules(context, "node.rules", @@ -1261,20 +1249,29 @@ static void check_properties(struct pw_impl_node *node) SPA_FLAG_UPDATE(node->rt.target.activation->flags, PW_NODE_ACTIVATION_FLAG_ASYNC, async); } - if ((str = pw_properties_get(node->properties, PW_KEY_MEDIA_CLASS)) != NULL && - (strstr(str, "/Sink") != NULL || strstr(str, "/Source") != NULL)) { - node->can_suspend = true; - } else { - node->can_suspend = false; + if ((str = pw_properties_get(node->properties, PW_KEY_NODE_PASSIVE)) == NULL) { + if ((str = pw_properties_get(node->properties, PW_KEY_MEDIA_CLASS)) != NULL && + (strstr(str, "/Duplex") || strstr(str, "/Sink") || strstr(str, "/Source"))) + str = "follow-suspend"; + else + str = "false"; + } + while ((s = pw_split_walk(str, ",\0", &len, &state))) { + char v[32] = { 0 }; + snprintf(v, sizeof(v), "%.*s", (int)len, s); + + if (spa_streq(v, "out")) + node->passive_mode[SPA_DIRECTION_OUTPUT] = PASSIVE_MODE_TRUE; + else if (spa_streq(v, "in")) + node->passive_mode[SPA_DIRECTION_INPUT] = PASSIVE_MODE_TRUE; + else if (spa_strstartswith(v, "out-")) + node->passive_mode[SPA_DIRECTION_OUTPUT] = passive_mode_from_string(v+4); + else if (spa_strstartswith(v, "in-")) + node->passive_mode[SPA_DIRECTION_INPUT] = passive_mode_from_string(v+3); + else + node->passive_mode[SPA_DIRECTION_INPUT] = + node->passive_mode[SPA_DIRECTION_OUTPUT] = passive_mode_from_string(v); } - if ((str = pw_properties_get(node->properties, PW_KEY_NODE_PASSIVE)) == NULL) - str = "false"; - if (spa_streq(str, "out")) - node->out_passive = true; - else if (spa_streq(str, "in")) - node->in_passive = true; - else - node->in_passive = node->out_passive = spa_atob(str); node->want_driver = pw_properties_get_bool(node->properties, PW_KEY_NODE_WANT_DRIVER, false); node->always_process = pw_properties_get_bool(node->properties, PW_KEY_NODE_ALWAYS_PROCESS, false); @@ -1325,10 +1322,6 @@ 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) @@ -1343,8 +1336,10 @@ static void check_properties(struct pw_impl_node *node) recalc_reason = "force rate changed"; } - pw_log_debug("%p: driver:%d recalc:%s active:%d", node, node->driver, - recalc_reason, node->active); + pw_log_debug("%p: driver:%d recalc:%s active:%d passive:%s:%s", node, node->driver, + recalc_reason, node->active, + passive_mode_to_string(node->passive_mode[0]), + passive_mode_to_string(node->passive_mode[1])); if (recalc_reason != NULL && node->active) pw_context_recalc_graph(context, recalc_reason); @@ -2635,8 +2630,13 @@ int pw_impl_node_for_each_param(struct pw_impl_node *node, spa_hook_remove(&listener); if (user_data.cache) { - pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL); - pi->user = 1; + if (SPA_RESULT_IS_OK(res) && !SPA_RESULT_IS_ASYNC(res)) { + pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL); + pi->user = 1; + } + else { + pw_param_clear(&impl->pending_list, SPA_ID_INVALID); + } } } return res; diff --git a/src/pipewire/impl-port.c b/src/pipewire/impl-port.c index ed6fe9f1e..c34060aee 100644 --- a/src/pipewire/impl-port.c +++ b/src/pipewire/impl-port.c @@ -537,10 +537,12 @@ static int check_properties(struct pw_impl_port *port) &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 ((str = pw_properties_get(port->properties, PW_KEY_PORT_PASSIVE)) == NULL) { + /* inherit passive state from parent node */ + port->passive_mode = node->passive_mode[port->direction]; + } else { + port->passive_mode = passive_mode_from_string(str); + } if (media_class != NULL && (strstr(media_class, "Sink") != NULL || @@ -1087,11 +1089,11 @@ int pw_impl_port_set_mix(struct pw_impl_port *port, struct spa_node *node, uint3 static int setup_mixer(struct pw_impl_port *port, const struct spa_pod *param) { - uint32_t media_type, media_subtype; + uint32_t media_type, media_subtype, n_items; int res; - const char *fallback_lib, *factory_name; + const char *fallback_lib, *factory_name, *str; struct spa_handle *handle; - struct spa_dict_item items[3]; + struct spa_dict_item items[4]; char quantum_limit[16]; void *iface; struct pw_context *context = port->node->context; @@ -1143,14 +1145,17 @@ static int setup_mixer(struct pw_impl_port *port, const struct spa_pod *param) return -ENOTSUP; } - items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_LIBRARY_NAME, fallback_lib); + n_items = 0; + items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LIBRARY_NAME, fallback_lib); spa_scnprintf(quantum_limit, sizeof(quantum_limit), "%u", context->settings.clock_quantum_limit); - items[1] = SPA_DICT_ITEM_INIT("clock.quantum-limit", quantum_limit); - items[2] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_LOOP_NAME, port->node->data_loop->name); + items[n_items++] = SPA_DICT_ITEM_INIT("clock.quantum-limit", quantum_limit); + items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_NODE_LOOP_NAME, port->node->data_loop->name); + if ((str = pw_properties_get(port->properties, "control.ump")) != NULL) + items[n_items++] = SPA_DICT_ITEM_INIT("control.ump", str); handle = pw_context_load_spa_handle(context, factory_name, - &SPA_DICT_INIT_ARRAY(items)); + &SPA_DICT_INIT(items, n_items)); if (handle == NULL) return -errno; @@ -1725,8 +1730,13 @@ int pw_impl_port_for_each_param(struct pw_impl_port *port, spa_hook_remove(&listener); if (user_data.cache) { - pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL); - pi->user = 1; + if (SPA_RESULT_IS_OK(res) && !SPA_RESULT_IS_ASYNC(res)) { + pw_param_update(&impl->param_list, &impl->pending_list, 0, NULL); + pi->user = 1; + } + else { + pw_param_clear(&impl->pending_list, SPA_ID_INVALID); + } } } @@ -1934,7 +1944,7 @@ int pw_impl_port_recalc_tag(struct pw_impl_port *port) count++; } } - param = count == 0 ? NULL : spa_tag_build_end(&b.b, &f); + param = spa_tag_build_end(&b.b, &f); old = port->tag[direction]; diff --git a/src/pipewire/keys.h b/src/pipewire/keys.h index 13694bc29..08e6fb2df 100644 --- a/src/pipewire/keys.h +++ b/src/pipewire/keys.h @@ -212,8 +212,9 @@ extern "C" { #define PW_KEY_NODE_VIRTUAL "node.virtual" /**< the node is some sort of virtual * object */ #define PW_KEY_NODE_PASSIVE "node.passive" /**< indicate that a node wants passive links - * on output/input/all ports when the value is - * "out"/"in"/"true" respectively */ + * on output/input/all ports. A ','-separated + * array of values "out"/"out-follow"/"in"/ + * "in-follow"/"true"/"follow" is accepted. */ #define PW_KEY_NODE_LINK_GROUP "node.link-group" /**< the node is internally linked to * nodes with the same link-group. Can be an * array of group names. */ @@ -247,7 +248,8 @@ extern "C" { #define PW_KEY_PORT_CACHE_PARAMS "port.cache-params" /**< cache the node port params */ #define PW_KEY_PORT_EXTRA "port.extra" /**< api specific extra port info, API name * should be prefixed. "jack:flags:56" */ -#define PW_KEY_PORT_PASSIVE "port.passive" /**< the ports wants passive links, since 0.3.67 */ +#define PW_KEY_PORT_PASSIVE "port.passive" /**< the ports wants passive links. Values + * can be "true" or "follow". 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 */ @@ -261,7 +263,8 @@ extern "C" { #define PW_KEY_LINK_OUTPUT_PORT "link.output.port" /**< output port id of a link */ #define PW_KEY_LINK_PASSIVE "link.passive" /**< indicate that a link is passive and * does not cause the graph to be - * runnable. */ + * runnable. Deprecated look at + * port.passive properties. */ #define PW_KEY_LINK_FEEDBACK "link.feedback" /**< indicate that a link is a feedback * link and the target will receive data * in the next cycle */ diff --git a/src/pipewire/mem.c b/src/pipewire/mem.c index 642a6b78e..32389ed85 100644 --- a/src/pipewire/mem.c +++ b/src/pipewire/mem.c @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -420,7 +421,10 @@ struct pw_memmap * pw_memblock_map(struct pw_memblock *block, m = memblock_find_mapping(b, flags, offset, size); if (m == NULL) { struct pw_map_range range; - pw_map_range_init(&range, offset, size, p->pagesize); + if (pw_map_range_init(&range, offset, size, p->pagesize) < 0) { + errno = EOVERFLOW; + return NULL; + } m = memblock_map(b, flags, range.offset, range.size); if (m == NULL) @@ -856,8 +860,10 @@ void pw_memblock_free(struct pw_memblock *block) } if (block->fd != -1 && !(block->flags & PW_MEMBLOCK_FLAG_DONT_CLOSE)) { - pw_log_debug("%p: close fd:%d", pool, block->fd); - close(block->fd); + int fd = spa_steal_fd(block->fd); + pw_log_debug("%p: block:%p close fd:%d", pool, block, fd); + if (close(fd) < 0) + pw_log_error("%p: block:%p close fd:%d failed: %m", pool, block, fd); } spa_hook_list_clean(&b->listener_list); diff --git a/src/pipewire/mem.h b/src/pipewire/mem.h index 52abbd54d..0c3ce73bc 100644 --- a/src/pipewire/mem.h +++ b/src/pipewire/mem.h @@ -178,14 +178,21 @@ struct pw_map_range { #define PW_MAP_RANGE_INIT (struct pw_map_range){ 0, } /** Calculate parameters to mmap() memory into \a range so that - * \a size bytes at \a offset can be mapped with mmap(). */ -PW_API_MEM void pw_map_range_init(struct pw_map_range *range, + * \a size bytes at \a offset can be mapped with mmap(). + * Returns 0 on success, -EOVERFLOW if offset + size overflows. */ +PW_API_MEM int pw_map_range_init(struct pw_map_range *range, uint32_t offset, uint32_t size, uint32_t page_size) { range->offset = SPA_ROUND_DOWN_N(offset, page_size); range->start = offset - range->offset; + if (size > UINT32_MAX - range->start) + return -EOVERFLOW; + /* Check that rounding up to page_size won't overflow */ + if (range->start + size > UINT32_MAX - (page_size - 1)) + return -EOVERFLOW; range->size = SPA_ROUND_UP_N(range->start + size, page_size); + return 0; } /** diff --git a/src/pipewire/pipewire.c b/src/pipewire/pipewire.c index 4451fa525..bccd48f35 100644 --- a/src/pipewire/pipewire.c +++ b/src/pipewire/pipewire.c @@ -232,6 +232,9 @@ static struct spa_handle *load_spa_handle(const char *lib, if (lib == NULL) lib = sup->support_lib; + while ((p = strstr(lib, "../")) != NULL) + lib = p + 3; + pw_log_debug("load lib:'%s' factory-name:'%s'", lib, factory_name); plugin = NULL; diff --git a/src/pipewire/private.h b/src/pipewire/private.h index 5582709ad..dd81178ff 100644 --- a/src/pipewire/private.h +++ b/src/pipewire/private.h @@ -364,6 +364,7 @@ pw_core_resource_errorf(struct pw_resource *resource, uint32_t id, int seq, #define pw_context_emit_global_removed(c,g) pw_context_emit(c, global_removed, 0, g) #define pw_context_emit_driver_added(c,n) pw_context_emit(c, driver_added, 1, n) #define pw_context_emit_driver_removed(c,n) pw_context_emit(c, driver_removed, 1, n) +#define pw_context_emit_recalc_graph(c) pw_context_emit(c, recalc_graph, 2) struct pw_context { struct pw_impl_core *core; /**< core object */ @@ -761,8 +762,6 @@ struct pw_impl_node { * is selected to drive the graph */ unsigned int visited:1; /**< for sorting */ unsigned int want_driver:1; /**< this node wants to be assigned to a driver */ - unsigned int in_passive:1; /**< node input links should be passive */ - unsigned int out_passive:1; /**< node output links should be passive */ unsigned int runnable:1; /**< node is runnable */ unsigned int freewheel:1; /**< if this is the freewheel driver */ unsigned int loopchecked:1; /**< for feedback loop checking */ @@ -779,15 +778,19 @@ struct pw_impl_node { unsigned int forced_quantum:1; unsigned int trigger:1; /**< has the TRIGGER property and needs an extra * trigger to start processing. */ - unsigned int can_suspend:1; unsigned int checked; /**< for sorting */ 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 */ +#define PASSIVE_MODE_FALSE 0 +#define PASSIVE_MODE_TRUE 1 +#define PASSIVE_MODE_FOLLOW 2 +#define PASSIVE_MODE_FOLLOW_SUSPEND 3 + int passive_mode[2]; /**< node input links should be passive */ + uint32_t transport; /**< latest transport request */ uint32_t port_user_data_size; /**< extra size for port user data */ @@ -961,8 +964,9 @@ struct pw_impl_port { struct spa_list node_link; bool added; } rt; /**< data only accessed from the data thread */ + int passive_mode; + 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 */ @@ -985,6 +989,30 @@ struct pw_impl_port { void *user_data; /**< extra user data */ }; +static inline const char* passive_mode_to_string(uint32_t passive_mode) +{ + switch (passive_mode) { + case PASSIVE_MODE_FALSE: + return "false"; + case PASSIVE_MODE_TRUE: + return "true"; + case PASSIVE_MODE_FOLLOW: + return "follow"; + case PASSIVE_MODE_FOLLOW_SUSPEND: + return "follow"; + } + return "unknown"; +} + +static inline uint32_t passive_mode_from_string(const char *str) +{ + if (spa_streq(str, "follow")) + return PASSIVE_MODE_FOLLOW; + else if (spa_streq(str, "follow-suspend")) + return PASSIVE_MODE_FOLLOW_SUSPEND; + return spa_atob(str); +} + struct pw_control_link { struct spa_list out_link; struct spa_list in_link; @@ -1040,7 +1068,6 @@ struct pw_impl_link { unsigned int feedback:1; unsigned int preparing:1; unsigned int prepared:1; - unsigned int passive:1; unsigned int destroyed:1; }; @@ -1269,6 +1296,8 @@ 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, ...); +int pw_context_set_freewheel(struct pw_context *context, bool freewheel); + int pw_proxy_init(struct pw_proxy *proxy, struct pw_core *core, const char *type, uint32_t version); void pw_proxy_remove(struct pw_proxy *proxy); diff --git a/src/pipewire/properties.c b/src/pipewire/properties.c index ac0aac0d0..3d7686f7a 100644 --- a/src/pipewire/properties.c +++ b/src/pipewire/properties.c @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include #include @@ -800,156 +800,32 @@ const char *pw_properties_iterate(const struct pw_properties *properties, void * return pw_array_get_unchecked(&impl->items, index, struct spa_dict_item)->key; } -#define NORMAL(c) ((c)->colors ? SPA_ANSI_RESET : "") -#define LITERAL(c) ((c)->colors ? SPA_ANSI_BRIGHT_MAGENTA : "") -#define NUMBER(c) ((c)->colors ? SPA_ANSI_BRIGHT_CYAN : "") -#define STRING(c) ((c)->colors ? SPA_ANSI_BRIGHT_GREEN : "") -#define KEY(c) ((c)->colors ? SPA_ANSI_BRIGHT_BLUE : "") -#define CONTAINER(c) ((c)->colors ? SPA_ANSI_BRIGHT_YELLOW : "") - -struct dump_config { - FILE *file; - int indent; - const char *sep; - bool colors; - bool recurse; -}; - -static int encode_string(struct dump_config *c, const char *before, - const char *val, int size, const char *after) -{ - FILE *f = c->file; - int i, len = 0; - len += fprintf(f, "%s\"", before); - for (i = 0; i < size; i++) { - char v = val[i]; - switch (v) { - case '\n': - len += fprintf(f, "\\n"); - break; - case '\r': - len += fprintf(f, "\\r"); - break; - case '\b': - len += fprintf(f, "\\b"); - break; - case '\t': - len += fprintf(f, "\\t"); - break; - case '\f': - len += fprintf(f, "\\f"); - break; - case '\\': case '"': - len += fprintf(f, "\\%c", v); - break; - default: - if (v > 0 && v < 0x20) - len += fprintf(f, "\\u%04x", v); - else - len += fprintf(f, "%c", v); - break; - } - } - len += fprintf(f, "\"%s", after); - return len-1; -} - -static int dump(struct dump_config *c, int indent, struct spa_json *it, const char *value, int len) -{ - FILE *file = c->file; - struct spa_json sub; - int count = 0; - char key[1024]; - - if (value == NULL || len == 0) { - fprintf(file, "%snull%s", LITERAL(c), NORMAL(c)); - } else if (spa_json_is_container(value, len) && !c->recurse) { - spa_json_enter_container(it, &sub, value[0]); - if (spa_json_container_len(&sub, value, len) == len) - fprintf(file, "%s%.*s%s", CONTAINER(c), len, value, NORMAL(c)); - else - encode_string(c, STRING(c), value, len, NORMAL(c)); - } else if (spa_json_is_array(value, len)) { - fprintf(file, "["); - spa_json_enter(it, &sub); - indent += c->indent; - while ((len = spa_json_next(&sub, &value)) > 0) { - fprintf(file, "%s%s%*s", count++ > 0 ? "," : "", - c->sep, indent, ""); - dump(c, indent, &sub, value, len); - } - indent -= c->indent; - fprintf(file, "%s%*s]", count > 0 ? c->sep : "", - count > 0 ? indent : 0, ""); - } else if (spa_json_is_object(value, len)) { - fprintf(file, "{"); - spa_json_enter(it, &sub); - indent += c->indent; - while ((len = spa_json_object_next(&sub, key, sizeof(key), &value)) > 0) { - fprintf(file, "%s%s%*s", - count++ > 0 ? "," : "", - c->sep, indent, ""); - encode_string(c, KEY(c), key, strlen(key), NORMAL(c)); - fprintf(file, ": "); - dump(c, indent, &sub, value, len); - } - indent -= c->indent; - fprintf(file, "%s%*s}", count > 0 ? c->sep : "", - count > 0 ? indent : 0, ""); - } else if (spa_json_is_null(value, len) || - spa_json_is_bool(value, len)) { - fprintf(file, "%s%.*s%s", LITERAL(c), len, value, NORMAL(c)); - } else if (spa_json_is_int(value, len) || - spa_json_is_float(value, len)) { - fprintf(file, "%s%.*s%s", NUMBER(c), len, value, NORMAL(c)); - } else if (spa_json_is_string(value, len)) { - fprintf(file, "%s%.*s%s", STRING(c), len, value, NORMAL(c)); - } else { - encode_string(c, STRING(c), value, len, NORMAL(c)); - } - return 0; -} - SPA_EXPORT int pw_properties_serialize_dict(FILE *f, const struct spa_dict *dict, uint32_t flags) { const struct spa_dict_item *it; - int count = 0; - struct dump_config cfg = { - .file = f, - .indent = flags & PW_PROPERTIES_FLAG_NL ? 2 : 0, - .sep = flags & PW_PROPERTIES_FLAG_NL ? "\n" : " ", - .colors = SPA_FLAG_IS_SET(flags, PW_PROPERTIES_FLAG_COLORS), - .recurse = SPA_FLAG_IS_SET(flags, PW_PROPERTIES_FLAG_RECURSE), - }, *c = &cfg; - const char *enc = flags & PW_PROPERTIES_FLAG_ARRAY ? "[]" : "{}"; + int count = 0, fl = 0; + struct spa_json_builder b; + bool array = flags & PW_PROPERTIES_FLAG_ARRAY; + bool recurse = flags & PW_PROPERTIES_FLAG_RECURSE; + + if (flags & PW_PROPERTIES_FLAG_NL) + fl |= SPA_JSON_BUILDER_FLAG_PRETTY; + if (flags & PW_PROPERTIES_FLAG_COLORS) + fl |= SPA_JSON_BUILDER_FLAG_COLOR; + if (flags & PW_PROPERTIES_FLAG_SIMPLE) + fl |= SPA_JSON_BUILDER_FLAG_SIMPLE; + + spa_json_builder_file(&b, f, fl); if (SPA_FLAG_IS_SET(flags, PW_PROPERTIES_FLAG_ENCLOSE)) - fprintf(f, "%c", enc[0]); + spa_json_builder_array_push(&b, array ? "[" : "{"); spa_dict_for_each(it, dict) { - char key[1024]; - int len; - const char *value; - struct spa_json sub; - - fprintf(f, "%s%s%*s", count == 0 ? "" : ",", c->sep, c->indent, ""); - - if (!(flags & PW_PROPERTIES_FLAG_ARRAY)) { - if (spa_json_encode_string(key, sizeof(key)-1, it->key) >= (int)sizeof(key)-1) - continue; - fprintf(f, "%s%s%s: ", KEY(c), key, NORMAL(c)); - } - value = it->value; - len = value ? strlen(value) : 0; - spa_json_init(&sub, value, len); - if (c->recurse && spa_json_next(&sub, &value) < 0) - break; - - dump(c, c->indent, &sub, value, len); + spa_json_builder_object_value(&b, recurse, array ? NULL : it->key, it->value); count++; } if (SPA_FLAG_IS_SET(flags, PW_PROPERTIES_FLAG_ENCLOSE)) - fprintf(f, "%s%c", c->sep, enc[1]); + spa_json_builder_pop(&b, array ? "]" : "}"); return count; } diff --git a/src/pipewire/properties.h b/src/pipewire/properties.h index 5dbcc283f..3138a8e15 100644 --- a/src/pipewire/properties.h +++ b/src/pipewire/properties.h @@ -154,6 +154,7 @@ pw_properties_iterate(const struct pw_properties *properties, void **state); #define PW_PROPERTIES_FLAG_ENCLOSE (1<<2) #define PW_PROPERTIES_FLAG_ARRAY (1<<3) #define PW_PROPERTIES_FLAG_COLORS (1<<4) +#define PW_PROPERTIES_FLAG_SIMPLE (1<<5) int pw_properties_serialize_dict(FILE *f, const struct spa_dict *dict, uint32_t flags); PW_API_PROPERTIES bool pw_properties_parse_bool(const char *value) { diff --git a/src/pipewire/stream.c b/src/pipewire/stream.c index 4ee2138bc..36b63c000 100644 --- a/src/pipewire/stream.c +++ b/src/pipewire/stream.c @@ -807,7 +807,10 @@ static int map_data(struct stream *impl, struct spa_data *data, int prot) void *ptr; struct pw_map_range range; - pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize); + if (pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize) < 0) { + pw_log_error("%p: invalid buffer map range", impl); + return -EOVERFLOW; + } ptr = mmap(NULL, range.size, prot, MAP_SHARED, data->fd, range.offset); if (ptr == MAP_FAILED) { @@ -837,7 +840,8 @@ static int unmap_data(struct stream *impl, struct spa_data *data) { struct pw_map_range range; - pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize); + if (pw_map_range_init(&range, data->mapoffset, data->maxsize, impl->context->sc_pagesize) < 0) + return -EOVERFLOW; if (munmap(SPA_PTROFF(data->data, -range.start, void), range.size) < 0) pw_log_warn("%p: failed to unmap: %m", impl); diff --git a/src/pipewire/thread.c b/src/pipewire/thread.c index 3af0b4a44..1637a8af7 100644 --- a/src/pipewire/thread.c +++ b/src/pipewire/thread.c @@ -92,9 +92,8 @@ static struct spa_thread *impl_create(void *object, pthread_t pt; pthread_attr_t *attr = NULL, attributes; const char *str; - int err, old_policy, new_policy; + int err; int (*create_func)(pthread_t *, const pthread_attr_t *attr, void *(*start)(void*), void *) = NULL; - struct sched_param sp; bool reset_on_fork = true; attr = pw_thread_fill_attr(props, &attributes); @@ -124,11 +123,19 @@ static struct spa_thread *impl_create(void *object, reset_on_fork = spa_atob(str); } +#ifdef SCHED_RESET_ON_FORK + int old_policy, new_policy; + struct sched_param sp; + pthread_getschedparam(pt, &old_policy, &sp); new_policy = old_policy; SPA_FLAG_UPDATE(new_policy, SCHED_RESET_ON_FORK, reset_on_fork); if (old_policy != new_policy) pthread_setschedparam(pt, new_policy, &sp); +#else + if (reset_on_fork) + pw_log_debug("SCHED_RESET_ON_FORK is not supported on this platform"); +#endif return (struct spa_thread*)pt; } diff --git a/src/tools/meson.build b/src/tools/meson.build index 8147906fb..c082f169f 100644 --- a/src/tools/meson.build +++ b/src/tools/meson.build @@ -95,6 +95,50 @@ if build_pw_cat summary({'Build pw-cat with FFmpeg integration': build_pw_cat_with_ffmpeg}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump tool') endif +build_avb_virtual = get_option('avb').require( + host_machine.system() == 'linux', + error_message: 'AVB support is only available on Linux' +).allowed() + +if build_avb_virtual + avb_tool_inc = include_directories('../modules') + avb_tool_sources = [ + 'pw-avb-virtual.c', + '../modules/module-avb/avb.c', + '../modules/module-avb/adp.c', + '../modules/module-avb/acmp.c', + '../modules/module-avb/aecp.c', + '../modules/module-avb/aecp-aem.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-available.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-control.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-name.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-clock-source.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-sampling-rate.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-deregister-unsolicited-notifications.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-register-unsolicited-notifications.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-format.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c', + '../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-configuration.c', + '../modules/module-avb/aecp-aem-cmds-resps/reply-unsol-helpers.c', + '../modules/module-avb/es-builder.c', + '../modules/module-avb/avdecc.c', + '../modules/module-avb/descriptors.c', + '../modules/module-avb/maap.c', + '../modules/module-avb/mmrp.c', + '../modules/module-avb/mrp.c', + '../modules/module-avb/msrp.c', + '../modules/module-avb/mvrp.c', + '../modules/module-avb/srp.c', + '../modules/module-avb/stream.c', + ] + executable('pw-avb-virtual', + avb_tool_sources, + install: true, + include_directories: [configinc, avb_tool_inc], + dependencies: [mathlib, dl_lib, rt_lib, pipewire_dep], + ) +endif + if dbus_dep.found() executable('pw-reserve', 'reserve.h', diff --git a/src/tools/pw-avb-virtual.c b/src/tools/pw-avb-virtual.c new file mode 100644 index 000000000..6b123949e --- /dev/null +++ b/src/tools/pw-avb-virtual.c @@ -0,0 +1,283 @@ +/* PipeWire */ +/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */ +/* SPDX-License-Identifier: MIT */ + +/** + * pw-avb-virtual: Create virtual AVB audio devices in the PipeWire graph. + * + * This tool creates virtual AVB talker/listener endpoints that appear + * as Audio/Source and Audio/Sink nodes in the PipeWire graph (visible + * in tools like Helvum). No AVB hardware or network access is needed — + * the loopback transport is used for all protocol and stream operations. + * + * The sink node consumes audio silently (data goes nowhere). + * The source node produces silence (no network data to receive). + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "module-avb/internal.h" +#include "module-avb/stream.h" +#include "module-avb/avb-transport-loopback.h" +#include "module-avb/descriptors.h" +#include "module-avb/mrp.h" +#include "module-avb/adp.h" +#include "module-avb/acmp.h" +#include "module-avb/aecp.h" +#include "module-avb/maap.h" +#include "module-avb/mmrp.h" +#include "module-avb/msrp.h" +#include "module-avb/mvrp.h" + +struct data { + struct pw_main_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct spa_hook core_listener; + + struct impl impl; + struct server *server; + + const char *opt_remote; + const char *opt_name; + bool opt_milan; +}; + +static void do_quit(void *data, int signal_number) +{ + struct data *d = data; + pw_main_loop_quit(d->loop); +} + +static void on_core_error(void *data, uint32_t id, int seq, + int res, const char *message) +{ + struct data *d = data; + + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) + pw_main_loop_quit(d->loop); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = on_core_error, +}; + +static struct server *create_virtual_server(struct data *data) +{ + struct impl *impl = &data->impl; + struct server *server; + struct stream *stream; + uint16_t idx; + char name_buf[256]; + + server = calloc(1, sizeof(*server)); + if (server == NULL) + return NULL; + + server->impl = impl; + server->ifname = strdup("virtual0"); + server->avb_mode = data->opt_milan ? AVB_MODE_MILAN_V12 : AVB_MODE_LEGACY; + server->transport = &avb_transport_loopback; + + spa_list_append(&impl->servers, &server->link); + spa_hook_list_init(&server->listener_list); + spa_list_init(&server->descriptors); + spa_list_init(&server->streams); + + if (server->transport->setup(server) < 0) + goto error; + + server->mrp = avb_mrp_new(server); + if (server->mrp == NULL) + goto error; + + avb_aecp_register(server); + server->maap = avb_maap_register(server); + server->mmrp = avb_mmrp_register(server); + server->msrp = avb_msrp_register(server); + server->mvrp = avb_mvrp_register(server); + avb_adp_register(server); + avb_acmp_register(server); + + server->domain_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN); + + avb_maap_reserve(server->maap, 1); + + init_descriptors(server); + + /* Update stream properties and activate */ + idx = 0; + spa_list_for_each(stream, &server->streams, link) { + if (stream->direction == SPA_DIRECTION_INPUT) { + snprintf(name_buf, sizeof(name_buf), "%s.source.%u", + data->opt_name, idx); + pw_stream_update_properties(stream->stream, + &SPA_DICT_INIT_ARRAY(((struct spa_dict_item[]) { + { PW_KEY_NODE_NAME, name_buf }, + { PW_KEY_NODE_DESCRIPTION, "AVB Virtual Source" }, + { PW_KEY_NODE_VIRTUAL, "true" }, + }))); + } else { + snprintf(name_buf, sizeof(name_buf), "%s.sink.%u", + data->opt_name, idx); + pw_stream_update_properties(stream->stream, + &SPA_DICT_INIT_ARRAY(((struct spa_dict_item[]) { + { PW_KEY_NODE_NAME, name_buf }, + { PW_KEY_NODE_DESCRIPTION, "AVB Virtual Sink" }, + { PW_KEY_NODE_VIRTUAL, "true" }, + }))); + } + + if (stream_activate_virtual(stream, idx) < 0) + pw_log_warn("failed to activate stream %u", idx); + + idx++; + } + + return server; + +error: + spa_list_remove(&server->link); + free(server->ifname); + free(server); + return NULL; +} + +static void show_help(const char *name) +{ + printf("%s [options]\n" + " -h, --help Show this help\n" + " --version Show version\n" + " -r, --remote NAME Remote daemon name\n" + " -n, --name PREFIX Node name prefix (default: avb-virtual)\n" + " -m, --milan Use Milan v1.2 mode (default: legacy)\n", + name); +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + struct pw_loop *l; + int res = -1; + static const struct option long_options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "remote", required_argument, NULL, 'r' }, + { "name", required_argument, NULL, 'n' }, + { "milan", no_argument, NULL, 'm' }, + { NULL, 0, NULL, 0 } + }; + int c; + + setlocale(LC_ALL, ""); + pw_init(&argc, &argv); + + data.opt_name = "avb-virtual"; + + while ((c = getopt_long(argc, argv, "hVr:n:m", long_options, NULL)) != -1) { + switch (c) { + case 'h': + show_help(argv[0]); + 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': + data.opt_remote = optarg; + break; + case 'n': + data.opt_name = optarg; + break; + case 'm': + data.opt_milan = true; + break; + default: + show_help(argv[0]); + return -1; + } + } + + data.loop = pw_main_loop_new(NULL); + if (data.loop == NULL) { + fprintf(stderr, "can't create main loop: %m\n"); + goto exit; + } + + l = pw_main_loop_get_loop(data.loop); + pw_loop_add_signal(l, SIGINT, do_quit, &data); + pw_loop_add_signal(l, SIGTERM, do_quit, &data); + + data.context = pw_context_new(l, NULL, 0); + if (data.context == NULL) { + fprintf(stderr, "can't create context: %m\n"); + goto exit; + } + + data.core = pw_context_connect(data.context, + pw_properties_new( + PW_KEY_REMOTE_NAME, data.opt_remote, + NULL), + 0); + if (data.core == NULL) { + fprintf(stderr, "can't connect to PipeWire: %m\n"); + goto exit; + } + + pw_core_add_listener(data.core, &data.core_listener, + &core_events, &data); + + /* Initialize the AVB impl */ + data.impl.loop = l; + data.impl.timer_queue = pw_context_get_timer_queue(data.context); + data.impl.context = data.context; + data.impl.core = data.core; + spa_list_init(&data.impl.servers); + + /* Create the virtual AVB server with streams */ + data.server = create_virtual_server(&data); + if (data.server == NULL) { + fprintf(stderr, "can't create virtual AVB server: %m\n"); + goto exit; + } + + fprintf(stdout, "Virtual AVB device running (%s mode). Press Ctrl-C to stop.\n", + data.opt_milan ? "Milan v1.2" : "legacy"); + + pw_main_loop_run(data.loop); + + res = 0; +exit: + if (data.server) + avdecc_server_free(data.server); + if (data.core) + pw_core_disconnect(data.core); + if (data.context) + pw_context_destroy(data.context); + if (data.loop) + pw_main_loop_destroy(data.loop); + pw_deinit(); + + return res; +} diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c index f90ebffc9..b87fc2ba8 100644 --- a/src/tools/pw-cat.c +++ b/src/tools/pw-cat.c @@ -196,55 +196,60 @@ struct data { }; static const struct format_info { - const char *name; + const char *sf_name; int sf_format; + uint32_t sf_width; + const char *spa_name; uint32_t spa_format; - uint32_t width; + uint32_t spa_width; +#define FORMAT_ENCODED (1<<0) + uint32_t flags; } 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 }, + { "ulaw", SF_FORMAT_ULAW, 1, "ulaw", SPA_AUDIO_FORMAT_ULAW, 1, 0 }, + { "alaw", SF_FORMAT_ULAW, 1, "alaw", SPA_AUDIO_FORMAT_ALAW, 1, 0 }, + { "s8", SF_FORMAT_PCM_S8, 1, "s8", SPA_AUDIO_FORMAT_S8, 1, 0 }, + { "u8", SF_FORMAT_PCM_U8, 1, "u8", SPA_AUDIO_FORMAT_U8, 1, 0 }, + { "s16", SF_FORMAT_PCM_16, 2, "s16", SPA_AUDIO_FORMAT_S16, 2, 0 }, + /* we read and write S24 as S32 with sndfile */ + { "s24", SF_FORMAT_PCM_24, 3, "s32", SPA_AUDIO_FORMAT_S32, 4, 0 }, + { "s32", SF_FORMAT_PCM_32, 4, "s32", SPA_AUDIO_FORMAT_S32, 4, 0 }, + { "f32", SF_FORMAT_FLOAT, 4, "f32", SPA_AUDIO_FORMAT_F32, 4, 0 }, + { "f64", SF_FORMAT_DOUBLE, 8, "f64", SPA_AUDIO_FORMAT_F32, 8, 0 }, - { "mp1", SF_FORMAT_MPEG_LAYER_I, SPA_AUDIO_FORMAT_F32, 1 }, - { "mp2", SF_FORMAT_MPEG_LAYER_II, SPA_AUDIO_FORMAT_F32, 1 }, - { "mp3", SF_FORMAT_MPEG_LAYER_III, SPA_AUDIO_FORMAT_F32, 1 }, - { "vorbis", SF_FORMAT_VORBIS, SPA_AUDIO_FORMAT_F32, 1 }, - { "opus", SF_FORMAT_OPUS, SPA_AUDIO_FORMAT_F32, 1 }, + { "mp1", SF_FORMAT_MPEG_LAYER_I, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "mp2", SF_FORMAT_MPEG_LAYER_II, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "mp3", SF_FORMAT_MPEG_LAYER_III, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "vorbis", SF_FORMAT_VORBIS, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "opus", SF_FORMAT_OPUS, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, - { "ima-adpcm", SF_FORMAT_IMA_ADPCM, SPA_AUDIO_FORMAT_F32, 1 }, - { "ms-adpcm", SF_FORMAT_MS_ADPCM, SPA_AUDIO_FORMAT_F32, 1 }, - { "nms-adpcm-16", SF_FORMAT_NMS_ADPCM_16, SPA_AUDIO_FORMAT_F32, 1 }, - { "nms-adpcm-24", SF_FORMAT_NMS_ADPCM_24, SPA_AUDIO_FORMAT_F32, 1 }, - { "nms-adpcm-32", SF_FORMAT_NMS_ADPCM_32, SPA_AUDIO_FORMAT_F32, 1 }, + { "ima-adpcm", SF_FORMAT_IMA_ADPCM, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "ms-adpcm", SF_FORMAT_MS_ADPCM, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "nms-adpcm-16", SF_FORMAT_NMS_ADPCM_16, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "nms-adpcm-24", SF_FORMAT_NMS_ADPCM_24, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "nms-adpcm-32", SF_FORMAT_NMS_ADPCM_32, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, - { "alac-16", SF_FORMAT_ALAC_16, SPA_AUDIO_FORMAT_F32, 1 }, - { "alac-20", SF_FORMAT_ALAC_20, SPA_AUDIO_FORMAT_F32, 1 }, - { "alac-24", SF_FORMAT_ALAC_24, SPA_AUDIO_FORMAT_F32, 1 }, - { "alac-32", SF_FORMAT_ALAC_32, SPA_AUDIO_FORMAT_F32, 1 }, + { "alac-16", SF_FORMAT_ALAC_16, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "alac-20", SF_FORMAT_ALAC_20, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "alac-24", SF_FORMAT_ALAC_24, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "alac-32", SF_FORMAT_ALAC_32, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, - { "gsm610", SF_FORMAT_GSM610, SPA_AUDIO_FORMAT_F32, 1 }, - { "g721-32", SF_FORMAT_G721_32, SPA_AUDIO_FORMAT_F32, 1 }, - { "g723-24", SF_FORMAT_G723_24, SPA_AUDIO_FORMAT_F32, 1 }, - { "g723-40", SF_FORMAT_G723_40, SPA_AUDIO_FORMAT_F32, 1 }, - { "dwvw-12", SF_FORMAT_DWVW_12, SPA_AUDIO_FORMAT_F32, 1 }, - { "dwvw-16", SF_FORMAT_DWVW_16, SPA_AUDIO_FORMAT_F32, 1 }, - { "dwvw-24", SF_FORMAT_DWVW_24, SPA_AUDIO_FORMAT_F32, 1 }, - { "vox", SF_FORMAT_VOX_ADPCM, SPA_AUDIO_FORMAT_F32, 1 }, - { "dpcm-16", SF_FORMAT_DPCM_16, SPA_AUDIO_FORMAT_F32, 1 }, - { "dpcm-8", SF_FORMAT_DPCM_8, SPA_AUDIO_FORMAT_F32, 1 }, + { "gsm610", SF_FORMAT_GSM610, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "g721-32", SF_FORMAT_G721_32, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "g723-24", SF_FORMAT_G723_24, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "g723-40", SF_FORMAT_G723_40, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "dwvw-12", SF_FORMAT_DWVW_12, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "dwvw-16", SF_FORMAT_DWVW_16, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "dwvw-24", SF_FORMAT_DWVW_24, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "vox", SF_FORMAT_VOX_ADPCM, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "dpcm-16", SF_FORMAT_DPCM_16, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, + { "dpcm-8", SF_FORMAT_DPCM_8, 1, "f32", SPA_AUDIO_FORMAT_F32, 4, FORMAT_ENCODED }, }; static const struct format_info *format_info_by_name(const char *str) { SPA_FOR_EACH_ELEMENT_VAR(format_info, i) - if (spa_streq(str, i->name)) + if (spa_streq(str, i->sf_name)) return i; return NULL; } @@ -263,7 +268,7 @@ static void list_formats(struct data *d) fprintf(stdout, _("Supported formats:\n")); SPA_FOR_EACH_ELEMENT_VAR(format_info, i) - fprintf(stdout, " %s\n", i->name); + fprintf(stdout, " %s\n", i->sf_name); } static int sf_playback_fill_x8(struct data *d, void *dest, unsigned int n_frames, bool *null_frame) @@ -1120,9 +1125,9 @@ enum { }; #ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION -#define OPTIONS "hvprmdosR:P:q:aM:n:c" +#define OPTIONS "hvprmdosR:P:q:aM:n:cC" #else -#define OPTIONS "hvprmdsR:P:q:aM:n:c" +#define OPTIONS "hvprmdsR:P:q:aM:n:cC" #endif static const struct option long_options[] = { @@ -1163,6 +1168,7 @@ static const struct option long_options[] = { { "force-midi", required_argument, NULL, 'M' }, { "sample-count", required_argument, NULL, 'n' }, { "midi-clip", no_argument, NULL, 'c' }, + { "monitor", no_argument, NULL, 'C' }, { NULL, 0, NULL, 0 } }; @@ -1187,6 +1193,7 @@ static void show_usage(const char *name, bool is_error) " --media-role Set media role (default %s)\n" " --target Set node target serial or name (default %s)\n" " 0 means don't link\n" + " -C --monitor Capture monitor ports (in recording mode)\n" " --latency Set node latency (default %s)\n" " Xunit (unit = s, ms, us, ns)\n" " or direct samples (256)\n" @@ -1676,8 +1683,13 @@ static int setup_raw(struct data *data) if (info == NULL) return -EINVAL; + if (info->flags & FORMAT_ENCODED) { + fprintf(stderr, "raw: raw encoded format %s not supported\n", info->sf_name); + return -ENOTSUP; + } + data->spa_format = info->spa_format; - data->stride = info->width * data->channels; + data->stride = info->spa_width * data->channels; data->fill = data->mode == mode_playback ? raw_play : raw_record; if (spa_streq(data->filename, "-")) { @@ -1696,7 +1708,7 @@ static int setup_raw(struct data *data) if (data->verbose) fprintf(stderr, "raw: rate=%u channels=%u fmt=%s samplesize=%u stride=%u\n", data->rate, data->channels, - info->name, info->width, data->stride); + info->spa_name, info->spa_width, data->stride); return 0; } @@ -1848,15 +1860,14 @@ static int setup_encodedfile(struct data *data) int num_channels; unsigned int stream_index; const AVCodecParameters *codecpar; - char path[256] = { 0 }; + char path[PATH_MAX]; /* We do not support record with encoded media */ if (data->mode == mode_record) { return -EINVAL; } - strcpy(path, "file:"); - strcat(path, data->filename); + spa_scnprintf(path, sizeof(path), "file:%s", data->filename); data->encoded.format_context = NULL; if ((ret = avformat_open_input(&data->encoded.format_context, path, NULL, NULL)) < 0) { @@ -2034,14 +2045,10 @@ static int setup_sndfile(struct data *data) if (data->verbose) fprintf(stderr, "PCM: fmt:%s rate:%u channels:%u width:%u\n", - fi->name, data->rate, data->channels, fi->width); - - /* we read and write S24 as S32 with sndfile */ - if (fi->spa_format == SPA_AUDIO_FORMAT_S24) - fi = format_info_by_sf_format(SF_FORMAT_PCM_32); + fi->spa_name, data->rate, data->channels, fi->spa_width); data->spa_format = fi->spa_format; - data->stride = fi->width * data->channels; + data->stride = fi->spa_width * data->channels; data->fill = data->mode == mode_playback ? playback_fill_fn(data->spa_format) : record_fill_fn(data->spa_format); @@ -2336,6 +2343,9 @@ int main(int argc, char *argv[]) case 'c': data.data_type = TYPE_MIDI2; break; + case 'C': + pw_properties_set(data.props, PW_KEY_STREAM_CAPTURE_SINK, "true"); + break; case OPT_LISTFORMATS: list_formats(&data); return EXIT_SUCCESS; diff --git a/src/tools/pw-config.c b/src/tools/pw-config.c index bfc64e427..ef8a345fd 100644 --- a/src/tools/pw-config.c +++ b/src/tools/pw-config.c @@ -25,6 +25,7 @@ struct data { bool opt_recurse; bool opt_newline; bool opt_colors; + bool opt_simple; struct pw_properties *conf; struct pw_properties *assemble; int count; @@ -39,6 +40,7 @@ static void print_all_properties(struct data *d, struct pw_properties *props) (d->opt_recurse ? PW_PROPERTIES_FLAG_RECURSE : 0) | (d->opt_colors ? PW_PROPERTIES_FLAG_COLORS : 0) | (d->array ? PW_PROPERTIES_FLAG_ARRAY : 0) | + (d->opt_simple ? PW_PROPERTIES_FLAG_SIMPLE : 0) | PW_PROPERTIES_FLAG_ENCLOSE); fprintf(stdout, "\n"); } @@ -128,7 +130,8 @@ static void show_help(const char *name, bool error) " -L, --no-newline Omit newlines after values\n" " -r, --recurse Reformat config sections recursively\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" + " -s, --spa SPA JSON output\n", name, DEFAULT_NAME, DEFAULT_PREFIX); } @@ -146,6 +149,7 @@ int main(int argc, char *argv[]) { "recurse", no_argument, NULL, 'r' }, { "no-colors", no_argument, NULL, 'N' }, { "color", optional_argument, NULL, 'C' }, + { "spa", no_argument, NULL, 's' }, { NULL, 0, NULL, 0} }; @@ -153,13 +157,14 @@ int main(int argc, char *argv[]) d.opt_prefix = NULL; d.opt_recurse = false; d.opt_newline = true; + d.opt_simple = false; if (getenv("NO_COLOR") == NULL && isatty(fileno(stdout))) d.opt_colors = true; d.opt_cmd = "paths"; pw_init(&argc, &argv); - while ((c = getopt_long(argc, argv, "hVn:p:LrNC", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hVn:p:LrNCs", long_options, NULL)) != -1) { switch (c) { case 'h': show_help(argv[0], false); @@ -201,6 +206,9 @@ int main(int argc, char *argv[]) return -1; } break; + case 's' : + d.opt_simple = true; + break; default: show_help(argv[0], true); return -1; diff --git a/src/tools/pw-dump.c b/src/tools/pw-dump.c index e72e7d257..c939b7302 100644 --- a/src/tools/pw-dump.c +++ b/src/tools/pw-dump.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -30,15 +31,6 @@ #define INDENT 2 -static bool colors = false; -static bool raw = false; - -#define NORMAL (colors ? SPA_ANSI_RESET : "") -#define LITERAL (colors ? SPA_ANSI_BRIGHT_MAGENTA : "") -#define NUMBER (colors ? SPA_ANSI_BRIGHT_CYAN : "") -#define STRING (colors ? SPA_ANSI_BRIGHT_GREEN : "") -#define KEY (colors ? SPA_ANSI_BRIGHT_BLUE : "") - struct data { struct pw_main_loop *loop; struct pw_context *context; @@ -55,20 +47,13 @@ struct data { const char *pattern; + struct spa_json_builder builder; + 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; + unsigned int recurse:1; }; struct param { @@ -217,133 +202,25 @@ static void object_destroy(struct object *o) free(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->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); - va_end(va); - d->state = (d->state & STATE_MASK) + STATE_COMMA; -} - -static void put_key(struct data *d, const char *key) -{ - int size = (strlen(key) + 1) * 4; - 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 += 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 -= 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); -} - -static void put_encoded_string(struct data *d, const char *key, const char *val) -{ - put_fmt(d, key, "%s%s%s", STRING, val, NORMAL); -} -static void put_string(struct data *d, const char *key, const char *val) -{ - int size = (strlen(val) + 1) * 4; - 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) -{ - put_fmt(d, key, "%s%s%s", LITERAL, val, NORMAL); -} - -static void put_int(struct data *d, const char *key, int64_t val) -{ - put_fmt(d, key, "%s%"PRIi64"%s", NUMBER, val, NORMAL); -} - -static void put_double(struct data *d, const char *key, double val) -{ - char buf[128]; - put_fmt(d, key, "%s%s%s", NUMBER, - spa_json_format_float(buf, sizeof(buf), (float)val), NORMAL); -} - -static void put_value(struct data *d, const char *key, const char *val) -{ - int64_t li; - float fv; - - if (val == NULL) - put_literal(d, key, "null"); - else if (spa_streq(val, "true") || spa_streq(val, "false")) - put_literal(d, key, val); - else if (spa_atoi64(val, &li, 10)) - put_int(d, key, li); - else if (spa_json_parse_float(val, strlen(val), &fv)) - put_double(d, key, fv); - else - put_string(d, key, val); -} static void put_dict(struct data *d, const char *key, struct spa_dict *dict) { const struct spa_dict_item *it; spa_dict_qsort(dict); - put_begin(d, key, "{", 0); + spa_json_builder_object_push(&d->builder, key, "{"); spa_dict_for_each(it, dict) - put_value(d, it->key, it->value); - put_end(d, "}", 0); + spa_json_builder_object_value(&d->builder, d->recurse, it->key, it->value); + spa_json_builder_pop(&d->builder, "}"); } static void put_pod_value(struct data *d, const char *key, const struct spa_type_info *info, uint32_t type, void *body, uint32_t size) { - if (key) - 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"); + spa_json_builder_object_bool(&d->builder, key, *(int32_t*)body); break; case SPA_TYPE_Id: { @@ -357,34 +234,34 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type snprintf(fallback, sizeof(fallback), "id-%08x", id); str = fallback; } - put_value(d, NULL, str); + spa_json_builder_object_string(&d->builder, key, str); break; } case SPA_TYPE_Int: if (size < sizeof(int32_t)) break; - put_int(d, NULL, *(int32_t*)body); + spa_json_builder_object_int(&d->builder, key, *(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); + spa_json_builder_object_int(&d->builder, key, *(int64_t*)body); break; case SPA_TYPE_Float: if (size < sizeof(float)) break; - put_double(d, NULL, *(float*)body); + spa_json_builder_object_double(&d->builder, key, *(float*)body); break; case SPA_TYPE_Double: if (size < sizeof(double)) break; - put_double(d, NULL, *(double*)body); + spa_json_builder_object_double(&d->builder, key, *(double*)body); break; case SPA_TYPE_String: if (size < 1 || ((const char *)body)[size - 1]) break; - put_string(d, NULL, (const char*)body); + spa_json_builder_object_string(&d->builder, key, (const char*)body); break; case SPA_TYPE_Rectangle: { @@ -393,10 +270,10 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type 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); - put_end(d, "}", STATE_SIMPLE); + spa_json_builder_object_push(&d->builder, key, "{-"); + spa_json_builder_object_int(&d->builder, "width", r->width); + spa_json_builder_object_int(&d->builder, "height", r->height); + spa_json_builder_pop(&d->builder, "}-"); break; } case SPA_TYPE_Fraction: @@ -406,10 +283,10 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type 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); - put_end(d, "}", STATE_SIMPLE); + spa_json_builder_object_push(&d->builder, key, "{-"); + spa_json_builder_object_int(&d->builder, "num", f->num); + spa_json_builder_object_int(&d->builder, "denom", f->denom); + spa_json_builder_pop(&d->builder, "}-"); break; } case SPA_TYPE_Array: @@ -421,10 +298,10 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type break; b = (struct spa_pod_array_body *)body; info = info && info->values ? info->values: info; - put_begin(d, NULL, "[", STATE_SIMPLE); + spa_json_builder_object_push(&d->builder, key, "[-"); SPA_POD_ARRAY_BODY_FOREACH(b, size, p) put_pod_value(d, NULL, info, b->child.type, p, b->child.size); - put_end(d, "]", STATE_SIMPLE); + spa_json_builder_pop(&d->builder, "]-"); break; } case SPA_TYPE_Choice: @@ -433,7 +310,7 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type int index = 0; if (b->type == SPA_CHOICE_None) { - put_pod_value(d, NULL, info, b->child.type, + put_pod_value(d, key, info, b->child.type, SPA_POD_CONTENTS(struct spa_pod, &b->child), b->child.size); } else { @@ -452,12 +329,12 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type case SPA_CHOICE_Range: labels = range_labels; max_labels = 3; - flags |= STATE_SIMPLE; + flags++; break; case SPA_CHOICE_Step: labels = step_labels; max_labels = 4; - flags |= STATE_SIMPLE; + flags++; break; case SPA_CHOICE_Enum: labels = enum_labels; @@ -474,7 +351,7 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type if (labels == NULL) break; - put_begin(d, NULL, "{", flags); + spa_json_builder_object_push(&d->builder, key, flags ? "{-" : "{"); SPA_POD_CHOICE_BODY_FOREACH(b, size, p) { if ((label = labels[SPA_CLAMP(index, 0, max_labels)]) == NULL) break; @@ -482,13 +359,13 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type put_pod_value(d, buffer, info, b->child.type, p, b->child.size); index++; } - put_end(d, "}", flags); + spa_json_builder_pop(&d->builder, flags ? "}-" : "}"); } break; } case SPA_TYPE_Object: { - put_begin(d, NULL, "{", 0); + spa_json_builder_object_push(&d->builder, key, "{"); struct spa_pod_object_body *b = (struct spa_pod_object_body *)body; struct spa_pod_prop *p; const struct spa_type_info *ti, *ii; @@ -515,27 +392,27 @@ static void put_pod_value(struct data *d, const char *key, const struct spa_type SPA_POD_CONTENTS(struct spa_pod_prop, p), p->value.size); } - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); break; } case SPA_TYPE_Struct: { struct spa_pod *b = (struct spa_pod *)body, *p; - put_begin(d, NULL, "[", 0); + spa_json_builder_object_push(&d->builder, key, "["); SPA_POD_FOREACH(b, size, p) put_pod_value(d, NULL, info, p->type, SPA_POD_BODY(p), p->size); - put_end(d, "]", 0); + spa_json_builder_pop(&d->builder, "]"); break; } case SPA_TYPE_None: - put_value(d, NULL, NULL); + spa_json_builder_object_null(&d->builder, key); break; } } static void put_pod(struct data *d, const char *key, const struct spa_pod *pod) { if (pod == NULL) { - put_value(d, key, NULL); + spa_json_builder_object_null(&d->builder, key); } else { put_pod_value(d, key, SPA_TYPE_ROOT, pod->type, SPA_POD_BODY(pod), pod->size); @@ -548,23 +425,24 @@ static void put_params(struct data *d, const char *key, { uint32_t i; - put_begin(d, key, "{", 0); + spa_json_builder_object_push(&d->builder, key, "{"); for (i = 0; i < n_params; i++) { struct spa_param_info *pi = ¶ms[i]; struct param *p; uint32_t flags; - flags = pi->flags & SPA_PARAM_INFO_READ ? 0 : STATE_SIMPLE; + flags = pi->flags & SPA_PARAM_INFO_READ ? 0 : 1; - put_begin(d, spa_debug_type_find_short_name(spa_type_param, pi->id), - "[", flags); + spa_json_builder_object_push(&d->builder, + spa_debug_type_find_short_name(spa_type_param, pi->id), + flags ? "[-" : "["); spa_list_for_each(p, list, link) { if (p->id == pi->id) put_pod(d, NULL, p->param); } - put_end(d, "]", flags); + spa_json_builder_pop(&d->builder, flags ? "]-" : "]"); } - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); } struct flags_info { @@ -576,12 +454,12 @@ static void put_flags(struct data *d, const char *key, uint64_t flags, const struct flags_info *info) { uint32_t i; - put_begin(d, key, "[", STATE_SIMPLE); + spa_json_builder_object_push(&d->builder, key, "[-"); for (i = 0; info[i].name != NULL; i++) { if (info[i].mask & flags) - put_string(d, NULL, info[i].name); + spa_json_builder_array_string(&d->builder, info[i].name); } - put_end(d, "]", STATE_SIMPLE); + spa_json_builder_pop(&d->builder, "]-"); } /* core */ @@ -595,15 +473,15 @@ static void core_dump(struct object *o) struct data *d = o->data; struct pw_core_info *i = d->info; - put_begin(d, "info", "{", 0); - put_int(d, "cookie", i->cookie); - put_value(d, "user-name", i->user_name); - put_value(d, "host-name", i->host_name); - put_value(d, "version", i->version); - put_value(d, "name", i->name); + spa_json_builder_object_push(&d->builder, "info", "{"); + spa_json_builder_object_int(&d->builder, "cookie", i->cookie); + spa_json_builder_object_string(&d->builder, "user-name", i->user_name); + spa_json_builder_object_string(&d->builder, "host-name", i->host_name); + spa_json_builder_object_string(&d->builder, "version", i->version); + spa_json_builder_object_string(&d->builder, "name", i->name); put_flags(d, "change-mask", i->change_mask, fl); put_dict(d, "props", i->props); - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); } static const struct class core_class = { @@ -624,10 +502,10 @@ static void client_dump(struct object *o) struct data *d = o->data; struct pw_client_info *i = o->info; - put_begin(d, "info", "{", 0); + spa_json_builder_object_push(&d->builder, "info", "{"); put_flags(d, "change-mask", i->change_mask, fl); put_dict(d, "props", i->props); - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); } static void client_event_info(void *data, const struct pw_client_info *info) @@ -683,13 +561,13 @@ static void module_dump(struct object *o) struct data *d = o->data; struct pw_module_info *i = o->info; - put_begin(d, "info", "{", 0); - put_value(d, "name", i->name); - put_value(d, "filename", i->filename); - put_value(d, "args", i->args); + spa_json_builder_object_push(&d->builder, "info", "{"); + spa_json_builder_object_string(&d->builder, "name", i->name); + spa_json_builder_object_string(&d->builder, "filename", i->filename); + spa_json_builder_object_value(&d->builder, d->recurse, "args", i->args); put_flags(d, "change-mask", i->change_mask, fl); put_dict(d, "props", i->props); - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); } static void module_event_info(void *data, const struct pw_module_info *info) @@ -745,13 +623,13 @@ static void factory_dump(struct object *o) struct data *d = o->data; struct pw_factory_info *i = o->info; - put_begin(d, "info", "{", 0); - put_value(d, "name", i->name); - put_value(d, "type", i->type); - put_int(d, "version", i->version); + spa_json_builder_object_push(&d->builder, "info", "{"); + spa_json_builder_object_string(&d->builder, "name", i->name); + spa_json_builder_object_string(&d->builder, "type", i->type); + spa_json_builder_object_int(&d->builder, "version", i->version); put_flags(d, "change-mask", i->change_mask, fl); put_dict(d, "props", i->props); - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); } static void factory_event_info(void *data, const struct pw_factory_info *info) @@ -808,11 +686,11 @@ static void device_dump(struct object *o) struct data *d = o->data; struct pw_device_info *i = o->info; - put_begin(d, "info", "{", 0); + spa_json_builder_object_push(&d->builder, "info", "{"); put_flags(d, "change-mask", i->change_mask, fl); put_dict(d, "props", i->props); put_params(d, "params", i->params, i->n_params, &o->param_list); - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); } static void device_event_info(void *data, const struct pw_device_info *info) @@ -904,17 +782,17 @@ static void node_dump(struct object *o) struct data *d = o->data; struct pw_node_info *i = o->info; - put_begin(d, "info", "{", 0); - put_int(d, "max-input-ports", i->max_input_ports); - put_int(d, "max-output-ports", i->max_output_ports); + spa_json_builder_object_push(&d->builder, "info", "{"); + spa_json_builder_object_int(&d->builder, "max-input-ports", i->max_input_ports); + spa_json_builder_object_int(&d->builder, "max-output-ports", i->max_output_ports); put_flags(d, "change-mask", i->change_mask, fl); - put_int(d, "n-input-ports", i->n_input_ports); - put_int(d, "n-output-ports", i->n_output_ports); - put_value(d, "state", pw_node_state_as_string(i->state)); - put_value(d, "error", i->error); + spa_json_builder_object_int(&d->builder, "n-input-ports", i->n_input_ports); + spa_json_builder_object_int(&d->builder, "n-output-ports", i->n_output_ports); + spa_json_builder_object_string(&d->builder, "state", pw_node_state_as_string(i->state)); + spa_json_builder_object_string(&d->builder, "error", i->error); put_dict(d, "props", i->props); put_params(d, "params", i->params, i->n_params, &o->param_list); - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); } static void node_event_info(void *data, const struct pw_node_info *info) @@ -1006,12 +884,12 @@ static void port_dump(struct object *o) struct data *d = o->data; struct pw_port_info *i = o->info; - put_begin(d, "info", "{", 0); - put_value(d, "direction", pw_direction_as_string(i->direction)); + spa_json_builder_object_push(&d->builder, "info", "{"); + spa_json_builder_object_string(&d->builder, "direction", pw_direction_as_string(i->direction)); put_flags(d, "change-mask", i->change_mask, fl); put_dict(d, "props", i->props); put_params(d, "params", i->params, i->n_params, &o->param_list); - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); } static void port_event_info(void *data, const struct pw_port_info *info) @@ -1101,17 +979,17 @@ static void link_dump(struct object *o) struct data *d = o->data; struct pw_link_info *i = o->info; - put_begin(d, "info", "{", 0); - put_int(d, "output-node-id", i->output_node_id); - put_int(d, "output-port-id", i->output_port_id); - put_int(d, "input-node-id", i->input_node_id); - put_int(d, "input-port-id", i->input_port_id); + spa_json_builder_object_push(&d->builder, "info", "{"); + spa_json_builder_object_int(&d->builder, "output-node-id", i->output_node_id); + spa_json_builder_object_int(&d->builder, "output-port-id", i->output_port_id); + spa_json_builder_object_int(&d->builder, "input-node-id", i->input_node_id); + spa_json_builder_object_int(&d->builder, "input-port-id", i->input_port_id); put_flags(d, "change-mask", i->change_mask, fl); - put_value(d, "state", pw_link_state_as_string(i->state)); - put_value(d, "error", i->error); + spa_json_builder_object_string(&d->builder, "state", pw_link_state_as_string(i->state)); + spa_json_builder_object_string(&d->builder, "error", i->error); put_pod(d, "format", i->format); put_dict(d, "props", i->props); - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); } static void link_event_info(void *data, const struct pw_link_info *info) @@ -1161,39 +1039,6 @@ static const struct class link_class = { .dump = link_dump, }; -static void json_dump_val(struct data *d, const char *key, struct spa_json *it, const char *value, int len) -{ - struct spa_json sub; - if (spa_json_is_array(value, len)) { - put_begin(d, key, "[", STATE_SIMPLE); - spa_json_enter(it, &sub); - while ((len = spa_json_next(&sub, &value)) > 0) { - json_dump_val(d, NULL, &sub, value, len); - } - put_end(d, "]", STATE_SIMPLE); - } else if (spa_json_is_object(value, len)) { - char val[1024]; - put_begin(d, key, "{", STATE_SIMPLE); - spa_json_enter(it, &sub); - while ((len = spa_json_object_next(&sub, val, sizeof(val), &value)) > 0) - json_dump_val(d, val, &sub, value, len); - put_end(d, "}", STATE_SIMPLE); - } else if (spa_json_is_string(value, len)) { - put_encoded_string(d, key, strndupa(value, len)); - } else { - put_value(d, key, strndupa(value, len)); - } -} - -static void json_dump(struct data *d, const char *key, const char *value) -{ - struct spa_json it[1]; - int len; - const char *val; - if ((len = spa_json_begin(&it[0], value, strlen(value), &val)) >= 0) - json_dump_val(d, key, &it[0], val, len); -} - /* metadata */ struct metadata_entry { @@ -1210,22 +1055,21 @@ static void metadata_dump(struct object *o) struct data *d = o->data; struct metadata_entry *e; put_dict(d, "props", &o->props->dict); - put_begin(d, "metadata", "[", 0); + spa_json_builder_object_push(&d->builder, "metadata", "["); spa_list_for_each(e, &o->data_list, link) { + bool recurse = false; if (e->changed == 0) continue; - put_begin(d, NULL, "{", STATE_SIMPLE); - put_int(d, "subject", e->subject); - put_value(d, "key", e->key); - put_value(d, "type", e->type); - if (e->type != NULL && spa_streq(e->type, "Spa:String:JSON")) - json_dump(d, "value", e->value); - else - put_value(d, "value", e->value); - put_end(d, "}", STATE_SIMPLE); + spa_json_builder_array_push(&d->builder, "{-"); + spa_json_builder_object_int(&d->builder, "subject", e->subject); + spa_json_builder_object_string(&d->builder, "key", e->key); + spa_json_builder_object_string(&d->builder, "type", e->type); + recurse = (e->type != NULL && spa_streq(e->type, "Spa:String:JSON")); + spa_json_builder_object_value(&d->builder, recurse, "value", e->value); + spa_json_builder_pop(&d->builder, "}-"); e->changed = 0; } - put_end(d, "]", 0); + spa_json_builder_pop(&d->builder, "]"); } static struct metadata_entry *metadata_find(struct object *o, uint32_t subject, const char *key) @@ -1442,18 +1286,20 @@ static void registry_event_global_remove(void *data, uint32_t id) return; if (d->monitor && (!d->pattern || object_matches(o, d->pattern))) { - d->state = STATE_FIRST; - if (d->state == STATE_FIRST) - put_begin(d, NULL, "[", 0); - put_begin(d, NULL, "{", 0); - put_int(d, "id", o->id); + d->state = 0; + if (d->state++ == 0) + spa_json_builder_array_push(&d->builder, "["); + spa_json_builder_array_push(&d->builder, "{"); + spa_json_builder_object_int(&d->builder, "id", o->id); if (o->class && o->class->dump) - put_value(d, "info", NULL); + spa_json_builder_object_null(&d->builder, "info"); else if (o->props) - put_value(d, "props", NULL); - put_end(d, "}", 0); - if (d->state != STATE_FIRST) - put_end(d, "]\n", 0); + spa_json_builder_object_null(&d->builder, "props"); + spa_json_builder_pop(&d->builder, "}"); + if (d->state != 0) { + spa_json_builder_pop(&d->builder, "]"); + fputs("\n", d->builder.f); + } } object_destroy(o); @@ -1478,28 +1324,30 @@ static void dump_objects(struct data *d) struct object *o; - d->state = STATE_FIRST; + d->state = 0; spa_list_for_each(o, &d->object_list, link) { if (d->pattern != NULL && !object_matches(o, d->pattern)) continue; if (o->changed == 0) continue; - if (d->state == STATE_FIRST) - put_begin(d, NULL, "[", 0); - put_begin(d, NULL, "{", 0); - put_int(d, "id", o->id); - put_value(d, "type", o->type); - put_int(d, "version", o->version); + if (d->state++ == 0) + spa_json_builder_array_push(&d->builder, "["); + spa_json_builder_array_push(&d->builder, "{"); + spa_json_builder_object_int(&d->builder, "id", o->id); + spa_json_builder_object_string(&d->builder, "type", o->type); + spa_json_builder_object_int(&d->builder, "version", o->version); put_flags(d, "permissions", o->permissions, fl); if (o->class && o->class->dump) o->class->dump(o); else if (o->props) put_dict(d, "props", &o->props->dict); - put_end(d, "}", 0); + spa_json_builder_pop(&d->builder, "}"); o->changed = 0; } - if (d->state != STATE_FIRST) - put_end(d, "]\n", 0); + if (d->state != 0) { + spa_json_builder_pop(&d->builder, "]"); + fputs("\n", d->builder.f); + } } static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message) @@ -1564,7 +1412,8 @@ static void show_help(struct data *data, const char *name, bool error) " -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", + " -s, --spa SPA JSON output\n" + " -c, --recurse Reformat values recursively\n", name); } @@ -1584,9 +1433,11 @@ int main(int argc, char *argv[]) { "raw", no_argument, NULL, 'R' }, { "indent", required_argument, NULL, 'i' }, { "spa", no_argument, NULL, 's' }, + { "recurse", no_argument, NULL, 'c' }, { NULL, 0, NULL, 0} }; - int c; + int c, flags = 0, indent = -1; + bool colors = false, raw = false; setlocale(LC_ALL, ""); pw_init(&argc, &argv); @@ -1596,11 +1447,7 @@ int main(int argc, char *argv[]) colors = true; setlinebuf(data.out); - data.comma_char = ","; - data.keysep_char = ":"; - data.indent = INDENT; - - while ((c = getopt_long(argc, argv, "hVr:mNC::Ri:s", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hVr:mNC::Ri:sc", long_options, NULL)) != -1) { switch (c) { case 'h' : show_help(&data, argv[0], false); @@ -1640,20 +1487,27 @@ int main(int argc, char *argv[]) } break; case 'i' : - data.indent = atoi(optarg); + indent = atoi(optarg); break; case 's' : - data.comma_char = ""; - data.keysep_char = " ="; - data.simple_string = true; + flags |= SPA_JSON_BUILDER_FLAG_SIMPLE; + break; + case 'c' : + data.recurse = true; break; default: show_help(&data, argv[0], true); return -1; } } - if (raw) - data.indent = 0; + if (!raw) + flags |= SPA_JSON_BUILDER_FLAG_PRETTY; + if (colors) + flags |= SPA_JSON_BUILDER_FLAG_COLOR; + + spa_json_builder_file(&data.builder, data.out, flags); + if (indent >= 0) + data.builder.indent = indent; if (optind < argc) data.pattern = argv[optind++]; diff --git a/src/tools/pw-mididump.c b/src/tools/pw-mididump.c index 277784bb1..882bd4702 100644 --- a/src/tools/pw-mididump.c +++ b/src/tools/pw-mididump.c @@ -33,7 +33,7 @@ struct data { struct pw_filter *filter; struct port *in_port; int64_t clock_time; - bool opt_midi1; + bool force_ump; }; @@ -174,7 +174,8 @@ static int dump_filter(struct data *data) PW_FILTER_PORT_FLAG_MAP_BUFFERS, sizeof(struct port), pw_properties_new( - PW_KEY_FORMAT_DSP, data->opt_midi1 ? "8 bit raw midi" : "32 bit raw UMP", + PW_KEY_FORMAT_DSP, + data->force_ump ? "32 bit raw UMP" : "8 bit raw midi", PW_KEY_PORT_NAME, "input", NULL), NULL, 0); @@ -198,7 +199,7 @@ static void show_help(const char *name, bool error) " -h, --help Show this help\n" " --version Show version\n" " -r, --remote Remote daemon name\n" - " -M, --force-midi Force midi format, one of \"midi\" or \"ump\",(default ump)\n", + " -M, --force-midi Force midi format, one of \"midi\" or \"ump\",(default midi)\n", name); } @@ -238,9 +239,9 @@ int main(int argc, char *argv[]) case 'M': if (spa_streq(optarg, "midi")) - data.opt_midi1 = true; + data.force_ump = false; else if (spa_streq(optarg, "ump")) - data.opt_midi1 = false; + data.force_ump = true; else { fprintf(stderr, "error: bad force-midi %s\n", optarg); show_help(argv[0], true); diff --git a/src/tools/pw-top.c b/src/tools/pw-top.c index 6b936a9bf..824559f6a 100644 --- a/src/tools/pw-top.c +++ b/src/tools/pw-top.c @@ -66,6 +66,20 @@ struct node { struct spa_hook object_listener; }; +struct filter_preset { + enum pw_node_state state; + enum pw_node_state followers; +}; + +struct filter_preset filter_presets[] = { + {PW_NODE_STATE_ERROR, PW_NODE_STATE_ERROR}, + {PW_NODE_STATE_IDLE, PW_NODE_STATE_ERROR}, + {PW_NODE_STATE_RUNNING, PW_NODE_STATE_ERROR}, + {PW_NODE_STATE_RUNNING, PW_NODE_STATE_IDLE}, +}; + +#define N_FILTER_PRESETS SPA_N_ELEMENTS(filter_presets) + struct data { struct pw_main_loop *loop; struct pw_context *context; @@ -91,6 +105,8 @@ struct data { unsigned int batch_mode:1; int iterations; + + int32_t filter_preset; }; struct point { @@ -559,14 +575,38 @@ static void do_refresh(struct data *d, bool force_refresh) { struct node *n, *t, *f; int y = 1; + struct filter_preset *filter = &filter_presets[d->filter_preset]; if (!d->pending_refresh && !force_refresh) return; if (!d->batch_mode) { + char statusbar[255] = { 0 }; + + if (!((filter->state == PW_NODE_STATE_ERROR) && + (filter->followers == PW_NODE_STATE_ERROR))) { + + strcpy(statusbar, "FILTER: "); + if (filter->state == PW_NODE_STATE_ERROR) + strcat(statusbar, "ALL"); + else for (enum pw_node_state showstate = PW_NODE_STATE_RUNNING; showstate >= PW_NODE_STATE_ERROR; showstate--) { + if (showstate >= filter->state) + strcat(statusbar, state_as_string(showstate, SPA_IO_POSITION_STATE_STOPPED)); + } + strcat(statusbar, "+"); + if (filter->followers == PW_NODE_STATE_ERROR) + strcat(statusbar, "ALL"); + else for (enum pw_node_state showstate = PW_NODE_STATE_RUNNING; showstate >= PW_NODE_STATE_ERROR; showstate--) { + if (showstate >= filter->followers) + strcat(statusbar, state_as_string(showstate, SPA_IO_POSITION_STATE_STOPPED)); + } + } + werase(d->win); wattron(d->win, A_REVERSE); wprintw(d->win, "%-*.*s", COLS, COLS, HEADER); + if ((size_t)COLS >= strlen(HEADER) + strlen(statusbar)) + mvwprintw(d->win, 0, COLS - strlen(statusbar), "%s", statusbar); wattroff(d->win, A_REVERSE); wprintw(d->win, "\n"); } else @@ -575,6 +615,8 @@ static void do_refresh(struct data *d, bool force_refresh) spa_list_for_each_safe(n, t, &d->node_list, link) { if (n->driver != n) continue; + if (n->state < filter->state) + continue; print_node(d, n, n, y++); if(!d->batch_mode && y > LINES) @@ -587,6 +629,9 @@ static void do_refresh(struct data *d, bool force_refresh) if (f->driver != n || f == n) continue; + if (f->state < filter->followers) + continue; + print_node(d, n, f, y++); if(!d->batch_mode && y > LINES) break; @@ -771,8 +816,9 @@ static void show_help(const char *name, bool error) { fprintf(error ? stderr : stdout, "Usage:\n%s [options]\n\n" "Options:\n" - " -b, --batch-mode run in non-interactive batch mode\n" + " -b, --batch-mode run in non-interactive batch mode\n" " -n, --iterations = NUMBER exit after NUMBER batch iterations\n" + " -f, --filter = NUMBER start with filter preset NUMBER selected\n" " -r, --remote Remote daemon name\n" "\n" " -h, --help Show this help\n" @@ -807,6 +853,14 @@ static void do_handle_io(void *data, int fd, uint32_t mask) case 'c': reset_xruns(d); break; + case 'f': + d->filter_preset = (d->filter_preset + 1) % N_FILTER_PRESETS; + do_refresh(d, !d->batch_mode); + break; + case 'F': + d->filter_preset = (d->filter_preset - 1 + N_FILTER_PRESETS) % N_FILTER_PRESETS; + do_refresh(d, !d->batch_mode); + break; default: do_refresh(d, !d->batch_mode); break; @@ -822,6 +876,7 @@ int main(int argc, char *argv[]) static const struct option long_options[] = { { "batch-mode", no_argument, NULL, 'b' }, { "iterations", required_argument, NULL, 'n' }, + { "filter", required_argument, NULL, 'f' }, { "remote", required_argument, NULL, 'r' }, { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, @@ -835,10 +890,11 @@ int main(int argc, char *argv[]) pw_init(&argc, &argv); data.iterations = -1; + data.filter_preset = 0; spa_list_init(&data.node_list); - while ((c = getopt_long(argc, argv, "hVr:o:bn:", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "hVr:o:bn:f:", long_options, NULL)) != -1) { switch (c) { case 'h': show_help(argv[0], false); @@ -857,6 +913,10 @@ int main(int argc, char *argv[]) case 'b': data.batch_mode = 1; break; + case 'f': + spa_atoi32(optarg, &data.filter_preset, 10); + data.filter_preset %= N_FILTER_PRESETS; + break; case 'n': spa_atoi32(optarg, &data.iterations, 10); break; diff --git a/test/avb-bugs.md b/test/avb-bugs.md new file mode 100644 index 000000000..eaafe4c6c --- /dev/null +++ b/test/avb-bugs.md @@ -0,0 +1,143 @@ +# AVB Module Bugs Found via Test Suite + +The following bugs were discovered by building a software test harness +for the AVB protocol stack. All have been fixed in the accompanying +patch series. + +## 1. Heap corruption in server_destroy_descriptors + +**File:** `src/modules/module-avb/internal.h` +**Commit:** `69c721006` + +`server_destroy_descriptors()` called `free(d->ptr)` followed by +`free(d)`, but `d->ptr` points into the same allocation as `d` +(set via `SPA_PTROFF(d, sizeof(struct descriptor), void)` in +`server_add_descriptor()`). This is a double-free / heap corruption +that could cause crashes or memory corruption when tearing down an +AVB server. + +**Fix:** Remove the erroneous `free(d->ptr)` call. + +## 2. NULL pointer dereference in MSRP notify dispatch + +**File:** `src/modules/module-avb/msrp.c`, `src/modules/module-avb/mvrp.c` +**Commit:** `b056e9f85` + +`msrp_notify()` unconditionally calls `dispatch[a->attr.type].notify()` +but the dispatch table entry for `AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED` +has `notify = NULL`. If a talker-failed attribute receives a registrar +state change (e.g., `RX_NEW` triggers `NOTIFY_NEW`), this crashes with +a NULL pointer dereference. The same unguarded pattern exists in +`mvrp_notify()`. + +**Fix:** Add `if (dispatch[a->attr.type].notify)` NULL check before +calling, matching the defensive pattern already used in the encode path. + +## 3. MRP NEW messages never transmitted + +**File:** `src/modules/module-avb/mrp.h`, `src/modules/module-avb/mrp.c`, +`src/modules/module-avb/msrp.c`, `src/modules/module-avb/mvrp.c` +**Commit:** `bc2c41daa` + +`AVB_MRP_SEND_NEW` was defined as `0`. The MSRP and MVRP event handlers +skip attributes with `if (!a->attr.mrp->pending_send)`, treating `0` as +"no pending send". Since the MRP state machine sets `pending_send` to +`AVB_MRP_SEND_NEW` (0) when an attribute in state VN or AN receives a +TX event, NEW messages were silently dropped instead of being +transmitted. This violates IEEE 802.1Q which requires NEW messages to +be sent when an attribute is first declared. + +In practice, the attribute would cycle through VN -> AN -> AA over +successive TX events, eventually sending a JOINMT instead of the +initial NEW. The protocol still functioned because JOINMT also +registers the attribute, but the initial declaration was lost. + +**Fix:** Shift all `AVB_MRP_SEND_*` values to start at 1, so that 0 +unambiguously means "no send pending". Update MSRP and MVRP encoders +to subtract 1 when encoding to the IEEE 802.1Q wire format. + +## 4. ACMP error responses sent with wrong message type + +**File:** `src/modules/module-avb/acmp.c` +**Commit:** `9f4147104` + +In `handle_connect_tx_command()` and `handle_disconnect_tx_command()`, +`AVB_PACKET_ACMP_SET_MESSAGE_TYPE()` is called after the `goto done` +jump target. When `find_stream()` fails (returns NULL), the code jumps +to `done:` without setting the message type, so the error response is +sent with the original command message type (e.g., +`CONNECT_TX_COMMAND = 0`) instead of the correct response type +(`CONNECT_TX_RESPONSE = 1`). + +A controller receiving this malformed response would not recognize it +as a response to its command and would eventually time out. + +**Fix:** Move `AVB_PACKET_ACMP_SET_MESSAGE_TYPE()` before the +`find_stream()` call so the response type is always set correctly. + +## 5. ACMP pending_destroy skips controller cleanup + +**File:** `src/modules/module-avb/acmp.c` +**Commit:** `9f4147104` + +`pending_destroy()` iterates with `list_id < PENDING_CONTROLLER` +(where `PENDING_CONTROLLER = 2`), which only cleans up +`PENDING_TALKER` (0) and `PENDING_LISTENER` (1) lists, skipping +`PENDING_CONTROLLER` (2). Any pending controller requests leak on +shutdown. + +**Fix:** Change `<` to `<=` to include the controller list. + +## 6. Legacy AECP handlers read payload at wrong offset + +**File:** `src/modules/module-avb/aecp-aem.c` + +`handle_acquire_entity_avb_legacy()` and `handle_lock_entity_avb_legacy()` +assign `const struct avb_packet_aecp_aem *p = m;` where `m` is the full +ethernet frame (starting with `struct avb_ethernet_header`). The handlers +then access `p->payload` to read the acquire/lock fields, but this reads +from `m + offsetof(avb_packet_aecp_aem, payload)` instead of the correct +`m + sizeof(avb_ethernet_header) + offsetof(avb_packet_aecp_aem, payload)`. +This causes `descriptor_type` and `descriptor_id` to be read from the +wrong position, leading to incorrect descriptor lookups. + +All other AEM command handlers (e.g., `handle_read_descriptor_common`) +correctly derive `p` via `SPA_PTROFF(h, sizeof(*h), void)`. + +**Fix:** Change `const struct avb_packet_aecp_aem *p = m;` to properly +skip the ethernet header: +```c +const struct avb_ethernet_header *h = m; +const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void); +``` + +## 7. Milan LOCK_ENTITY error response uses wrong packet pointer + +**File:** `src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c` + +In `handle_cmd_lock_entity_milan_v12()`, when `server_find_descriptor()` +returns NULL, `reply_status()` is called with `p` (the AEM packet pointer +past the ethernet header) instead of `m` (the full ethernet frame). +`reply_status()` assumes its third argument is the full frame and casts +it as `struct avb_ethernet_header *`. With the wrong pointer, the +response ethernet header (including destination MAC) is corrupted. + +**Fix:** Change `reply_status(aecp, ..., p, len)` to +`reply_status(aecp, ..., m, len)`. + +## 8. Lock entity re-lock timeout uses wrong units + +**File:** `src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c` + +When a controller that already holds the lock sends another lock request +(to refresh it), the expire timeout is extended by: +```c +lock->base_info.expire_timeout += + AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND; +``` +`AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND` is `60` (raw seconds), +but `expire_timeout` is in nanoseconds. This adds only 60 nanoseconds +instead of 60 seconds. The initial lock correctly uses +`AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND * SPA_NSEC_PER_SEC`. + +**Fix:** Multiply by `SPA_NSEC_PER_SEC` to match the nanosecond units. diff --git a/test/meson.build b/test/meson.build index 5e38db383..55443bad3 100644 --- a/test/meson.build +++ b/test/meson.build @@ -163,3 +163,42 @@ if valgrind.found() env : valgrind_env, timeout_multiplier : 3) endif + +if build_module_avb + avb_test_inc = [pwtest_inc, include_directories('../src/modules')] + avb_module_sources = [ + '../src/modules/module-avb/avb.c', + '../src/modules/module-avb/adp.c', + '../src/modules/module-avb/acmp.c', + '../src/modules/module-avb/aecp.c', + '../src/modules/module-avb/aecp-aem.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-available.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-control.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-name.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-clock-source.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-sampling-rate.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-deregister-unsolicited-notifications.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-register-unsolicited-notifications.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-format.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-configuration.c', + '../src/modules/module-avb/aecp-aem-cmds-resps/reply-unsol-helpers.c', + '../src/modules/module-avb/es-builder.c', + '../src/modules/module-avb/avdecc.c', + '../src/modules/module-avb/descriptors.c', + '../src/modules/module-avb/maap.c', + '../src/modules/module-avb/mmrp.c', + '../src/modules/module-avb/mrp.c', + '../src/modules/module-avb/msrp.c', + '../src/modules/module-avb/mvrp.c', + '../src/modules/module-avb/srp.c', + '../src/modules/module-avb/stream.c', + ] + test('test-avb', + executable('test-avb', + ['test-avb.c'] + avb_module_sources, + include_directories: avb_test_inc, + dependencies: [spa_dep, pipewire_dep, mathlib, dl_lib, rt_lib], + link_with: pwtest_lib) + ) +endif diff --git a/test/test-avb-utils.h b/test/test-avb-utils.h new file mode 100644 index 000000000..d4552f985 --- /dev/null +++ b/test/test-avb-utils.h @@ -0,0 +1,356 @@ +/* AVB test utilities */ +/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */ +/* SPDX-License-Identifier: MIT */ + +#ifndef TEST_AVB_UTILS_H +#define TEST_AVB_UTILS_H + +#include + +#include "module-avb/internal.h" +#include "module-avb/packets.h" +#include "module-avb/adp.h" +#include "module-avb/acmp.h" +#include "module-avb/mrp.h" +#include "module-avb/msrp.h" +#include "module-avb/mvrp.h" +#include "module-avb/mmrp.h" +#include "module-avb/maap.h" +#include "module-avb/aecp.h" +#include "module-avb/aecp-aem.h" +#include "module-avb/aecp-aem-descriptors.h" +#include "module-avb/aecp-aem-state.h" +#include "module-avb/iec61883.h" +#include "module-avb/aaf.h" +#include "module-avb/stream.h" +#include "module-avb/descriptors.h" +#include "module-avb/avb-transport-loopback.h" + +#define server_emit_message(s,n,m,l) \ + spa_hook_list_call(&(s)->listener_list, struct server_events, message, 0, n, m, l) +#define server_emit_periodic(s,n) \ + spa_hook_list_call(&(s)->listener_list, struct server_events, periodic, 0, n) + +/** + * Create a test AVB server with loopback transport. + * All protocol handlers are registered. No network access required. + */ +static inline struct server *avb_test_server_new(struct impl *impl) +{ + struct server *server; + + server = calloc(1, sizeof(*server)); + if (server == NULL) + return NULL; + + server->impl = impl; + server->ifname = strdup("test0"); + server->avb_mode = AVB_MODE_LEGACY; + server->transport = &avb_transport_loopback; + + spa_list_append(&impl->servers, &server->link); + spa_hook_list_init(&server->listener_list); + spa_list_init(&server->descriptors); + spa_list_init(&server->streams); + + if (server->transport->setup(server) < 0) + goto error; + + server->mrp = avb_mrp_new(server); + if (server->mrp == NULL) + goto error; + + avb_aecp_register(server); + server->maap = avb_maap_register(server); + server->mmrp = avb_mmrp_register(server); + server->msrp = avb_msrp_register(server); + server->mvrp = avb_mvrp_register(server); + avb_adp_register(server); + avb_acmp_register(server); + + server->domain_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN); + + avb_mrp_attribute_begin(server->domain_attr->mrp, 0); + avb_mrp_attribute_join(server->domain_attr->mrp, 0, true); + + /* Add a minimal entity descriptor so ADP can advertise. + * We skip init_descriptors() because it creates streams that + * need a pw_core connection. */ + { + struct avb_aem_desc_entity entity; + memset(&entity, 0, sizeof(entity)); + entity.entity_id = htobe64(server->entity_id); + entity.entity_model_id = htobe64(0x0001000000000001ULL); + entity.entity_capabilities = htonl( + AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED | + AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED | + AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED); + entity.talker_stream_sources = htons(1); + entity.talker_capabilities = htons( + AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED | + AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE); + entity.listener_stream_sinks = htons(1); + entity.listener_capabilities = htons( + AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED | + AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK); + entity.configurations_count = htons(1); + server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0, + sizeof(entity), &entity); + } + + return server; + +error: + free(server->ifname); + free(server); + return NULL; +} + +static inline void avb_test_server_free(struct server *server) +{ + avdecc_server_free(server); +} + +/** + * Inject a raw packet into the server's protocol dispatch. + * This simulates receiving a packet from the network. + */ +static inline void avb_test_inject_packet(struct server *server, + uint64_t now, const void *data, int len) +{ + server_emit_message(server, now, data, len); +} + +/** + * Trigger the periodic callback with a given timestamp. + * Use this to advance time and test timeout/readvertise logic. + */ +static inline void avb_test_tick(struct server *server, uint64_t now) +{ + server_emit_periodic(server, now); +} + +/** + * Build an ADP entity available packet. + * Returns the packet size, or -1 on error. + */ +static inline int avb_test_build_adp_entity_available( + uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint64_t entity_id, + int valid_time) +{ + struct avb_ethernet_header *h; + struct avb_packet_adp *p; + size_t len = sizeof(*h) + sizeof(*p); + static const uint8_t bmac[6] = AVB_BROADCAST_MAC; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, bmac, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = (struct avb_packet_adp *)(buf + sizeof(*h)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP); + AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH); + AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE); + AVB_PACKET_ADP_SET_VALID_TIME(p, valid_time); + p->entity_id = htobe64(entity_id); + + return len; +} + +/** + * Build an ADP entity departing packet. + */ +static inline int avb_test_build_adp_entity_departing( + uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint64_t entity_id) +{ + struct avb_ethernet_header *h; + struct avb_packet_adp *p; + size_t len = sizeof(*h) + sizeof(*p); + static const uint8_t bmac[6] = AVB_BROADCAST_MAC; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, bmac, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = (struct avb_packet_adp *)(buf + sizeof(*h)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP); + AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH); + AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING); + p->entity_id = htobe64(entity_id); + + return len; +} + +/** + * Build an ADP entity discover packet. + */ +static inline int avb_test_build_adp_entity_discover( + uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint64_t entity_id) +{ + struct avb_ethernet_header *h; + struct avb_packet_adp *p; + size_t len = sizeof(*h) + sizeof(*p); + static const uint8_t bmac[6] = AVB_BROADCAST_MAC; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, bmac, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = (struct avb_packet_adp *)(buf + sizeof(*h)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP); + AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH); + AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER); + p->entity_id = htobe64(entity_id); + + return len; +} + +/** + * Build an AECP AEM command packet for injection. + * Returns packet size, or -1 on error. + */ +static inline int avb_test_build_aecp_aem( + uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint64_t target_guid, + uint64_t controller_guid, + uint16_t sequence_id, + uint16_t command_type, + const void *payload, size_t payload_size) +{ + struct avb_ethernet_header *h; + struct avb_packet_aecp_aem *p; + size_t len = sizeof(*h) + sizeof(*p) + payload_size; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 }, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = SPA_PTROFF(h, sizeof(*h), void); + AVB_PACKET_SET_SUBTYPE(&p->aecp.hdr, AVB_SUBTYPE_AECP); + AVB_PACKET_AECP_SET_MESSAGE_TYPE(&p->aecp, AVB_AECP_MESSAGE_TYPE_AEM_COMMAND); + AVB_PACKET_AECP_SET_STATUS(&p->aecp, 0); + AVB_PACKET_SET_LENGTH(&p->aecp.hdr, payload_size + 12); + p->aecp.target_guid = htobe64(target_guid); + p->aecp.controller_guid = htobe64(controller_guid); + p->aecp.sequence_id = htons(sequence_id); + AVB_PACKET_AEM_SET_COMMAND_TYPE(p, command_type); + + if (payload && payload_size > 0) + memcpy(p->payload, payload, payload_size); + + return len; +} + +/** + * Create a test AVB server in Milan v1.2 mode with loopback transport. + * The entity descriptor is properly sized for Milan state (lock, unsol). + */ +static inline struct server *avb_test_server_new_milan(struct impl *impl) +{ + struct server *server; + + server = calloc(1, sizeof(*server)); + if (server == NULL) + return NULL; + + server->impl = impl; + server->ifname = strdup("test0"); + server->avb_mode = AVB_MODE_MILAN_V12; + server->transport = &avb_transport_loopback; + + spa_list_append(&impl->servers, &server->link); + spa_hook_list_init(&server->listener_list); + spa_list_init(&server->descriptors); + spa_list_init(&server->streams); + + if (server->transport->setup(server) < 0) + goto error; + + server->mrp = avb_mrp_new(server); + if (server->mrp == NULL) + goto error; + + avb_aecp_register(server); + server->maap = avb_maap_register(server); + server->mmrp = avb_mmrp_register(server); + server->msrp = avb_msrp_register(server); + server->mvrp = avb_mvrp_register(server); + avb_adp_register(server); + avb_acmp_register(server); + + server->domain_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN); + + avb_mrp_attribute_begin(server->domain_attr->mrp, 0); + avb_mrp_attribute_join(server->domain_attr->mrp, 0, true); + + /* Add Milan-sized entity descriptor with lock/unsol state */ + { + struct aecp_aem_entity_milan_state entity_state; + memset(&entity_state, 0, sizeof(entity_state)); + entity_state.state.desc.entity_id = htobe64(server->entity_id); + entity_state.state.desc.entity_model_id = htobe64(0x0001000000000001ULL); + entity_state.state.desc.entity_capabilities = htonl( + AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED | + AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED | + AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED); + entity_state.state.desc.talker_stream_sources = htons(1); + entity_state.state.desc.talker_capabilities = htons( + AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED | + AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE); + entity_state.state.desc.listener_stream_sinks = htons(1); + entity_state.state.desc.listener_capabilities = htons( + AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED | + AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK); + entity_state.state.desc.configurations_count = htons(1); + server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0, + sizeof(entity_state), &entity_state); + } + + return server; + +error: + free(server->ifname); + free(server); + return NULL; +} + +#endif /* TEST_AVB_UTILS_H */ diff --git a/test/test-avb.c b/test/test-avb.c new file mode 100644 index 000000000..ac1966a68 --- /dev/null +++ b/test/test-avb.c @@ -0,0 +1,4017 @@ +/* AVB tests */ +/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */ +/* SPDX-License-Identifier: MIT */ + +#include "pwtest.h" + +#include + +#include "module-avb/aecp-aem-descriptors.h" +#include "module-avb/aecp-aem.h" +#include "module-avb/aecp-aem-types.h" +#include "module-avb/aecp-aem-cmds-resps/cmd-lock-entity.h" +#include "test-avb-utils.h" + +static struct impl *test_impl_new(void) +{ + struct impl *impl; + struct pw_main_loop *ml; + struct pw_context *context; + + pw_init(0, NULL); + + ml = pw_main_loop_new(NULL); + pwtest_ptr_notnull(ml); + + context = pw_context_new(pw_main_loop_get_loop(ml), + pw_properties_new( + PW_KEY_CONFIG_NAME, "null", + NULL), 0); + pwtest_ptr_notnull(context); + + impl = calloc(1, sizeof(*impl)); + pwtest_ptr_notnull(impl); + + impl->loop = pw_main_loop_get_loop(ml); + impl->timer_queue = pw_context_get_timer_queue(context); + impl->context = context; + spa_list_init(&impl->servers); + + return impl; +} + +static void test_impl_free(struct impl *impl) +{ + struct server *s; + spa_list_consume(s, &impl->servers, link) + avb_test_server_free(s); + free(impl); + pw_deinit(); +} + +/* + * Test: inject an ADP ENTITY_AVAILABLE packet and verify + * that the server processes it without error. + */ +PWTEST(avb_adp_entity_available) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 }; + uint64_t remote_entity_id = 0x020000fffe000002ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Build and inject an entity available packet from a remote device */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 10); + pwtest_int_gt(len, 0); + + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* The packet should have been processed without crashing. + * We can't easily inspect ADP internal state without exposing it, + * but we can verify the server is still functional by doing another + * inject and triggering periodic. */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: inject ENTITY_AVAILABLE then ENTITY_DEPARTING for the same entity. + */ +PWTEST(avb_adp_entity_departing) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x03 }; + uint64_t remote_entity_id = 0x020000fffe000003ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* First make the entity known */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 10); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Now send departing */ + len = avb_test_build_adp_entity_departing(pkt, sizeof(pkt), + remote_mac, remote_entity_id); + pwtest_int_gt(len, 0); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: inject ENTITY_DISCOVER with entity_id=0 (discover all). + * The server should respond with its own entity advertisement + * once it has one (after periodic runs check_advertise). + */ +PWTEST(avb_adp_entity_discover) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x04 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Trigger periodic to let the server advertise its own entity + * (check_advertise reads the entity descriptor) */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Send discover-all (entity_id = 0) */ + len = avb_test_build_adp_entity_discover(pkt, sizeof(pkt), + remote_mac, 0); + pwtest_int_gt(len, 0); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* The server should have sent an advertise response */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: entity timeout — add an entity, then advance time past + * valid_time + 2 seconds and verify periodic cleans it up. + */ +PWTEST(avb_adp_entity_timeout) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x05 }; + uint64_t remote_entity_id = 0x020000fffe000005ULL; + int valid_time = 10; /* seconds */ + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Add entity */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, valid_time); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Tick at various times before timeout — entity should survive */ + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 10 * SPA_NSEC_PER_SEC); + + /* Tick past valid_time + 2 seconds from last_time (1s + 12s = 13s) */ + avb_test_tick(server, 14 * SPA_NSEC_PER_SEC); + + /* The entity should have been timed out and cleaned up. + * If the entity was still present and had advertise=true, a departing + * packet would be sent. Inject a discover to verify: if the entity + * is gone, no response for that specific entity_id. */ + + avb_loopback_clear_packets(server); + len = avb_test_build_adp_entity_discover(pkt, sizeof(pkt), + remote_mac, remote_entity_id); + avb_test_inject_packet(server, 15 * SPA_NSEC_PER_SEC, pkt, len); + + /* Remote entities don't have advertise=true, so even before timeout + * a discover for them wouldn't generate a response. But at least + * the timeout path was exercised without crashes. */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: basic MRP attribute lifecycle — create, begin, join. + */ +PWTEST(avb_mrp_attribute_lifecycle) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create an MSRP talker attribute */ + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + pwtest_ptr_notnull(attr->mrp); + + /* Begin and join the attribute */ + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Tick to process the MRP state machine */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: server with Milan v1.2 mode. + */ +PWTEST(avb_milan_server_create) +{ + struct impl *impl; + struct server *server; + + impl = test_impl_new(); + + /* Create a Milan-mode server manually */ + server = calloc(1, sizeof(*server)); + pwtest_ptr_notnull(server); + + server->impl = impl; + server->ifname = strdup("test0"); + server->avb_mode = AVB_MODE_MILAN_V12; + server->transport = &avb_transport_loopback; + + spa_list_append(&impl->servers, &server->link); + spa_hook_list_init(&server->listener_list); + spa_list_init(&server->descriptors); + + pwtest_int_eq(server->transport->setup(server), 0); + + server->mrp = avb_mrp_new(server); + pwtest_ptr_notnull(server->mrp); + + avb_aecp_register(server); + server->maap = avb_maap_register(server); + server->mmrp = avb_mmrp_register(server); + server->msrp = avb_msrp_register(server); + server->mvrp = avb_mvrp_register(server); + avb_adp_register(server); + avb_acmp_register(server); + + server->domain_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN); + + avb_mrp_attribute_begin(server->domain_attr->mrp, 0); + avb_mrp_attribute_join(server->domain_attr->mrp, 0, true); + + /* Add minimal entity descriptor (skip init_descriptors which needs pw_core) */ + { + struct avb_aem_desc_entity entity; + memset(&entity, 0, sizeof(entity)); + entity.entity_id = htobe64(server->entity_id); + entity.entity_model_id = htobe64(0x0001000000000001ULL); + entity.configurations_count = htons(1); + server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0, + sizeof(entity), &entity); + } + + /* Verify Milan mode was set correctly */ + pwtest_str_eq(get_avb_mode_str(server->avb_mode), "Milan V1.2"); + + /* Tick to exercise periodic handlers with Milan descriptors */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 3: MRP State Machine Tests + * ===================================================================== + */ + +/* + * Test: MRP attribute begin sets initial state, join(new=true) enables + * pending_send after TX event via periodic tick. + */ +PWTEST(avb_mrp_begin_join_new_tx) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create a talker attribute */ + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + + /* After begin, pending_send should be 0 */ + avb_mrp_attribute_begin(attr->mrp, 0); + pwtest_int_eq(attr->mrp->pending_send, 0); + + /* Join with new=true */ + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Tick to let timers initialize (first periodic skips events) */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + + /* Tick past join timer (100ms) to trigger TX event */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* After TX, pending_send should be set (NEW=0 encoded as non-zero + * only if the state machine decided to send). The VN state on TX + * produces SEND_NEW. But pending_send is only written if joined=true. */ + /* We mainly verify no crash and that the state machine ran. */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP attribute join then leave cycle. + * After leave, the attribute should eventually stop sending. + */ +PWTEST(avb_mrp_join_leave_cycle) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Let the state machine run a few cycles */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* Now leave */ + avb_mrp_attribute_leave(attr->mrp, 2 * SPA_NSEC_PER_SEC); + + /* After leave, pending_send should reflect leaving state. + * The next TX event should send LV and transition to VO. */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* After the TX, pending_send should be 0 (joined is false, + * so pending_send is not updated by the state machine). */ + pwtest_int_eq(attr->mrp->pending_send, 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP attribute receives RX_NEW, which triggers a registrar + * notification (NOTIFY_NEW). Verify via a notification tracker. + */ +struct notify_tracker { + int new_count; + int join_count; + int leave_count; + uint8_t last_notify; +}; + +static void track_mrp_notify(void *data, uint64_t now, + struct avb_mrp_attribute *attr, uint8_t notify) +{ + struct notify_tracker *t = data; + t->last_notify = notify; + switch (notify) { + case AVB_MRP_NOTIFY_NEW: + t->new_count++; + break; + case AVB_MRP_NOTIFY_JOIN: + t->join_count++; + break; + case AVB_MRP_NOTIFY_LEAVE: + t->leave_count++; + break; + } +} + +static const struct avb_mrp_events test_mrp_events = { + AVB_VERSION_MRP_EVENTS, + .notify = track_mrp_notify, +}; + +PWTEST(avb_mrp_rx_new_notification) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + struct spa_hook listener; + struct notify_tracker tracker = { 0 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Register a global MRP listener to track notifications */ + avb_mrp_add_listener(server->mrp, &listener, &test_mrp_events, &tracker); + + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Simulate receiving NEW from a peer */ + avb_mrp_attribute_rx_event(attr->mrp, 1 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_NEW); + + /* RX_NEW should trigger NOTIFY_NEW on the registrar */ + pwtest_int_eq(tracker.new_count, 1); + pwtest_int_eq(tracker.last_notify, AVB_MRP_NOTIFY_NEW); + + /* Simulate receiving JOININ from a peer (already IN, no new notification) */ + avb_mrp_attribute_rx_event(attr->mrp, 2 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_JOININ); + /* Registrar was already IN, so no additional JOIN notification */ + pwtest_int_eq(tracker.join_count, 0); + + spa_hook_remove(&listener); + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP registrar leave timer — after RX_LV, the registrar enters + * LV state. After MRP_LVTIMER_MS (1000ms), LV_TIMER fires and + * registrar transitions to MT with NOTIFY_LEAVE. + */ +PWTEST(avb_mrp_registrar_leave_timer) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + struct spa_hook listener; + struct notify_tracker tracker = { 0 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_mrp_add_listener(server->mrp, &listener, &test_mrp_events, &tracker); + + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Get registrar to IN state via RX_NEW */ + avb_mrp_attribute_rx_event(attr->mrp, 1 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_NEW); + pwtest_int_eq(tracker.new_count, 1); + + /* RX_LV transitions registrar IN -> LV, sets leave_timeout */ + avb_mrp_attribute_rx_event(attr->mrp, 2 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_LV); + + /* Tick before the leave timer expires — no LEAVE notification yet */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC + 500 * SPA_NSEC_PER_MSEC); + pwtest_int_eq(tracker.leave_count, 0); + + /* Tick after the leave timer expires (1000ms after RX_LV at 2s = 3s) */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC + 100 * SPA_NSEC_PER_MSEC); + pwtest_int_eq(tracker.leave_count, 1); + + spa_hook_remove(&listener); + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Multiple MRP attributes coexist — events applied to all. + */ +PWTEST(avb_mrp_multiple_attributes) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr1, *attr2; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + attr1 = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + attr2 = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_LISTENER); + pwtest_ptr_notnull(attr1); + pwtest_ptr_notnull(attr2); + + avb_mrp_attribute_begin(attr1->mrp, 0); + avb_mrp_attribute_join(attr1->mrp, 0, true); + + avb_mrp_attribute_begin(attr2->mrp, 0); + avb_mrp_attribute_join(attr2->mrp, 0, false); + + /* Periodic tick should apply to both attributes without crash */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 3: MSRP Tests + * ===================================================================== + */ + +/* + * Test: Create each MSRP attribute type and verify fields. + */ +PWTEST(avb_msrp_attribute_types) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *talker, *talker_fail, *listener_attr, *domain; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create all four MSRP attribute types */ + talker = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(talker); + pwtest_int_eq(talker->type, AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + + talker_fail = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED); + pwtest_ptr_notnull(talker_fail); + pwtest_int_eq(talker_fail->type, AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED); + + listener_attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_LISTENER); + pwtest_ptr_notnull(listener_attr); + pwtest_int_eq(listener_attr->type, AVB_MSRP_ATTRIBUTE_TYPE_LISTENER); + + domain = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + pwtest_ptr_notnull(domain); + pwtest_int_eq(domain->type, AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + + /* Configure talker with stream parameters */ + talker->attr.talker.stream_id = htobe64(0x020000fffe000001ULL); + talker->attr.talker.vlan_id = htons(AVB_DEFAULT_VLAN); + talker->attr.talker.tspec_max_frame_size = htons(256); + talker->attr.talker.tspec_max_interval_frames = htons( + AVB_MSRP_TSPEC_MAX_INTERVAL_FRAMES_DEFAULT); + talker->attr.talker.priority = AVB_MSRP_PRIORITY_DEFAULT; + talker->attr.talker.rank = AVB_MSRP_RANK_DEFAULT; + + /* Configure listener for same stream */ + listener_attr->attr.listener.stream_id = htobe64(0x020000fffe000001ULL); + listener_attr->param = AVB_MSRP_LISTENER_PARAM_READY; + + /* Begin and join all attributes */ + avb_mrp_attribute_begin(talker->mrp, 0); + avb_mrp_attribute_join(talker->mrp, 0, true); + avb_mrp_attribute_begin(talker_fail->mrp, 0); + avb_mrp_attribute_join(talker_fail->mrp, 0, true); + avb_mrp_attribute_begin(listener_attr->mrp, 0); + avb_mrp_attribute_join(listener_attr->mrp, 0, true); + avb_mrp_attribute_begin(domain->mrp, 0); + avb_mrp_attribute_join(domain->mrp, 0, true); + + /* Tick to exercise all attribute types through the state machine */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MSRP domain attribute encode/transmit via loopback. + * After join+TX, the domain attribute should produce a packet. + */ +PWTEST(avb_msrp_domain_transmit) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *domain; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* The test server already has a domain_attr, but create another + * to test independent domain attribute behavior */ + domain = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + domain->attr.domain.sr_class_id = 7; + domain->attr.domain.sr_class_priority = 2; + domain->attr.domain.sr_class_vid = htons(100); + + avb_mrp_attribute_begin(domain->mrp, 0); + avb_mrp_attribute_join(domain->mrp, 0, true); + + /* Let timers initialize and then trigger TX */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* MSRP should have transmitted a packet with domain data */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MSRP talker advertise encode/transmit via loopback. + */ +PWTEST(avb_msrp_talker_transmit) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *talker; + uint64_t stream_id = 0x020000fffe000001ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + talker = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(talker); + + talker->attr.talker.stream_id = htobe64(stream_id); + talker->attr.talker.vlan_id = htons(AVB_DEFAULT_VLAN); + talker->attr.talker.tspec_max_frame_size = htons(256); + talker->attr.talker.tspec_max_interval_frames = htons(1); + talker->attr.talker.priority = AVB_MSRP_PRIORITY_DEFAULT; + talker->attr.talker.rank = AVB_MSRP_RANK_DEFAULT; + + avb_mrp_attribute_begin(talker->mrp, 0); + avb_mrp_attribute_join(talker->mrp, 0, true); + + /* Let timers initialize */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Trigger TX */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* Should have transmitted the talker advertise */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + /* Read the packet and verify it contains valid MSRP data */ + { + uint8_t buf[2048]; + int len; + struct avb_packet_mrp *mrp_pkt; + + len = avb_loopback_get_packet(server, buf, sizeof(buf)); + pwtest_int_gt(len, (int)sizeof(struct avb_packet_mrp)); + + mrp_pkt = (struct avb_packet_mrp *)buf; + pwtest_int_eq(mrp_pkt->version, AVB_MRP_PROTOCOL_VERSION); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 3: MRP Packet Parsing Tests + * ===================================================================== + */ + +struct parse_tracker { + int check_header_count; + int attr_event_count; + int process_count; + uint8_t last_attr_type; + uint8_t last_event; + uint8_t last_param; +}; + +static bool test_check_header(void *data, const void *hdr, + size_t *hdr_size, bool *has_params) +{ + struct parse_tracker *t = data; + const struct avb_packet_mrp_hdr *h = hdr; + t->check_header_count++; + + /* Accept attribute types 1-4 (MSRP-like) */ + if (h->attribute_type < 1 || h->attribute_type > 4) + return false; + + *hdr_size = sizeof(struct avb_packet_msrp_msg); + *has_params = (h->attribute_type == AVB_MSRP_ATTRIBUTE_TYPE_LISTENER); + return true; +} + +static int test_attr_event(void *data, uint64_t now, + uint8_t attribute_type, uint8_t event) +{ + struct parse_tracker *t = data; + t->attr_event_count++; + return 0; +} + +static int test_process(void *data, uint64_t now, + uint8_t attribute_type, const void *value, + uint8_t event, uint8_t param, int index) +{ + struct parse_tracker *t = data; + t->process_count++; + t->last_attr_type = attribute_type; + t->last_event = event; + t->last_param = param; + return 0; +} + +static const struct avb_mrp_parse_info test_parse_info = { + AVB_VERSION_MRP_PARSE_INFO, + .check_header = test_check_header, + .attr_event = test_attr_event, + .process = test_process, +}; + +/* + * Test: Parse a minimal MRP packet with a single domain value. + */ +PWTEST(avb_mrp_parse_single_domain) +{ + struct impl *impl; + struct server *server; + struct parse_tracker tracker = { 0 }; + uint8_t buf[256]; + int pos = 0; + int res; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(buf, 0, sizeof(buf)); + + /* Build MRP packet manually: + * [ethernet header + version] already at offset 0 */ + { + struct avb_packet_mrp *mrp = (struct avb_packet_mrp *)buf; + mrp->version = AVB_MRP_PROTOCOL_VERSION; + pos = sizeof(struct avb_packet_mrp); + } + + /* MSRP message header for domain (type=4, length=4) */ + { + struct avb_packet_msrp_msg *msg = + (struct avb_packet_msrp_msg *)(buf + pos); + struct avb_packet_mrp_vector *v; + struct avb_packet_msrp_domain *d; + uint8_t *ev; + + msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN; + msg->attribute_length = sizeof(struct avb_packet_msrp_domain); + + v = (struct avb_packet_mrp_vector *)msg->attribute_list; + v->lva = 0; + AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1); + + d = (struct avb_packet_msrp_domain *)v->first_value; + d->sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + d->sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + d->sr_class_vid = htons(AVB_DEFAULT_VLAN); + + /* Event byte: 1 value, event=JOININ(1), packed as 1*36 = 36 */ + ev = (uint8_t *)(d + 1); + *ev = AVB_MRP_ATTRIBUTE_EVENT_JOININ * 36; + + msg->attribute_list_length = htons( + sizeof(*v) + sizeof(*d) + 1 + 2); /* +2 for vector end mark */ + + /* Vector end mark */ + pos += sizeof(*msg) + sizeof(*v) + sizeof(*d) + 1; + buf[pos++] = 0; + buf[pos++] = 0; + + /* Attribute end mark */ + buf[pos++] = 0; + buf[pos++] = 0; + } + + res = avb_mrp_parse_packet(server->mrp, 1 * SPA_NSEC_PER_SEC, + buf, pos, &test_parse_info, &tracker); + + pwtest_int_eq(res, 0); + pwtest_int_eq(tracker.check_header_count, 1); + pwtest_int_eq(tracker.process_count, 1); + pwtest_int_eq(tracker.last_attr_type, AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN); + pwtest_int_eq(tracker.last_event, AVB_MRP_ATTRIBUTE_EVENT_JOININ); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Parse MRP packet with LVA (leave-all) flag set. + */ +PWTEST(avb_mrp_parse_with_lva) +{ + struct impl *impl; + struct server *server; + struct parse_tracker tracker = { 0 }; + uint8_t buf[256]; + int pos = 0; + int res; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(buf, 0, sizeof(buf)); + + { + struct avb_packet_mrp *mrp = (struct avb_packet_mrp *)buf; + mrp->version = AVB_MRP_PROTOCOL_VERSION; + pos = sizeof(struct avb_packet_mrp); + } + + { + struct avb_packet_msrp_msg *msg = + (struct avb_packet_msrp_msg *)(buf + pos); + struct avb_packet_mrp_vector *v; + struct avb_packet_msrp_domain *d; + uint8_t *ev; + + msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN; + msg->attribute_length = sizeof(struct avb_packet_msrp_domain); + + v = (struct avb_packet_mrp_vector *)msg->attribute_list; + v->lva = 1; /* Set LVA flag */ + AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1); + + d = (struct avb_packet_msrp_domain *)v->first_value; + d->sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + d->sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + d->sr_class_vid = htons(AVB_DEFAULT_VLAN); + + ev = (uint8_t *)(d + 1); + *ev = AVB_MRP_ATTRIBUTE_EVENT_NEW * 36; + + msg->attribute_list_length = htons( + sizeof(*v) + sizeof(*d) + 1 + 2); + + pos += sizeof(*msg) + sizeof(*v) + sizeof(*d) + 1; + buf[pos++] = 0; + buf[pos++] = 0; + buf[pos++] = 0; + buf[pos++] = 0; + } + + res = avb_mrp_parse_packet(server->mrp, 1 * SPA_NSEC_PER_SEC, + buf, pos, &test_parse_info, &tracker); + + pwtest_int_eq(res, 0); + pwtest_int_eq(tracker.check_header_count, 1); + pwtest_int_eq(tracker.attr_event_count, 1); /* LVA event fired */ + pwtest_int_eq(tracker.process_count, 1); + pwtest_int_eq(tracker.last_event, AVB_MRP_ATTRIBUTE_EVENT_NEW); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Parse MRP packet with multiple values (3 values per event byte). + * Verifies the base-6 event decoding logic. + */ +PWTEST(avb_mrp_parse_three_values) +{ + struct impl *impl; + struct server *server; + struct parse_tracker tracker = { 0 }; + uint8_t buf[256]; + int pos = 0; + int res; + uint8_t ev0 = AVB_MRP_ATTRIBUTE_EVENT_NEW; /* 0 */ + uint8_t ev1 = AVB_MRP_ATTRIBUTE_EVENT_JOININ; /* 1 */ + uint8_t ev2 = AVB_MRP_ATTRIBUTE_EVENT_MT; /* 4 */ + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(buf, 0, sizeof(buf)); + + { + struct avb_packet_mrp *mrp = (struct avb_packet_mrp *)buf; + mrp->version = AVB_MRP_PROTOCOL_VERSION; + pos = sizeof(struct avb_packet_mrp); + } + + { + struct avb_packet_msrp_msg *msg = + (struct avb_packet_msrp_msg *)(buf + pos); + struct avb_packet_mrp_vector *v; + struct avb_packet_msrp_domain *d; + uint8_t *ev; + + msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN; + msg->attribute_length = sizeof(struct avb_packet_msrp_domain); + + v = (struct avb_packet_mrp_vector *)msg->attribute_list; + v->lva = 0; + AVB_MRP_VECTOR_SET_NUM_VALUES(v, 3); + + /* First value (domain data) — all 3 values share the same + * first_value pointer in the parse callback */ + d = (struct avb_packet_msrp_domain *)v->first_value; + d->sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT; + d->sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT; + d->sr_class_vid = htons(AVB_DEFAULT_VLAN); + + /* Pack 3 events into 1 byte: ev0*36 + ev1*6 + ev2 */ + ev = (uint8_t *)(d + 1); + *ev = ev0 * 36 + ev1 * 6 + ev2; + + msg->attribute_list_length = htons( + sizeof(*v) + sizeof(*d) + 1 + 2); + + pos += sizeof(*msg) + sizeof(*v) + sizeof(*d) + 1; + buf[pos++] = 0; + buf[pos++] = 0; + buf[pos++] = 0; + buf[pos++] = 0; + } + + res = avb_mrp_parse_packet(server->mrp, 1 * SPA_NSEC_PER_SEC, + buf, pos, &test_parse_info, &tracker); + + pwtest_int_eq(res, 0); + pwtest_int_eq(tracker.process_count, 3); + /* The last value processed should have event MT (4) */ + pwtest_int_eq(tracker.last_event, AVB_MRP_ATTRIBUTE_EVENT_MT); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MSRP talker-failed attribute with notification. + * This tests the NULL notify crash that was fixed. + */ +PWTEST(avb_msrp_talker_failed_notify) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *talker_fail; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + talker_fail = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED); + pwtest_ptr_notnull(talker_fail); + + talker_fail->attr.talker_fail.talker.stream_id = + htobe64(0x020000fffe000001ULL); + talker_fail->attr.talker_fail.failure_code = AVB_MRP_FAIL_BANDWIDTH; + + avb_mrp_attribute_begin(talker_fail->mrp, 0); + avb_mrp_attribute_join(talker_fail->mrp, 0, true); + + /* Simulate receiving NEW from a peer — this triggers NOTIFY_NEW + * which calls msrp_notify -> dispatch[TALKER_FAILED].notify. + * Before the fix, this would crash with NULL pointer dereference. */ + avb_mrp_attribute_rx_event(talker_fail->mrp, 1 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_NEW); + + /* If we get here without crashing, the NULL check fix works */ + + /* Also exercise periodic to verify full lifecycle */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 4: ACMP Integration Tests + * ===================================================================== + */ + +/** + * Build an ACMP packet for injection into a server. + * Returns packet size, or -1 on error. + */ +static int avb_test_build_acmp(uint8_t *buf, size_t bufsize, + const uint8_t src_mac[6], + uint8_t message_type, + uint64_t controller_guid, + uint64_t talker_guid, + uint64_t listener_guid, + uint16_t talker_unique_id, + uint16_t listener_unique_id, + uint16_t sequence_id) +{ + struct avb_ethernet_header *h; + struct avb_packet_acmp *p; + size_t len = sizeof(*h) + sizeof(*p); + static const uint8_t acmp_mac[6] = AVB_BROADCAST_MAC; + + if (bufsize < len) + return -1; + + memset(buf, 0, len); + + h = (struct avb_ethernet_header *)buf; + memcpy(h->dest, acmp_mac, 6); + memcpy(h->src, src_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p = (struct avb_packet_acmp *)(buf + sizeof(*h)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ACMP); + AVB_PACKET_ACMP_SET_MESSAGE_TYPE(p, message_type); + AVB_PACKET_ACMP_SET_STATUS(p, AVB_ACMP_STATUS_SUCCESS); + p->controller_guid = htobe64(controller_guid); + p->talker_guid = htobe64(talker_guid); + p->listener_guid = htobe64(listener_guid); + p->talker_unique_id = htons(talker_unique_id); + p->listener_unique_id = htons(listener_unique_id); + p->sequence_id = htons(sequence_id); + + return len; +} + +/* + * Test: ACMP GET_TX_STATE_COMMAND should respond with NOT_SUPPORTED. + */ +PWTEST(avb_acmp_not_supported) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x10 }; + uint64_t remote_entity_id = 0x020000fffe000010ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send GET_TX_STATE_COMMAND to our server as talker */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, + remote_entity_id, /* controller */ + server->entity_id, /* talker = us */ + 0, /* listener */ + 0, 0, 42); + pwtest_int_gt(len, 0); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Server should respond with NOT_SUPPORTED */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + /* Read response and verify it's a GET_TX_STATE_RESPONSE with NOT_SUPPORTED */ + { + uint8_t rbuf[256]; + int rlen; + struct avb_packet_acmp *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(resp), + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_STATUS(resp), + AVB_ACMP_STATUS_NOT_SUPPORTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP CONNECT_TX_COMMAND to our server with no streams + * should respond with TALKER_NO_STREAM_INDEX. + */ +PWTEST(avb_acmp_connect_tx_no_stream) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x11 }; + uint64_t remote_entity_id = 0x020000fffe000011ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send CONNECT_TX_COMMAND — we have no streams configured */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND, + remote_entity_id, /* controller */ + server->entity_id, /* talker = us */ + remote_entity_id, /* listener */ + 0, 0, 1); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should respond with CONNECT_TX_RESPONSE + TALKER_NO_STREAM_INDEX */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[256]; + int rlen; + struct avb_packet_acmp *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(resp), + AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_STATUS(resp), + AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP message addressed to a different entity_id is ignored. + */ +PWTEST(avb_acmp_wrong_entity_ignored) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x12 }; + uint64_t other_entity = 0xDEADBEEFCAFE0001ULL; + uint64_t controller_entity = 0x020000fffe000012ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* CONNECT_TX_COMMAND addressed to a different talker — should be ignored */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND, + controller_entity, + other_entity, /* talker = NOT us */ + controller_entity, /* listener */ + 0, 0, 1); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* No response should be sent since the GUID doesn't match */ + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* CONNECT_RX_COMMAND addressed to a different listener — also ignored */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND, + controller_entity, + other_entity, /* talker */ + other_entity, /* listener = NOT us */ + 0, 0, 2); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* Still no response */ + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP CONNECT_RX_COMMAND to our server as listener. + * Should create a pending request and forward CONNECT_TX_COMMAND to talker. + */ +PWTEST(avb_acmp_connect_rx_forward) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x20 }; + uint64_t controller_entity = 0x020000fffe000020ULL; + uint64_t talker_entity = 0x020000fffe000030ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send CONNECT_RX_COMMAND to us as listener */ + len = avb_test_build_acmp(pkt, sizeof(pkt), controller_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND, + controller_entity, + talker_entity, /* talker = remote */ + server->entity_id, /* listener = us */ + 0, 0, 100); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* We should have forwarded a CONNECT_TX_COMMAND to the talker */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[256]; + int rlen; + struct avb_packet_acmp *cmd; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + cmd = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(cmd), + AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_COMMAND); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP pending timeout and retry behavior. + * After CONNECT_RX_COMMAND, the listener creates a pending request. + * After timeout (2000ms for CONNECT_TX), it should retry once. + * After second timeout, it should be cleaned up. + */ +PWTEST(avb_acmp_pending_timeout) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x21 }; + uint64_t controller_entity = 0x020000fffe000021ULL; + uint64_t talker_entity = 0x020000fffe000031ULL; + int pkt_count_after_forward; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Create a pending request via CONNECT_RX_COMMAND */ + len = avb_test_build_acmp(pkt, sizeof(pkt), controller_mac, + AVB_ACMP_MESSAGE_TYPE_CONNECT_RX_COMMAND, + controller_entity, + talker_entity, + server->entity_id, + 0, 0, 200); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Count packets after initial forward */ + pkt_count_after_forward = avb_loopback_get_packet_count(server); + pwtest_int_gt(pkt_count_after_forward, 0); + + /* Drain the packet queue */ + avb_loopback_clear_packets(server); + + /* Tick before timeout (2000ms) — no retry yet */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Tick after timeout (1s + 2000ms = 3s) — should retry */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC + 100 * SPA_NSEC_PER_MSEC); + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + avb_loopback_clear_packets(server); + + /* Tick after second timeout — should give up (no more retries) */ + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + /* The pending was freed, no more retries */ + + /* Tick again — should be clean, no crashes */ + avb_test_tick(server, 6 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP message with wrong EtherType or subtype is filtered. + */ +PWTEST(avb_acmp_packet_filtering) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x13 }; + struct avb_ethernet_header *h; + struct avb_packet_acmp *p; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Build a valid-looking ACMP packet but with wrong EtherType */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, + 0, server->entity_id, 0, 0, 0, 1); + h = (struct avb_ethernet_header *)pkt; + h->type = htons(0x1234); /* Wrong EtherType */ + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Build packet with wrong subtype */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, + 0, server->entity_id, 0, 0, 0, 2); + p = (struct avb_packet_acmp *)(pkt + sizeof(struct avb_ethernet_header)); + AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP); /* Wrong subtype */ + + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Build packet with correct parameters — should get response */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_GET_TX_STATE_COMMAND, + 0, server->entity_id, 0, 0, 0, 3); + avb_test_inject_packet(server, 3 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 5: AECP/AEM Entity Model Tests + * ===================================================================== + */ + +/* + * Test: AECP READ_DESCRIPTOR for the entity descriptor. + * Verifies that a valid READ_DESCRIPTOR command returns SUCCESS + * with the entity descriptor data. + */ +PWTEST(avb_aecp_read_descriptor_entity) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x40 }; + uint64_t controller_id = 0x020000fffe000040ULL; + struct avb_packet_aecp_aem_read_descriptor rd; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(&rd, 0, sizeof(rd)); + rd.configuration = 0; + rd.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + rd.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get a response */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)(sizeof(struct avb_ethernet_header) + + sizeof(struct avb_packet_aecp_aem))); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + + /* Should be AEM_RESPONSE with SUCCESS */ + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + + /* Response should include the descriptor data, making it + * larger than just the header + read_descriptor payload */ + pwtest_int_gt(rlen, (int)(sizeof(struct avb_ethernet_header) + + sizeof(struct avb_packet_aecp_aem) + + sizeof(struct avb_packet_aecp_aem_read_descriptor))); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP READ_DESCRIPTOR for a non-existent descriptor. + * Should return NO_SUCH_DESCRIPTOR error. + */ +PWTEST(avb_aecp_read_descriptor_not_found) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x41 }; + uint64_t controller_id = 0x020000fffe000041ULL; + struct avb_packet_aecp_aem_read_descriptor rd; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Request a descriptor type that doesn't exist */ + memset(&rd, 0, sizeof(rd)); + rd.descriptor_type = htons(AVB_AEM_DESC_AUDIO_UNIT); + rd.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP message filtering — wrong EtherType and subtype. + */ +PWTEST(avb_aecp_packet_filtering) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x42 }; + uint64_t controller_id = 0x020000fffe000042ULL; + struct avb_packet_aecp_aem_read_descriptor rd; + struct avb_ethernet_header *h; + struct avb_packet_aecp_aem *p; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(&rd, 0, sizeof(rd)); + rd.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + rd.descriptor_id = htons(0); + + /* Wrong EtherType — should be filtered */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + h = (struct avb_ethernet_header *)pkt; + h->type = htons(0x1234); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Wrong subtype — should be filtered */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 2, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + p = SPA_PTROFF(pkt, sizeof(struct avb_ethernet_header), void); + AVB_PACKET_SET_SUBTYPE(&p->aecp.hdr, AVB_SUBTYPE_ADP); + + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Correct packet — should get a response */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 3, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + avb_test_inject_packet(server, 3 * SPA_NSEC_PER_SEC, pkt, len); + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP unsupported message types (ADDRESS_ACCESS, AVC, VENDOR_UNIQUE). + * Should return NOT_IMPLEMENTED. + */ +PWTEST(avb_aecp_unsupported_message_types) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x43 }; + uint64_t controller_id = 0x020000fffe000043ULL; + struct avb_packet_aecp_aem *p; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Build a basic AECP packet, then change message type to ADDRESS_ACCESS */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + NULL, 0); + + p = SPA_PTROFF(pkt, sizeof(struct avb_ethernet_header), void); + AVB_PACKET_AECP_SET_MESSAGE_TYPE(&p->aecp, + AVB_AECP_MESSAGE_TYPE_ADDRESS_ACCESS_COMMAND); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_header *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(resp), + AVB_AECP_STATUS_NOT_IMPLEMENTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AEM command not in the legacy command table. + * Should return NOT_IMPLEMENTED. + */ +PWTEST(avb_aecp_aem_not_implemented) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x44 }; + uint64_t controller_id = 0x020000fffe000044ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* REBOOT command is not in the legacy table */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_REBOOT, + NULL, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP ACQUIRE_ENTITY (legacy) with valid entity descriptor. + * Tests the fix for the pointer offset bug in handle_acquire_entity_avb_legacy. + */ +PWTEST(avb_aecp_acquire_entity_legacy) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x45 }; + uint64_t controller_id = 0x020000fffe000045ULL; + struct avb_packet_aecp_aem_acquire acq; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Acquire the entity descriptor */ + memset(&acq, 0, sizeof(acq)); + acq.flags = 0; + acq.owner_guid = htobe64(controller_id); + acq.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + acq.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_ACQUIRE_ENTITY, + &acq, sizeof(acq)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP LOCK_ENTITY (legacy) with valid entity descriptor. + * Tests the fix for the pointer offset bug in handle_lock_entity_avb_legacy. + */ +PWTEST(avb_aecp_lock_entity_legacy) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x46 }; + uint64_t controller_id = 0x020000fffe000046ULL; + struct avb_packet_aecp_aem_acquire lock_pkt; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Lock the entity descriptor (lock uses same struct as acquire) */ + memset(&lock_pkt, 0, sizeof(lock_pkt)); + lock_pkt.flags = 0; + lock_pkt.owner_guid = htobe64(controller_id); + lock_pkt.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + lock_pkt.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_pkt, sizeof(lock_pkt)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan ENTITY_AVAILABLE command. + * Verifies the entity available handler returns lock status. + */ +PWTEST(avb_aecp_entity_available_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x47 }; + uint64_t controller_id = 0x020000fffe000047ULL; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* ENTITY_AVAILABLE has no payload */ + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_ENTITY_AVAILABLE, + NULL, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan LOCK_ENTITY — lock, verify locked, unlock. + * Tests lock semantics and the reply_status pointer fix. + */ +PWTEST(avb_aecp_lock_entity_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x48 }; + uint64_t controller_id = 0x020000fffe000048ULL; + uint64_t other_controller = 0x020000fffe000049ULL; + struct avb_packet_aecp_aem_lock lock_payload; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* Lock the entity */ + memset(&lock_payload, 0, sizeof(lock_payload)); + lock_payload.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + lock_payload.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_payload, sizeof(lock_payload)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get SUCCESS for the lock */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + /* Another controller tries to lock — should get ENTITY_LOCKED */ + avb_loopback_clear_packets(server); + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, other_controller, 2, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_payload, sizeof(lock_payload)); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_ENTITY_LOCKED); + } + + /* Original controller unlocks */ + avb_loopback_clear_packets(server); + lock_payload.flags = htonl(AECP_AEM_LOCK_ENTITY_FLAG_UNLOCK); + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 3, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_payload, sizeof(lock_payload)); + avb_test_inject_packet(server, 3 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan LOCK_ENTITY for non-entity descriptor returns NOT_SUPPORTED. + */ +PWTEST(avb_aecp_lock_non_entity_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x4A }; + uint64_t controller_id = 0x020000fffe00004AULL; + struct avb_packet_aecp_aem_lock lock_payload; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* Try to lock AUDIO_UNIT descriptor (not entity) */ + memset(&lock_payload, 0, sizeof(lock_payload)); + lock_payload.descriptor_type = htons(AVB_AEM_DESC_AUDIO_UNIT); + lock_payload.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_LOCK_ENTITY, + &lock_payload, sizeof(lock_payload)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get NO_SUCH_DESCRIPTOR (audio_unit doesn't exist) */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + /* Bug fix verified: reply_status now gets the full frame pointer, + * so the response is correctly formed */ + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan ACQUIRE_ENTITY returns NOT_SUPPORTED. + * Milan v1.2 does not implement acquire. + */ +PWTEST(avb_aecp_acquire_entity_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x4B }; + uint64_t controller_id = 0x020000fffe00004BULL; + struct avb_packet_aecp_aem_acquire acq; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + memset(&acq, 0, sizeof(acq)); + acq.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + acq.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_ACQUIRE_ENTITY, + &acq, sizeof(acq)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NOT_SUPPORTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Milan READ_DESCRIPTOR works the same as legacy. + */ +PWTEST(avb_aecp_read_descriptor_milan) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x4C }; + uint64_t controller_id = 0x020000fffe00004CULL; + struct avb_packet_aecp_aem_read_descriptor rd; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + memset(&rd, 0, sizeof(rd)); + rd.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + rd.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_READ_DESCRIPTOR, + &rd, sizeof(rd)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 7: Additional Protocol Coverage Tests + * ===================================================================== + */ + +/* + * Test: MAAP conflict detection — verify the 4 overlap cases in + * maap_check_conflict(). MAAP uses the pool 91:e0:f0:00:xx:xx, + * so only the last 2 bytes (offset) matter for overlap checks. + * + * We can't call maap_check_conflict() directly (it's static), but + * we can test the MAAP state machine via packet injection. + * When a PROBE packet is received that conflicts with our reservation + * in ANNOUNCE state, the server should send a DEFEND packet. + * When in PROBE state, a conflict causes re-probing (new address). + */ +PWTEST(avb_maap_conflict_probe_in_announce) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + struct avb_ethernet_header *h; + struct avb_packet_maap *p; + size_t len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x50 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* avb_maap_reserve(server->maap, 1) was called in avb_test_server_new + * via the test-avb-utils.h helper (through init of domain_attr etc). + * The MAAP starts in STATE_PROBE. We need to advance it to STATE_ANNOUNCE + * by ticking through the probe retransmits (3 probes). */ + + /* Tick through probe retransmits — probe_count starts at 3, + * each tick past timeout sends a probe and decrements. + * PROBE_INTERVAL_MS = 500, so tick at 600ms intervals. + * After 3 probes, state transitions to ANNOUNCE. */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 4 * SPA_NSEC_PER_SEC); + + avb_loopback_clear_packets(server); + + /* Build a MAAP PROBE packet that overlaps with our reserved range. + * We use the base pool address with our server's MAAP offset. + * Since we can't read the internal offset, we use the full pool + * range to guarantee overlap. */ + memset(pkt, 0, sizeof(pkt)); + h = (struct avb_ethernet_header *)pkt; + p = SPA_PTROFF(h, sizeof(*h), void); + len = sizeof(*h) + sizeof(*p); + + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 }, 6); + memcpy(h->src, remote_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p->hdr.subtype = AVB_SUBTYPE_MAAP; + AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p)); + AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1); + AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, AVB_MAAP_MESSAGE_TYPE_PROBE); + + /* Request the entire pool — guaranteed to overlap with any reservation */ + AVB_PACKET_MAAP_SET_REQUEST_START(p, + ((uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0x00, 0x00 })); + AVB_PACKET_MAAP_SET_REQUEST_COUNT(p, 0xFE00); + + /* Inject — in ANNOUNCE state, a conflicting PROBE should trigger DEFEND */ + avb_test_inject_packet(server, 5 * SPA_NSEC_PER_SEC, pkt, len); + + /* The server should NOT crash. If it was in ANNOUNCE state, + * it sends a DEFEND. If still in PROBE, it picks a new address. + * Either way, the conflict detection logic was exercised. */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MAAP DEFEND packet causes re-probing when conflict overlaps + * with our address during PROBE state. + */ +PWTEST(avb_maap_defend_causes_reprobe) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + struct avb_ethernet_header *h; + struct avb_packet_maap *p; + size_t len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x51 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* MAAP is in PROBE state after reserve. Send a DEFEND packet + * with conflict range covering the entire pool. */ + memset(pkt, 0, sizeof(pkt)); + h = (struct avb_ethernet_header *)pkt; + p = SPA_PTROFF(h, sizeof(*h), void); + len = sizeof(*h) + sizeof(*p); + + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 }, 6); + memcpy(h->src, remote_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p->hdr.subtype = AVB_SUBTYPE_MAAP; + AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p)); + AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1); + AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, AVB_MAAP_MESSAGE_TYPE_DEFEND); + + /* Set conflict range to cover the whole pool */ + AVB_PACKET_MAAP_SET_CONFLICT_START(p, + ((uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0x00, 0x00 })); + AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p, 0xFE00); + + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should have re-probed — exercise the state machine without crash */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MAAP ANNOUNCE packet with conflict triggers re-probe. + * ANNOUNCE is handled via handle_defend() in the code. + */ +PWTEST(avb_maap_announce_conflict) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + struct avb_ethernet_header *h; + struct avb_packet_maap *p; + size_t len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x52 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(pkt, 0, sizeof(pkt)); + h = (struct avb_ethernet_header *)pkt; + p = SPA_PTROFF(h, sizeof(*h), void); + len = sizeof(*h) + sizeof(*p); + + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 }, 6); + memcpy(h->src, remote_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p->hdr.subtype = AVB_SUBTYPE_MAAP; + AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p)); + AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1); + AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, AVB_MAAP_MESSAGE_TYPE_ANNOUNCE); + + /* Conflict range covers entire pool */ + AVB_PACKET_MAAP_SET_CONFLICT_START(p, + ((uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0x00, 0x00 })); + AVB_PACKET_MAAP_SET_CONFLICT_COUNT(p, 0xFE00); + + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MAAP no-conflict — PROBE packet with non-overlapping range. + */ +PWTEST(avb_maap_no_conflict) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + struct avb_ethernet_header *h; + struct avb_packet_maap *p; + size_t len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x53 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + memset(pkt, 0, sizeof(pkt)); + h = (struct avb_ethernet_header *)pkt; + p = SPA_PTROFF(h, sizeof(*h), void); + len = sizeof(*h) + sizeof(*p); + + memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x00, 0xff, 0x00 }, 6); + memcpy(h->src, remote_mac, 6); + h->type = htons(AVB_TSN_ETH); + + p->hdr.subtype = AVB_SUBTYPE_MAAP; + AVB_PACKET_SET_LENGTH(&p->hdr, sizeof(*p)); + AVB_PACKET_MAAP_SET_MAAP_VERSION(p, 1); + AVB_PACKET_MAAP_SET_MESSAGE_TYPE(p, AVB_MAAP_MESSAGE_TYPE_PROBE); + + /* Use a different base prefix — won't match (memcmp of first 4 bytes fails) */ + AVB_PACKET_MAAP_SET_REQUEST_START(p, + ((uint8_t[]){ 0x91, 0xe0, 0xf1, 0x00, 0x00, 0x00 })); + AVB_PACKET_MAAP_SET_REQUEST_COUNT(p, 1); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* No conflict — no DEFEND should be sent (even if in ANNOUNCE state) */ + /* We can't check packet count reliably since MAAP uses send() on its + * own fd, not the loopback transport. But the path was exercised. */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP DISCONNECT_RX_COMMAND flow. + * Send DISCONNECT_RX_COMMAND to our server as listener. + * Should forward DISCONNECT_TX_COMMAND to the talker. + */ +PWTEST(avb_acmp_disconnect_rx_forward) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x60 }; + uint64_t controller_entity = 0x020000fffe000060ULL; + uint64_t talker_entity = 0x020000fffe000070ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send DISCONNECT_RX_COMMAND to us as listener */ + len = avb_test_build_acmp(pkt, sizeof(pkt), controller_mac, + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND, + controller_entity, + talker_entity, /* talker = remote */ + server->entity_id, /* listener = us */ + 0, 0, 300); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should forward a DISCONNECT_TX_COMMAND to the talker */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[256]; + int rlen; + struct avb_packet_acmp *cmd; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)sizeof(struct avb_ethernet_header)); + + cmd = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(cmd), + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP DISCONNECT_TX_COMMAND to our server as talker with no streams. + * Should respond with TALKER_NO_STREAM_INDEX. + */ +PWTEST(avb_acmp_disconnect_tx_no_stream) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x61 }; + uint64_t remote_entity_id = 0x020000fffe000061ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Send DISCONNECT_TX_COMMAND — we have no streams */ + len = avb_test_build_acmp(pkt, sizeof(pkt), remote_mac, + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_COMMAND, + remote_entity_id, /* controller */ + server->entity_id, /* talker = us */ + remote_entity_id, /* listener */ + 0, 0, 1); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should respond with DISCONNECT_TX_RESPONSE + TALKER_NO_STREAM_INDEX */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[256]; + struct avb_packet_acmp *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = (struct avb_packet_acmp *)(rbuf + sizeof(struct avb_ethernet_header)); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_MESSAGE_TYPE(resp), + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_ACMP_GET_STATUS(resp), + AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ACMP disconnect pending timeout. + * DISCONNECT_TX_COMMAND timeout is 200ms, much shorter than CONNECT_TX (2000ms). + * After DISCONNECT_RX_COMMAND, the pending should timeout faster. + */ +PWTEST(avb_acmp_disconnect_pending_timeout) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x62 }; + uint64_t controller_entity = 0x020000fffe000062ULL; + uint64_t talker_entity = 0x020000fffe000072ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_loopback_clear_packets(server); + + /* Create pending via DISCONNECT_RX_COMMAND */ + len = avb_test_build_acmp(pkt, sizeof(pkt), controller_mac, + AVB_ACMP_MESSAGE_TYPE_DISCONNECT_RX_COMMAND, + controller_entity, + talker_entity, + server->entity_id, + 0, 0, 400); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + avb_loopback_clear_packets(server); + + /* Tick before timeout (200ms) — no retry yet */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 100 * SPA_NSEC_PER_MSEC); + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + /* Tick after timeout (200ms) — should retry */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 300 * SPA_NSEC_PER_MSEC); + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + avb_loopback_clear_packets(server); + + /* Tick again after second timeout — should be freed */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + /* No crash — pending was cleaned up */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_AVB_INFO command with AVB_INTERFACE descriptor. + * Adds an AVB_INTERFACE descriptor, injects GET_AVB_INFO, and + * verifies the response contains gptp_grandmaster_id and domain_number. + */ +PWTEST(avb_aecp_get_avb_info) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x70 }; + uint64_t controller_id = 0x020000fffe000070ULL; + struct avb_packet_aecp_aem_get_avb_info avb_info_req; + uint64_t test_clock_id = 0x0200000000000042ULL; + uint8_t test_domain = 7; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Add an AVB_INTERFACE descriptor to the server */ + { + struct avb_aem_desc_avb_interface avb_iface; + memset(&avb_iface, 0, sizeof(avb_iface)); + avb_iface.clock_identity = htobe64(test_clock_id); + avb_iface.domain_number = test_domain; + avb_iface.interface_flags = htons( + AVB_AEM_DESC_AVB_INTERFACE_FLAG_GPTP_GRANDMASTER_SUPPORTED); + server_add_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0, + sizeof(avb_iface), &avb_iface); + } + + /* Build GET_AVB_INFO command */ + memset(&avb_info_req, 0, sizeof(avb_info_req)); + avb_info_req.descriptor_type = htons(AVB_AEM_DESC_AVB_INTERFACE); + avb_info_req.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_AVB_INFO, + &avb_info_req, sizeof(avb_info_req)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get a SUCCESS response */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + int rlen; + struct avb_packet_aecp_aem *resp; + struct avb_packet_aecp_aem_get_avb_info *info; + + rlen = avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + pwtest_int_gt(rlen, (int)(sizeof(struct avb_ethernet_header) + + sizeof(struct avb_packet_aecp_aem))); + + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + + /* Verify the response payload */ + info = (struct avb_packet_aecp_aem_get_avb_info *)resp->payload; + pwtest_int_eq(be64toh(info->gptp_grandmaster_id), test_clock_id); + pwtest_int_eq(info->gptp_domain_number, test_domain); + pwtest_int_eq(ntohl(info->propagation_delay), 0u); + pwtest_int_eq(ntohs(info->msrp_mappings_count), 0); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_AVB_INFO with wrong descriptor type returns NOT_IMPLEMENTED. + * The handler requires AVB_AEM_DESC_AVB_INTERFACE specifically. + */ +PWTEST(avb_aecp_get_avb_info_wrong_type) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x71 }; + uint64_t controller_id = 0x020000fffe000071ULL; + struct avb_packet_aecp_aem_get_avb_info avb_info_req; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Request GET_AVB_INFO for entity descriptor (wrong type) */ + memset(&avb_info_req, 0, sizeof(avb_info_req)); + avb_info_req.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + avb_info_req.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_AVB_INFO, + &avb_info_req, sizeof(avb_info_req)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get NOT_IMPLEMENTED (descriptor exists but is wrong type) */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP leave-all timer fires and triggers global LVA event. + * After LVA_TIMER_MS (10000ms), the leave-all timer fires, sending + * RX_LVA to all attributes and setting leave_all=true for the next TX. + */ +PWTEST(avb_mrp_leave_all_timer) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + struct spa_hook listener; + struct notify_tracker tracker = { 0 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + avb_mrp_add_listener(server->mrp, &listener, &test_mrp_events, &tracker); + + /* Create and join an attribute */ + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + pwtest_ptr_notnull(attr); + + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* Get registrar to IN state */ + avb_mrp_attribute_rx_event(attr->mrp, 1 * SPA_NSEC_PER_SEC, + AVB_MRP_ATTRIBUTE_EVENT_NEW); + pwtest_int_eq(tracker.new_count, 1); + + /* Initialize timers with first tick */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + + /* Tick at various times before LVA timeout — no leave-all yet */ + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 9 * SPA_NSEC_PER_SEC); + + /* Tick past LVA timeout (10000ms from the first tick at 1s = 11s) */ + avb_test_tick(server, 12 * SPA_NSEC_PER_SEC); + + /* The LVA event should have been processed without crash. + * The TX_LVA event is combined with the join timer TX, + * which may produce SEND_LVA type transmissions. */ + + spa_hook_remove(&listener); + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MRP periodic timer fires at 1000ms intervals. + * The periodic event is applied globally to all attributes. + */ +PWTEST(avb_mrp_periodic_timer) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *attr; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + attr = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_ADVERTISE); + avb_mrp_attribute_begin(attr->mrp, 0); + avb_mrp_attribute_join(attr->mrp, 0, true); + + /* First tick initializes timers */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + + /* Tick just before periodic timeout (1000ms) — no periodic event yet */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 500 * SPA_NSEC_PER_MSEC); + + /* Tick past periodic timeout */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC + 100 * SPA_NSEC_PER_MSEC); + + /* Tick multiple periodic intervals to exercise repeated timer */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + avb_test_tick(server, 4 * SPA_NSEC_PER_SEC + 300 * SPA_NSEC_PER_MSEC); + + /* No crash — periodic timer logic works correctly */ + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MSRP talker-failed processing via MRP packet parsing. + * Build an MSRP packet containing a talker-failed attribute and + * inject it. Verifies process_talker_fail() processes correctly. + */ +PWTEST(avb_msrp_talker_failed_process) +{ + struct impl *impl; + struct server *server; + struct avb_msrp_attribute *talker_fail; + uint64_t stream_id = 0x020000fffe000080ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create a talker-failed attribute that matches the stream_id + * we'll send in the MSRP packet. This ensures process_talker_fail() + * finds a matching attribute and calls avb_mrp_attribute_rx_event(). */ + talker_fail = avb_msrp_attribute_new(server->msrp, + AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED); + pwtest_ptr_notnull(talker_fail); + + talker_fail->attr.talker_fail.talker.stream_id = htobe64(stream_id); + talker_fail->attr.talker_fail.failure_code = AVB_MRP_FAIL_BANDWIDTH; + + avb_mrp_attribute_begin(talker_fail->mrp, 0); + avb_mrp_attribute_join(talker_fail->mrp, 0, true); + + /* Build an MSRP packet with a talker-failed message. + * The MSRP packet parser will dispatch to process_talker_fail() + * when it sees attribute_type = TALKER_FAILED. */ + { + uint8_t buf[512]; + int pos = 0; + struct avb_packet_mrp *mrp_pkt; + struct avb_packet_msrp_msg *msg; + struct avb_packet_mrp_vector *v; + struct avb_packet_msrp_talker_fail *tf; + struct avb_packet_mrp_footer *f; + uint8_t *ev; + size_t attr_list_length; + + memset(buf, 0, sizeof(buf)); + + /* MRP header */ + mrp_pkt = (struct avb_packet_mrp *)buf; + mrp_pkt->version = AVB_MRP_PROTOCOL_VERSION; + /* Fill in the ethernet header part */ + { + static const uint8_t msrp_mac[6] = { 0x91, 0xe0, 0xf0, 0x00, 0xe5, 0x00 }; + memcpy(mrp_pkt->eth.dest, msrp_mac, 6); + memcpy(mrp_pkt->eth.src, server->mac_addr, 6); + mrp_pkt->eth.type = htons(AVB_TSN_ETH); + } + pos = sizeof(struct avb_packet_mrp); + + /* MSRP talker-failed message */ + msg = (struct avb_packet_msrp_msg *)(buf + pos); + msg->attribute_type = AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED; + msg->attribute_length = sizeof(struct avb_packet_msrp_talker_fail); + + v = (struct avb_packet_mrp_vector *)msg->attribute_list; + v->lva = 0; + AVB_MRP_VECTOR_SET_NUM_VALUES(v, 1); + + tf = (struct avb_packet_msrp_talker_fail *)v->first_value; + tf->talker.stream_id = htobe64(stream_id); + tf->talker.vlan_id = htons(AVB_DEFAULT_VLAN); + tf->talker.tspec_max_frame_size = htons(256); + tf->talker.tspec_max_interval_frames = htons(1); + tf->talker.priority = AVB_MSRP_PRIORITY_DEFAULT; + tf->talker.rank = AVB_MSRP_RANK_DEFAULT; + tf->failure_code = AVB_MRP_FAIL_BANDWIDTH; + + ev = (uint8_t *)(tf + 1); + *ev = AVB_MRP_ATTRIBUTE_EVENT_NEW * 36; /* single value, NEW event */ + + attr_list_length = sizeof(*v) + sizeof(*tf) + 1 + sizeof(*f); + msg->attribute_list_length = htons(attr_list_length); + + f = SPA_PTROFF(ev, 1, void); + f->end_mark = 0; + + pos += sizeof(*msg) + sizeof(*v) + sizeof(*tf) + 1 + sizeof(*f); + + /* Attribute end mark */ + buf[pos++] = 0; + buf[pos++] = 0; + + /* Inject the MSRP packet */ + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, buf, pos); + } + + /* If we get here, process_talker_fail() was invoked without crash. + * The attribute's RX_NEW event would have been applied. */ + + /* Exercise periodic to verify ongoing stability */ + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 8: MVRP/MMRP, ADP Edge Cases, Descriptor, and AECP Command Tests + * ===================================================================== + */ + +/* + * Test: MVRP attribute creation and lifecycle. + * Create a VID attribute, begin, join, and exercise the state machine. + */ +PWTEST(avb_mvrp_attribute_lifecycle) +{ + struct impl *impl; + struct server *server; + struct avb_mvrp_attribute *vid; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + vid = avb_mvrp_attribute_new(server->mvrp, + AVB_MVRP_ATTRIBUTE_TYPE_VID); + pwtest_ptr_notnull(vid); + pwtest_int_eq(vid->type, AVB_MVRP_ATTRIBUTE_TYPE_VID); + + /* Configure VLAN ID */ + vid->attr.vid.vlan = htons(AVB_DEFAULT_VLAN); + + /* Begin and join */ + avb_mrp_attribute_begin(vid->mrp, 0); + avb_mrp_attribute_join(vid->mrp, 0, true); + + /* Tick through MRP state machine */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + avb_test_tick(server, 2 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MVRP VID attribute transmit via loopback. + * After join + TX timer, MVRP should encode and send a VID packet. + */ +PWTEST(avb_mvrp_vid_transmit) +{ + struct impl *impl; + struct server *server; + struct avb_mvrp_attribute *vid; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + vid = avb_mvrp_attribute_new(server->mvrp, + AVB_MVRP_ATTRIBUTE_TYPE_VID); + pwtest_ptr_notnull(vid); + + vid->attr.vid.vlan = htons(100); + + avb_mrp_attribute_begin(vid->mrp, 0); + avb_mrp_attribute_join(vid->mrp, 0, true); + + /* Initialize timers */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Trigger TX */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + /* MVRP should have sent a packet */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: MMRP attribute creation — both SERVICE_REQUIREMENT and MAC types. + */ +PWTEST(avb_mmrp_attribute_types) +{ + struct impl *impl; + struct server *server; + struct avb_mmrp_attribute *svc, *mac_attr; + static const uint8_t test_mac[6] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Create service requirement attribute */ + svc = avb_mmrp_attribute_new(server->mmrp, + AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT); + pwtest_ptr_notnull(svc); + pwtest_int_eq(svc->type, AVB_MMRP_ATTRIBUTE_TYPE_SERVICE_REQUIREMENT); + + memcpy(svc->attr.service_requirement.addr, test_mac, 6); + + /* Create MAC attribute */ + mac_attr = avb_mmrp_attribute_new(server->mmrp, + AVB_MMRP_ATTRIBUTE_TYPE_MAC); + pwtest_ptr_notnull(mac_attr); + pwtest_int_eq(mac_attr->type, AVB_MMRP_ATTRIBUTE_TYPE_MAC); + + memcpy(mac_attr->attr.mac.addr, test_mac, 6); + + /* Begin and join both */ + avb_mrp_attribute_begin(svc->mrp, 0); + avb_mrp_attribute_join(svc->mrp, 0, true); + avb_mrp_attribute_begin(mac_attr->mrp, 0); + avb_mrp_attribute_join(mac_attr->mrp, 0, true); + + /* Tick to exercise MRP state machine with both types */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC + 200 * SPA_NSEC_PER_MSEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ADP duplicate ENTITY_AVAILABLE is idempotent. + * Injecting the same entity_id twice should not create duplicate entries; + * last_time is updated. + */ +PWTEST(avb_adp_duplicate_entity_available) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x80 }; + uint64_t remote_entity_id = 0x020000fffe000080ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Inject entity available twice with same entity_id */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 10); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should not crash, and entity list should be consistent */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + /* Inject departing — only one entity to remove */ + len = avb_test_build_adp_entity_departing(pkt, sizeof(pkt), + remote_mac, remote_entity_id); + avb_test_inject_packet(server, 4 * SPA_NSEC_PER_SEC, pkt, len); + + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ADP targeted discover for a specific entity_id. + * Only the entity with matching ID should respond. + */ +PWTEST(avb_adp_targeted_discover) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x81 }; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Let the server advertise first */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Send targeted discover for our own entity */ + len = avb_test_build_adp_entity_discover(pkt, sizeof(pkt), + remote_mac, server->entity_id); + pwtest_int_gt(len, 0); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should respond since the entity_id matches ours */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + avb_loopback_clear_packets(server); + + /* Send targeted discover for a non-existent entity */ + len = avb_test_build_adp_entity_discover(pkt, sizeof(pkt), + remote_mac, 0xDEADBEEFCAFE0001ULL); + avb_test_inject_packet(server, 3 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should NOT respond — entity doesn't exist */ + pwtest_int_eq(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ADP re-advertisement timing — the server should re-advertise + * at valid_time/2 intervals when ticked periodically. + */ +PWTEST(avb_adp_readvertise_timing) +{ + struct impl *impl; + struct server *server; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* First tick — should advertise (check_advertise creates entity) */ + avb_test_tick(server, 1 * SPA_NSEC_PER_SEC); + avb_loopback_clear_packets(server); + + /* Tick at 3s — too early for re-advertise (valid_time=10, re-adv at 5s) */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + /* Might or might not have packets depending on other protocols */ + + avb_loopback_clear_packets(server); + + /* Tick at 7s — past re-advertise interval (valid_time/2 = 5s from 1s = 6s) */ + avb_test_tick(server, 7 * SPA_NSEC_PER_SEC); + + /* Should have re-advertised */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: ADP entity departure before timeout removes entity immediately. + */ +PWTEST(avb_adp_departure_before_timeout) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[256]; + int len; + static const uint8_t remote_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x82 }; + uint64_t remote_entity_id = 0x020000fffe000082ULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Add entity */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 30); /* long valid_time */ + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Immediate departing — before any timeout */ + len = avb_test_build_adp_entity_departing(pkt, sizeof(pkt), + remote_mac, remote_entity_id); + avb_test_inject_packet(server, 2 * SPA_NSEC_PER_SEC, pkt, len); + + /* Entity should be removed immediately, not waiting for timeout */ + avb_test_tick(server, 3 * SPA_NSEC_PER_SEC); + + /* Re-add the same entity — should work if old one was properly removed */ + len = avb_test_build_adp_entity_available(pkt, sizeof(pkt), + remote_mac, remote_entity_id, 10); + avb_test_inject_packet(server, 4 * SPA_NSEC_PER_SEC, pkt, len); + + avb_test_tick(server, 5 * SPA_NSEC_PER_SEC); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: Descriptor lookup edge cases — find existing, missing, multiple types. + */ +PWTEST(avb_descriptor_lookup_edge_cases) +{ + struct impl *impl; + struct server *server; + const struct descriptor *desc; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Entity descriptor should exist (added by avb_test_server_new) */ + desc = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 0); + pwtest_ptr_notnull(desc); + pwtest_int_eq(desc->type, AVB_AEM_DESC_ENTITY); + pwtest_int_eq(desc->index, 0); + pwtest_int_gt((int)desc->size, 0); + + /* Non-existent descriptor type */ + desc = server_find_descriptor(server, AVB_AEM_DESC_AUDIO_UNIT, 0); + pwtest_ptr_null(desc); + + /* Non-existent index for existing type */ + desc = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 1); + pwtest_ptr_null(desc); + + /* Add multiple descriptors and verify independent lookup */ + { + struct avb_aem_desc_avb_interface avb_iface; + memset(&avb_iface, 0, sizeof(avb_iface)); + server_add_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0, + sizeof(avb_iface), &avb_iface); + } + + /* Both descriptors should be findable */ + desc = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 0); + pwtest_ptr_notnull(desc); + desc = server_find_descriptor(server, AVB_AEM_DESC_AVB_INTERFACE, 0); + pwtest_ptr_notnull(desc); + + /* Invalid descriptor type still returns NULL */ + desc = server_find_descriptor(server, AVB_AEM_DESC_INVALID, 0); + pwtest_ptr_null(desc); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: server_add_descriptor with data copy — verify data is correctly + * stored and retrievable. + */ +PWTEST(avb_descriptor_data_integrity) +{ + struct impl *impl; + struct server *server; + const struct descriptor *desc; + struct avb_aem_desc_entity entity; + struct avb_aem_desc_entity *retrieved; + uint64_t test_entity_id = 0x0123456789ABCDEFULL; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Remove existing entity descriptor via server_destroy_descriptors + * and add a new one with known data */ + server_destroy_descriptors(server); + + memset(&entity, 0, sizeof(entity)); + entity.entity_id = htobe64(test_entity_id); + entity.entity_model_id = htobe64(0x0001000000000002ULL); + entity.configurations_count = htons(2); + strncpy(entity.entity_name, "Test Entity", sizeof(entity.entity_name) - 1); + + server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0, + sizeof(entity), &entity); + + /* Retrieve and verify */ + desc = server_find_descriptor(server, AVB_AEM_DESC_ENTITY, 0); + pwtest_ptr_notnull(desc); + pwtest_int_eq((int)desc->size, (int)sizeof(entity)); + + retrieved = desc->ptr; + pwtest_int_eq(be64toh(retrieved->entity_id), test_entity_id); + pwtest_int_eq(ntohs(retrieved->configurations_count), 2); + pwtest_int_eq(strncmp(retrieved->entity_name, "Test Entity", 11), 0); + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_CONFIGURATION command. + * Verify it returns the current_configuration from the entity descriptor. + */ +PWTEST(avb_aecp_get_configuration) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x90 }; + uint64_t controller_id = 0x020000fffe000090ULL; + struct avb_packet_aecp_aem_setget_configuration cfg_req; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Build GET_CONFIGURATION command — no descriptor type/id needed, + * it always looks up ENTITY descriptor 0 internally */ + memset(&cfg_req, 0, sizeof(cfg_req)); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_CONFIGURATION, + &cfg_req, sizeof(cfg_req)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get SUCCESS response */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + struct avb_packet_aecp_aem_setget_configuration *cfg_resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + + /* Verify configuration_index is 0 (default) */ + cfg_resp = (struct avb_packet_aecp_aem_setget_configuration *)resp->payload; + pwtest_int_eq(ntohs(cfg_resp->configuration_index), 0); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_SAMPLING_RATE command. + * Add an AUDIO_UNIT descriptor with a known sampling rate and verify + * the response contains it. + */ +PWTEST(avb_aecp_get_sampling_rate) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x91 }; + uint64_t controller_id = 0x020000fffe000091ULL; + struct avb_packet_aecp_aem_setget_sampling_rate sr_req; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Add an AUDIO_UNIT descriptor with a sampling rate */ + { + /* Allocate space for audio_unit + 1 sampling rate entry */ + uint8_t au_buf[sizeof(struct avb_aem_desc_audio_unit) + + sizeof(union avb_aem_desc_sampling_rate)]; + struct avb_aem_desc_audio_unit *au; + union avb_aem_desc_sampling_rate *sr; + + memset(au_buf, 0, sizeof(au_buf)); + au = (struct avb_aem_desc_audio_unit *)au_buf; + au->sampling_rates_count = htons(1); + au->sampling_rates_offset = htons(sizeof(*au)); + + /* Set current sampling rate to 48000 Hz + * pull_frequency is a uint32_t with frequency in bits [31:3] and pull in [2:0] */ + au->current_sampling_rate.pull_frequency = htonl(48000 << 3); + + /* Add one supported rate */ + sr = (union avb_aem_desc_sampling_rate *)(au_buf + sizeof(*au)); + sr->pull_frequency = htonl(48000 << 3); + + server_add_descriptor(server, AVB_AEM_DESC_AUDIO_UNIT, 0, + sizeof(au_buf), au_buf); + } + + /* Build GET_SAMPLING_RATE command */ + memset(&sr_req, 0, sizeof(sr_req)); + sr_req.descriptor_type = htons(AVB_AEM_DESC_AUDIO_UNIT); + sr_req.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_SAMPLING_RATE, + &sr_req, sizeof(sr_req)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get SUCCESS */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_SAMPLING_RATE with wrong descriptor type. + * Should return NOT_IMPLEMENTED. + */ +PWTEST(avb_aecp_get_sampling_rate_wrong_type) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x92 }; + uint64_t controller_id = 0x020000fffe000092ULL; + struct avb_packet_aecp_aem_setget_sampling_rate sr_req; + + impl = test_impl_new(); + server = avb_test_server_new(impl); + pwtest_ptr_notnull(server); + + /* Request GET_SAMPLING_RATE for entity descriptor (wrong type) */ + memset(&sr_req, 0, sizeof(sr_req)); + sr_req.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + sr_req.descriptor_id = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_SAMPLING_RATE, + &sr_req, sizeof(sr_req)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NOT_IMPLEMENTED); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_NAME for entity descriptor — retrieves entity_name. + */ +PWTEST(avb_aecp_get_name_entity) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x93 }; + uint64_t controller_id = 0x020000fffe000093ULL; + struct avb_packet_aecp_aem_setget_name name_req; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* GET_NAME for entity descriptor, name_index=0 (entity_name) */ + memset(&name_req, 0, sizeof(name_req)); + name_req.descriptor_type = htons(AVB_AEM_DESC_ENTITY); + name_req.descriptor_index = htons(0); + name_req.name_index = htons(0); + name_req.configuration_index = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_NAME, + &name_req, sizeof(name_req)); + pwtest_int_gt(len, 0); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + /* Should get SUCCESS */ + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_MESSAGE_TYPE(&resp->aecp), + AVB_AECP_MESSAGE_TYPE_AEM_RESPONSE); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_SUCCESS); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * Test: AECP GET_NAME with missing descriptor returns NO_SUCH_DESCRIPTOR. + */ +PWTEST(avb_aecp_get_name_missing_descriptor) +{ + struct impl *impl; + struct server *server; + uint8_t pkt[512]; + int len; + static const uint8_t controller_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x94 }; + uint64_t controller_id = 0x020000fffe000094ULL; + struct avb_packet_aecp_aem_setget_name name_req; + + impl = test_impl_new(); + server = avb_test_server_new_milan(impl); + pwtest_ptr_notnull(server); + + /* GET_NAME for AUDIO_UNIT which doesn't exist in test server */ + memset(&name_req, 0, sizeof(name_req)); + name_req.descriptor_type = htons(AVB_AEM_DESC_AUDIO_UNIT); + name_req.descriptor_index = htons(0); + name_req.name_index = htons(0); + + len = avb_test_build_aecp_aem(pkt, sizeof(pkt), controller_mac, + server->entity_id, controller_id, 1, + AVB_AECP_AEM_CMD_GET_NAME, + &name_req, sizeof(name_req)); + + avb_loopback_clear_packets(server); + avb_test_inject_packet(server, 1 * SPA_NSEC_PER_SEC, pkt, len); + + pwtest_int_gt(avb_loopback_get_packet_count(server), 0); + { + uint8_t rbuf[2048]; + struct avb_packet_aecp_aem *resp; + + avb_loopback_get_packet(server, rbuf, sizeof(rbuf)); + resp = SPA_PTROFF(rbuf, sizeof(struct avb_ethernet_header), void); + pwtest_int_eq((int)AVB_PACKET_AECP_GET_STATUS(&resp->aecp), + AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR); + } + + test_impl_free(impl); + + return PWTEST_PASS; +} + +/* + * ===================================================================== + * Phase 6: AVTP Audio Data Path Tests + * ===================================================================== + */ + +/* + * Test: Verify IEC61883 packet struct layout and size. + * The struct must be exactly 24 bytes (packed) for the header, + * followed by the flexible payload array. + */ +PWTEST(avb_iec61883_packet_layout) +{ + struct avb_packet_iec61883 pkt; + struct avb_frame_header fh; + + /* IEC61883 header (packed) with CIP fields = 32 bytes */ + pwtest_int_eq((int)sizeof(struct avb_packet_iec61883), 32); + + /* Frame header with 802.1Q tag should be 18 bytes */ + pwtest_int_eq((int)sizeof(struct avb_frame_header), 18); + + /* Total PDU header = frame_header + iec61883 = 50 bytes */ + pwtest_int_eq((int)(sizeof(fh) + sizeof(pkt)), 50); + + /* Verify critical field positions by setting and reading */ + memset(&pkt, 0, sizeof(pkt)); + pkt.subtype = AVB_SUBTYPE_61883_IIDC; + pwtest_int_eq(pkt.subtype, 0x00); + + pkt.sv = 1; + pkt.tv = 1; + pkt.seq_num = 42; + pkt.stream_id = htobe64(0x020000fffe000001ULL); + pkt.timestamp = htonl(1000000); + pkt.data_len = htons(200); + pkt.tag = 0x1; + pkt.channel = 0x1f; + pkt.tcode = 0xa; + pkt.sid = 0x3f; + pkt.dbs = 8; + pkt.qi2 = 0x2; + pkt.format_id = 0x10; + pkt.fdf = 0x2; + pkt.syt = htons(0x0008); + pkt.dbc = 0; + + /* Read back and verify */ + pwtest_int_eq(pkt.seq_num, 42); + pwtest_int_eq(pkt.dbs, 8); + pwtest_int_eq(be64toh(pkt.stream_id), 0x020000fffe000001ULL); + pwtest_int_eq(ntohs(pkt.data_len), 200); + pwtest_int_eq((int)pkt.sv, 1); + pwtest_int_eq((int)pkt.tv, 1); + + return PWTEST_PASS; +} + +/* + * Test: Verify AAF packet struct layout. + */ +PWTEST(avb_aaf_packet_layout) +{ + struct avb_packet_aaf pkt; + + /* AAF header should be 24 bytes (same as IEC61883) */ + pwtest_int_eq((int)sizeof(struct avb_packet_aaf), 24); + + memset(&pkt, 0, sizeof(pkt)); + pkt.subtype = AVB_SUBTYPE_AAF; + pkt.sv = 1; + pkt.tv = 1; + pkt.seq_num = 99; + pkt.stream_id = htobe64(0x020000fffe000002ULL); + pkt.timestamp = htonl(2000000); + pkt.format = AVB_AAF_FORMAT_INT_24BIT; + pkt.nsr = AVB_AAF_PCM_NSR_48KHZ; + pkt.chan_per_frame = 8; + pkt.bit_depth = 24; + pkt.data_len = htons(192); /* 6 frames * 8 channels * 4 bytes */ + pkt.sp = AVB_AAF_PCM_SP_NORMAL; + + pwtest_int_eq(pkt.subtype, AVB_SUBTYPE_AAF); + pwtest_int_eq(pkt.seq_num, 99); + pwtest_int_eq(pkt.format, AVB_AAF_FORMAT_INT_24BIT); + pwtest_int_eq((int)pkt.nsr, AVB_AAF_PCM_NSR_48KHZ); + pwtest_int_eq(pkt.chan_per_frame, 8); + pwtest_int_eq(pkt.bit_depth, 24); + pwtest_int_eq(ntohs(pkt.data_len), 192); + + return PWTEST_PASS; +} + +/* + * Test: 802.1Q frame header construction for AVB. + */ +PWTEST(avb_frame_header_construction) +{ + struct avb_frame_header h; + static const uint8_t dest[6] = { 0x91, 0xe0, 0xf0, 0x00, 0x01, 0x00 }; + static const uint8_t src[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 }; + int prio = 3; + int vlan_id = 2; + + memset(&h, 0, sizeof(h)); + memcpy(h.dest, dest, 6); + memcpy(h.src, src, 6); + h.type = htons(0x8100); /* 802.1Q VLAN tag */ + h.prio_cfi_id = htons((prio << 13) | vlan_id); + h.etype = htons(0x22f0); /* AVB/TSN EtherType */ + + /* Verify the 802.1Q header */ + pwtest_int_eq(ntohs(h.type), 0x8100); + pwtest_int_eq(ntohs(h.etype), 0x22f0); + + /* Extract priority from prio_cfi_id */ + pwtest_int_eq((ntohs(h.prio_cfi_id) >> 13) & 0x7, prio); + /* Extract VLAN ID (lower 12 bits) */ + pwtest_int_eq(ntohs(h.prio_cfi_id) & 0xFFF, vlan_id); + + return PWTEST_PASS; +} + +/* + * Test: PDU size calculations for various audio configurations. + * Verifies the math used in setup_pdu(). + */ +PWTEST(avb_pdu_size_calculations) +{ + size_t hdr_size, payload_size, pdu_size; + int64_t pdu_period; + + /* Default config: 8 channels, S24_32_BE (4 bytes), 6 frames/PDU, 48kHz */ + int channels = 8; + int sample_size = 4; /* S24_32_BE */ + int frames_per_pdu = 6; + int rate = 48000; + int stride = channels * sample_size; + + hdr_size = sizeof(struct avb_frame_header) + sizeof(struct avb_packet_iec61883); + payload_size = stride * frames_per_pdu; + pdu_size = hdr_size + payload_size; + pdu_period = SPA_NSEC_PER_SEC * frames_per_pdu / rate; + + /* Header: 18 (frame) + 32 (iec61883) = 50 bytes */ + pwtest_int_eq((int)hdr_size, 50); + + /* Payload: 8 ch * 4 bytes * 6 frames = 192 bytes */ + pwtest_int_eq((int)payload_size, 192); + + /* Total PDU: 50 + 192 = 242 bytes */ + pwtest_int_eq((int)pdu_size, 242); + + /* PDU period: 6/48000 seconds = 125000 ns = 125 us */ + pwtest_int_eq((int)pdu_period, 125000); + + /* Stride: 8 * 4 = 32 bytes per frame */ + pwtest_int_eq(stride, 32); + + /* IEC61883 data_len field = payload + 8 CIP header bytes */ + pwtest_int_eq((int)(payload_size + 8), 200); + + /* 2-channel configuration */ + channels = 2; + stride = channels * sample_size; + payload_size = stride * frames_per_pdu; + pwtest_int_eq((int)payload_size, 48); + pwtest_int_eq(stride, 8); + + return PWTEST_PASS; +} + +/* + * Test: Ringbuffer audio data round-trip. + * Write audio frames to the ringbuffer, read them back, verify integrity. + */ +PWTEST(avb_ringbuffer_audio_roundtrip) +{ + struct spa_ringbuffer ring; + uint8_t buffer[BUFFER_SIZE]; + int stride = 32; /* 8 channels * 4 bytes */ + int frames = 48; /* 48 frames = 1ms at 48kHz */ + int n_bytes = frames * stride; + uint8_t write_data[2048]; + uint8_t read_data[2048]; + uint32_t index; + int32_t avail; + + spa_ringbuffer_init(&ring); + + /* Fill write_data with a recognizable pattern */ + for (int i = 0; i < n_bytes; i++) + write_data[i] = (uint8_t)(i & 0xFF); + + /* Write to ringbuffer */ + avail = spa_ringbuffer_get_write_index(&ring, &index); + pwtest_int_eq(avail, 0); + + spa_ringbuffer_write_data(&ring, buffer, sizeof(buffer), + index % sizeof(buffer), write_data, n_bytes); + index += n_bytes; + spa_ringbuffer_write_update(&ring, index); + + /* Read back from ringbuffer */ + avail = spa_ringbuffer_get_read_index(&ring, &index); + pwtest_int_eq(avail, n_bytes); + + spa_ringbuffer_read_data(&ring, buffer, sizeof(buffer), + index % sizeof(buffer), read_data, n_bytes); + index += n_bytes; + spa_ringbuffer_read_update(&ring, index); + + /* Verify data integrity */ + pwtest_int_eq(memcmp(write_data, read_data, n_bytes), 0); + + /* After read, buffer should be empty */ + avail = spa_ringbuffer_get_read_index(&ring, &index); + pwtest_int_eq(avail, 0); + + return PWTEST_PASS; +} + +/* + * Test: Ringbuffer wrap-around behavior with multiple writes. + * Simulates multiple PDU-sized writes filling past the buffer end. + */ +PWTEST(avb_ringbuffer_wraparound) +{ + struct spa_ringbuffer ring; + uint8_t *buffer; + int stride = 32; + int frames_per_pdu = 6; + int payload_size = stride * frames_per_pdu; /* 192 bytes */ + int num_writes = (BUFFER_SIZE / payload_size) + 5; /* Write past buffer end */ + uint8_t write_data[192]; + uint8_t read_data[192]; + uint32_t w_index, r_index; + int32_t avail; + + buffer = calloc(1, BUFFER_SIZE); + pwtest_ptr_notnull(buffer); + + spa_ringbuffer_init(&ring); + + /* Write many PDU payloads, reading as we go to prevent overrun */ + for (int i = 0; i < num_writes; i++) { + /* Fill with per-PDU pattern */ + memset(write_data, (uint8_t)(i + 1), payload_size); + + avail = spa_ringbuffer_get_write_index(&ring, &w_index); + spa_ringbuffer_write_data(&ring, buffer, BUFFER_SIZE, + w_index % BUFFER_SIZE, write_data, payload_size); + w_index += payload_size; + spa_ringbuffer_write_update(&ring, w_index); + + /* Read it back immediately */ + avail = spa_ringbuffer_get_read_index(&ring, &r_index); + pwtest_int_eq(avail, payload_size); + + spa_ringbuffer_read_data(&ring, buffer, BUFFER_SIZE, + r_index % BUFFER_SIZE, read_data, payload_size); + r_index += payload_size; + spa_ringbuffer_read_update(&ring, r_index); + + /* Verify the pattern survived the wrap-around */ + for (int j = 0; j < payload_size; j++) { + if (read_data[j] != (uint8_t)(i + 1)) { + free(buffer); + return PWTEST_FAIL; + } + } + } + + free(buffer); + + return PWTEST_PASS; +} + +/* + * Test: IEC61883 packet receive simulation. + * Builds IEC61883 packets and writes their payload into a ringbuffer, + * mirroring the logic of handle_iec61883_packet(). + */ +PWTEST(avb_iec61883_receive_simulation) +{ + struct spa_ringbuffer ring; + uint8_t *rb_buffer; + uint8_t pkt_buf[2048]; + struct avb_frame_header *h; + struct avb_packet_iec61883 *p; + int channels = 8; + int sample_size = 4; + int stride = channels * sample_size; + int frames_per_pdu = 6; + int payload_size = stride * frames_per_pdu; /* 192 bytes */ + int n_packets = 10; + uint32_t index; + int32_t filled; + uint8_t read_data[192]; + + rb_buffer = calloc(1, BUFFER_SIZE); + pwtest_ptr_notnull(rb_buffer); + spa_ringbuffer_init(&ring); + + for (int i = 0; i < n_packets; i++) { + /* Build a receive packet like on_socket_data() would see */ + memset(pkt_buf, 0, sizeof(pkt_buf)); + h = (struct avb_frame_header *)pkt_buf; + p = SPA_PTROFF(h, sizeof(*h), void); + + p->subtype = AVB_SUBTYPE_61883_IIDC; + p->sv = 1; + p->tv = 1; + p->seq_num = i; + p->stream_id = htobe64(0x020000fffe000001ULL); + p->timestamp = htonl(i * 125000); + p->data_len = htons(payload_size + 8); /* payload + 8 CIP bytes */ + p->tag = 0x1; + p->dbs = channels; + p->dbc = i * frames_per_pdu; + + /* Fill payload with audio-like pattern */ + for (int j = 0; j < payload_size; j++) + p->payload[j] = (uint8_t)((i * payload_size + j) & 0xFF); + + /* Simulate handle_iec61883_packet() logic */ + { + int n_bytes = ntohs(p->data_len) - 8; + pwtest_int_eq(n_bytes, payload_size); + + filled = spa_ringbuffer_get_write_index(&ring, &index); + + if (filled + (int32_t)n_bytes <= (int32_t)BUFFER_SIZE) { + spa_ringbuffer_write_data(&ring, rb_buffer, BUFFER_SIZE, + index % BUFFER_SIZE, p->payload, n_bytes); + index += n_bytes; + spa_ringbuffer_write_update(&ring, index); + } + } + } + + /* Verify all packets were received */ + filled = spa_ringbuffer_get_read_index(&ring, &index); + pwtest_int_eq(filled, n_packets * payload_size); + + /* Read back first packet's data and verify */ + spa_ringbuffer_read_data(&ring, rb_buffer, BUFFER_SIZE, + index % BUFFER_SIZE, read_data, payload_size); + + for (int j = 0; j < payload_size; j++) { + if (read_data[j] != (uint8_t)(j & 0xFF)) { + free(rb_buffer); + return PWTEST_FAIL; + } + } + + free(rb_buffer); + + return PWTEST_PASS; +} + +/* + * Test: IEC61883 transmit PDU construction simulation. + * Builds PDU like setup_pdu() + flush_write() would, verifies structure. + */ +PWTEST(avb_iec61883_transmit_pdu) +{ + uint8_t pdu[2048]; + struct avb_frame_header *h; + struct avb_packet_iec61883 *p; + static const uint8_t dest[6] = { 0x91, 0xe0, 0xf0, 0x00, 0x01, 0x00 }; + static const uint8_t src[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 }; + int channels = 8; + int stride = channels * 4; + int frames_per_pdu = 6; + int payload_size = stride * frames_per_pdu; + int prio = 3; + int vlan_id = 2; + uint64_t stream_id = 0x020000fffe000001ULL; + + /* Simulate setup_pdu() */ + memset(pdu, 0, sizeof(pdu)); + h = (struct avb_frame_header *)pdu; + p = SPA_PTROFF(h, sizeof(*h), void); + + memcpy(h->dest, dest, 6); + memcpy(h->src, src, 6); + h->type = htons(0x8100); + h->prio_cfi_id = htons((prio << 13) | vlan_id); + h->etype = htons(0x22f0); + + p->subtype = AVB_SUBTYPE_61883_IIDC; + p->sv = 1; + p->stream_id = htobe64(stream_id); + p->data_len = htons(payload_size + 8); + p->tag = 0x1; + p->channel = 0x1f; + p->tcode = 0xa; + p->sid = 0x3f; + p->dbs = channels; + p->qi2 = 0x2; + p->format_id = 0x10; + p->fdf = 0x2; + p->syt = htons(0x0008); + + /* Simulate flush_write() per-PDU setup */ + p->seq_num = 0; + p->tv = 1; + p->timestamp = htonl(125000); + p->dbc = 0; + + /* Verify the PDU */ + pwtest_int_eq(p->subtype, AVB_SUBTYPE_61883_IIDC); + pwtest_int_eq(be64toh(p->stream_id), stream_id); + pwtest_int_eq(ntohs(p->data_len), payload_size + 8); + pwtest_int_eq(p->dbs, channels); + pwtest_int_eq(p->seq_num, 0); + pwtest_int_eq((int)ntohl(p->timestamp), 125000); + pwtest_int_eq(p->dbc, 0); + pwtest_int_eq(ntohs(h->etype), 0x22f0); + + /* Simulate second PDU — verify sequence and DBC advance */ + p->seq_num = 1; + p->timestamp = htonl(250000); + p->dbc = frames_per_pdu; + + pwtest_int_eq(p->seq_num, 1); + pwtest_int_eq(p->dbc, frames_per_pdu); + pwtest_int_eq((int)ntohl(p->timestamp), 250000); + + return PWTEST_PASS; +} + +/* + * Test: Ringbuffer overrun detection. + * Simulates the overrun check in handle_iec61883_packet(). + */ +PWTEST(avb_ringbuffer_overrun) +{ + struct spa_ringbuffer ring; + uint8_t *buffer; + uint8_t data[256]; + uint32_t index; + int32_t filled; + int payload_size = 192; + int overrun_count = 0; + + buffer = calloc(1, BUFFER_SIZE); + pwtest_ptr_notnull(buffer); + spa_ringbuffer_init(&ring); + + memset(data, 0xAA, sizeof(data)); + + /* Fill the buffer to capacity */ + int max_writes = BUFFER_SIZE / payload_size; + for (int i = 0; i < max_writes; i++) { + filled = spa_ringbuffer_get_write_index(&ring, &index); + if (filled + payload_size > (int32_t)BUFFER_SIZE) { + overrun_count++; + break; + } + spa_ringbuffer_write_data(&ring, buffer, BUFFER_SIZE, + index % BUFFER_SIZE, data, payload_size); + index += payload_size; + spa_ringbuffer_write_update(&ring, index); + } + + /* Try one more write — should detect overrun */ + filled = spa_ringbuffer_get_write_index(&ring, &index); + if (filled + payload_size > (int32_t)BUFFER_SIZE) + overrun_count++; + + /* Should have hit at least one overrun */ + pwtest_int_gt(overrun_count, 0); + + /* Verify data still readable from the full buffer */ + filled = spa_ringbuffer_get_read_index(&ring, &index); + pwtest_int_gt(filled, 0); + + free(buffer); + + return PWTEST_PASS; +} + +/* + * Test: Sequence number wrapping at 256 (uint8_t). + * Verifies that sequence numbers wrap correctly as in flush_write(). + */ +PWTEST(avb_sequence_number_wrapping) +{ + uint8_t seq = 0; + uint8_t dbc = 0; + int frames_per_pdu = 6; + + /* Simulate 300 PDU transmissions — seq wraps at 256 */ + for (int i = 0; i < 300; i++) { + pwtest_int_eq(seq, (uint8_t)(i & 0xFF)); + seq++; + dbc += frames_per_pdu; + } + + /* After 300 PDUs: seq = 300 & 0xFF = 44, dbc = 300*6 = 1800 & 0xFF = 8 */ + pwtest_int_eq(seq, (uint8_t)(300 & 0xFF)); + pwtest_int_eq(dbc, (uint8_t)(300 * frames_per_pdu)); + + return PWTEST_PASS; +} + +PWTEST_SUITE(avb) +{ + /* Phase 2: ADP and basic tests */ + pwtest_add(avb_adp_entity_available, PWTEST_NOARG); + pwtest_add(avb_adp_entity_departing, PWTEST_NOARG); + pwtest_add(avb_adp_entity_discover, PWTEST_NOARG); + pwtest_add(avb_adp_entity_timeout, PWTEST_NOARG); + pwtest_add(avb_mrp_attribute_lifecycle, PWTEST_NOARG); + pwtest_add(avb_milan_server_create, PWTEST_NOARG); + + /* Phase 3: MRP state machine tests */ + pwtest_add(avb_mrp_begin_join_new_tx, PWTEST_NOARG); + pwtest_add(avb_mrp_join_leave_cycle, PWTEST_NOARG); + pwtest_add(avb_mrp_rx_new_notification, PWTEST_NOARG); + pwtest_add(avb_mrp_registrar_leave_timer, PWTEST_NOARG); + pwtest_add(avb_mrp_multiple_attributes, PWTEST_NOARG); + + /* Phase 3: MSRP tests */ + pwtest_add(avb_msrp_attribute_types, PWTEST_NOARG); + pwtest_add(avb_msrp_domain_transmit, PWTEST_NOARG); + pwtest_add(avb_msrp_talker_transmit, PWTEST_NOARG); + pwtest_add(avb_msrp_talker_failed_notify, PWTEST_NOARG); + + /* Phase 3: MRP packet parsing tests */ + pwtest_add(avb_mrp_parse_single_domain, PWTEST_NOARG); + pwtest_add(avb_mrp_parse_with_lva, PWTEST_NOARG); + pwtest_add(avb_mrp_parse_three_values, PWTEST_NOARG); + + /* Phase 4: ACMP integration tests */ + pwtest_add(avb_acmp_not_supported, PWTEST_NOARG); + pwtest_add(avb_acmp_connect_tx_no_stream, PWTEST_NOARG); + pwtest_add(avb_acmp_wrong_entity_ignored, PWTEST_NOARG); + pwtest_add(avb_acmp_connect_rx_forward, PWTEST_NOARG); + pwtest_add(avb_acmp_pending_timeout, PWTEST_NOARG); + pwtest_add(avb_acmp_packet_filtering, PWTEST_NOARG); + + /* Phase 5: AECP/AEM entity model tests */ + pwtest_add(avb_aecp_read_descriptor_entity, PWTEST_NOARG); + pwtest_add(avb_aecp_read_descriptor_not_found, PWTEST_NOARG); + pwtest_add(avb_aecp_packet_filtering, PWTEST_NOARG); + pwtest_add(avb_aecp_unsupported_message_types, PWTEST_NOARG); + pwtest_add(avb_aecp_aem_not_implemented, PWTEST_NOARG); + pwtest_add(avb_aecp_acquire_entity_legacy, PWTEST_NOARG); + pwtest_add(avb_aecp_lock_entity_legacy, PWTEST_NOARG); + pwtest_add(avb_aecp_entity_available_milan, PWTEST_NOARG); + pwtest_add(avb_aecp_lock_entity_milan, PWTEST_NOARG); + pwtest_add(avb_aecp_lock_non_entity_milan, PWTEST_NOARG); + pwtest_add(avb_aecp_acquire_entity_milan, PWTEST_NOARG); + pwtest_add(avb_aecp_read_descriptor_milan, PWTEST_NOARG); + + /* Phase 7: Additional protocol coverage tests */ + pwtest_add(avb_maap_conflict_probe_in_announce, PWTEST_NOARG); + pwtest_add(avb_maap_defend_causes_reprobe, PWTEST_NOARG); + pwtest_add(avb_maap_announce_conflict, PWTEST_NOARG); + pwtest_add(avb_maap_no_conflict, PWTEST_NOARG); + pwtest_add(avb_acmp_disconnect_rx_forward, PWTEST_NOARG); + pwtest_add(avb_acmp_disconnect_tx_no_stream, PWTEST_NOARG); + pwtest_add(avb_acmp_disconnect_pending_timeout, PWTEST_NOARG); + pwtest_add(avb_aecp_get_avb_info, PWTEST_NOARG); + pwtest_add(avb_aecp_get_avb_info_wrong_type, PWTEST_NOARG); + pwtest_add(avb_mrp_leave_all_timer, PWTEST_NOARG); + pwtest_add(avb_mrp_periodic_timer, PWTEST_NOARG); + pwtest_add(avb_msrp_talker_failed_process, PWTEST_NOARG); + + /* Phase 8: MVRP/MMRP, ADP edge cases, descriptor, AECP command tests */ + pwtest_add(avb_mvrp_attribute_lifecycle, PWTEST_NOARG); + pwtest_add(avb_mvrp_vid_transmit, PWTEST_NOARG); + pwtest_add(avb_mmrp_attribute_types, PWTEST_NOARG); + pwtest_add(avb_adp_duplicate_entity_available, PWTEST_NOARG); + pwtest_add(avb_adp_targeted_discover, PWTEST_NOARG); + pwtest_add(avb_adp_readvertise_timing, PWTEST_NOARG); + pwtest_add(avb_adp_departure_before_timeout, PWTEST_NOARG); + pwtest_add(avb_descriptor_lookup_edge_cases, PWTEST_NOARG); + pwtest_add(avb_descriptor_data_integrity, PWTEST_NOARG); + pwtest_add(avb_aecp_get_configuration, PWTEST_NOARG); + pwtest_add(avb_aecp_get_sampling_rate, PWTEST_NOARG); + pwtest_add(avb_aecp_get_sampling_rate_wrong_type, PWTEST_NOARG); + pwtest_add(avb_aecp_get_name_entity, PWTEST_NOARG); + pwtest_add(avb_aecp_get_name_missing_descriptor, PWTEST_NOARG); + + /* Phase 6: AVTP audio data path tests */ + pwtest_add(avb_iec61883_packet_layout, PWTEST_NOARG); + pwtest_add(avb_aaf_packet_layout, PWTEST_NOARG); + pwtest_add(avb_frame_header_construction, PWTEST_NOARG); + pwtest_add(avb_pdu_size_calculations, PWTEST_NOARG); + pwtest_add(avb_ringbuffer_audio_roundtrip, PWTEST_NOARG); + pwtest_add(avb_ringbuffer_wraparound, PWTEST_NOARG); + pwtest_add(avb_iec61883_receive_simulation, PWTEST_NOARG); + pwtest_add(avb_iec61883_transmit_pdu, PWTEST_NOARG); + pwtest_add(avb_ringbuffer_overrun, PWTEST_NOARG); + pwtest_add(avb_sequence_number_wrapping, PWTEST_NOARG); + + return PWTEST_PASS; +} diff --git a/test/test-context.c b/test/test-context.c index 093e7b388..967c631fb 100644 --- a/test/test-context.c +++ b/test/test-context.c @@ -29,6 +29,7 @@ PWTEST(context_abi) void (*global_removed) (void *data, struct pw_global *global); void (*driver_added) (void *data, struct pw_impl_node *node); void (*driver_removed) (void *data, struct pw_impl_node *node); + void (*recalc_graph) (void *data); } test = { PW_VERSION_CONTEXT_EVENTS, NULL }; pw_init(0, NULL); @@ -40,8 +41,9 @@ PWTEST(context_abi) TEST_FUNC(ev, test, global_removed); TEST_FUNC(ev, test, driver_added); TEST_FUNC(ev, test, driver_removed); + TEST_FUNC(ev, test, recalc_graph); - pwtest_int_eq(PW_VERSION_CONTEXT_EVENTS, 1); + pwtest_int_eq(PW_VERSION_CONTEXT_EVENTS, 2); pwtest_int_eq(sizeof(ev), sizeof(test)); pw_deinit(); diff --git a/test/test-mempool.c b/test/test-mempool.c index 36f69c571..3793b23c4 100644 --- a/test/test-mempool.c +++ b/test/test-mempool.c @@ -41,9 +41,74 @@ PWTEST(mempool_issue4884) return PWTEST_PASS; } -PWTEST_SUITE(pw_mempool) +PWTEST(map_range_overflow) { - pwtest_add(mempool_issue4884, PWTEST_NOARG); + /* + * Test that pw_map_range_init rejects offset + size combinations + * that would overflow uint32_t, which could cause mmap with a + * truncated size and subsequent out-of-bounds access. + */ + struct pw_map_range range; + uint32_t page_size = 4096; + int res; + + /* Normal case: should succeed */ + res = pw_map_range_init(&range, 0, 4096, page_size); + pwtest_int_eq(res, 0); + pwtest_int_eq(range.offset, 0u); + pwtest_int_eq(range.start, 0u); + pwtest_int_eq(range.size, 4096u); + + /* Page-aligned offset: should succeed */ + res = pw_map_range_init(&range, 4096, 4096, page_size); + pwtest_int_eq(res, 0); + pwtest_int_eq(range.offset, 4096u); + pwtest_int_eq(range.start, 0u); + pwtest_int_eq(range.size, 4096u); + + /* Non-aligned offset: start gets the remainder */ + res = pw_map_range_init(&range, 100, 4096, page_size); + pwtest_int_eq(res, 0); + pwtest_int_eq(range.offset, 0u); + pwtest_int_eq(range.start, 100u); + + /* size=0: should succeed */ + res = pw_map_range_init(&range, 0, 0, page_size); + pwtest_int_eq(res, 0); + + /* Overflow: non-aligned offset causes start > 0, then start + size wraps */ + res = pw_map_range_init(&range, 4095, 0xFFFFF002, page_size); + pwtest_int_lt(res, 0); + + /* Overflow: max size with any non-zero start */ + res = pw_map_range_init(&range, 1, UINT32_MAX, page_size); + pwtest_int_lt(res, 0); + + /* Both large but page-aligned: start=0, start+size=0x80000000, + * round-up doesn't overflow, so this should succeed */ + res = pw_map_range_init(&range, 0x80000000, 0x80000000, page_size); + pwtest_int_eq(res, 0); + + /* Non-aligned offset but still fits: start=1, start+size=0x80000001 */ + res = pw_map_range_init(&range, 0x80000001, 0x80000000, page_size); + pwtest_int_eq(res, 0); + + /* Overflow: round-up of start+size would exceed uint32 */ + res = pw_map_range_init(&range, 1, UINT32_MAX - 1, page_size); + pwtest_int_lt(res, 0); + + /* start=0, size=UINT32_MAX: start + size doesn't wrap, but + * SPA_ROUND_UP_N to page_size would overflow, so must fail */ + res = pw_map_range_init(&range, 0, UINT32_MAX, page_size); + pwtest_int_lt(res, 0); + + return PWTEST_PASS; +} + +PWTEST_SUITE(pw_mempool) +{ + pwtest_add(mempool_issue4884, PWTEST_NOARG); + pwtest_add(map_range_overflow, PWTEST_NOARG); return PWTEST_PASS; } diff --git a/test/test-properties.c b/test/test-properties.c index 459e1e15e..48d7858db 100644 --- a/test/test-properties.c +++ b/test/test-properties.c @@ -466,7 +466,7 @@ PWTEST(properties_serialize_dict_stack_overflow) fp = fopen(tmpfile, "we"); pwtest_ptr_notnull(fp); r = pw_properties_serialize_dict(fp, &dict, 0); - pwtest_int_eq(r, 1); + pwtest_int_eq(r, 2); fclose(fp); free(long_value); diff --git a/test/test-spa-json.c b/test/test-spa-json.c index 0c3c46f59..1c0aada16 100644 --- a/test/test-spa-json.c +++ b/test/test-spa-json.c @@ -350,6 +350,12 @@ PWTEST(json_parse) expect_string(&it[0], "hello"); expect_end(&it[0]); + json = "xy{}"; + spa_json_init(&it[0], json, strlen(json)); + expect_string_or_bare(&it[0], "xy"); + expect_object(&it[0], &it[1]); + expect_end(&it[0]); + /* top-level context */ json = "x y x y"; spa_json_init(&it[0], json, strlen(json)); @@ -702,21 +708,40 @@ PWTEST(json_float_check) struct { const char *str; int res; + int jsonres; } val[] = { - { "0.0", 1 }, - { ".0", 1 }, - { "+.0E0", 1 }, - { "-.0e0", 1 }, + { "0.0", 1, 1}, + { ".0", 1, 0 }, + { "+.0E0", 1, 0 }, + { "-.0e0", 1, 0 }, + + { "0,0", 0, 0 }, + { "0.0.5", 0, 0 }, + { "0x0", 1, 0 }, + { "0x0.0", 1, 0 }, + { "E10", 0, 0 }, + { "e20", 0, 0 }, + { " 0.0", 1, 0 }, + { "0.0 ", 0, 0 }, + { " 0.0 ", 0, 0 }, + + { "+", 0, 0 }, + { "+0", 1, 0 }, + { "-", 0, 0 }, + { "-0", 1, 1 }, + { "-0", 1, 1 }, + { "-01", 1, 0 }, + { "-00", 1, 0 }, + { "-1", 1, 1 }, + { "-10", 1, 1 }, + { "-.", 0, 0 }, + { "-0.", 1, 0 }, + { "-01.", 1, 0 }, + { "-1.", 1, 0 }, + + { "-.0", 1, 0 }, + { "-1E10", 1, 1 }, - { "0,0", 0 }, - { "0.0.5", 0 }, - { "0x0", 0 }, - { "0x0.0", 0 }, - { "E10", 0 }, - { "e20", 0 }, - { " 0.0", 0 }, - { "0.0 ", 0 }, - { " 0.0 ", 0 }, }; unsigned i; float v; @@ -724,6 +749,9 @@ PWTEST(json_float_check) for (i = 0; i < SPA_N_ELEMENTS(val); i++) { pwtest_int_eq(spa_json_parse_float(val[i].str, strlen(val[i].str), &v), val[i].res); } + for (i = 0; i < SPA_N_ELEMENTS(val); i++) { + pwtest_int_eq(spa_json_is_json_number(val[i].str, strlen(val[i].str)), val[i].jsonres); + } return PWTEST_PASS; } @@ -944,6 +972,7 @@ PWTEST(json_data) "n_array_missing_value.json", "n_array_number_and_comma.json", "n_array_number_and_several_commas.json", + "n_array_inner_array_no_comma.json", "n_object_comma_instead_of_colon.json", "n_object_double_colon.json", "n_object_missing_semicolon.json", @@ -974,6 +1003,12 @@ PWTEST(json_data) "n_number_-2..json", "n_number_hex_1_digit.json", "n_number_hex_2_digits.json", + "n_number_infinity.json", + "n_number_+Inf.json", + "n_number_Inf.json", + "n_number_minus_infinity.json", + "n_number_-NaN.json", + "n_number_NaN.json", "n_number_neg_int_starting_with_zero.json", "n_number_neg_real_without_int_part.json", "n_number_real_without_fractional_part.json",