summaryrefslogtreecommitdiffstats
path: root/src/tools
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/tools/dsffile.c266
-rw-r--r--src/tools/dsffile.h52
-rw-r--r--src/tools/meson.build88
-rw-r--r--src/tools/midifile.c740
-rw-r--r--src/tools/midifile.h64
-rw-r--r--src/tools/pw-cat.c1971
-rw-r--r--src/tools/pw-cli.c2375
-rw-r--r--src/tools/pw-dot.c1169
-rw-r--r--src/tools/pw-dump.c1664
-rw-r--r--src/tools/pw-link.c912
-rw-r--r--src/tools/pw-loopback.c284
-rw-r--r--src/tools/pw-metadata.c297
-rw-r--r--src/tools/pw-mididump.c233
-rw-r--r--src/tools/pw-mon.c875
-rw-r--r--src/tools/pw-profiler.c665
-rw-r--r--src/tools/pw-reserve.c253
-rw-r--r--src/tools/pw-top.c862
-rw-r--r--src/tools/reserve.c527
-rw-r--r--src/tools/reserve.h82
19 files changed, 13379 insertions, 0 deletions
diff --git a/src/tools/dsffile.c b/src/tools/dsffile.c
new file mode 100644
index 0000000..9c6ce0c
--- /dev/null
+++ b/src/tools/dsffile.c
@@ -0,0 +1,266 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <math.h>
+
+#include <spa/utils/string.h>
+
+#include "dsffile.h"
+
+struct dsf_file {
+ uint8_t *data;
+ size_t size;
+
+ int mode;
+ int fd;
+
+ struct dsf_file_info info;
+
+ uint8_t *p;
+ size_t offset;
+};
+
+static inline uint32_t parse_le32(const uint8_t *in)
+{
+ return in[0] | (in[1] << 8) | (in[2] << 16) | (in[3] << 24);
+}
+
+static inline uint64_t parse_le64(const uint8_t *in)
+{
+ uint64_t res = in[0];
+ res |= ((uint64_t)in[1]) << 8;
+ res |= ((uint64_t)in[2]) << 16;
+ res |= ((uint64_t)in[3]) << 24;
+ res |= ((uint64_t)in[4]) << 32;
+ res |= ((uint64_t)in[5]) << 40;
+ res |= ((uint64_t)in[6]) << 48;
+ res |= ((uint64_t)in[7]) << 56;
+ return res;
+}
+
+static inline int f_avail(struct dsf_file *f)
+{
+ if (f->p < f->data + f->size)
+ return f->size + f->data - f->p;
+ return 0;
+}
+
+static int read_DSD(struct dsf_file *f)
+{
+ uint64_t size;
+
+ if (f_avail(f) < 28 ||
+ memcmp(f->p, "DSD ", 4) != 0)
+ return -EINVAL;
+
+ size = parse_le64(f->p + 4); /* size of this chunk */
+ parse_le64(f->p + 12); /* total size */
+ parse_le64(f->p + 20); /* metadata */
+ f->p += size;
+ return 0;
+}
+
+static int read_fmt(struct dsf_file *f)
+{
+ uint64_t size;
+
+ if (f_avail(f) < 52 ||
+ memcmp(f->p, "fmt ", 4) != 0)
+ return -EINVAL;
+
+ size = parse_le64(f->p + 4); /* size of this chunk */
+ if (parse_le32(f->p + 12) != 1) /* version */
+ return -EINVAL;
+ if (parse_le32(f->p + 16) != 0) /* format id */
+ return -EINVAL;
+
+ f->info.channel_type = parse_le32(f->p + 20);
+ f->info.channels = parse_le32(f->p + 24);
+ f->info.rate = parse_le32(f->p + 28);
+ f->info.lsb = parse_le32(f->p + 32) == 1;
+ f->info.samples = parse_le64(f->p + 36);
+ f->info.blocksize = parse_le32(f->p + 44);
+ f->p += size;
+ return 0;
+}
+
+static int read_data(struct dsf_file *f)
+{
+ uint64_t size;
+
+ if (f_avail(f) < 12 ||
+ memcmp(f->p, "data", 4) != 0)
+ return -EINVAL;
+
+ size = parse_le64(f->p + 4); /* size of this chunk */
+ f->info.length = size - 12;
+ f->p += 12;
+ return 0;
+}
+
+static int open_read(struct dsf_file *f, const char *filename, struct dsf_file_info *info)
+{
+ int res;
+ struct stat st;
+
+ if ((f->fd = open(filename, O_RDONLY)) < 0) {
+ res = -errno;
+ goto exit;
+ }
+ if (fstat(f->fd, &st) < 0) {
+ res = -errno;
+ goto exit_close;
+ }
+ f->size = st.st_size;
+
+ f->data = mmap(NULL, f->size, PROT_READ, MAP_SHARED, f->fd, 0);
+ if (f->data == MAP_FAILED) {
+ res = -errno;
+ goto exit_close;
+ }
+
+ f->p = f->data;
+
+ if ((res = read_DSD(f)) < 0)
+ goto exit_unmap;
+ if ((res = read_fmt(f)) < 0)
+ goto exit_unmap;
+ if ((res = read_data(f)) < 0)
+ goto exit_unmap;
+
+ f->mode = 1;
+ *info = f->info;
+ return 0;
+
+exit_unmap:
+ munmap(f->data, f->size);
+exit_close:
+ close(f->fd);
+exit:
+ return res;
+}
+
+struct dsf_file *
+dsf_file_open(const char *filename, const char *mode, struct dsf_file_info *info)
+{
+ int res;
+ struct dsf_file *f;
+
+ f = calloc(1, sizeof(struct dsf_file));
+ if (f == NULL)
+ return NULL;
+
+ if (spa_streq(mode, "r")) {
+ if ((res = open_read(f, filename, info)) < 0)
+ goto exit_free;
+ } else {
+ res = -EINVAL;
+ goto exit_free;
+ }
+ return f;
+
+exit_free:
+ free(f);
+ errno = -res;
+ return NULL;
+}
+
+static const uint8_t bitrev[256] = {
+ 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
+ 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
+ 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
+ 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
+ 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
+ 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
+ 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
+ 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
+ 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
+ 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
+ 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
+ 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
+ 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
+ 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
+ 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
+ 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff,
+};
+
+ssize_t
+dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_layout *layout)
+{
+ uint8_t *d = data;
+ int step = SPA_ABS(layout->interleave);
+ bool rev = layout->lsb != f->info.lsb;
+ size_t total, block, offset, pos, scale;
+
+ block = f->offset / f->info.blocksize;
+ offset = block * f->info.blocksize * f->info.channels;
+ pos = f->offset % f->info.blocksize;
+ scale = SPA_CLAMP(f->info.rate / (44100u * 64u), 1u, 4u);
+
+ samples *= step;
+ samples *= scale;
+
+ for (total = 0; total < samples && offset + pos < f->info.length; total++) {
+ const uint8_t *s = f->p + offset + pos;
+ uint32_t i;
+
+ for (i = 0; i < layout->channels; i++) {
+ const uint8_t *c = &s[f->info.blocksize * i];
+ int j;
+
+ if (layout->interleave > 0) {
+ for (j = 0; j < step; j++)
+ *d++ = rev ? bitrev[c[j]] : c[j];
+ } else {
+ for (j = step-1; j >= 0; j--)
+ *d++ = rev ? bitrev[c[j]] : c[j];
+ }
+ }
+ pos += step;
+ if (pos == f->info.blocksize) {
+ pos = 0;
+ offset += f->info.blocksize * f->info.channels;
+ }
+ }
+ f->offset += total * step;
+
+ return total;
+}
+
+int dsf_file_close(struct dsf_file *f)
+{
+ if (f->mode == 1) {
+ munmap(f->data, f->size);
+ } else
+ return -EINVAL;
+
+ close(f->fd);
+ free(f);
+ return 0;
+}
diff --git a/src/tools/dsffile.h b/src/tools/dsffile.h
new file mode 100644
index 0000000..616dbb1
--- /dev/null
+++ b/src/tools/dsffile.h
@@ -0,0 +1,52 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+#include <spa/utils/defs.h>
+
+struct dsf_file;
+
+struct dsf_file_info {
+ uint32_t channel_type;
+ uint32_t channels;
+ uint32_t rate;
+ bool lsb;
+ uint64_t samples;
+ uint64_t length;
+ uint32_t blocksize;
+};
+
+struct dsf_layout {
+ int32_t interleave;
+ uint32_t channels;
+ bool lsb;
+};
+
+struct dsf_file * dsf_file_open(const char *filename, const char *mode, struct dsf_file_info *info);
+
+ssize_t dsf_file_read(struct dsf_file *f, void *data, size_t samples, const struct dsf_layout *layout);
+
+int dsf_file_close(struct dsf_file *f);
+
diff --git a/src/tools/meson.build b/src/tools/meson.build
new file mode 100644
index 0000000..6623e7a
--- /dev/null
+++ b/src/tools/meson.build
@@ -0,0 +1,88 @@
+tools_sources = [
+ [ 'pw-mon', [ 'pw-mon.c' ] ],
+ [ 'pw-dot', [ 'pw-dot.c' ] ],
+ [ 'pw-dump', [ 'pw-dump.c' ] ],
+ [ 'pw-profiler', [ 'pw-profiler.c' ] ],
+ [ 'pw-mididump', [ 'pw-mididump.c', 'midifile.c' ] ],
+ [ 'pw-metadata', [ 'pw-metadata.c' ] ],
+ [ 'pw-loopback', [ 'pw-loopback.c' ] ],
+ [ 'pw-link', [ 'pw-link.c' ] ],
+]
+
+foreach t : tools_sources
+ executable(t.get(0),
+ t.get(1),
+ install: true,
+ dependencies : [pipewire_dep, mathlib],
+ )
+endforeach
+
+executable('pw-cli',
+ 'pw-cli.c',
+ install: true,
+ dependencies: [pipewire_dep, readline_dep]
+)
+
+if ncurses_dep.found()
+ executable('pw-top',
+ 'pw-top.c',
+ install: true,
+ dependencies : [pipewire_dep, ncurses_dep],
+ )
+endif
+
+build_pw_cat = false
+build_pw_cat_with_ffmpeg = false
+pwcat_deps = [ sndfile_dep ]
+
+if get_option('pw-cat').allowed() and sndfile_dep.found()
+ build_pw_cat = true
+
+ if pw_cat_ffmpeg.allowed() and avcodec_dep.found() and avformat_dep.found()
+ pwcat_deps += avcodec_dep
+ pwcat_deps += avformat_dep
+ build_pw_cat_with_ffmpeg = true
+ endif
+
+ pwcat_sources = [
+ 'pw-cat.c',
+ 'midifile.c',
+ 'dsffile.c',
+ ]
+
+ pwcat_aliases = [
+ 'pw-play',
+ 'pw-record',
+ 'pw-midiplay',
+ 'pw-midirecord',
+ 'pw-dsdplay',
+ ]
+
+ executable('pw-cat',
+ pwcat_sources,
+ install: true,
+ dependencies : [pwcat_deps, pipewire_dep, mathlib],
+ )
+
+ foreach alias : pwcat_aliases
+ dst = pipewire_bindir / alias
+ cmd = 'ln -fs @0@ $DESTDIR@1@'.format('pw-cat', dst)
+ meson.add_install_script('sh', '-c', cmd)
+ endforeach
+elif not sndfile_dep.found() and get_option('pw-cat').enabled()
+ error('pw-cat is enabled but required dependency `sndfile` was not found.')
+endif
+summary({'Build pw-cat tool': build_pw_cat}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump tool')
+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
+
+if dbus_dep.found()
+ executable('pw-reserve',
+ 'reserve.h',
+ 'reserve.c',
+ 'pw-reserve.c',
+ install: true,
+ dependencies : [dbus_dep, pipewire_dep],
+ )
+endif
diff --git a/src/tools/midifile.c b/src/tools/midifile.c
new file mode 100644
index 0000000..5276ade
--- /dev/null
+++ b/src/tools/midifile.c
@@ -0,0 +1,740 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <math.h>
+
+#include <spa/utils/string.h>
+
+#include "midifile.h"
+
+#define DEFAULT_TEMPO 500000 /* 500ms per quarter note (120 BPM) is the default */
+
+struct midi_track {
+ uint16_t id;
+
+ uint8_t *data;
+ uint32_t size;
+
+ uint8_t *p;
+ int64_t tick;
+ unsigned int eof:1;
+ uint8_t event[4];
+};
+
+struct midi_file {
+ uint8_t *data;
+ size_t size;
+
+ int mode;
+ int fd;
+
+ struct midi_file_info info;
+ uint32_t length;
+ uint32_t tempo;
+
+ uint8_t *p;
+ int64_t tick;
+ double tick_sec;
+ double tick_start;
+
+ struct midi_track tracks[64];
+};
+
+static inline uint16_t parse_be16(const uint8_t *in)
+{
+ return (in[0] << 8) | in[1];
+}
+
+static inline uint32_t parse_be32(const uint8_t *in)
+{
+ return (in[0] << 24) | (in[1] << 16) | (in[2] << 8) | in[3];
+}
+
+static inline int mf_avail(struct midi_file *mf)
+{
+ if (mf->p < mf->data + mf->size)
+ return mf->size + mf->data - mf->p;
+ return 0;
+}
+
+static inline int tr_avail(struct midi_track *tr)
+{
+ if (tr->eof)
+ return 0;
+ if (tr->p < tr->data + tr->size)
+ return tr->size + tr->data - tr->p;
+ tr->eof = true;
+ return 0;
+}
+
+static int read_mthd(struct midi_file *mf)
+{
+ if (mf_avail(mf) < 14 ||
+ memcmp(mf->p, "MThd", 4) != 0)
+ return -EINVAL;
+
+ mf->length = parse_be32(mf->p + 4);
+ mf->info.format = parse_be16(mf->p + 8);
+ mf->info.ntracks = parse_be16(mf->p + 10);
+ mf->info.division = parse_be16(mf->p + 12);
+
+ mf->p += 14;
+ return 0;
+}
+
+static int read_mtrk(struct midi_file *mf, struct midi_track *track)
+{
+ if (mf_avail(mf) < 8 ||
+ memcmp(mf->p, "MTrk", 4) != 0)
+ return -EINVAL;
+
+ track->data = track->p = mf->p + 8;
+ track->size = parse_be32(mf->p + 4);
+
+ mf->p = track->data + track->size;
+ if (mf->p > mf->data + mf->size)
+ return -EINVAL;
+
+ return 0;
+}
+
+static int parse_varlen(struct midi_file *mf, struct midi_track *tr, uint32_t *result)
+{
+ uint32_t value = 0;
+
+ while (tr_avail(tr) > 0) {
+ uint8_t b = *tr->p++;
+ value = (value << 7) | (b & 0x7f);
+ if ((b & 0x80) == 0)
+ break;
+ }
+ *result = value;
+ return 0;
+}
+
+static int open_read(struct midi_file *mf, const char *filename, struct midi_file_info *info)
+{
+ int res;
+ uint16_t i;
+ struct stat st;
+
+ if ((mf->fd = open(filename, O_RDONLY)) < 0) {
+ res = -errno;
+ goto exit;
+ }
+ if (fstat(mf->fd, &st) < 0) {
+ res = -errno;
+ goto exit_close;
+ }
+ mf->size = st.st_size;
+
+ mf->data = mmap(NULL, mf->size, PROT_READ, MAP_SHARED, mf->fd, 0);
+ if (mf->data == MAP_FAILED) {
+ res = -errno;
+ goto exit_close;
+ }
+
+ mf->p = mf->data;
+
+ if ((res = read_mthd(mf)) < 0)
+ goto exit_unmap;
+
+ mf->tempo = DEFAULT_TEMPO;
+ mf->tick = 0;
+
+ for (i = 0; i < mf->info.ntracks; i++) {
+ struct midi_track *tr = &mf->tracks[i];
+ uint32_t delta_time;
+
+ if ((res = read_mtrk(mf, tr)) < 0)
+ goto exit_unmap;
+
+ if ((res = parse_varlen(mf, tr, &delta_time)) < 0)
+ goto exit_unmap;
+
+ tr->tick = delta_time;
+ tr->id = i;
+ }
+ mf->mode = 1;
+ *info = mf->info;
+ return 0;
+
+exit_unmap:
+ munmap(mf->data, mf->size);
+exit_close:
+ close(mf->fd);
+exit:
+ return res;
+}
+
+static inline int write_n(int fd, const void *buf, int count)
+{
+ return write(fd, buf, count) == (ssize_t)count ? count : -errno;
+}
+
+static inline int write_be16(int fd, uint16_t val)
+{
+ uint8_t buf[2] = { val >> 8, val };
+ return write_n(fd, buf, 2);
+}
+
+static inline int write_be32(int fd, uint32_t val)
+{
+ uint8_t buf[4] = { val >> 24, val >> 16, val >> 8, val };
+ return write_n(fd, buf, 4);
+}
+
+#define CHECK_RES(expr) if ((res = (expr)) < 0) return res
+
+static int write_headers(struct midi_file *mf)
+{
+ struct midi_track *tr = &mf->tracks[0];
+ int res;
+
+ lseek(mf->fd, 0, SEEK_SET);
+
+ mf->length = 6;
+ CHECK_RES(write_n(mf->fd, "MThd", 4));
+ CHECK_RES(write_be32(mf->fd, mf->length));
+ CHECK_RES(write_be16(mf->fd, mf->info.format));
+ CHECK_RES(write_be16(mf->fd, mf->info.ntracks));
+ CHECK_RES(write_be16(mf->fd, mf->info.division));
+
+ CHECK_RES(write_n(mf->fd, "MTrk", 4));
+ CHECK_RES(write_be32(mf->fd, tr->size));
+
+ return 0;
+}
+
+static int open_write(struct midi_file *mf, const char *filename, struct midi_file_info *info)
+{
+ int res;
+
+ if (info->format != 0)
+ return -EINVAL;
+ if (info->ntracks == 0)
+ info->ntracks = 1;
+ else if (info->ntracks != 1)
+ return -EINVAL;
+ if (info->division == 0)
+ info->division = 96;
+
+ if ((mf->fd = open(filename, O_WRONLY | O_CREAT, 0660)) < 0) {
+ res = -errno;
+ goto exit;
+ }
+ mf->mode = 2;
+ mf->tempo = DEFAULT_TEMPO;
+ mf->info = *info;
+
+ res = write_headers(mf);
+exit:
+ return res;
+}
+
+struct midi_file *
+midi_file_open(const char *filename, const char *mode, struct midi_file_info *info)
+{
+ int res;
+ struct midi_file *mf;
+
+ mf = calloc(1, sizeof(struct midi_file));
+ if (mf == NULL)
+ return NULL;
+
+ if (spa_streq(mode, "r")) {
+ if ((res = open_read(mf, filename, info)) < 0)
+ goto exit_free;
+ } else if (spa_streq(mode, "w")) {
+ if ((res = open_write(mf, filename, info)) < 0)
+ goto exit_free;
+ } else {
+ res = -EINVAL;
+ goto exit_free;
+ }
+ return mf;
+
+exit_free:
+ free(mf);
+ errno = -res;
+ return NULL;
+}
+
+int midi_file_close(struct midi_file *mf)
+{
+ int res;
+
+ if (mf->mode == 1) {
+ munmap(mf->data, mf->size);
+ } else if (mf->mode == 2) {
+ uint8_t buf[4] = { 0x00, 0xff, 0x2f, 0x00 };
+ CHECK_RES(write_n(mf->fd, buf, 4));
+ mf->tracks[0].size += 4;
+ CHECK_RES(write_headers(mf));
+ } else
+ return -EINVAL;
+
+ close(mf->fd);
+ free(mf);
+ return 0;
+}
+
+static int peek_next(struct midi_file *mf, struct midi_event *ev)
+{
+ struct midi_track *tr, *found = NULL;
+ uint16_t i;
+
+ for (i = 0; i < mf->info.ntracks; i++) {
+ tr = &mf->tracks[i];
+ if (tr_avail(tr) == 0)
+ continue;
+ if (found == NULL || tr->tick < found->tick)
+ found = tr;
+ }
+ if (found == NULL)
+ return 0;
+
+ ev->track = found->id;
+ ev->sec = mf->tick_sec + ((found->tick - mf->tick_start) * (double)mf->tempo) / (1000000.0 * mf->info.division);
+ return 1;
+}
+
+int midi_file_next_time(struct midi_file *mf, double *sec)
+{
+ struct midi_event ev;
+ int res;
+
+ if ((res = peek_next(mf, &ev)) <= 0)
+ return res;
+
+ *sec = ev.sec;
+ return 1;
+}
+
+int midi_file_read_event(struct midi_file *mf, struct midi_event *event)
+{
+ struct midi_track *tr;
+ uint32_t delta_time, size;
+ uint8_t status, meta;
+ int res, running;
+
+ if ((res = peek_next(mf, event)) <= 0)
+ return res;
+
+ tr = &mf->tracks[event->track];
+ status = *tr->p;
+
+ running = (status & 0x80) == 0;
+ if (running) {
+ status = tr->event[0];
+ event->data = tr->event;
+ } else {
+ event->data = tr->p++;
+ tr->event[0] = status;
+ }
+
+ switch (status) {
+ case 0xc0 ... 0xdf:
+ size = 2;
+ break;
+
+ case 0x80 ... 0xbf:
+ case 0xe0 ... 0xef:
+ size = 3;
+ break;
+
+ case 0xff:
+ meta = *tr->p++;
+
+ if ((res = parse_varlen(mf, tr, &size)) < 0)
+ return res;
+
+ event->meta.offset = tr->p - event->data;
+ event->meta.size = size;
+
+ switch (meta) {
+ case 0x2f:
+ tr->eof = true;
+ break;
+ case 0x51:
+ if (size < 3)
+ return -EINVAL;
+ mf->tick_sec = event->sec;
+ mf->tick_start = tr->tick;
+ event->meta.parsed.tempo.uspqn = mf->tempo = (tr->p[0]<<16) | (tr->p[1]<<8) | tr->p[2];
+ break;
+ }
+ size += tr->p - event->data;
+ break;
+
+ case 0xf0:
+ case 0xf7:
+ if ((res = parse_varlen(mf, tr, &size)) < 0)
+ return res;
+ size += tr->p - event->data;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ event->size = size;
+
+ if (running) {
+ memcpy(&event->data[1], tr->p, size - 1);
+ tr->p += size - 1;
+ } else {
+ tr->p = event->data + event->size;
+ }
+
+ if ((res = parse_varlen(mf, tr, &delta_time)) < 0)
+ return res;
+
+ tr->tick += delta_time;
+ return 1;
+}
+
+static int write_varlen(struct midi_file *mf, struct midi_track *tr, uint32_t value)
+{
+ uint64_t buffer;
+ uint8_t b;
+ int res;
+
+ buffer = value & 0x7f;
+ while ((value >>= 7)) {
+ buffer <<= 8;
+ buffer |= ((value & 0x7f) | 0x80);
+ }
+ do {
+ b = buffer & 0xff;
+ CHECK_RES(write_n(mf->fd, &b, 1));
+ tr->size++;
+ buffer >>= 8;
+ } while (b & 0x80);
+
+ return 0;
+}
+
+int midi_file_write_event(struct midi_file *mf, const struct midi_event *event)
+{
+ struct midi_track *tr;
+ uint32_t tick;
+ int res;
+
+ spa_return_val_if_fail(event != NULL, -EINVAL);
+ spa_return_val_if_fail(mf != NULL, -EINVAL);
+ spa_return_val_if_fail(event->track == 0, -EINVAL);
+ spa_return_val_if_fail(event->size > 1, -EINVAL);
+
+ tr = &mf->tracks[event->track];
+
+ tick = event->sec * (1000000.0 * mf->info.division) / (double)mf->tempo;
+
+ CHECK_RES(write_varlen(mf, tr, tick - tr->tick));
+ tr->tick = tick;
+
+ CHECK_RES(write_n(mf->fd, event->data, event->size));
+ tr->size += event->size;
+
+ return 0;
+}
+
+static const char * const event_names[] = {
+ "Text", "Copyright", "Sequence/Track Name",
+ "Instrument", "Lyric", "Marker", "Cue Point",
+ "Program Name", "Device (Port) Name"
+};
+
+static const char * const note_names[] = {
+ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
+};
+
+static const char * const controller_names[128] = {
+ [0] = "Bank Select (coarse)",
+ [1] = "Modulation Wheel (coarse)",
+ [2] = "Breath controller (coarse)",
+ [4] = "Foot Pedal (coarse)",
+ [5] = "Portamento Time (coarse)",
+ [6] = "Data Entry (coarse)",
+ [7] = "Volume (coarse)",
+ [8] = "Balance (coarse)",
+ [10] = "Pan position (coarse)",
+ [11] = "Expression (coarse)",
+ [12] = "Effect Control 1 (coarse)",
+ [13] = "Effect Control 2 (coarse)",
+ [16] = "General Purpose Slider 1",
+ [17] = "General Purpose Slider 2",
+ [18] = "General Purpose Slider 3",
+ [19] = "General Purpose Slider 4",
+ [32] = "Bank Select (fine)",
+ [33] = "Modulation Wheel (fine)",
+ [34] = "Breath (fine)",
+ [36] = "Foot Pedal (fine)",
+ [37] = "Portamento Time (fine)",
+ [38] = "Data Entry (fine)",
+ [39] = "Volume (fine)",
+ [40] = "Balance (fine)",
+ [42] = "Pan position (fine)",
+ [43] = "Expression (fine)",
+ [44] = "Effect Control 1 (fine)",
+ [45] = "Effect Control 2 (fine)",
+ [64] = "Hold Pedal (on/off)",
+ [65] = "Portamento (on/off)",
+ [66] = "Sustenuto Pedal (on/off)",
+ [67] = "Soft Pedal (on/off)",
+ [68] = "Legato Pedal (on/off)",
+ [69] = "Hold 2 Pedal (on/off)",
+ [70] = "Sound Variation",
+ [71] = "Sound Timbre",
+ [72] = "Sound Release Time",
+ [73] = "Sound Attack Time",
+ [74] = "Sound Brightness",
+ [75] = "Sound Control 6",
+ [76] = "Sound Control 7",
+ [77] = "Sound Control 8",
+ [78] = "Sound Control 9",
+ [79] = "Sound Control 10",
+ [80] = "General Purpose Button 1 (on/off)",
+ [81] = "General Purpose Button 2 (on/off)",
+ [82] = "General Purpose Button 3 (on/off)",
+ [83] = "General Purpose Button 4 (on/off)",
+ [91] = "Effects Level",
+ [92] = "Tremulo Level",
+ [93] = "Chorus Level",
+ [94] = "Celeste Level",
+ [95] = "Phaser Level",
+ [96] = "Data Button increment",
+ [97] = "Data Button decrement",
+ [98] = "Non-registered Parameter (fine)",
+ [99] = "Non-registered Parameter (coarse)",
+ [100] = "Registered Parameter (fine)",
+ [101] = "Registered Parameter (coarse)",
+ [120] = "All Sound Off",
+ [121] = "All Controllers Off",
+ [122] = "Local Keyboard (on/off)",
+ [123] = "All Notes Off",
+ [124] = "Omni Mode Off",
+ [125] = "Omni Mode On",
+ [126] = "Mono Operation",
+ [127] = "Poly Operation",
+};
+
+static const char * const program_names[] = {
+ "Acoustic Grand", "Bright Acoustic", "Electric Grand", "Honky-Tonk",
+ "Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet",
+ "Celesta", "Glockenspiel", "Music Box", "Vibraphone", "Marimba",
+ "Xylophone", "Tubular Bells", "Dulcimer", "Drawbar Organ", "Percussive Organ",
+ "Rock Organ", "Church Organ", "Reed Organ", "Accoridan", "Harmonica",
+ "Tango Accordion", "Nylon String Guitar", "Steel String Guitar",
+ "Electric Jazz Guitar", "Electric Clean Guitar", "Electric Muted Guitar",
+ "Overdriven Guitar", "Distortion Guitar", "Guitar Harmonics",
+ "Acoustic Bass", "Electric Bass (fingered)", "Electric Bass (picked)",
+ "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2",
+ "Violin", "Viola", "Cello", "Contrabass", "Tremolo Strings", "Pizzicato Strings",
+ "Orchestral Strings", "Timpani", "String Ensemble 1", "String Ensemble 2",
+ "SynthStrings 1", "SynthStrings 2", "Choir Aahs", "Voice Oohs", "Synth Voice",
+ "Orchestra Hit", "Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn",
+ "Brass Section", "SynthBrass 1", "SynthBrass 2", "Soprano Sax", "Alto Sax",
+ "Tenor Sax", "Baritone Sax", "Oboe", "English Horn", "Bassoon", "Clarinet",
+ "Piccolo", "Flute", "Recorder", "Pan Flute", "Blown Bottle", "Skakuhachi",
+ "Whistle", "Ocarina", "Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)",
+ "Lead 4 (chiff)", "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)",
+ "Lead 8 (bass+lead)", "Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)",
+ "Pad 4 (choir)", "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)",
+ "Pad 8 (sweep)", "FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)",
+ "FX 4 (atmosphere)", "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)",
+ "FX 8 (sci-fi)", "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bagpipe",
+ "Fiddle", "Shanai", "Tinkle Bell", "Agogo", "Steel Drums", "Woodblock",
+ "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal", "Guitar Fret Noise",
+ "Breath Noise", "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter",
+ "Applause", "Gunshot"
+};
+
+static const char * const smpte_rates[] = {
+ "24 fps",
+ "25 fps",
+ "30 fps (drop frame)",
+ "30 fps (non drop frame)"
+};
+
+static const char * const major_keys[] = {
+ "Unknown major", "Fb", "Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F",
+ "C", "G", "D", "A", "E", "B", "F#", "C#", "G#", "Unknown major"
+};
+
+static const char * const minor_keys[] = {
+ "Unknown minor", "Dbm", "Abm", "Ebm", "Bbm", "Fm", "Cm", "Gm", "Dm",
+ "Am", "Em", "Bm", "F#m", "C#m", "G#m", "D#m", "A#m", "E#m", "Unknown minor"
+};
+
+static const char *controller_name(uint8_t ctrl)
+{
+ if (ctrl > 127 ||
+ controller_names[ctrl] == NULL)
+ return "Unknown";
+ return controller_names[ctrl];
+}
+
+static void dump_mem(FILE *out, const char *label, uint8_t *data, uint32_t size)
+{
+ fprintf(out, "%s: ", label);
+ while (size--)
+ fprintf(out, "%02x ", *data++);
+}
+
+int midi_file_dump_event(FILE *out, const struct midi_event *ev)
+{
+ fprintf(out, "track:%2d sec:%f ", ev->track, ev->sec);
+
+ switch (ev->data[0]) {
+ case 0x80 ... 0x8f:
+ fprintf(out, "Note Off (channel %2d): note %3s%d, velocity %3d",
+ (ev->data[0] & 0x0f) + 1,
+ note_names[ev->data[1] % 12], ev->data[1] / 12 -1,
+ ev->data[2]);
+ break;
+ case 0x90 ... 0x9f:
+ fprintf(out, "Note On (channel %2d): note %3s%d, velocity %3d",
+ (ev->data[0] & 0x0f) + 1,
+ note_names[ev->data[1] % 12], ev->data[1] / 12 -1,
+ ev->data[2]);
+ break;
+ case 0xa0 ... 0xaf:
+ fprintf(out, "Aftertouch (channel %2d): note %3s%d, pressure %3d",
+ (ev->data[0] & 0x0f) + 1,
+ note_names[ev->data[1] % 12], ev->data[1] / 12 -1,
+ ev->data[2]);
+ break;
+ case 0xb0 ... 0xbf:
+ fprintf(out, "Controller (channel %2d): controller %3d (%s), value %3d",
+ (ev->data[0] & 0x0f) + 1, ev->data[1],
+ controller_name(ev->data[1]), ev->data[2]);
+ break;
+ case 0xc0 ... 0xcf:
+ fprintf(out, "Program (channel %2d): program %3d (%s)",
+ (ev->data[0] & 0x0f) + 1, ev->data[1],
+ program_names[ev->data[1]]);
+ break;
+ case 0xd0 ... 0xdf:
+ fprintf(out, "Channel Pressure (channel %2d): pressure %3d",
+ (ev->data[0] & 0x0f) + 1, ev->data[1]);
+ break;
+ case 0xe0 ... 0xef:
+ fprintf(out, "Pitch Bend (channel %2d): value %d", (ev->data[0] & 0x0f) + 1,
+ ((int)ev->data[2] << 7 | ev->data[1]) - 0x2000);
+ break;
+ case 0xf0:
+ case 0xf7:
+ dump_mem(out, "SysEx", ev->data, ev->size);
+ break;
+ case 0xf1:
+ fprintf(out, "MIDI Time Code Quarter Frame: type %d values %d",
+ ev->data[0] >> 4, ev->data[0] & 0xf);
+ break;
+ case 0xf2:
+ fprintf(out, "Song Position Pointer: value %d",
+ ((int)ev->data[1] << 7 | ev->data[0]));
+ break;
+ case 0xf3:
+ fprintf(out, "Song Select: value %d", (ev->data[0] & 0x7f));
+ break;
+ case 0xf6:
+ fprintf(out, "Tune Request");
+ break;
+ case 0xf8:
+ fprintf(out, "Timing Clock");
+ break;
+ case 0xfa:
+ fprintf(out, "Start Sequence");
+ break;
+ case 0xfb:
+ fprintf(out, "Continue Sequence");
+ break;
+ case 0xfc:
+ fprintf(out, "Stop Sequence");
+ break;
+ case 0xfe:
+ fprintf(out, "Active Sensing");
+ break;
+ case 0xff:
+ fprintf(out, "Meta: ");
+ switch (ev->data[1]) {
+ case 0x00:
+ fprintf(out, "Sequence Number %3d %3d", ev->data[3], ev->data[4]);
+ break;
+ case 0x01 ... 0x09:
+ fprintf(out, "%s: %s", event_names[ev->data[1] - 1], &ev->data[ev->meta.offset]);
+ break;
+ case 0x20:
+ fprintf(out, "Channel Prefix: %03d", ev->data[3]);
+ break;
+ case 0x21:
+ fprintf(out, "Midi Port: %03d", ev->data[3]);
+ break;
+ case 0x2f:
+ fprintf(out, "End Of Track");
+ break;
+ case 0x51:
+ fprintf(out, "Tempo: %d microseconds per quarter note, %.2f BPM",
+ ev->meta.parsed.tempo.uspqn,
+ 60000000.0 / (double)ev->meta.parsed.tempo.uspqn);
+ break;
+ case 0x54:
+ fprintf(out, "SMPTE Offset: %s %02d:%02d:%02d:%02d.%03d",
+ smpte_rates[(ev->data[3] & 0x60) >> 5],
+ ev->data[3] & 0x1f, ev->data[4], ev->data[5],
+ ev->data[6], ev->data[7]);
+ break;
+ case 0x58:
+ fprintf(out, "Time Signature: %d/%d, %d clocks per click, %d notated 32nd notes per quarter note",
+ ev->data[3], (int)pow(2, ev->data[4]), ev->data[5], ev->data[6]);
+ break;
+ case 0x59:
+ {
+ int sf = ev->data[3];
+ fprintf(out, "Key Signature: %d %s: %s", abs(sf),
+ sf > 0 ? "sharps" : "flats",
+ ev->data[4] == 0 ?
+ major_keys[SPA_CLAMP(sf + 9, 0, 18)] :
+ minor_keys[SPA_CLAMP(sf + 9, 0, 18)]);
+ break;
+ }
+ case 0x7f:
+ dump_mem(out, "Sequencer", ev->data, ev->size);
+ break;
+ default:
+ dump_mem(out, "Invalid", ev->data, ev->size);
+ }
+ break;
+ default:
+ dump_mem(out, "Unknown", ev->data, ev->size);
+ break;
+ }
+ fprintf(out, "\n");
+ return 0;
+}
diff --git a/src/tools/midifile.h b/src/tools/midifile.h
new file mode 100644
index 0000000..6c69df4
--- /dev/null
+++ b/src/tools/midifile.h
@@ -0,0 +1,64 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+
+#include <spa/utils/defs.h>
+
+struct midi_file;
+
+struct midi_event {
+ uint32_t track;
+ double sec;
+ uint8_t *data;
+ uint32_t size;
+ struct {
+ uint32_t offset;
+ uint32_t size;
+ union {
+ struct {
+ uint32_t uspqn; /* microseconds per quarter note */
+ } tempo;
+ } parsed;
+ } meta;
+};
+
+struct midi_file_info {
+ uint16_t format;
+ uint16_t ntracks;
+ uint16_t division;
+};
+
+struct midi_file *
+midi_file_open(const char *filename, const char *mode, struct midi_file_info *info);
+
+int midi_file_close(struct midi_file *mf);
+
+int midi_file_next_time(struct midi_file *mf, double *sec);
+
+int midi_file_read_event(struct midi_file *mf, struct midi_event *event);
+
+int midi_file_write_event(struct midi_file *mf, const struct midi_event *event);
+
+int midi_file_dump_event(FILE *out, const struct midi_event *event);
diff --git a/src/tools/pw-cat.c b/src/tools/pw-cat.c
new file mode 100644
index 0000000..068fac1
--- /dev/null
+++ b/src/tools/pw-cat.c
@@ -0,0 +1,1971 @@
+/* PipeWire - pw-cat
+ *
+ * Copyright © 2020 Konsulko Group
+
+ * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <time.h>
+#include <math.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <assert.h>
+#include <ctype.h>
+#include <locale.h>
+
+#include <sndfile.h>
+
+#include <spa/param/audio/layout.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/type-info.h>
+#include <spa/param/props.h>
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/i18n.h>
+#include <pipewire/extensions/metadata.h>
+
+#include "config.h"
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+#include <libavformat/avformat.h>
+#include <libavcodec/avcodec.h>
+#endif
+
+#include "midifile.h"
+#include "dsffile.h"
+
+#define DEFAULT_MEDIA_TYPE "Audio"
+#define DEFAULT_MIDI_MEDIA_TYPE "Midi"
+#define DEFAULT_MEDIA_CATEGORY_PLAYBACK "Playback"
+#define DEFAULT_MEDIA_CATEGORY_RECORD "Capture"
+#define DEFAULT_MEDIA_ROLE "Music"
+#define DEFAULT_TARGET "auto"
+#define DEFAULT_LATENCY_PLAY "100ms"
+#define DEFAULT_LATENCY_REC "none"
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_FORMAT "s16"
+#define DEFAULT_VOLUME 1.0
+#define DEFAULT_QUALITY 4
+
+enum mode {
+ mode_none,
+ mode_playback,
+ mode_record
+};
+
+enum unit {
+ unit_none,
+ unit_samples,
+ unit_sec,
+ unit_msec,
+ unit_usec,
+ unit_nsec,
+};
+
+struct data;
+
+typedef int (*fill_fn)(struct data *d, void *dest, unsigned int n_frames);
+
+struct channelmap {
+ int n_channels;
+ int channels[SPA_AUDIO_MAX_CHANNELS];
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_stream *stream;
+ struct spa_hook stream_listener;
+
+ struct spa_source *timer;
+
+ enum mode mode;
+ bool verbose;
+#define TYPE_PCM 0
+#define TYPE_MIDI 1
+#define TYPE_DSD 2
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+#define TYPE_ENCODED 3
+#endif
+ int data_type;
+ const char *remote_name;
+ const char *media_type;
+ const char *media_category;
+ const char *media_role;
+ const char *channel_map;
+ const char *format;
+ const char *target;
+ const char *latency;
+ struct pw_properties *props;
+
+ const char *filename;
+ SNDFILE *file;
+
+ unsigned int bitrate;
+ unsigned int rate;
+ int channels;
+ struct channelmap channelmap;
+ unsigned int stride;
+ enum unit latency_unit;
+ unsigned int latency_value;
+ int quality;
+
+ enum spa_audio_format spa_format;
+
+ float volume;
+ bool volume_is_set;
+
+ fill_fn fill;
+
+ struct spa_io_position *position;
+ bool drained;
+ uint64_t clock_time;
+
+ struct {
+ struct midi_file *file;
+ struct midi_file_info info;
+ } midi;
+ struct {
+ struct dsf_file *file;
+ struct dsf_file_info info;
+ struct dsf_layout layout;
+ } dsf;
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ FILE *encoded_file;
+ AVFormatContext *fmt_context;
+ AVStream *astream;
+ AVCodecContext *ctx;
+ enum AVSampleFormat sfmt;
+#endif
+};
+
+#define STR_FMTS "(ulaw|alaw|u8|s8|s16|s32|f32|f64)"
+
+static const struct format_info {
+ const char *name;
+ int sf_format;
+ uint32_t spa_format;
+ uint32_t width;
+} format_info[] = {
+ { "ulaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ULAW, 1 },
+ { "alaw", SF_FORMAT_ULAW, SPA_AUDIO_FORMAT_ALAW, 1 },
+ { "s8", SF_FORMAT_PCM_S8, SPA_AUDIO_FORMAT_S8, 1 },
+ { "u8", SF_FORMAT_PCM_U8, SPA_AUDIO_FORMAT_U8, 1 },
+ { "s16", SF_FORMAT_PCM_16, SPA_AUDIO_FORMAT_S16, 2 },
+ { "s24", SF_FORMAT_PCM_24, SPA_AUDIO_FORMAT_S24, 3 },
+ { "s32", SF_FORMAT_PCM_32, SPA_AUDIO_FORMAT_S32, 4 },
+ { "f32", SF_FORMAT_FLOAT, SPA_AUDIO_FORMAT_F32, 4 },
+ { "f64", SF_FORMAT_DOUBLE, SPA_AUDIO_FORMAT_F32, 8 },
+};
+
+static 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))
+ return i;
+ return NULL;
+}
+
+static const struct format_info *format_info_by_sf_format(int format)
+{
+ int sub_type = (format & SF_FORMAT_SUBMASK);
+ SPA_FOR_EACH_ELEMENT_VAR(format_info, i)
+ if (i->sf_format == sub_type)
+ return i;
+ return NULL;
+}
+
+static int sf_playback_fill_x8(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ rn = sf_read_raw(d->file, dest, n_frames * d->stride);
+ return (int)rn / d->stride;
+}
+
+static int sf_playback_fill_s16(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(short) == sizeof(int16_t));
+ rn = sf_readf_short(d->file, dest, n_frames);
+ return (int)rn;
+}
+
+static int sf_playback_fill_s32(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(int) == sizeof(int32_t));
+ rn = sf_readf_int(d->file, dest, n_frames);
+ return (int)rn;
+}
+
+static int sf_playback_fill_f32(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(float) == 4);
+ rn = sf_readf_float(d->file, dest, n_frames);
+ return (int)rn;
+}
+
+static int sf_playback_fill_f64(struct data *d, void *dest, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(double) == 8);
+ rn = sf_readf_double(d->file, dest, n_frames);
+ return (int)rn;
+}
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+static int encoded_playback_fill(struct data *d, void *dest, unsigned int n_frames)
+{
+ int ret, size = 0;
+ uint8_t buffer[16384] = { 0 };
+
+ ret = fread(buffer, 1, 16384, d->encoded_file);
+ if (ret > 0) {
+ memcpy(dest, buffer, ret);
+ size = ret;
+ }
+
+ return (int)size;
+}
+
+static int avcodec_ctx_to_info(struct data *data, AVCodecContext *ctx, struct spa_audio_info *info)
+{
+ int32_t profile;
+
+ switch (ctx->codec_id) {
+ case AV_CODEC_ID_VORBIS:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_vorbis;
+ info->info.vorbis.rate = data->rate;
+ info->info.vorbis.channels = data->channels;
+ break;
+ case AV_CODEC_ID_MP3:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_mp3;
+ info->info.mp3.rate = data->rate;
+ info->info.mp3.channels = data->channels;
+ break;
+ case AV_CODEC_ID_AAC:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_aac;
+ info->info.aac.rate = data->rate;
+ info->info.aac.channels = data->channels;
+ info->info.aac.bitrate = data->bitrate;
+ info->info.aac.stream_format = SPA_AUDIO_AAC_STREAM_FORMAT_RAW;
+ break;
+ case AV_CODEC_ID_WMAV1:
+ case AV_CODEC_ID_WMAV2:
+ case AV_CODEC_ID_WMAPRO:
+ case AV_CODEC_ID_WMAVOICE:
+ case AV_CODEC_ID_WMALOSSLESS:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_wma;
+ switch (ctx->codec_tag) {
+ /* TODO see if these hex constants can be replaced by named constants from FFmpeg */
+ case 0x161:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA9;
+ break;
+ case 0x162:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA9_PRO;
+ break;
+ case 0x163:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS;
+ break;
+ case 0x166:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA10;
+ break;
+ case 0x167:
+ profile = SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS;
+ break;
+ default:
+ fprintf(stderr, "error: invalid WMA profile\n");
+ return -EINVAL;
+ }
+ info->info.wma.rate = data->rate;
+ info->info.wma.channels = data->channels;
+ info->info.wma.bitrate = data->bitrate;
+ info->info.wma.block_align = ctx->block_align;
+ info->info.wma.profile = profile;
+ break;
+ case AV_CODEC_ID_FLAC:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_flac;
+ info->info.flac.rate = data->rate;
+ info->info.flac.channels = data->channels;
+ break;
+ case AV_CODEC_ID_ALAC:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_alac;
+ info->info.alac.rate = data->rate;
+ info->info.alac.channels = data->channels;
+ break;
+ case AV_CODEC_ID_APE:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_ape;
+ info->info.ape.rate = data->rate;
+ info->info.ape.channels = data->channels;
+ break;
+ case AV_CODEC_ID_RA_144:
+ case AV_CODEC_ID_RA_288:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_ra;
+ info->info.ra.rate = data->rate;
+ info->info.ra.channels = data->channels;
+ break;
+ case AV_CODEC_ID_AMR_NB:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_amr;
+ info->info.amr.rate = data->rate;
+ info->info.amr.channels = data->channels;
+ info->info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_NB;
+ break;
+ case AV_CODEC_ID_AMR_WB:
+ info->media_subtype = SPA_MEDIA_SUBTYPE_amr;
+ info->info.amr.rate = data->rate;
+ info->info.amr.channels = data->channels;
+ info->info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_WB;
+ break;
+ default:
+ fprintf(stderr, "Unsupported encoded media subtype\n");
+ return -EINVAL;
+ }
+ return 0;
+}
+#endif
+
+static inline fill_fn
+playback_fill_fn(uint32_t fmt)
+{
+ switch (fmt) {
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_ULAW:
+ case SPA_AUDIO_FORMAT_ALAW:
+ return sf_playback_fill_x8;
+ case SPA_AUDIO_FORMAT_S16_LE:
+ case SPA_AUDIO_FORMAT_S16_BE:
+ /* sndfile check */
+ if (sizeof(int16_t) != sizeof(short))
+ return NULL;
+ return sf_playback_fill_s16;
+ case SPA_AUDIO_FORMAT_S32_LE:
+ case SPA_AUDIO_FORMAT_S32_BE:
+ /* sndfile check */
+ if (sizeof(int32_t) != sizeof(int))
+ return NULL;
+ return sf_playback_fill_s32;
+ case SPA_AUDIO_FORMAT_F32_LE:
+ case SPA_AUDIO_FORMAT_F32_BE:
+ /* sndfile check */
+ if (sizeof(float) != 4)
+ return NULL;
+ return sf_playback_fill_f32;
+ case SPA_AUDIO_FORMAT_F64_LE:
+ case SPA_AUDIO_FORMAT_F64_BE:
+ if (sizeof(double) != 8)
+ return NULL;
+ return sf_playback_fill_f64;
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ case SPA_AUDIO_FORMAT_ENCODED:
+ return encoded_playback_fill;
+#endif
+ default:
+ break;
+ }
+ return NULL;
+}
+
+static int sf_record_fill_x8(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ rn = sf_write_raw(d->file, src, n_frames * d->stride);
+ return (int)rn / d->stride;
+}
+
+static int sf_record_fill_s16(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(short) == sizeof(int16_t));
+ rn = sf_writef_short(d->file, src, n_frames);
+ return (int)rn;
+}
+
+static int sf_record_fill_s32(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(int) == sizeof(int32_t));
+ rn = sf_writef_int(d->file, src, n_frames);
+ return (int)rn;
+}
+
+static int sf_record_fill_f32(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(float) == 4);
+ rn = sf_writef_float(d->file, src, n_frames);
+ return (int)rn;
+}
+
+static int sf_record_fill_f64(struct data *d, void *src, unsigned int n_frames)
+{
+ sf_count_t rn;
+
+ assert(sizeof(double) == 8);
+ rn = sf_writef_double(d->file, src, n_frames);
+ return (int)rn;
+}
+
+static inline fill_fn
+record_fill_fn(uint32_t fmt)
+{
+ switch (fmt) {
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_U8:
+ case SPA_AUDIO_FORMAT_ULAW:
+ case SPA_AUDIO_FORMAT_ALAW:
+ return sf_record_fill_x8;
+ case SPA_AUDIO_FORMAT_S16_LE:
+ case SPA_AUDIO_FORMAT_S16_BE:
+ /* sndfile check */
+ if (sizeof(int16_t) != sizeof(short))
+ return NULL;
+ return sf_record_fill_s16;
+ case SPA_AUDIO_FORMAT_S32_LE:
+ case SPA_AUDIO_FORMAT_S32_BE:
+ /* sndfile check */
+ if (sizeof(int32_t) != sizeof(int))
+ return NULL;
+ return sf_record_fill_s32;
+ case SPA_AUDIO_FORMAT_F32_LE:
+ case SPA_AUDIO_FORMAT_F32_BE:
+ /* sndfile check */
+ if (sizeof(float) != 4)
+ return NULL;
+ return sf_record_fill_f32;
+ case SPA_AUDIO_FORMAT_F64_LE:
+ case SPA_AUDIO_FORMAT_F64_BE:
+ /* sndfile check */
+ if (sizeof(double) != 8)
+ return NULL;
+ return sf_record_fill_f64;
+ default:
+ break;
+ }
+ return NULL;
+}
+
+static int channelmap_from_sf(struct channelmap *map)
+{
+ static const enum spa_audio_channel table[] = {
+ [SF_CHANNEL_MAP_MONO] = SPA_AUDIO_CHANNEL_MONO,
+ [SF_CHANNEL_MAP_LEFT] = SPA_AUDIO_CHANNEL_FL, /* libsndfile distinguishes left and front-left, which we don't */
+ [SF_CHANNEL_MAP_RIGHT] = SPA_AUDIO_CHANNEL_FR,
+ [SF_CHANNEL_MAP_CENTER] = SPA_AUDIO_CHANNEL_FC,
+ [SF_CHANNEL_MAP_FRONT_LEFT] = SPA_AUDIO_CHANNEL_FL,
+ [SF_CHANNEL_MAP_FRONT_RIGHT] = SPA_AUDIO_CHANNEL_FR,
+ [SF_CHANNEL_MAP_FRONT_CENTER] = SPA_AUDIO_CHANNEL_FC,
+ [SF_CHANNEL_MAP_REAR_CENTER] = SPA_AUDIO_CHANNEL_RC,
+ [SF_CHANNEL_MAP_REAR_LEFT] = SPA_AUDIO_CHANNEL_RL,
+ [SF_CHANNEL_MAP_REAR_RIGHT] = SPA_AUDIO_CHANNEL_RR,
+ [SF_CHANNEL_MAP_LFE] = SPA_AUDIO_CHANNEL_LFE,
+ [SF_CHANNEL_MAP_FRONT_LEFT_OF_CENTER] = SPA_AUDIO_CHANNEL_FLC,
+ [SF_CHANNEL_MAP_FRONT_RIGHT_OF_CENTER] = SPA_AUDIO_CHANNEL_FRC,
+ [SF_CHANNEL_MAP_SIDE_LEFT] = SPA_AUDIO_CHANNEL_SL,
+ [SF_CHANNEL_MAP_SIDE_RIGHT] = SPA_AUDIO_CHANNEL_SR,
+ [SF_CHANNEL_MAP_TOP_CENTER] = SPA_AUDIO_CHANNEL_TC,
+ [SF_CHANNEL_MAP_TOP_FRONT_LEFT] = SPA_AUDIO_CHANNEL_TFL,
+ [SF_CHANNEL_MAP_TOP_FRONT_RIGHT] = SPA_AUDIO_CHANNEL_TFR,
+ [SF_CHANNEL_MAP_TOP_FRONT_CENTER] = SPA_AUDIO_CHANNEL_TFC,
+ [SF_CHANNEL_MAP_TOP_REAR_LEFT] = SPA_AUDIO_CHANNEL_TRL,
+ [SF_CHANNEL_MAP_TOP_REAR_RIGHT] = SPA_AUDIO_CHANNEL_TRR,
+ [SF_CHANNEL_MAP_TOP_REAR_CENTER] = SPA_AUDIO_CHANNEL_TRC
+ };
+ int i;
+
+ for (i = 0; i < map->n_channels; i++) {
+ if (map->channels[i] >= 0 && map->channels[i] < (int) SPA_N_ELEMENTS(table))
+ map->channels[i] = table[map->channels[i]];
+ else
+ map->channels[i] = SPA_AUDIO_CHANNEL_UNKNOWN;
+ }
+ return 0;
+}
+struct mapping {
+ const char *name;
+ unsigned int channels;
+ unsigned int values[32];
+};
+
+static const struct mapping maps[] =
+{
+ { "mono", SPA_AUDIO_LAYOUT_Mono },
+ { "stereo", SPA_AUDIO_LAYOUT_Stereo },
+ { "surround-21", SPA_AUDIO_LAYOUT_2_1 },
+ { "quad", SPA_AUDIO_LAYOUT_Quad },
+ { "surround-22", SPA_AUDIO_LAYOUT_2_2 },
+ { "surround-40", SPA_AUDIO_LAYOUT_4_0 },
+ { "surround-31", SPA_AUDIO_LAYOUT_3_1 },
+ { "surround-41", SPA_AUDIO_LAYOUT_4_1 },
+ { "surround-50", SPA_AUDIO_LAYOUT_5_0 },
+ { "surround-51", SPA_AUDIO_LAYOUT_5_1 },
+ { "surround-51r", SPA_AUDIO_LAYOUT_5_1R },
+ { "surround-70", SPA_AUDIO_LAYOUT_7_0 },
+ { "surround-71", SPA_AUDIO_LAYOUT_7_1 },
+};
+
+static unsigned int find_channel(const char *name)
+{
+ int i;
+
+ for (i = 0; spa_type_audio_channel[i].name; i++) {
+ if (spa_streq(name, spa_debug_type_short_name(spa_type_audio_channel[i].name)))
+ return spa_type_audio_channel[i].type;
+ }
+ return SPA_AUDIO_CHANNEL_UNKNOWN;
+}
+
+static int parse_channelmap(const char *channel_map, struct channelmap *map)
+{
+ int i, nch;
+ char **ch;
+
+ SPA_FOR_EACH_ELEMENT_VAR(maps, m) {
+ if (spa_streq(m->name, channel_map)) {
+ map->n_channels = m->channels;
+ spa_memcpy(map->channels, &m->values,
+ map->n_channels * sizeof(unsigned int));
+ return 0;
+ }
+ }
+
+ ch = pw_split_strv(channel_map, ",", SPA_AUDIO_MAX_CHANNELS, &nch);
+ if (ch == NULL)
+ return -1;
+
+ map->n_channels = nch;
+ for (i = 0; i < map->n_channels; i++) {
+ int c = find_channel(ch[i]);
+ map->channels[i] = c;
+ }
+ pw_free_strv(ch);
+ return 0;
+}
+
+static int channelmap_default(struct channelmap *map, int n_channels)
+{
+ switch(n_channels) {
+ case 1:
+ parse_channelmap("mono", map);
+ break;
+ case 2:
+ parse_channelmap("stereo", map);
+ break;
+ case 3:
+ parse_channelmap("surround-21", map);
+ break;
+ case 4:
+ parse_channelmap("quad", map);
+ break;
+ case 5:
+ parse_channelmap("surround-50", map);
+ break;
+ case 6:
+ parse_channelmap("surround-51", map);
+ break;
+ case 7:
+ parse_channelmap("surround-70", map);
+ break;
+ case 8:
+ parse_channelmap("surround-71", map);
+ break;
+ default:
+ n_channels = 0;
+ break;
+ }
+ map->n_channels = n_channels;
+ return 0;
+}
+
+static void channelmap_print(struct channelmap *map)
+{
+ int i;
+
+ for (i = 0; i < map->n_channels; i++) {
+ const char *name = spa_debug_type_find_name(spa_type_audio_channel, map->channels[i]);
+ if (name == NULL)
+ name = ":UNK";
+ printf("%s%s", spa_debug_type_short_name(name), i + 1 < map->n_channels ? "," : "");
+ }
+}
+
+static void on_core_info(void *userdata, const struct pw_core_info *info)
+{
+ struct data *data = userdata;
+
+ if (data->verbose)
+ printf("remote %"PRIu32" is named \"%s\"\n",
+ info->id, info->name);
+}
+
+static void on_core_error(void *userdata, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *data = userdata;
+
+ fprintf(stderr, "remote error: id=%"PRIu32" seq:%d res:%d (%s): %s\n",
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(data->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .info = on_core_info,
+ .error = on_core_error,
+};
+
+static void
+on_state_changed(void *userdata, enum pw_stream_state old,
+ enum pw_stream_state state, const char *error)
+{
+ struct data *data = userdata;
+ int ret;
+
+ if (data->verbose)
+ printf("stream state changed %s -> %s\n",
+ pw_stream_state_as_string(old),
+ pw_stream_state_as_string(state));
+
+ switch (state) {
+ case PW_STREAM_STATE_STREAMING:
+ if (!data->volume_is_set) {
+ ret = pw_stream_set_control(data->stream,
+ SPA_PROP_volume, 1, &data->volume,
+ 0);
+ if (data->verbose)
+ printf("stream set volume to %.3f - %s\n", data->volume,
+ ret == 0 ? "success" : "FAILED");
+
+ data->volume_is_set = true;
+ }
+ if (data->verbose) {
+ struct timespec timeout = {0, 1}, interval = {1, 0};
+ struct pw_loop *l = pw_main_loop_get_loop(data->loop);
+ pw_loop_update_timer(l, data->timer, &timeout, &interval, false);
+ printf("stream node %"PRIu32"\n",
+ pw_stream_get_node_id(data->stream));
+ }
+ break;
+ case PW_STREAM_STATE_PAUSED:
+ if (data->verbose) {
+ struct timespec timeout = {0, 0}, interval = {0, 0};
+ struct pw_loop *l = pw_main_loop_get_loop(data->loop);
+ pw_loop_update_timer(l, data->timer, &timeout, &interval, false);
+ }
+ break;
+ case PW_STREAM_STATE_ERROR:
+ printf("stream node %"PRIu32" error: %s\n",
+ pw_stream_get_node_id(data->stream),
+ error);
+ pw_main_loop_quit(data->loop);
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+on_io_changed(void *userdata, uint32_t id, void *data, uint32_t size)
+{
+ struct data *d = userdata;
+
+ switch (id) {
+ case SPA_IO_Position:
+ d->position = data;
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param)
+{
+ struct data *data = userdata;
+ struct spa_audio_info info = { 0 };
+ int err;
+
+ if (data->verbose)
+ printf("stream param change: %s\n",
+ spa_debug_type_find_name(spa_type_param, id));
+
+ if (id != SPA_PARAM_Format || param == NULL)
+ return;
+
+ if ((err = spa_format_parse(param, &info.media_type, &info.media_subtype)) < 0)
+ return;
+
+ if (info.media_type != SPA_MEDIA_TYPE_audio ||
+ info.media_subtype != SPA_MEDIA_SUBTYPE_dsd)
+ return;
+
+ if (spa_format_audio_dsd_parse(param, &info.info.dsd) < 0)
+ return;
+
+ data->dsf.layout.interleave = info.info.dsd.interleave,
+ data->dsf.layout.channels = info.info.dsd.channels;
+ data->dsf.layout.lsb = info.info.dsd.bitorder == SPA_PARAM_BITORDER_lsb;
+
+ data->stride = data->dsf.layout.channels * SPA_ABS(data->dsf.layout.interleave);
+
+ if (data->verbose) {
+ printf("DSD: channels:%d bitorder:%s interleave:%d stride:%d\n",
+ data->dsf.layout.channels,
+ data->dsf.layout.lsb ? "lsb" : "msb",
+ data->dsf.layout.interleave,
+ data->stride);
+ }
+}
+
+static void on_process(void *userdata)
+{
+ struct data *data = userdata;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ struct spa_data *d;
+ int n_frames, n_fill_frames;
+ uint8_t *p;
+ bool have_data;
+ uint32_t offset, size;
+
+ if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL)
+ return;
+
+ buf = b->buffer;
+ d = &buf->datas[0];
+
+ have_data = false;
+
+ if ((p = d->data) == NULL)
+ return;
+
+ if (data->mode == mode_playback) {
+ n_frames = d->maxsize / data->stride;
+ n_frames = SPA_MIN(n_frames, (int)b->requested);
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ n_fill_frames = data->fill(data, p, n_frames);
+
+ if (n_fill_frames > 0 || n_frames == 0) {
+ d->chunk->offset = 0;
+ if (data->data_type == TYPE_ENCODED) {
+ d->chunk->stride = 0;
+ // encoded_playback_fill returns number of bytes
+ // read and not number of frames like other
+ // functions for raw audio.
+ d->chunk->size = n_fill_frames;
+ b->size = n_fill_frames;
+ } else {
+ d->chunk->stride = data->stride;
+ d->chunk->size = n_fill_frames * data->stride;
+ b->size = n_frames;
+ }
+ have_data = true;
+ } else if (n_fill_frames < 0) {
+ fprintf(stderr, "fill error %d\n", n_fill_frames);
+ } else {
+ if (data->verbose)
+ printf("drain start\n");
+ }
+#else
+ n_fill_frames = data->fill(data, p, n_frames);
+
+ if (n_fill_frames > 0 || n_frames == 0) {
+ d->chunk->offset = 0;
+ d->chunk->stride = data->stride;
+ d->chunk->size = n_fill_frames * data->stride;
+ have_data = true;
+ b->size = n_frames;
+ } else if (n_fill_frames < 0) {
+ fprintf(stderr, "fill error %d\n", n_fill_frames);
+ } else {
+ if (data->verbose)
+ printf("drain start\n");
+ }
+#endif
+ } else {
+ offset = SPA_MIN(d->chunk->offset, d->maxsize);
+ size = SPA_MIN(d->chunk->size, d->maxsize - offset);
+
+ p += offset;
+
+ n_frames = size / data->stride;
+
+ n_fill_frames = data->fill(data, p, n_frames);
+
+ have_data = true;
+ }
+
+ if (have_data) {
+ pw_stream_queue_buffer(data->stream, b);
+ return;
+ }
+
+ if (data->mode == mode_playback)
+ pw_stream_flush(data->stream, true);
+}
+
+static void on_drained(void *userdata)
+{
+ struct data *data = userdata;
+
+ if (data->verbose)
+ printf("stream drained\n");
+
+ data->drained = true;
+ pw_main_loop_quit(data->loop);
+}
+
+static const struct pw_stream_events stream_events = {
+ PW_VERSION_STREAM_EVENTS,
+ .state_changed = on_state_changed,
+ .io_changed = on_io_changed,
+ .param_changed = on_param_changed,
+ .process = on_process,
+ .drained = on_drained
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+static void do_print_delay(void *userdata, uint64_t expirations)
+{
+ struct data *data = userdata;
+ struct pw_time time;
+ pw_stream_get_time_n(data->stream, &time, sizeof(time));
+ printf("stream time: now:%"PRIi64" rate:%u/%u ticks:%"PRIu64
+ " delay:%"PRIi64" queued:%"PRIu64
+ " buffered:%"PRIi64" buffers:%u avail:%u\n",
+ time.now,
+ time.rate.num, time.rate.denom,
+ time.ticks, time.delay, time.queued, time.buffered,
+ time.queued_buffers, time.avail_buffers);
+}
+
+enum {
+ OPT_VERSION = 1000,
+ OPT_MEDIA_TYPE,
+ OPT_MEDIA_CATEGORY,
+ OPT_MEDIA_ROLE,
+ OPT_TARGET,
+ OPT_LATENCY,
+ OPT_RATE,
+ OPT_CHANNELS,
+ OPT_CHANNELMAP,
+ OPT_FORMAT,
+ OPT_VOLUME,
+};
+
+static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, OPT_VERSION},
+ { "verbose", no_argument, NULL, 'v' },
+
+ { "record", no_argument, NULL, 'r' },
+ { "playback", no_argument, NULL, 'p' },
+ { "midi", no_argument, NULL, 'm' },
+
+ { "remote", required_argument, NULL, 'R' },
+
+ { "media-type", required_argument, NULL, OPT_MEDIA_TYPE },
+ { "media-category", required_argument, NULL, OPT_MEDIA_CATEGORY },
+ { "media-role", required_argument, NULL, OPT_MEDIA_ROLE },
+ { "target", required_argument, NULL, OPT_TARGET },
+ { "latency", required_argument, NULL, OPT_LATENCY },
+ { "properties", required_argument, NULL, 'P' },
+
+ { "rate", required_argument, NULL, OPT_RATE },
+ { "channels", required_argument, NULL, OPT_CHANNELS },
+ { "channel-map", required_argument, NULL, OPT_CHANNELMAP },
+ { "format", required_argument, NULL, OPT_FORMAT },
+ { "volume", required_argument, NULL, OPT_VOLUME },
+ { "quality", required_argument, NULL, 'q' },
+
+ { NULL, 0, NULL, 0 }
+};
+
+static void show_usage(const char *name, bool is_error)
+{
+ FILE *fp;
+
+ fp = is_error ? stderr : stdout;
+
+ fprintf(fp,
+ _("%s [options] [<file>|-]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -v, --verbose Enable verbose operations\n"
+ "\n"), name);
+
+ fprintf(fp,
+ _(" -R, --remote Remote daemon name\n"
+ " --media-type Set media type (default %s)\n"
+ " --media-category Set media category (default %s)\n"
+ " --media-role Set media role (default %s)\n"
+ " --target Set node target serial or name (default %s)\n"
+ " 0 means don't link\n"
+ " --latency Set node latency (default %s)\n"
+ " Xunit (unit = s, ms, us, ns)\n"
+ " or direct samples (256)\n"
+ " the rate is the one of the source file\n"
+ " -P --properties Set node properties\n"
+ "\n"),
+ DEFAULT_MEDIA_TYPE,
+ DEFAULT_MEDIA_CATEGORY_PLAYBACK,
+ DEFAULT_MEDIA_ROLE,
+ DEFAULT_TARGET, DEFAULT_LATENCY_PLAY);
+
+ fprintf(fp,
+ _(" --rate Sample rate (req. for rec) (default %u)\n"
+ " --channels Number of channels (req. for rec) (default %u)\n"
+ " --channel-map Channel map\n"
+ " one of: \"stereo\", \"surround-51\",... or\n"
+ " comma separated list of channel names: eg. \"FL,FR\"\n"
+ " --format Sample format %s (req. for rec) (default %s)\n"
+ " --volume Stream volume 0-1.0 (default %.3f)\n"
+ " -q --quality Resampler quality (0 - 15) (default %d)\n"
+ "\n"),
+ DEFAULT_RATE,
+ DEFAULT_CHANNELS,
+ STR_FMTS, DEFAULT_FORMAT,
+ DEFAULT_VOLUME,
+ DEFAULT_QUALITY);
+
+ if (spa_streq(name, "pw-cat")) {
+ fputs(
+ _(" -p, --playback Playback mode\n"
+ " -r, --record Recording mode\n"
+ " -m, --midi Midi mode\n"
+ " -d, --dsd DSD mode\n"
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ " -o, --encoded Encoded mode\n"
+#endif
+ "\n"), fp);
+ }
+}
+
+static int midi_play(struct data *d, void *src, unsigned int n_frames)
+{
+ int res;
+ struct spa_pod_builder b;
+ struct spa_pod_frame f;
+ uint32_t first_frame, last_frame;
+ bool have_data = false;
+
+ spa_zero(b);
+ spa_pod_builder_init(&b, src, n_frames);
+
+ spa_pod_builder_push_sequence(&b, &f, 0);
+
+ first_frame = d->clock_time;
+ last_frame = first_frame + d->position->clock.duration;
+ d->clock_time = last_frame;
+
+ while (1) {
+ uint32_t frame;
+ struct midi_event ev;
+
+ res = midi_file_next_time(d->midi.file, &ev.sec);
+ if (res <= 0) {
+ if (have_data)
+ break;
+ return res;
+ }
+
+ frame = ev.sec * d->position->clock.rate.denom;
+ if (frame < first_frame)
+ frame = 0;
+ else if (frame < last_frame)
+ frame -= first_frame;
+ else
+ break;
+
+ midi_file_read_event(d->midi.file, &ev);
+
+ if (d->verbose)
+ midi_file_dump_event(stdout, &ev);
+
+ if (ev.data[0] == 0xff)
+ continue;
+
+ spa_pod_builder_control(&b, frame, SPA_CONTROL_Midi);
+ spa_pod_builder_bytes(&b, ev.data, ev.size);
+ have_data = true;
+ }
+ spa_pod_builder_pop(&b, &f);
+
+ return b.state.offset;
+}
+
+static int midi_record(struct data *d, void *src, unsigned int n_frames)
+{
+ struct spa_pod *pod;
+ struct spa_pod_control *c;
+ uint32_t frame;
+
+ frame = d->clock_time;
+ d->clock_time += d->position->clock.duration;
+
+ if ((pod = spa_pod_from_data(src, n_frames, 0, n_frames)) == NULL)
+ return 0;
+ if (!spa_pod_is_sequence(pod))
+ return 0;
+
+ SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) {
+ struct midi_event ev;
+
+ if (c->type != SPA_CONTROL_Midi)
+ continue;
+
+ ev.track = 0;
+ ev.sec = (frame + c->offset) / (float) d->position->clock.rate.denom;
+ ev.data = SPA_POD_BODY(&c->value),
+ ev.size = SPA_POD_BODY_SIZE(&c->value);
+
+ if (d->verbose)
+ midi_file_dump_event(stdout, &ev);
+
+ midi_file_write_event(d->midi.file, &ev);
+ }
+ return 0;
+}
+
+static int setup_midifile(struct data *data)
+{
+ if (data->mode == mode_record) {
+ spa_zero(data->midi.info);
+ data->midi.info.format = 0;
+ data->midi.info.ntracks = 1;
+ data->midi.info.division = 0;
+ }
+
+ data->midi.file = midi_file_open(data->filename,
+ data->mode == mode_playback ? "r" : "w",
+ &data->midi.info);
+ if (data->midi.file == NULL) {
+ fprintf(stderr, "midifile: can't read midi file '%s': %m\n", data->filename);
+ return -errno;
+ }
+
+ if (data->verbose)
+ printf("midifile: opened file \"%s\" format %08x ntracks:%d div:%d\n",
+ data->filename,
+ data->midi.info.format, data->midi.info.ntracks,
+ data->midi.info.division);
+
+ data->fill = data->mode == mode_playback ? midi_play : midi_record;
+ data->stride = 1;
+
+ return 0;
+}
+
+struct dsd_layout_info {
+ uint32_t type;
+ struct spa_audio_layout_info info;
+};
+static const struct dsd_layout_info dsd_layouts[] = {
+ { 1, { SPA_AUDIO_LAYOUT_Mono, }, },
+ { 2, { SPA_AUDIO_LAYOUT_Stereo, }, },
+ { 3, { SPA_AUDIO_LAYOUT_2FC }, },
+ { 4, { SPA_AUDIO_LAYOUT_Quad }, },
+ { 5, { SPA_AUDIO_LAYOUT_3_1 }, },
+ { 6, { SPA_AUDIO_LAYOUT_5_0R }, },
+ { 7, { SPA_AUDIO_LAYOUT_5_1R }, },
+};
+
+static int dsf_play(struct data *d, void *src, unsigned int n_frames)
+{
+ return dsf_file_read(d->dsf.file, src, n_frames, &d->dsf.layout);
+}
+
+static int setup_dsffile(struct data *data)
+{
+ if (data->mode == mode_record)
+ return -ENOTSUP;
+
+ data->dsf.file = dsf_file_open(data->filename, "r", &data->dsf.info);
+ if (data->dsf.file == NULL) {
+ fprintf(stderr, "dsffile: can't read dsf file '%s': %m\n", data->filename);
+ return -errno;
+ }
+
+ if (data->verbose)
+ printf("dsffile: opened file \"%s\" channels:%d rate:%d samples:%"PRIu64" bitorder:%s\n",
+ data->filename,
+ data->dsf.info.channels, data->dsf.info.rate,
+ data->dsf.info.samples,
+ data->dsf.info.lsb ? "lsb" : "msb");
+
+ data->fill = dsf_play;
+
+ return 0;
+}
+
+static int stdout_record(struct data *d, void *src, unsigned int n_frames)
+{
+ return fwrite(src, d->stride, n_frames, stdout);
+}
+
+static int stdin_play(struct data *d, void *src, unsigned int n_frames)
+{
+ return fread(src, d->stride, n_frames, stdin);
+}
+
+static int setup_pipe(struct data *data)
+{
+ const struct format_info *info;
+
+ if (data->format == NULL)
+ data->format = DEFAULT_FORMAT;
+ if (data->channels == 0)
+ data->channels = DEFAULT_CHANNELS;
+ if (data->rate == 0)
+ data->rate = DEFAULT_RATE;
+ if (data->channelmap.n_channels == 0)
+ channelmap_default(&data->channelmap, data->channels);
+
+ info = format_info_by_name(data->format);
+ if (info == NULL)
+ return -EINVAL;
+
+ data->spa_format = info->spa_format;
+ data->stride = info->width * data->channels;
+ data->fill = data->mode == mode_playback ? stdin_play : stdout_record;
+
+ if (data->verbose)
+ printf("PIPE: rate=%u channels=%u fmt=%s samplesize=%u stride=%u\n",
+ data->rate, data->channels,
+ info->name, info->width, data->stride);
+
+ return 0;
+}
+
+static int fill_properties(struct data *data)
+{
+ static const char * const table[] = {
+ [SF_STR_TITLE] = PW_KEY_MEDIA_TITLE,
+ [SF_STR_COPYRIGHT] = PW_KEY_MEDIA_COPYRIGHT,
+ [SF_STR_SOFTWARE] = PW_KEY_MEDIA_SOFTWARE,
+ [SF_STR_ARTIST] = PW_KEY_MEDIA_ARTIST,
+ [SF_STR_COMMENT] = PW_KEY_MEDIA_COMMENT,
+ [SF_STR_DATE] = PW_KEY_MEDIA_DATE
+ };
+
+ SF_INFO sfi;
+ SF_FORMAT_INFO fi;
+ int res;
+ unsigned c;
+ const char *s, *t;
+
+ for (c = 0; c < SPA_N_ELEMENTS(table); c++) {
+ if (table[c] == NULL)
+ continue;
+
+ if ((s = sf_get_string(data->file, c)) == NULL ||
+ *s == '\0')
+ continue;
+
+ pw_properties_set(data->props, table[c], s);
+ }
+
+ spa_zero(sfi);
+ if ((res = sf_command(data->file, SFC_GET_CURRENT_SF_INFO, &sfi, sizeof(sfi)))) {
+ pw_log_error("sndfile: %s", sf_error_number(res));
+ return -EIO;
+ }
+
+ spa_zero(fi);
+ fi.format = sfi.format;
+ if (sf_command(data->file, SFC_GET_FORMAT_INFO, &fi, sizeof(fi)) == 0 && fi.name)
+ pw_properties_set(data->props, PW_KEY_MEDIA_FORMAT, fi.name);
+
+ s = pw_properties_get(data->props, PW_KEY_MEDIA_TITLE);
+ t = pw_properties_get(data->props, PW_KEY_MEDIA_ARTIST);
+ if (s && t)
+ pw_properties_setf(data->props, PW_KEY_MEDIA_NAME,
+ "'%s' / '%s'", s, t);
+
+ return 0;
+}
+static void format_from_filename(SF_INFO *info, const char *filename)
+{
+ int i, count = 0;
+ int format = -1;
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+ info->format |= SF_ENDIAN_BIG;
+#else
+ info->format |= SF_ENDIAN_LITTLE;
+#endif
+
+ if (sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(int)) != 0)
+ count = 0;
+
+ for (i = 0; i < count; i++) {
+ SF_FORMAT_INFO fi;
+
+ spa_zero(fi);
+ fi.format = i;
+ if (sf_command(NULL, SFC_GET_FORMAT_MAJOR, &fi, sizeof(fi)) != 0)
+ continue;
+
+ if (spa_strendswith(filename, fi.extension)) {
+ format = fi.format;
+ break;
+ }
+ }
+ if (format == -1)
+ format = SF_FORMAT_WAV;
+ if (format == SF_FORMAT_WAV && info->channels > 2)
+ format = SF_FORMAT_WAVEX;
+
+ info->format |= format;
+
+ if (format == SF_FORMAT_OGG || format == SF_FORMAT_FLAC)
+ info->format = (info->format & ~SF_FORMAT_ENDMASK) | SF_ENDIAN_FILE;
+ if (format == SF_FORMAT_OGG)
+ info->format = (info->format & ~SF_FORMAT_SUBMASK) | SF_FORMAT_VORBIS;
+}
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+static int setup_encodedfile(struct data *data)
+{
+ int ret;
+ int bits_per_sample;
+ int num_channels;
+ char path[256] = { 0 };
+
+ /* We do not support record with encoded media */
+ if (data->mode == mode_record) {
+ return -EINVAL;
+ }
+
+ strcpy(path, "file:");
+ strcat(path, data->filename);
+
+ data->fmt_context = NULL;
+ ret = avformat_open_input(&data->fmt_context, path, NULL, NULL);
+ if (ret < 0) {
+ fprintf(stderr, "Failed to open input\n");
+ return -EINVAL;
+ }
+
+ avformat_find_stream_info (data->fmt_context, NULL);
+
+ data->ctx = avcodec_alloc_context3(NULL);
+ if (!data->ctx) {
+ fprintf(stderr, "Could not allocate audio codec context\n");
+ avformat_close_input(&data->fmt_context);
+ return -EINVAL;
+ }
+
+ // We expect only one stream with audio
+ data->astream = data->fmt_context->streams[0];
+ avcodec_parameters_to_context (data->ctx, data->astream->codecpar);
+
+ if (data->ctx->codec_type != AVMEDIA_TYPE_AUDIO) {
+ fprintf(stderr, "Not an audio file\n");
+ avformat_close_input(&data->fmt_context);
+ return -EINVAL;
+ }
+
+ printf("Number of streams: %d Codec id: %x\n", data->fmt_context->nb_streams,
+ data->ctx->codec_id);
+
+ /* FFmpeg 5.1 (which contains libavcodec 59.37.100) introduced
+ * a new channel layout API and deprecated the old one. */
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 37, 100)
+ num_channels = data->ctx->ch_layout.nb_channels;
+#else
+ num_channels = data->ctx->channels;
+#endif
+
+ data->rate = data->ctx->sample_rate;
+ data->channels = num_channels;
+ data->sfmt = data->ctx->sample_fmt;
+ data->stride = 1; // Don't care
+
+ bits_per_sample = av_get_bits_per_sample(data->ctx->codec_id);
+ data->bitrate = bits_per_sample ?
+ data->ctx->sample_rate * num_channels * bits_per_sample : data->ctx->bit_rate;
+
+ data->spa_format = SPA_AUDIO_FORMAT_ENCODED;
+ data->fill = playback_fill_fn(data->spa_format);
+
+ if (data->verbose)
+ printf("Opened file \"%s\" sample format %08x channels:%d rate:%d bitrate: %d\n",
+ data->filename, data->ctx->sample_fmt, data->channels,
+ data->rate, data->bitrate);
+
+ if (data->fill == NULL) {
+ fprintf(stderr, "Unhandled encoded format %d\n", data->spa_format);
+ avformat_close_input(&data->fmt_context);
+ return -EINVAL;
+ }
+
+ avformat_close_input(&data->fmt_context);
+
+ data->encoded_file = fopen(data->filename, "rb");
+ if (!data->encoded_file) {
+ fprintf(stderr, "Failed to open file\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+#endif
+
+static int setup_sndfile(struct data *data)
+{
+ const struct format_info *fi = NULL;
+ SF_INFO info;
+
+ spa_zero(info);
+ /* for record, you fill in the info first */
+ if (data->mode == mode_record) {
+ if (data->format == NULL)
+ data->format = DEFAULT_FORMAT;
+ if (data->channels == 0)
+ data->channels = DEFAULT_CHANNELS;
+ if (data->rate == 0)
+ data->rate = DEFAULT_RATE;
+ if (data->channelmap.n_channels == 0)
+ channelmap_default(&data->channelmap, data->channels);
+
+ if ((fi = format_info_by_name(data->format)) == NULL) {
+ fprintf(stderr, "error: unknown format \"%s\"\n", data->format);
+ return -EINVAL;
+ }
+ memset(&info, 0, sizeof(info));
+ info.samplerate = data->rate;
+ info.channels = data->channels;
+ info.format = fi->sf_format;
+ format_from_filename(&info, data->filename);
+ }
+
+ data->file = sf_open(data->filename,
+ data->mode == mode_playback ? SFM_READ : SFM_WRITE,
+ &info);
+ if (!data->file) {
+ fprintf(stderr, "sndfile: failed to open audio file \"%s\": %s\n",
+ data->filename, sf_strerror(NULL));
+ return -EIO;
+ }
+
+ if (data->verbose)
+ printf("sndfile: opened file \"%s\" format %08x channels:%d rate:%d\n",
+ data->filename, info.format, info.channels, info.samplerate);
+ if (data->channels > 0 && info.channels != data->channels) {
+ fprintf(stderr, "sndfile: given channels (%u) don't match file channels (%d)\n",
+ data->channels, info.channels);
+ return -EINVAL;
+ }
+
+ data->rate = info.samplerate;
+ data->channels = info.channels;
+
+ if (data->mode == mode_playback) {
+ if (data->channelmap.n_channels == 0) {
+ bool def = false;
+
+ if (sf_command(data->file, SFC_GET_CHANNEL_MAP_INFO,
+ data->channelmap.channels,
+ sizeof(data->channelmap.channels[0]) * data->channels)) {
+ data->channelmap.n_channels = data->channels;
+ if (channelmap_from_sf(&data->channelmap) < 0)
+ data->channelmap.n_channels = 0;
+ }
+ if (data->channelmap.n_channels == 0) {
+ channelmap_default(&data->channelmap, data->channels);
+ def = true;
+ }
+ if (data->verbose) {
+ printf("sndfile: using %s channel map: ", def ? "default" : "file");
+ channelmap_print(&data->channelmap);
+ printf("\n");
+ }
+ }
+ fill_properties(data);
+
+ /* try native format first, else decode to float */
+ if ((fi = format_info_by_sf_format(info.format)) == NULL)
+ fi = format_info_by_sf_format(SF_FORMAT_FLOAT);
+
+ }
+ if (fi == NULL)
+ return -EIO;
+
+ if (data->verbose)
+ printf("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);
+
+ data->spa_format = fi->spa_format;
+ data->stride = fi->width * data->channels;
+ data->fill = data->mode == mode_playback ?
+ playback_fill_fn(data->spa_format) :
+ record_fill_fn(data->spa_format);
+
+ if (data->fill == NULL) {
+ fprintf(stderr, "PCM: unhandled format %d\n", data->spa_format);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int setup_properties(struct data *data)
+{
+ const char *s;
+ unsigned int nom = 0;
+
+ if (data->quality >= 0)
+ pw_properties_setf(data->props, "resample.quality", "%d", data->quality);
+
+ if (data->rate)
+ pw_properties_setf(data->props, PW_KEY_NODE_RATE, "1/%u", data->rate);
+
+ data->latency_unit = unit_none;
+
+ s = data->latency;
+ while (*s && isdigit(*s))
+ s++;
+ if (!*s)
+ data->latency_unit = unit_samples;
+ else if (spa_streq(s, "none"))
+ data->latency_unit = unit_none;
+ else if (spa_streq(s, "s") || spa_streq(s, "sec") || spa_streq(s, "secs"))
+ data->latency_unit = unit_sec;
+ else if (spa_streq(s, "ms") || spa_streq(s, "msec") || spa_streq(s, "msecs"))
+ data->latency_unit = unit_msec;
+ else if (spa_streq(s, "us") || spa_streq(s, "usec") || spa_streq(s, "usecs"))
+ data->latency_unit = unit_usec;
+ else if (spa_streq(s, "ns") || spa_streq(s, "nsec") || spa_streq(s, "nsecs"))
+ data->latency_unit = unit_nsec;
+ else {
+ fprintf(stderr, "error: bad latency value %s (bad unit)\n", data->latency);
+ return -EINVAL;
+ }
+ data->latency_value = atoi(data->latency);
+ if (!data->latency_value && data->latency_unit != unit_none) {
+ fprintf(stderr, "error: bad latency value %s (is zero)\n", data->latency);
+ return -EINVAL;
+ }
+
+ switch (data->latency_unit) {
+ case unit_sec:
+ nom = data->latency_value * data->rate;
+ break;
+ case unit_msec:
+ nom = nearbyint((data->latency_value * data->rate) / 1000.0);
+ break;
+ case unit_usec:
+ nom = nearbyint((data->latency_value * data->rate) / 1000000.0);
+ break;
+ case unit_nsec:
+ nom = nearbyint((data->latency_value * data->rate) / 1000000000.0);
+ break;
+ case unit_samples:
+ nom = data->latency_value;
+ break;
+ default:
+ nom = 0;
+ break;
+ }
+
+ if (data->verbose)
+ printf("rate:%d latency:%u (%.3fs)\n",
+ data->rate, nom, data->rate ? (double)nom/data->rate : 0.0f);
+ if (nom)
+ pw_properties_setf(data->props, PW_KEY_NODE_LATENCY, "%u/%u", nom, data->rate);
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ struct pw_loop *l;
+ const struct spa_pod *params[1];
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const char *prog;
+ int exit_code = EXIT_FAILURE, c, ret;
+ enum pw_stream_flags flags = 0;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ flags |= PW_STREAM_FLAG_AUTOCONNECT;
+
+ prog = argv[0];
+ if ((prog = strrchr(argv[0], '/')) != NULL)
+ prog++;
+ else
+ prog = argv[0];
+
+ /* prime the mode from the program name */
+ if (spa_streq(prog, "pw-play")) {
+ data.mode = mode_playback;
+ data.data_type = TYPE_PCM;
+ } else if (spa_streq(prog, "pw-record")) {
+ data.mode = mode_record;
+ data.data_type = TYPE_PCM;
+ } else if (spa_streq(prog, "pw-midiplay")) {
+ data.mode = mode_playback;
+ data.data_type = TYPE_MIDI;
+ } else if (spa_streq(prog, "pw-midirecord")) {
+ data.mode = mode_record;
+ data.data_type = TYPE_MIDI;
+ } else if (spa_streq(prog, "pw-dsdplay")) {
+ data.mode = mode_playback;
+ data.data_type = TYPE_DSD;
+ } else
+ data.mode = mode_none;
+
+ /* negative means no volume adjustment */
+ data.volume = -1.0;
+ data.quality = -1;
+ data.props = pw_properties_new(
+ PW_KEY_APP_NAME, prog,
+ PW_KEY_NODE_NAME, prog,
+ NULL);
+
+ if (data.props == NULL) {
+ fprintf(stderr, "error: pw_properties_new() failed: %m\n");
+ goto error_no_props;
+ }
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ while ((c = getopt_long(argc, argv, "hvprmdoR:q:P:", long_options, NULL)) != -1) {
+#else
+ while ((c = getopt_long(argc, argv, "hvprmdR:q:P:", long_options, NULL)) != -1) {
+#endif
+
+ switch (c) {
+
+ case 'h':
+ show_usage(prog, false);
+ return EXIT_SUCCESS;
+
+ case OPT_VERSION:
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ prog,
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+
+ case 'v':
+ data.verbose = true;
+ break;
+
+ case 'p':
+ data.mode = mode_playback;
+ break;
+
+ case 'r':
+ data.mode = mode_record;
+ break;
+
+ case 'm':
+ data.data_type = TYPE_MIDI;
+ break;
+
+ case 'd':
+ data.data_type = TYPE_DSD;
+ break;
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ case 'o':
+ data.data_type = TYPE_ENCODED;
+ break;
+#endif
+
+ case 'R':
+ data.remote_name = optarg;
+ break;
+
+ case 'q':
+ data.quality = atoi(optarg);
+ break;
+
+ case OPT_MEDIA_TYPE:
+ data.media_type = optarg;
+ break;
+
+ case OPT_MEDIA_CATEGORY:
+ data.media_category = optarg;
+ break;
+
+ case OPT_MEDIA_ROLE:
+ data.media_role = optarg;
+ break;
+
+ case 'P':
+ pw_properties_update_string(data.props, optarg, strlen(optarg));
+ break;
+
+ case OPT_TARGET:
+ data.target = optarg;
+ if (spa_streq(data.target, "0")) {
+ data.target = NULL;
+ flags &= ~PW_STREAM_FLAG_AUTOCONNECT;
+ }
+ break;
+
+ case OPT_LATENCY:
+ data.latency = optarg;
+ break;
+
+ case OPT_RATE:
+ ret = atoi(optarg);
+ if (ret <= 0) {
+ fprintf(stderr, "error: bad rate %d\n", ret);
+ goto error_usage;
+ }
+ data.rate = (unsigned int)ret;
+ break;
+
+ case OPT_CHANNELS:
+ ret = atoi(optarg);
+ if (ret <= 0) {
+ fprintf(stderr, "error: bad channels %d\n", ret);
+ goto error_usage;
+ }
+ data.channels = (unsigned int)ret;
+ break;
+
+ case OPT_CHANNELMAP:
+ data.channel_map = optarg;
+ break;
+
+ case OPT_FORMAT:
+ data.format = optarg;
+ break;
+
+ case OPT_VOLUME:
+ data.volume = atof(optarg);
+ break;
+ default:
+ goto error_usage;
+ }
+ }
+
+ if (data.mode == mode_none) {
+ fprintf(stderr, "error: one of the playback/record options must be provided\n");
+ goto error_usage;
+ }
+
+ if (!data.media_type) {
+ switch (data.data_type) {
+ case TYPE_MIDI:
+ data.media_type = DEFAULT_MIDI_MEDIA_TYPE;
+ break;
+ default:
+ data.media_type = DEFAULT_MEDIA_TYPE;
+ break;
+ }
+ }
+ if (!data.media_category)
+ data.media_category = data.mode == mode_playback ?
+ DEFAULT_MEDIA_CATEGORY_PLAYBACK :
+ DEFAULT_MEDIA_CATEGORY_RECORD;
+ if (!data.media_role)
+ data.media_role = DEFAULT_MEDIA_ROLE;
+
+ if (!data.latency)
+ data.latency = data.mode == mode_playback ?
+ DEFAULT_LATENCY_PLAY :
+ DEFAULT_LATENCY_REC;
+ if (data.channel_map != NULL) {
+ if (parse_channelmap(data.channel_map, &data.channelmap) < 0) {
+ fprintf(stderr, "error: can parse channel-map \"%s\"\n", data.channel_map);
+ goto error_usage;
+
+ } else {
+ if (data.channels > 0 && data.channelmap.n_channels != data.channels) {
+ fprintf(stderr, "error: channels and channel-map incompatible\n");
+ goto error_usage;
+ }
+ data.channels = data.channelmap.n_channels;
+ }
+ }
+ if (data.volume < 0)
+ data.volume = DEFAULT_VOLUME;
+
+ if (optind >= argc) {
+ fprintf(stderr, "error: filename or - argument missing\n");
+ goto error_usage;
+ }
+ data.filename = argv[optind++];
+
+ pw_properties_set(data.props, PW_KEY_MEDIA_TYPE, data.media_type);
+ pw_properties_set(data.props, PW_KEY_MEDIA_CATEGORY, data.media_category);
+ pw_properties_set(data.props, PW_KEY_MEDIA_ROLE, data.media_role);
+ pw_properties_set(data.props, PW_KEY_MEDIA_FILENAME, data.filename);
+ pw_properties_set(data.props, PW_KEY_MEDIA_NAME, data.filename);
+ pw_properties_set(data.props, PW_KEY_TARGET_OBJECT, data.target);
+
+ /* make a main loop. If you already have another main loop, you can add
+ * the fd of this pipewire mainloop to it. */
+ data.loop = pw_main_loop_new(NULL);
+ if (!data.loop) {
+ fprintf(stderr, "error: pw_main_loop_new() failed: %m\n");
+ goto error_no_main_loop;
+ }
+
+ 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,
+ pw_properties_new(
+ PW_KEY_CONFIG_NAME, "client-rt.conf",
+ NULL),
+ 0);
+ if (!data.context) {
+ fprintf(stderr, "error: pw_context_new() failed: %m\n");
+ goto error_no_context;
+ }
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, data.remote_name,
+ NULL),
+ 0);
+ if (!data.core) {
+ fprintf(stderr, "error: pw_context_connect() failed: %m\n");
+ goto error_ctx_connect_failed;
+ }
+ pw_core_add_listener(data.core, &data.core_listener, &core_events, &data);
+
+ if (spa_streq(data.filename, "-")) {
+ ret = setup_pipe(&data);
+ } else {
+ switch (data.data_type) {
+ case TYPE_PCM:
+ ret = setup_sndfile(&data);
+ break;
+ case TYPE_MIDI:
+ ret = setup_midifile(&data);
+ break;
+ case TYPE_DSD:
+ ret = setup_dsffile(&data);
+ break;
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ case TYPE_ENCODED:
+ ret = setup_encodedfile(&data);
+ break;
+#endif
+ default:
+ ret = -ENOTSUP;
+ break;
+ }
+ }
+ if (ret < 0) {
+ fprintf(stderr, "error: open failed: %s\n", spa_strerror(ret));
+ switch (ret) {
+ case -EIO:
+ goto error_bad_file;
+ case -EINVAL:
+ default:
+ goto error_usage;
+ }
+ }
+ ret = setup_properties(&data);
+
+ switch (data.data_type) {
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ case TYPE_ENCODED:
+ {
+ struct spa_audio_info info;
+
+ spa_zero(info);
+ info.media_type = SPA_MEDIA_TYPE_audio;
+
+ ret = avcodec_ctx_to_info(&data, data.ctx, &info);
+ if (ret < 0) {
+ if (data.encoded_file) {
+ fclose(data.encoded_file);
+ }
+ goto error_bad_file;
+ }
+ params[0] = spa_format_audio_build(&b, SPA_PARAM_EnumFormat, &info);
+ break;
+ }
+#endif
+ case TYPE_PCM:
+ {
+ struct spa_audio_info_raw info;
+ info = SPA_AUDIO_INFO_RAW_INIT(
+ .flags = data.channelmap.n_channels ? 0 : SPA_AUDIO_FLAG_UNPOSITIONED,
+ .format = data.spa_format,
+ .rate = data.rate,
+ .channels = data.channels);
+
+ if (data.channelmap.n_channels)
+ memcpy(info.position, data.channelmap.channels, data.channels * sizeof(int));
+
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info);
+ break;
+ }
+ case TYPE_MIDI:
+ params[0] = spa_pod_builder_add_object(&b,
+ SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
+ SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_application),
+ SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_control));
+
+ pw_properties_set(data.props, PW_KEY_FORMAT_DSP, "8 bit raw midi");
+ break;
+ case TYPE_DSD:
+ {
+ struct spa_audio_info_dsd info;
+
+ spa_zero(info);
+ info.channels = data.dsf.info.channels;
+ info.rate = data.dsf.info.rate / 8;
+
+ SPA_FOR_EACH_ELEMENT_VAR(dsd_layouts, i) {
+ if (i->type != data.dsf.info.channel_type)
+ continue;
+ info.channels = i->info.n_channels;
+ memcpy(info.position, i->info.position,
+ info.channels * sizeof(uint32_t));
+ }
+ params[0] = spa_format_audio_dsd_build(&b, SPA_PARAM_EnumFormat, &info);
+ break;
+ }
+ }
+
+ data.stream = pw_stream_new(data.core, prog, data.props);
+ data.props = NULL;
+
+ if (data.stream == NULL) {
+ fprintf(stderr, "error: failed to create stream: %m\n");
+ goto error_no_stream;
+ }
+ pw_stream_add_listener(data.stream, &data.stream_listener, &stream_events, &data);
+
+ if (data.verbose)
+ printf("connecting %s stream; target=%s\n",
+ data.mode == mode_playback ? "playback" : "record",
+ data.target);
+
+ if (data.verbose)
+ data.timer = pw_loop_add_timer(l, do_print_delay, &data);
+
+ ret = pw_stream_connect(data.stream,
+ data.mode == mode_playback ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT,
+ PW_ID_ANY,
+ flags |
+ PW_STREAM_FLAG_MAP_BUFFERS,
+ params, 1);
+ if (ret < 0) {
+ fprintf(stderr, "error: failed connect: %s\n", spa_strerror(ret));
+ goto error_connect_fail;
+ }
+
+ if (data.verbose) {
+ const struct pw_properties *props;
+ void *pstate;
+ const char *key, *val;
+
+ if ((props = pw_stream_get_properties(data.stream)) != NULL) {
+ printf("stream properties:\n");
+ pstate = NULL;
+ while ((key = pw_properties_iterate(props, &pstate)) != NULL &&
+ (val = pw_properties_get(props, key)) != NULL) {
+ printf("\t%s = \"%s\"\n", key, val);
+ }
+ }
+ }
+
+ /* and wait while we let things run */
+ pw_main_loop_run(data.loop);
+
+#ifdef HAVE_PW_CAT_FFMPEG_INTEGRATION
+ if (data.encoded_file)
+ fclose(data.encoded_file);
+#endif
+
+ /* we're returning OK only if got to the point to drain */
+ if (data.drained)
+ exit_code = EXIT_SUCCESS;
+
+error_connect_fail:
+ if (data.stream) {
+ spa_hook_remove(&data.stream_listener);
+ pw_stream_destroy(data.stream);
+ }
+error_no_stream:
+error_bad_file:
+ spa_hook_remove(&data.core_listener);
+ pw_core_disconnect(data.core);
+error_ctx_connect_failed:
+ pw_context_destroy(data.context);
+error_no_context:
+ pw_main_loop_destroy(data.loop);
+error_no_props:
+error_no_main_loop:
+ pw_properties_free(data.props);
+ if (data.file)
+ sf_close(data.file);
+ if (data.midi.file)
+ midi_file_close(data.midi.file);
+ pw_deinit();
+ return exit_code;
+
+error_usage:
+ show_usage(prog, true);
+ return EXIT_FAILURE;
+}
diff --git a/src/tools/pw-cli.c b/src/tools/pw-cli.c
new file mode 100644
index 0000000..53a3384
--- /dev/null
+++ b/src/tools/pw-cli.c
@@ -0,0 +1,2375 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <unistd.h>
+#include <errno.h>
+#include <stdio.h>
+#include <signal.h>
+#include <string.h>
+#include <ctype.h>
+#if !defined(__FreeBSD__) && !defined(__MidnightBSD__)
+#include <alloca.h>
+#endif
+#include <getopt.h>
+#include <fnmatch.h>
+#ifdef HAVE_READLINE
+#include <readline/readline.h>
+#include <readline/history.h>
+#endif
+#include <locale.h>
+
+#if !defined(FNM_EXTMATCH)
+#define FNM_EXTMATCH 0
+#endif
+
+#define spa_debug(fmt,...) printf(fmt"\n", ## __VA_ARGS__)
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/debug/pod.h>
+#include <spa/utils/keys.h>
+#include <spa/utils/json-pod.h>
+#include <spa/pod/builder.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/i18n.h>
+
+#include <pipewire/extensions/session-manager.h>
+
+static const char WHITESPACE[] = " \t";
+static char prompt[64];
+
+struct remote_data;
+struct proxy_data;
+
+typedef void (*info_func_t) (struct proxy_data *pd);
+
+struct class {
+ const char *type;
+ uint32_t version;
+ const void *events;
+ pw_destroy_t destroy;
+ info_func_t info;
+ const char *name_key;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct spa_list remotes;
+ struct remote_data *current;
+
+ struct pw_map vars;
+ unsigned int interactive:1;
+ unsigned int monitoring:1;
+ unsigned int quit:1;
+};
+
+struct global {
+ struct remote_data *rd;
+ uint32_t id;
+ uint32_t permissions;
+ uint32_t version;
+ char *type;
+ const struct class *class;
+ struct pw_proxy *proxy;
+ bool info_pending;
+ struct pw_properties *properties;
+};
+
+struct remote_data {
+ struct spa_list link;
+ struct data *data;
+
+ char *name;
+ uint32_t id;
+
+ int prompt_pending;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ struct spa_hook proxy_core_listener;
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_map globals;
+};
+
+
+struct proxy_data {
+ struct remote_data *rd;
+ struct global *global;
+ struct pw_proxy *proxy;
+ void *info;
+ const struct class *class;
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+};
+
+struct command {
+ const char *name;
+ const char *alias;
+ const char *description;
+ bool (*func) (struct data *data, const char *cmd, char *args, char **error);
+};
+
+static struct spa_dict * global_props(struct global *global);
+static struct global * obj_global(struct remote_data *rd, uint32_t id);
+static int children_of(struct remote_data *rd, uint32_t parent_id,
+ const char *child_type, uint32_t **children);
+
+static void print_properties(struct spa_dict *props, char mark, bool header)
+{
+ const struct spa_dict_item *item;
+
+ if (header)
+ printf("%c\tproperties:\n", mark);
+ if (props == NULL || props->n_items == 0) {
+ if (header)
+ printf("\t\tnone\n");
+ return;
+ }
+
+ spa_dict_for_each(item, props) {
+ printf("%c\t\t%s = \"%s\"\n", mark, item->key, item->value);
+ }
+}
+
+static void print_params(struct spa_param_info *params, uint32_t n_params, char mark, bool header)
+{
+ uint32_t i;
+
+ if (header)
+ printf("%c\tparams: (%u)\n", mark, n_params);
+ if (params == NULL || n_params == 0) {
+ if (header)
+ printf("\t\tnone\n");
+ return;
+ }
+ for (i = 0; i < n_params; i++) {
+ const struct spa_type_info *type_info = spa_type_param;
+
+ printf("%c\t %d (%s) %c%c\n",
+ params[i].user > 0 ? mark : ' ', params[i].id,
+ spa_debug_type_find_name(type_info, params[i].id),
+ params[i].flags & SPA_PARAM_INFO_READ ? 'r' : '-',
+ params[i].flags & SPA_PARAM_INFO_WRITE ? 'w' : '-');
+ params[i].user = 0;
+ }
+}
+
+static bool do_not_implemented(struct data *data, const char *cmd, char *args, char **error)
+{
+ *error = spa_aprintf("Command \"%s\" not yet implemented", cmd);
+ return false;
+}
+
+static bool do_help(struct data *data, const char *cmd, char *args, char **error);
+static bool do_load_module(struct data *data, const char *cmd, char *args, char **error);
+static bool do_list_objects(struct data *data, const char *cmd, char *args, char **error);
+static bool do_connect(struct data *data, const char *cmd, char *args, char **error);
+static bool do_disconnect(struct data *data, const char *cmd, char *args, char **error);
+static bool do_list_remotes(struct data *data, const char *cmd, char *args, char **error);
+static bool do_switch_remote(struct data *data, const char *cmd, char *args, char **error);
+static bool do_info(struct data *data, const char *cmd, char *args, char **error);
+static bool do_create_device(struct data *data, const char *cmd, char *args, char **error);
+static bool do_create_node(struct data *data, const char *cmd, char *args, char **error);
+static bool do_destroy(struct data *data, const char *cmd, char *args, char **error);
+static bool do_create_link(struct data *data, const char *cmd, char *args, char **error);
+static bool do_export_node(struct data *data, const char *cmd, char *args, char **error);
+static bool do_enum_params(struct data *data, const char *cmd, char *args, char **error);
+static bool do_set_param(struct data *data, const char *cmd, char *args, char **error);
+static bool do_permissions(struct data *data, const char *cmd, char *args, char **error);
+static bool do_get_permissions(struct data *data, const char *cmd, char *args, char **error);
+static bool do_send_command(struct data *data, const char *cmd, char *args, char **error);
+static bool do_quit(struct data *data, const char *cmd, char *args, char **error);
+
+#define DUMP_NAMES "Core|Module|Device|Node|Port|Factory|Client|Link|Session|Endpoint|EndpointStream"
+
+static const struct command command_list[] = {
+ { "help", "h", "Show this help", do_help },
+ { "load-module", "lm", "Load a module. <module-name> [<module-arguments>]", do_load_module },
+ { "unload-module", "um", "Unload a module. <module-var>", do_not_implemented },
+ { "connect", "con", "Connect to a remote. [<remote-name>]", do_connect },
+ { "disconnect", "dis", "Disconnect from a remote. [<remote-var>]", do_disconnect },
+ { "list-remotes", "lr", "List connected remotes.", do_list_remotes },
+ { "switch-remote", "sr", "Switch between current remotes. [<remote-var>]", do_switch_remote },
+ { "list-objects", "ls", "List objects or current remote. [<interface>]", do_list_objects },
+ { "info", "i", "Get info about an object. <object-id>|all", do_info },
+ { "create-device", "cd", "Create a device from a factory. <factory-name> [<properties>]", do_create_device },
+ { "create-node", "cn", "Create a node from a factory. <factory-name> [<properties>]", do_create_node },
+ { "destroy", "d", "Destroy a global object. <object-id>", do_destroy },
+ { "create-link", "cl", "Create a link between nodes. <node-id> <port-id> <node-id> <port-id> [<properties>]", do_create_link },
+ { "export-node", "en", "Export a local node to the current remote. <node-id> [remote-var]", do_export_node },
+ { "enum-params", "e", "Enumerate params of an object <object-id> <param-id>", do_enum_params },
+ { "set-param", "s", "Set param of an object <object-id> <param-id> <param-json>", do_set_param },
+ { "permissions", "sp", "Set permissions for a client <client-id> <object> <permission>", do_permissions },
+ { "get-permissions", "gp", "Get permissions of a client <client-id>", do_get_permissions },
+ { "send-command", "c", "Send a command <object-id>", do_send_command },
+ { "quit", "q", "Quit", do_quit },
+};
+
+static bool do_quit(struct data *data, const char *cmd, char *args, char **error)
+{
+ pw_main_loop_quit(data->loop);
+ data->quit = true;
+ return true;
+}
+
+static bool do_help(struct data *data, const char *cmd, char *args, char **error)
+{
+ printf("Available commands:\n");
+ SPA_FOR_EACH_ELEMENT_VAR(command_list, c) {
+ char cmd[256];
+ snprintf(cmd, sizeof(cmd), "%s | %s", c->name, c->alias);
+ printf("\t%-20.20s\t%s\n", cmd, c->description);
+ }
+ return true;
+}
+
+static bool do_load_module(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct pw_impl_module *module;
+ char *a[2];
+ int n;
+ uint32_t id;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <module-name> [<module-arguments>]", cmd);
+ return false;
+ }
+
+ module = pw_context_load_module(data->context, a[0], n == 2 ? a[1] : NULL, NULL);
+ if (module == NULL) {
+ *error = spa_aprintf("Could not load module");
+ return false;
+ }
+
+ id = pw_map_insert_new(&data->vars, module);
+ if (data->interactive)
+ printf("%d = @module:%d\n", id, pw_global_get_id(pw_impl_module_get_global(module)));
+
+ return true;
+}
+
+static void on_core_info(void *_data, const struct pw_core_info *info)
+{
+ struct remote_data *rd = _data;
+ free(rd->name);
+ rd->name = info->name ? strdup(info->name) : NULL;
+ if (rd->data->interactive)
+ printf("remote %d is named '%s'\n", rd->id, rd->name);
+}
+
+static void set_prompt(struct remote_data *rd)
+{
+ snprintf(prompt, sizeof(prompt), "%s>> ", rd->name);
+#ifdef HAVE_READLINE
+ rl_set_prompt(prompt);
+#else
+ printf("%s", prompt);
+ fflush(stdout);
+#endif
+}
+
+static void on_core_done(void *_data, uint32_t id, int seq)
+{
+ struct remote_data *rd = _data;
+ struct data *d = rd->data;
+
+ if (seq == rd->prompt_pending) {
+ if (d->interactive) {
+ set_prompt(rd);
+ rd->data->monitoring = true;
+ } else {
+ pw_main_loop_quit(d->loop);
+ }
+ }
+}
+
+static bool global_matches(struct global *g, const char *pattern)
+{
+ const char *str;
+
+ if (g->properties == NULL)
+ return false;
+
+ if (strstr(g->type, pattern) != NULL)
+ return true;
+ if ((str = pw_properties_get(g->properties, PW_KEY_OBJECT_PATH)) != NULL &&
+ fnmatch(pattern, str, FNM_EXTMATCH) == 0)
+ return true;
+ if ((str = pw_properties_get(g->properties, PW_KEY_OBJECT_SERIAL)) != NULL &&
+ spa_streq(pattern, str))
+ return true;
+ if (g->class != NULL && g->class->name_key != NULL &&
+ (str = pw_properties_get(g->properties, g->class->name_key)) != NULL &&
+ fnmatch(pattern, str, FNM_EXTMATCH) == 0)
+ return true;
+
+ return false;
+}
+
+static int print_global(void *obj, void *data)
+{
+ struct global *global = obj;
+ const char *filter = data;
+
+ if (global == NULL)
+ return 0;
+
+ if (filter && !global_matches(global, filter))
+ return 0;
+
+ printf("\tid %d, type %s/%d\n", global->id,
+ global->type, global->version);
+ if (global->properties)
+ print_properties(&global->properties->dict, ' ', false);
+
+ return 0;
+}
+
+
+static bool bind_global(struct remote_data *rd, struct global *global, char **error);
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct remote_data *rd = data;
+ struct global *global;
+ size_t size;
+ char *error;
+ bool ret;
+
+ global = calloc(1, sizeof(struct global));
+ global->rd = rd;
+ global->id = id;
+ global->permissions = permissions;
+ global->type = strdup(type);
+ global->version = version;
+ global->properties = props ? pw_properties_new_dict(props) : NULL;
+
+ if (rd->data->monitoring) {
+ printf("remote %d added global: ", rd->id);
+ print_global(global, NULL);
+ }
+
+ size = pw_map_get_size(&rd->globals);
+ while (id > size)
+ pw_map_insert_at(&rd->globals, size++, NULL);
+ pw_map_insert_at(&rd->globals, id, global);
+
+ /* immediately bind the object always */
+ ret = bind_global(rd, global, &error);
+ if (!ret) {
+ if (rd->data->interactive)
+ fprintf(stderr, "Error: \"%s\"\n", error);
+ free(error);
+ }
+}
+
+static int destroy_global(void *obj, void *data)
+{
+ struct global *global = obj;
+
+ if (global == NULL)
+ return 0;
+
+ if (global->proxy)
+ pw_proxy_destroy(global->proxy);
+ pw_map_insert_at(&global->rd->globals, global->id, NULL);
+ pw_properties_free(global->properties);
+ free(global->type);
+ free(global);
+ return 0;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct remote_data *rd = data;
+ struct global *global;
+
+ global = pw_map_lookup(&rd->globals, id);
+ if (global == NULL) {
+ fprintf(stderr, "remote %d removed unknown global %d\n", rd->id, id);
+ return;
+ }
+
+ if (rd->data->monitoring) {
+ printf("remote %d removed global: ", rd->id);
+ print_global(global, NULL);
+ }
+
+ destroy_global(global, rd);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static struct global *find_global(struct remote_data *rd, const char *pattern)
+{
+ uint32_t id;
+ union pw_map_item *item;
+
+ if (spa_atou32(pattern, &id, 0))
+ return pw_map_lookup(&rd->globals, id);
+
+ pw_array_for_each(item, &rd->globals.items) {
+ struct global *g = item->data;
+ if (pw_map_item_is_free(item) || g == NULL)
+ continue;
+ if (global_matches(g, pattern))
+ return g;
+ }
+ return NULL;
+}
+
+static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message)
+{
+ struct remote_data *rd = _data;
+ struct data *data = rd->data;
+
+ pw_log_error("remote %p: error id:%u seq:%d res:%d (%s): %s", rd,
+ id, seq, res, spa_strerror(res), message);
+
+ if (id == PW_ID_CORE && res == -EPIPE)
+ pw_main_loop_quit(data->loop);
+}
+
+static const struct pw_core_events remote_core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .info = on_core_info,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void on_core_destroy(void *_data)
+{
+ struct remote_data *rd = _data;
+ struct data *data = rd->data;
+
+ spa_list_remove(&rd->link);
+
+ spa_hook_remove(&rd->core_listener);
+ spa_hook_remove(&rd->proxy_core_listener);
+
+ pw_map_remove(&data->vars, rd->id);
+ pw_map_for_each(&rd->globals, destroy_global, rd);
+ pw_map_clear(&rd->globals);
+
+ if (data->current == rd)
+ data->current = NULL;
+ free(rd->name);
+}
+
+static const struct pw_proxy_events proxy_core_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .destroy = on_core_destroy,
+};
+
+static void remote_data_free(struct remote_data *rd)
+{
+ spa_hook_remove(&rd->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)rd->registry);
+ pw_core_disconnect(rd->core);
+}
+
+static bool do_connect(struct data *data, const char *cmd, char *args, char **error)
+{
+ char *a[1];
+ int n;
+ struct pw_properties *props = NULL;
+ struct pw_core *core;
+ struct remote_data *rd;
+
+ n = args ? pw_split_ip(args, WHITESPACE, 1, a) : 0;
+ if (n == 1) {
+ props = pw_properties_new(PW_KEY_REMOTE_NAME, a[0], NULL);
+ }
+ core = pw_context_connect(data->context, props, sizeof(struct remote_data));
+ if (core == NULL) {
+ *error = spa_aprintf("failed to connect: %m");
+ return false;
+ }
+
+ rd = pw_proxy_get_user_data((struct pw_proxy*)core);
+ rd->core = core;
+ rd->data = data;
+ pw_map_init(&rd->globals, 64, 16);
+ rd->id = pw_map_insert_new(&data->vars, rd);
+ spa_list_append(&data->remotes, &rd->link);
+
+ if (rd->data->interactive)
+ printf("%d = @remote:%p\n", rd->id, rd->core);
+
+ data->current = rd;
+
+ pw_core_add_listener(rd->core,
+ &rd->core_listener,
+ &remote_core_events, rd);
+ pw_proxy_add_listener((struct pw_proxy*)rd->core,
+ &rd->proxy_core_listener,
+ &proxy_core_events, rd);
+ rd->registry = pw_core_get_registry(rd->core, PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(rd->registry,
+ &rd->registry_listener,
+ &registry_events, rd);
+ rd->prompt_pending = pw_core_sync(rd->core, 0, 0);
+
+ return true;
+}
+
+static bool do_disconnect(struct data *data, const char *cmd, char *args, char **error)
+{
+ char *a[1];
+ int n;
+ uint32_t idx;
+ struct remote_data *rd = data->current;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n >= 1) {
+ idx = atoi(a[0]);
+ rd = pw_map_lookup(&data->vars, idx);
+ if (rd == NULL)
+ goto no_remote;
+
+ }
+ if (rd)
+ remote_data_free(rd);
+
+ if (data->current == NULL) {
+ if (spa_list_is_empty(&data->remotes)) {
+ return true;
+ }
+ data->current = spa_list_last(&data->remotes, struct remote_data, link);
+ }
+
+ return true;
+
+ no_remote:
+ *error = spa_aprintf("Remote %d does not exist", idx);
+ return false;
+}
+
+static bool do_list_remotes(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd;
+
+ spa_list_for_each(rd, &data->remotes, link)
+ printf("\t%d = @remote:%p '%s'\n", rd->id, rd->core, rd->name);
+
+ return true;
+}
+
+static bool do_switch_remote(struct data *data, const char *cmd, char *args, char **error)
+{
+ char *a[1];
+ int n, idx = 0;
+ struct remote_data *rd;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n == 1)
+ idx = atoi(a[0]);
+
+ rd = pw_map_lookup(&data->vars, idx);
+ if (rd == NULL)
+ goto no_remote;
+
+ spa_list_remove(&rd->link);
+ spa_list_append(&data->remotes, &rd->link);
+ data->current = rd;
+
+ return true;
+
+ no_remote:
+ *error = spa_aprintf("Remote %d does not exist", idx);
+ return false;
+}
+
+#define MARK_CHANGE(f) ((((info)->change_mask & (f))) ? '*' : ' ')
+
+static void info_global(struct proxy_data *pd)
+{
+ struct global *global = pd->global;
+
+ if (global == NULL)
+ return;
+
+ printf("\tid: %d\n", global->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(global->permissions));
+ printf("\ttype: %s/%d\n", global->type, global->version);
+}
+
+static void info_core(struct proxy_data *pd)
+{
+ struct pw_core_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tcookie: %u\n", info->cookie);
+ printf("\tuser-name: \"%s\"\n", info->user_name);
+ printf("\thost-name: \"%s\"\n", info->host_name);
+ printf("\tversion: \"%s\"\n", info->version);
+ printf("\tname: \"%s\"\n", info->name);
+ print_properties(info->props, MARK_CHANGE(PW_CORE_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_module(struct proxy_data *pd)
+{
+ struct pw_module_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tname: \"%s\"\n", info->name);
+ printf("\tfilename: \"%s\"\n", info->filename);
+ printf("\targs: \"%s\"\n", info->args);
+ print_properties(info->props, MARK_CHANGE(PW_MODULE_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_node(struct proxy_data *pd)
+{
+ struct pw_node_info *info = pd->info;
+
+ info_global(pd);
+ printf("%c\tinput ports: %u/%u\n", MARK_CHANGE(PW_NODE_CHANGE_MASK_INPUT_PORTS),
+ info->n_input_ports, info->max_input_ports);
+ printf("%c\toutput ports: %u/%u\n", MARK_CHANGE(PW_NODE_CHANGE_MASK_OUTPUT_PORTS),
+ info->n_output_ports, info->max_output_ports);
+ printf("%c\tstate: \"%s\"", MARK_CHANGE(PW_NODE_CHANGE_MASK_STATE),
+ pw_node_state_as_string(info->state));
+ if (info->state == PW_NODE_STATE_ERROR && info->error)
+ printf(" \"%s\"\n", info->error);
+ else
+ printf("\n");
+ print_properties(info->props, MARK_CHANGE(PW_NODE_CHANGE_MASK_PROPS), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(PW_NODE_CHANGE_MASK_PARAMS), true);
+ info->change_mask = 0;
+}
+
+static void info_port(struct proxy_data *pd)
+{
+ struct pw_port_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tdirection: \"%s\"\n", pw_direction_as_string(info->direction));
+ print_properties(info->props, MARK_CHANGE(PW_PORT_CHANGE_MASK_PROPS), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(PW_PORT_CHANGE_MASK_PARAMS), true);
+ info->change_mask = 0;
+}
+
+static void info_factory(struct proxy_data *pd)
+{
+ struct pw_factory_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tname: \"%s\"\n", info->name);
+ printf("\tobject-type: %s/%d\n", info->type, info->version);
+ print_properties(info->props, MARK_CHANGE(PW_FACTORY_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_client(struct proxy_data *pd)
+{
+ struct pw_client_info *info = pd->info;
+
+ info_global(pd);
+ print_properties(info->props, MARK_CHANGE(PW_CLIENT_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_link(struct proxy_data *pd)
+{
+ struct pw_link_info *info = pd->info;
+
+ info_global(pd);
+ printf("\toutput-node-id: %u\n", info->output_node_id);
+ printf("\toutput-port-id: %u\n", info->output_port_id);
+ printf("\tinput-node-id: %u\n", info->input_node_id);
+ printf("\tinput-port-id: %u\n", info->input_port_id);
+
+ printf("%c\tstate: \"%s\"", MARK_CHANGE(PW_LINK_CHANGE_MASK_STATE),
+ pw_link_state_as_string(info->state));
+ if (info->state == PW_LINK_STATE_ERROR && info->error)
+ printf(" \"%s\"\n", info->error);
+ else
+ printf("\n");
+ printf("%c\tformat:\n", MARK_CHANGE(PW_LINK_CHANGE_MASK_FORMAT));
+ if (info->format)
+ spa_debug_pod(2, NULL, info->format);
+ else
+ printf("\t\tnone\n");
+ print_properties(info->props, MARK_CHANGE(PW_LINK_CHANGE_MASK_PROPS), true);
+ info->change_mask = 0;
+}
+
+static void info_device(struct proxy_data *pd)
+{
+ struct pw_device_info *info = pd->info;
+
+ info_global(pd);
+ print_properties(info->props, MARK_CHANGE(PW_DEVICE_CHANGE_MASK_PROPS), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(PW_DEVICE_CHANGE_MASK_PARAMS), true);
+ info->change_mask = 0;
+}
+
+static void info_session(struct proxy_data *pd)
+{
+ struct pw_session_info *info = pd->info;
+
+ info_global(pd);
+ print_properties(info->props, MARK_CHANGE(0), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(1), true);
+ info->change_mask = 0;
+}
+
+static void info_endpoint(struct proxy_data *pd)
+{
+ struct pw_endpoint_info *info = pd->info;
+ const char *direction;
+
+ info_global(pd);
+ printf("\tname: %s\n", info->name);
+ printf("\tmedia-class: %s\n", info->media_class);
+ switch(info->direction) {
+ case PW_DIRECTION_OUTPUT:
+ direction = "source";
+ break;
+ case PW_DIRECTION_INPUT:
+ direction = "sink";
+ break;
+ default:
+ direction = "invalid";
+ break;
+ }
+ printf("\tdirection: %s\n", direction);
+ printf("\tflags: 0x%x\n", info->flags);
+ printf("%c\tstreams: %u\n", MARK_CHANGE(0), info->n_streams);
+ printf("%c\tsession: %u\n", MARK_CHANGE(1), info->session_id);
+ print_properties(info->props, MARK_CHANGE(2), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(3), true);
+ info->change_mask = 0;
+}
+
+static void info_endpoint_stream(struct proxy_data *pd)
+{
+ struct pw_endpoint_stream_info *info = pd->info;
+
+ info_global(pd);
+ printf("\tid: %u\n", info->id);
+ printf("\tendpoint-id: %u\n", info->endpoint_id);
+ printf("\tname: %s\n", info->name);
+ print_properties(info->props, MARK_CHANGE(1), true);
+ print_params(info->params, info->n_params, MARK_CHANGE(2), true);
+ info->change_mask = 0;
+}
+
+static void core_event_info(void *data, const struct pw_core_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d core %d changed\n", rd->id, info->id);
+ pd->info = pw_core_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_core(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .info = core_event_info
+};
+
+
+static void module_event_info(void *data, const struct pw_module_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d module %d changed\n", rd->id, info->id);
+ pd->info = pw_module_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_module(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info
+};
+
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d node %d changed\n", rd->id, info->id);
+ pd->info = pw_node_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_node(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static void event_param(void *_data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct proxy_data *data = _data;
+ struct remote_data *rd = data->rd;
+
+ if (rd->data->interactive)
+ printf("remote %d object %d param %d index %d\n",
+ rd->id, data->global->id, id, index);
+
+ spa_debug_pod(2, NULL, param);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = event_param
+};
+
+
+static void port_event_info(void *data, const struct pw_port_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d port %d changed\n", rd->id, info->id);
+ pd->info = pw_port_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_port(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = port_event_info,
+ .param = event_param
+};
+
+static void factory_event_info(void *data, const struct pw_factory_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d factory %d changed\n", rd->id, info->id);
+ pd->info = pw_factory_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_factory(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_factory_events factory_events = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = factory_event_info
+};
+
+static void client_event_info(void *data, const struct pw_client_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d client %d changed\n", rd->id, info->id);
+ pd->info = pw_client_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_client(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static void client_event_permissions(void *_data, uint32_t index,
+ uint32_t n_permissions, const struct pw_permission *permissions)
+{
+ struct proxy_data *data = _data;
+ struct remote_data *rd = data->rd;
+ uint32_t i;
+
+ printf("remote %d node %d index %d\n",
+ rd->id, data->global->id, index);
+
+ for (i = 0; i < n_permissions; i++) {
+ if (permissions[i].id == PW_ID_ANY)
+ printf(" default:");
+ else
+ printf(" %u:", permissions[i].id);
+ printf(" "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(permissions[i].permissions));
+ }
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info,
+ .permissions = client_event_permissions
+};
+
+static void link_event_info(void *data, const struct pw_link_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d link %d changed\n", rd->id, info->id);
+ pd->info = pw_link_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_link(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_link_events link_events = {
+ PW_VERSION_LINK_EVENTS,
+ .info = link_event_info
+};
+
+
+static void device_event_info(void *data, const struct pw_device_info *info)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ if (pd->info && rd->data->monitoring)
+ printf("remote %d device %d changed\n", rd->id, info->id);
+ pd->info = pw_device_info_update(pd->info, info);
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_device(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info,
+ .param = event_param
+};
+
+static void session_info_free(struct pw_session_info *info)
+{
+ free(info->params);
+ pw_properties_free ((struct pw_properties *)info->props);
+ free(info);
+}
+
+static void session_event_info(void *data,
+ const struct pw_session_info *update)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ struct pw_session_info *info = pd->info;
+
+ if (!info) {
+ info = pd->info = calloc(1, sizeof(*info));
+ info->id = update->id;
+ }
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free(info->params);
+ info->params = malloc(info->n_params * sizeof(struct spa_param_info));
+ memcpy(info->params, update->params,
+ info->n_params * sizeof(struct spa_param_info));
+ }
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS) {
+ pw_properties_free ((struct pw_properties *)info->props);
+ info->props =
+ (struct spa_dict *) pw_properties_new_dict (update->props);
+ }
+
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_session(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_session_events session_events = {
+ PW_VERSION_SESSION_EVENTS,
+ .info = session_event_info,
+ .param = event_param
+};
+
+static void endpoint_info_free(struct pw_endpoint_info *info)
+{
+ free(info->name);
+ free(info->media_class);
+ free(info->params);
+ pw_properties_free ((struct pw_properties *)info->props);
+ free(info);
+}
+
+static void endpoint_event_info(void *data,
+ const struct pw_endpoint_info *update)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ struct pw_endpoint_info *info = pd->info;
+
+ if (!info) {
+ info = pd->info = calloc(1, sizeof(*info));
+ info->id = update->id;
+ info->name = update->name ? strdup(update->name) : NULL;
+ info->media_class = update->media_class ? strdup(update->media_class) : NULL;
+ info->direction = update->direction;
+ info->flags = update->flags;
+ }
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_STREAMS)
+ info->n_streams = update->n_streams;
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_SESSION)
+ info->session_id = update->session_id;
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free(info->params);
+ info->params = malloc(info->n_params * sizeof(struct spa_param_info));
+ memcpy(info->params, update->params,
+ info->n_params * sizeof(struct spa_param_info));
+ }
+ if (update->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS) {
+ pw_properties_free ((struct pw_properties *)info->props);
+ info->props =
+ (struct spa_dict *) pw_properties_new_dict (update->props);
+ }
+
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_endpoint(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_endpoint_events endpoint_events = {
+ PW_VERSION_ENDPOINT_EVENTS,
+ .info = endpoint_event_info,
+ .param = event_param
+};
+
+static void endpoint_stream_info_free(struct pw_endpoint_stream_info *info)
+{
+ free(info->name);
+ free(info->params);
+ pw_properties_free ((struct pw_properties *)info->props);
+ free(info);
+}
+
+static void endpoint_stream_event_info(void *data,
+ const struct pw_endpoint_stream_info *update)
+{
+ struct proxy_data *pd = data;
+ struct remote_data *rd = pd->rd;
+ struct pw_endpoint_stream_info *info = pd->info;
+
+ if (!info) {
+ info = pd->info = calloc(1, sizeof(*info));
+ info->id = update->id;
+ info->endpoint_id = update->endpoint_id;
+ info->name = update->name ? strdup(update->name) : NULL;
+ }
+ if (update->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS) {
+ info->n_params = update->n_params;
+ free(info->params);
+ info->params = malloc(info->n_params * sizeof(struct spa_param_info));
+ memcpy(info->params, update->params,
+ info->n_params * sizeof(struct spa_param_info));
+ }
+ if (update->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS) {
+ pw_properties_free ((struct pw_properties *)info->props);
+ info->props =
+ (struct spa_dict *) pw_properties_new_dict (update->props);
+ }
+
+ if (pd->global == NULL)
+ pd->global = pw_map_lookup(&rd->globals, info->id);
+ if (pd->global && pd->global->info_pending) {
+ info_endpoint_stream(pd);
+ pd->global->info_pending = false;
+ }
+}
+
+static const struct pw_endpoint_stream_events endpoint_stream_events = {
+ PW_VERSION_ENDPOINT_STREAM_EVENTS,
+ .info = endpoint_stream_event_info,
+ .param = event_param
+};
+
+static void
+removed_proxy (void *data)
+{
+ struct proxy_data *pd = data;
+ pw_proxy_destroy(pd->proxy);
+}
+
+static void
+destroy_proxy (void *data)
+{
+ struct proxy_data *pd = data;
+
+ spa_hook_remove(&pd->proxy_listener);
+ spa_hook_remove(&pd->object_listener);
+
+ if (pd->global)
+ pd->global->proxy = NULL;
+
+ if (pd->info == NULL)
+ return;
+
+ if (pd->class->destroy)
+ pd->class->destroy(pd->info);
+ pd->info = NULL;
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = removed_proxy,
+ .destroy = destroy_proxy,
+};
+
+static bool do_list_objects(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ pw_map_for_each(&rd->globals, print_global, args);
+ return true;
+}
+
+static const struct class core_class = {
+ .type = PW_TYPE_INTERFACE_Core,
+ .version = PW_VERSION_CORE,
+ .events = &core_events,
+ .destroy = (pw_destroy_t) pw_core_info_free,
+ .info = info_core,
+ .name_key = PW_KEY_CORE_NAME,
+};
+static const struct class module_class = {
+ .type = PW_TYPE_INTERFACE_Module,
+ .version = PW_VERSION_MODULE,
+ .events = &module_events,
+ .destroy = (pw_destroy_t) pw_module_info_free,
+ .info = info_module,
+ .name_key = PW_KEY_MODULE_NAME,
+};
+
+static const struct class factory_class = {
+ .type = PW_TYPE_INTERFACE_Factory,
+ .version = PW_VERSION_FACTORY,
+ .events = &factory_events,
+ .destroy = (pw_destroy_t) pw_factory_info_free,
+ .info = info_factory,
+ .name_key = PW_KEY_FACTORY_NAME,
+};
+
+static const struct class client_class = {
+ .type = PW_TYPE_INTERFACE_Client,
+ .version = PW_VERSION_CLIENT,
+ .events = &client_events,
+ .destroy = (pw_destroy_t) pw_client_info_free,
+ .info = info_client,
+ .name_key = PW_KEY_APP_NAME,
+};
+static const struct class device_class = {
+ .type = PW_TYPE_INTERFACE_Device,
+ .version = PW_VERSION_DEVICE,
+ .events = &device_events,
+ .destroy = (pw_destroy_t) pw_device_info_free,
+ .info = info_device,
+ .name_key = PW_KEY_DEVICE_NAME,
+};
+static const struct class node_class = {
+ .type = PW_TYPE_INTERFACE_Node,
+ .version = PW_VERSION_NODE,
+ .events = &node_events,
+ .destroy = (pw_destroy_t) pw_node_info_free,
+ .info = info_node,
+ .name_key = PW_KEY_NODE_NAME,
+};
+static const struct class port_class = {
+ .type = PW_TYPE_INTERFACE_Port,
+ .version = PW_VERSION_PORT,
+ .events = &port_events,
+ .destroy = (pw_destroy_t) pw_port_info_free,
+ .info = info_port,
+ .name_key = PW_KEY_PORT_NAME,
+};
+static const struct class link_class = {
+ .type = PW_TYPE_INTERFACE_Link,
+ .version = PW_VERSION_LINK,
+ .events = &link_events,
+ .destroy = (pw_destroy_t) pw_link_info_free,
+ .info = info_link,
+};
+static const struct class session_class = {
+ .type = PW_TYPE_INTERFACE_Session,
+ .version = PW_VERSION_SESSION,
+ .events = &session_events,
+ .destroy = (pw_destroy_t) session_info_free,
+ .info = info_session,
+};
+static const struct class endpoint_class = {
+ .type = PW_TYPE_INTERFACE_Endpoint,
+ .version = PW_VERSION_ENDPOINT,
+ .events = &endpoint_events,
+ .destroy = (pw_destroy_t) endpoint_info_free,
+ .info = info_endpoint,
+};
+static const struct class endpoint_stream_class = {
+ .type = PW_TYPE_INTERFACE_EndpointStream,
+ .version = PW_VERSION_ENDPOINT_STREAM,
+ .events = &endpoint_stream_events,
+ .destroy = (pw_destroy_t) endpoint_stream_info_free,
+ .info = info_endpoint_stream,
+};
+static const struct class metadata_class = {
+ .type = PW_TYPE_INTERFACE_Metadata,
+ .version = PW_VERSION_METADATA,
+ .name_key = PW_KEY_METADATA_NAME,
+};
+
+static const struct class *classes[] =
+{
+ &core_class,
+ &module_class,
+ &factory_class,
+ &client_class,
+ &device_class,
+ &node_class,
+ &port_class,
+ &link_class,
+ &session_class,
+ &endpoint_class,
+ &endpoint_stream_class,
+ &metadata_class,
+};
+
+static const struct class *find_class(const char *type, uint32_t version)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(classes, c) {
+ if (spa_streq((*c)->type, type) &&
+ (*c)->version <= version)
+ return *c;
+ }
+ return NULL;
+}
+
+static bool bind_global(struct remote_data *rd, struct global *global, char **error)
+{
+ const struct class *class;
+ struct proxy_data *pd;
+ struct pw_proxy *proxy;
+
+ class = find_class(global->type, global->version);
+ if (class == NULL) {
+ *error = spa_aprintf("unsupported type %s", global->type);
+ return false;
+ }
+ global->class = class;
+
+ proxy = pw_registry_bind(rd->registry,
+ global->id,
+ global->type,
+ class->version,
+ sizeof(struct proxy_data));
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->rd = rd;
+ pd->global = global;
+ pd->proxy = proxy;
+ pd->class = class;
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, class->events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+
+ global->proxy = proxy;
+
+ rd->prompt_pending = pw_core_sync(rd->core, 0, 0);
+
+ return true;
+}
+
+static bool do_global_info(struct global *global, char **error)
+{
+ struct remote_data *rd = global->rd;
+ struct proxy_data *pd;
+
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ global->info_pending = true;
+ } else {
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (pd->class->info)
+ pd->class->info(pd);
+ }
+ return true;
+}
+static int do_global_info_all(void *obj, void *data)
+{
+ struct global *global = obj;
+ char *error;
+
+ if (global == NULL)
+ return 0;
+
+ if (!do_global_info(global, &error)) {
+ fprintf(stderr, "info: %s\n", error);
+ free(error);
+ }
+ return 0;
+}
+
+static bool do_info(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[1];
+ int n;
+ struct global *global;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <object-id>|all", cmd);
+ return false;
+ }
+ if (spa_streq(a[0], "all")) {
+ pw_map_for_each(&rd->globals, do_global_info_all, NULL);
+ }
+ else {
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ return do_global_info(global, error);
+ }
+ return true;
+}
+
+static bool do_create_device(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[2];
+ int n;
+ uint32_t id;
+ struct pw_proxy *proxy;
+ struct pw_properties *props = NULL;
+ struct proxy_data *pd;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <factory-name> [<properties>]", cmd);
+ return false;
+ }
+ if (n == 2)
+ props = pw_properties_new_string(a[1]);
+
+ proxy = pw_core_create_object(rd->core, a[0],
+ PW_TYPE_INTERFACE_Device,
+ PW_VERSION_DEVICE,
+ props ? &props->dict : NULL,
+ sizeof(struct proxy_data));
+
+ pw_properties_free(props);
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->rd = rd;
+ pd->proxy = proxy;
+ pd->class = &device_class;
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, &device_events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+
+ id = pw_map_insert_new(&data->vars, proxy);
+ if (rd->data->interactive)
+ printf("%d = @proxy:%d\n", id, pw_proxy_get_id(proxy));
+
+ return true;
+}
+
+static bool do_create_node(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[2];
+ int n;
+ uint32_t id;
+ struct pw_proxy *proxy;
+ struct pw_properties *props = NULL;
+ struct proxy_data *pd;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <factory-name> [<properties>]", cmd);
+ return false;
+ }
+ if (n == 2)
+ props = pw_properties_new_string(a[1]);
+
+ proxy = pw_core_create_object(rd->core, a[0],
+ PW_TYPE_INTERFACE_Node,
+ PW_VERSION_NODE,
+ props ? &props->dict : NULL,
+ sizeof(struct proxy_data));
+
+ pw_properties_free(props);
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->rd = rd;
+ pd->proxy = proxy;
+ pd->class = &node_class;
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, &node_events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+
+ id = pw_map_insert_new(&data->vars, proxy);
+ if (rd->data->interactive)
+ printf("%d = @proxy:%d\n", id, pw_proxy_get_id(proxy));
+
+ return true;
+}
+
+static bool do_destroy(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[1];
+ int n;
+ struct global *global;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <object-id>", cmd);
+ return false;
+ }
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ pw_registry_destroy(rd->registry, global->id);
+
+ return true;
+}
+
+static struct global *
+obj_global_port(struct remote_data *rd, struct global *global, const char *port_direction, const char *port_id)
+{
+ struct global *global_port_found = NULL;
+ uint32_t *ports = NULL;
+ int port_count;
+
+ port_count = children_of(rd, global->id, PW_TYPE_INTERFACE_Port, &ports);
+
+ if (port_count <= 0)
+ return NULL;
+
+ for (int i = 0; i < port_count; i++) {
+ struct global *global_port = obj_global(rd, ports[i]);
+
+ if (!global_port)
+ continue;
+
+ struct spa_dict *props_port = global_props(global_port);
+
+ if (spa_streq(spa_dict_lookup(props_port, "port.direction"), port_direction)
+ && spa_streq(spa_dict_lookup(props_port, "port.id"), port_id)) {
+ global_port_found = global_port;
+ break;
+ }
+ }
+
+ free(ports);
+ return global_port_found;
+}
+
+static void create_link_with_properties(struct data *data, struct pw_properties *props)
+{
+ struct remote_data *rd = data->current;
+ uint32_t id;
+ struct pw_proxy *proxy;
+ struct proxy_data *pd;
+
+ proxy = (struct pw_proxy*)pw_core_create_object(rd->core,
+ "link-factory",
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ props ? &props->dict : NULL,
+ sizeof(struct proxy_data));
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->rd = rd;
+ pd->proxy = proxy;
+ pd->class = &link_class;
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, &link_events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+
+ id = pw_map_insert_new(&data->vars, proxy);
+ if (rd->data->interactive)
+ printf("%d = @proxy:%d\n", id, pw_proxy_get_id((struct pw_proxy*)proxy));
+}
+
+static bool do_create_link(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[5];
+ int n;
+ struct pw_properties *props = NULL;
+
+ n = pw_split_ip(args, WHITESPACE, 5, a);
+ if (n < 4) {
+ *error = spa_aprintf("%s <node-id> <port> <node-id> <port> [<properties>]", cmd);
+ return false;
+ }
+ if (n == 5)
+ props = pw_properties_new_string(a[4]);
+ else
+ props = pw_properties_new(NULL, NULL);
+
+ if (!spa_streq(a[0], "-"))
+ pw_properties_set(props, PW_KEY_LINK_OUTPUT_NODE, a[0]);
+ if (!spa_streq(a[1], "-"))
+ pw_properties_set(props, PW_KEY_LINK_OUTPUT_PORT, a[1]);
+ if (!spa_streq(a[2], "-"))
+ pw_properties_set(props, PW_KEY_LINK_INPUT_NODE, a[2]);
+ if (!spa_streq(a[3], "-"))
+ pw_properties_set(props, PW_KEY_LINK_INPUT_PORT, a[3]);
+
+ if (spa_streq(a[1], "*") && spa_streq(a[3], "*")) {
+ struct global *global_out, *global_in;
+ struct proxy_data *pd_out, *pd_in;
+ uint32_t n_output_ports, n_input_ports;
+
+ global_out = find_global(rd, a[0]);
+ if (global_out == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ global_in = find_global(rd, a[2]);
+ if (global_in == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[2]);
+ return false;
+ }
+
+ pd_out = pw_proxy_get_user_data(global_out->proxy);
+ pd_in = pw_proxy_get_user_data(global_in->proxy);
+
+ n_output_ports = ((struct pw_node_info *)pd_out->info)->n_output_ports;
+ n_input_ports = ((struct pw_node_info *)pd_in->info)->n_input_ports;
+
+ if (n_output_ports != n_input_ports) {
+ *error = spa_aprintf("%s: Number of ports don't match (%u != %u)", cmd, n_output_ports, n_input_ports);
+ return false;
+ }
+
+ for (uint32_t i = 0; i < n_output_ports; i++) {
+ char port_id[4];
+ struct global *global_port_out, *global_port_in;
+
+ snprintf(port_id, 4, "%d", i);
+
+ global_port_out = obj_global_port(rd, global_out, "out", port_id);
+ global_port_in = obj_global_port(rd, global_in, "in", port_id);
+
+ if (!global_port_out || !global_port_in)
+ continue;
+
+ pw_properties_setf(props, PW_KEY_LINK_OUTPUT_PORT, "%d", global_port_out->id);
+ pw_properties_setf(props, PW_KEY_LINK_INPUT_PORT, "%d", global_port_in->id);
+
+ create_link_with_properties(data, props);
+ }
+ } else
+ create_link_with_properties(data, props);
+
+ pw_properties_free(props);
+
+ return true;
+}
+
+static bool do_export_node(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ struct pw_global *global;
+ struct pw_node *node;
+ struct pw_proxy *proxy;
+ char *a[2];
+ int n, idx;
+ uint32_t id;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <node-id> [<remote-var>]", cmd);
+ return false;
+ }
+ if (n == 2) {
+ idx = atoi(a[1]);
+ rd = pw_map_lookup(&data->vars, idx);
+ if (rd == NULL)
+ goto no_remote;
+ }
+
+ global = pw_context_find_global(data->context, atoi(a[0]));
+ if (global == NULL) {
+ *error = spa_aprintf("object %d does not exist", atoi(a[0]));
+ return false;
+ }
+ if (!pw_global_is_type(global, PW_TYPE_INTERFACE_Node)) {
+ *error = spa_aprintf("object %d is not a node", atoi(a[0]));
+ return false;
+ }
+ node = pw_global_get_object(global);
+ proxy = pw_core_export(rd->core, PW_TYPE_INTERFACE_Node, NULL, node, 0);
+
+ id = pw_map_insert_new(&data->vars, proxy);
+ if (rd->data->interactive)
+ printf("%d = @proxy:%d\n", id, pw_proxy_get_id((struct pw_proxy*)proxy));
+
+ return true;
+
+ no_remote:
+ *error = spa_aprintf("Remote %d does not exist", idx);
+ return false;
+}
+
+static bool do_enum_params(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[2];
+ int n;
+ uint32_t param_id;
+ const struct spa_type_info *ti;
+ struct global *global;
+
+ n = pw_split_ip(args, WHITESPACE, 2, a);
+ if (n < 2) {
+ *error = spa_aprintf("%s <object-id> <param-id>", cmd);
+ return false;
+ }
+
+ ti = spa_debug_type_find_short(spa_type_param, a[1]);
+ if (ti == NULL) {
+ *error = spa_aprintf("%s: unknown param type: %s", cmd, a[1]);
+ return false;
+ }
+ param_id = ti->type;
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Node))
+ pw_node_enum_params((struct pw_node*)global->proxy, 0,
+ param_id, 0, 0, NULL);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Port))
+ pw_port_enum_params((struct pw_port*)global->proxy, 0,
+ param_id, 0, 0, NULL);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device))
+ pw_device_enum_params((struct pw_device*)global->proxy, 0,
+ param_id, 0, 0, NULL);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Endpoint))
+ pw_endpoint_enum_params((struct pw_endpoint*)global->proxy, 0,
+ param_id, 0, 0, NULL);
+ else {
+ *error = spa_aprintf("enum-params not implemented on object %d type:%s",
+ atoi(a[0]), global->type);
+ return false;
+ }
+ return true;
+}
+
+static bool do_set_param(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[3];
+ int res, n;
+ uint32_t param_id;
+ struct global *global;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_type_info *ti;
+ struct spa_pod *pod;
+
+ n = pw_split_ip(args, WHITESPACE, 3, a);
+ if (n < 3) {
+ *error = spa_aprintf("%s <object-id> <param-id> <param-json>", cmd);
+ return false;
+ }
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+
+ ti = spa_debug_type_find_short(spa_type_param, a[1]);
+ if (ti == NULL) {
+ *error = spa_aprintf("%s: unknown param type: %s", cmd, a[1]);
+ return false;
+ }
+ if ((res = spa_json_to_pod(&b, 0, ti, a[2], strlen(a[2]))) < 0) {
+ *error = spa_aprintf("%s: can't make pod: %s", cmd, spa_strerror(res));
+ return false;
+ }
+ if ((pod = spa_pod_builder_deref(&b, 0)) == NULL) {
+ *error = spa_aprintf("%s: can't make pod", cmd);
+ return false;
+ }
+ spa_debug_pod(0, NULL, pod);
+
+ param_id = ti->type;
+
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Node))
+ pw_node_set_param((struct pw_node*)global->proxy,
+ param_id, 0, pod);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device))
+ pw_device_set_param((struct pw_device*)global->proxy,
+ param_id, 0, pod);
+ else if (spa_streq(global->type, PW_TYPE_INTERFACE_Endpoint))
+ pw_endpoint_set_param((struct pw_endpoint*)global->proxy,
+ param_id, 0, pod);
+ else {
+ *error = spa_aprintf("set-param not implemented on object %d type:%s",
+ atoi(a[0]), global->type);
+ return false;
+ }
+ return true;
+}
+
+static bool do_permissions(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[3];
+ int n;
+ uint32_t p;
+ struct global *global;
+ struct pw_permission permissions[1];
+
+ n = pw_split_ip(args, WHITESPACE, 3, a);
+ if (n < 3) {
+ *error = spa_aprintf("%s <client-id> <object> <permission>", cmd);
+ return false;
+ }
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (!spa_streq(global->type, PW_TYPE_INTERFACE_Client)) {
+ *error = spa_aprintf("object %d is not a client", atoi(a[0]));
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+
+ p = strtol(a[2], NULL, 0);
+ if (rd->data->interactive)
+ printf("setting permissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(p));
+
+ permissions[0] = PW_PERMISSION_INIT(atoi(a[1]), p);
+ pw_client_update_permissions((struct pw_client*)global->proxy,
+ 1, permissions);
+
+ return true;
+}
+
+static bool do_get_permissions(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[3];
+ int n;
+ struct global *global;
+
+ n = pw_split_ip(args, WHITESPACE, 1, a);
+ if (n < 1) {
+ *error = spa_aprintf("%s <client-id>", cmd);
+ return false;
+ }
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (!spa_streq(global->type, PW_TYPE_INTERFACE_Client)) {
+ *error = spa_aprintf("object %d is not a client", atoi(a[0]));
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+ pw_client_get_permissions((struct pw_client*)global->proxy,
+ 0, UINT32_MAX);
+
+ return true;
+}
+
+static bool do_send_command(struct data *data, const char *cmd, char *args, char **error)
+{
+ struct remote_data *rd = data->current;
+ char *a[3];
+ int res, n;
+ struct global *global;
+ uint8_t buffer[1024];
+ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
+ const struct spa_type_info *ti;
+ struct spa_pod *pod;
+
+ n = pw_split_ip(args, WHITESPACE, 3, a);
+ if (n < 3) {
+ *error = spa_aprintf("%s <object-id> <command-id> <command-json>", cmd);
+ return false;
+ }
+
+ global = find_global(rd, a[0]);
+ if (global == NULL) {
+ *error = spa_aprintf("%s: unknown global '%s'", cmd, a[0]);
+ return false;
+ }
+ if (global->proxy == NULL) {
+ if (!bind_global(rd, global, error))
+ return false;
+ }
+
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Node)) {
+ ti = spa_debug_type_find_short(spa_type_node_command_id, a[1]);
+ } else {
+ *error = spa_aprintf("send-command not implemented on object %d type:%s",
+ atoi(a[0]), global->type);
+ return false;
+ }
+
+ if (ti == NULL) {
+ *error = spa_aprintf("%s: unknown node command type: %s", cmd, a[1]);
+ return false;
+ }
+ if ((res = spa_json_to_pod(&b, 0, ti, a[2], strlen(a[2]))) < 0) {
+ *error = spa_aprintf("%s: can't make pod: %s", cmd, spa_strerror(res));
+ return false;
+ }
+ if ((pod = spa_pod_builder_deref(&b, 0)) == NULL) {
+ *error = spa_aprintf("%s: can't make pod", cmd);
+ return false;
+ }
+ spa_debug_pod(0, NULL, pod);
+
+ pw_node_send_command((struct pw_node*)global->proxy, (struct spa_command*)pod);
+ return true;
+}
+
+static struct global *
+obj_global(struct remote_data *rd, uint32_t id)
+{
+ struct global *global;
+ struct proxy_data *pd;
+
+ if (!rd)
+ return NULL;
+
+ global = pw_map_lookup(&rd->globals, id);
+ if (!global)
+ return NULL;
+
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (!pd || !pd->info)
+ return NULL;
+
+ return global;
+}
+
+static struct spa_dict *
+global_props(struct global *global)
+{
+ struct proxy_data *pd;
+
+ if (!global)
+ return NULL;
+
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (!pd || !pd->info)
+ return NULL;
+
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Core))
+ return ((struct pw_core_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Module))
+ return ((struct pw_module_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Device))
+ return ((struct pw_device_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Node))
+ return ((struct pw_node_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Port))
+ return ((struct pw_port_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Factory))
+ return ((struct pw_factory_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Client))
+ return ((struct pw_client_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Link))
+ return ((struct pw_link_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Session))
+ return ((struct pw_session_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_Endpoint))
+ return ((struct pw_endpoint_info *)pd->info)->props;
+ if (spa_streq(global->type, PW_TYPE_INTERFACE_EndpointStream))
+ return ((struct pw_endpoint_stream_info *)pd->info)->props;
+
+ return NULL;
+}
+
+static const char *
+global_lookup(struct global *global, const char *key)
+{
+ struct spa_dict *dict;
+
+ dict = global_props(global);
+ if (!dict)
+ return NULL;
+ return spa_dict_lookup(dict, key);
+}
+
+
+static int
+children_of(struct remote_data *rd, uint32_t parent_id,
+ const char *child_type, uint32_t **children)
+{
+ const char *parent_type;
+ union pw_map_item *item;
+ struct global *global;
+ struct proxy_data *pd;
+ const char *parent_key = NULL, *child_key = NULL;
+ const char *parent_value = NULL, *child_value = NULL;
+ int pass, i, count;
+
+ if (!rd || !children)
+ return -1;
+
+ /* get the device info */
+ global = obj_global(rd, parent_id);
+ if (!global)
+ return -1;
+ parent_type = global->type;
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (!pd || !pd->info)
+ return -1;
+
+ /* supported combinations */
+ if (spa_streq(parent_type, PW_TYPE_INTERFACE_Device) &&
+ spa_streq(child_type, PW_TYPE_INTERFACE_Node)) {
+ parent_key = PW_KEY_OBJECT_ID;
+ child_key = PW_KEY_DEVICE_ID;
+ } else if (spa_streq(parent_type, PW_TYPE_INTERFACE_Node) &&
+ spa_streq(child_type, PW_TYPE_INTERFACE_Port)) {
+ parent_key = PW_KEY_OBJECT_ID;
+ child_key = PW_KEY_NODE_ID;
+ } else if (spa_streq(parent_type, PW_TYPE_INTERFACE_Module) &&
+ spa_streq(child_type, PW_TYPE_INTERFACE_Factory)) {
+ parent_key = PW_KEY_OBJECT_ID;
+ child_key = PW_KEY_MODULE_ID;
+ } else if (spa_streq(parent_type, PW_TYPE_INTERFACE_Factory) &&
+ spa_streq(child_type, PW_TYPE_INTERFACE_Device)) {
+ parent_key = PW_KEY_OBJECT_ID;
+ child_key = PW_KEY_FACTORY_ID;
+ } else
+ return -1;
+
+ /* get the parent key value */
+ if (parent_key) {
+ parent_value = global_lookup(global, parent_key);
+ if (!parent_value)
+ return -1;
+ }
+
+ count = 0;
+ *children = NULL;
+ i = 0;
+ for (pass = 1; pass <= 2; pass++) {
+ if (pass == 2) {
+ count = i;
+ if (!count)
+ return 0;
+
+ *children = malloc(sizeof(uint32_t) * count);
+ if (!*children)
+ return -1;
+ }
+ i = 0;
+ pw_array_for_each(item, &rd->globals.items) {
+ if (pw_map_item_is_free(item) || item->data == NULL)
+ continue;
+
+ global = item->data;
+
+ if (!spa_streq(global->type, child_type))
+ continue;
+
+ pd = pw_proxy_get_user_data(global->proxy);
+ if (!pd || !pd->info)
+ return -1;
+
+ if (child_key) {
+ /* get the device path */
+ child_value = global_lookup(global, child_key);
+ if (!child_value)
+ continue;
+ }
+
+ /* match? */
+ if (!spa_streq(parent_value, child_value))
+ continue;
+
+ if (*children)
+ (*children)[i] = global->id;
+ i++;
+
+ }
+ }
+ return count;
+}
+
+#define INDENT(_level) \
+ ({ \
+ int __level = (_level); \
+ char *_indent = alloca(__level + 1); \
+ memset(_indent, '\t', __level); \
+ _indent[__level] = '\0'; \
+ (const char *)_indent; \
+ })
+
+static bool parse(struct data *data, char *buf, char **error)
+{
+ char *a[2];
+ int n;
+ char *p, *cmd, *args;
+
+ if ((p = strchr(buf, '#')))
+ *p = '\0';
+
+ p = pw_strip(buf, "\n\r \t");
+
+ if (*p == '\0')
+ return true;
+
+ n = pw_split_ip(p, WHITESPACE, 2, a);
+ if (n < 1)
+ return true;
+
+ cmd = a[0];
+ args = n > 1 ? a[1] : "";
+
+ SPA_FOR_EACH_ELEMENT_VAR(command_list, c) {
+ if (spa_streq(c->name, cmd) ||
+ spa_streq(c->alias, cmd)) {
+ return c->func(data, cmd, args, error);
+ }
+ }
+ *error = spa_aprintf("Command \"%s\" does not exist. Type 'help' for usage.", cmd);
+ return false;
+}
+
+/* We need a global variable, readline doesn't have a closure arg */
+static struct data *input_dataptr;
+
+static void input_process_line(char *line)
+{
+ struct data *d = input_dataptr;
+ char *error;
+
+ if (!line)
+ line = strdup("quit");
+
+ if (line[0] != '\0') {
+#ifdef HAVE_READLINE
+ add_history(line);
+#endif
+ if (!parse(d, line, &error)) {
+ fprintf(stderr, "Error: \"%s\"\n", error);
+ free(error);
+ }
+ }
+ free(line);
+}
+
+static void do_input(void *data, int fd, uint32_t mask)
+{
+ struct data *d = data;
+
+ if (mask & SPA_IO_IN) {
+ input_dataptr = d;
+#ifdef HAVE_READLINE
+ rl_callback_read_char();
+#else
+ {
+ char *line = NULL;
+ size_t s = 0;
+
+ if (getline(&line, &s, stdin) < 0) {
+ free(line);
+ line = NULL;
+ }
+ input_process_line(line);
+ }
+#endif
+
+ if (d->current == NULL)
+ pw_main_loop_quit(d->loop);
+ else {
+ struct remote_data *rd = d->current;
+ if (rd->core)
+ rd->prompt_pending = pw_core_sync(rd->core, 0, 0);
+ }
+ }
+}
+
+#ifdef HAVE_READLINE
+static char *
+readline_match_command(const char *text, int state)
+{
+ static size_t idx;
+ static int len;
+
+ if (!state) {
+ idx = 0;
+ len = strlen(text);
+ }
+
+ while (idx < SPA_N_ELEMENTS(command_list)) {
+ const char *name = command_list[idx].name;
+ const char *alias = command_list[idx].alias;
+
+ idx++;
+ if (spa_strneq(name, text, len) || spa_strneq(alias, text, len))
+ return strdup(name);
+ }
+
+ return NULL;
+}
+
+static char **
+readline_command_completion(const char *text, int start, int end)
+{
+ char **matches = NULL;
+
+ /* Only try to complete the first word in a line */
+ if (start == 0)
+ matches = rl_completion_matches(text, readline_match_command);
+
+ /* Don't fall back to filename completion */
+ rl_attempted_completion_over = true;
+
+ return matches;
+}
+
+static void readline_init(void)
+{
+ rl_attempted_completion_function = readline_command_completion;
+ rl_callback_handler_install(">> ", input_process_line);
+}
+
+static void readline_cleanup(void)
+{
+ rl_callback_handler_remove();
+}
+#endif
+
+static void do_quit_on_signal(void *data, int signal_number)
+{
+ struct data *d = data;
+ d->quit = true;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, _("%s [options] [command]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -d, --daemon Start as daemon (Default false)\n"
+ " -r, --remote Remote daemon name\n"
+ " -m, --monitor Monitor activity\n\n"),
+ name);
+
+ do_help(data, "help", "", NULL);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ char *opt_remote = NULL;
+ char *error;
+ bool daemon = false, monitor = false;
+ struct remote_data *rd;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "daemon", no_argument, NULL, 'd' },
+ { "remote", required_argument, NULL, 'r' },
+ { NULL, 0, NULL, 0}
+ };
+ int c, i;
+
+ setlinebuf(stdout);
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ while ((c = getopt_long(argc, argv, "hVmdr:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(&data, argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'd':
+ daemon = true;
+ break;
+ case 'm':
+ monitor = true;
+ break;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "Broken installation: %m\n");
+ return -1;
+ }
+ l = pw_main_loop_get_loop(data.loop);
+ pw_loop_add_signal(l, SIGINT, do_quit_on_signal, &data);
+ pw_loop_add_signal(l, SIGTERM, do_quit_on_signal, &data);
+
+ spa_list_init(&data.remotes);
+ pw_map_init(&data.vars, 64, 16);
+
+ data.context = pw_context_new(l,
+ pw_properties_new(
+ PW_KEY_CORE_DAEMON, daemon ? "true" : NULL,
+ NULL),
+ 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "Can't create context: %m\n");
+ return -1;
+ }
+
+ pw_context_load_module(data.context, "libpipewire-module-link-factory", NULL, NULL);
+
+ if (!do_connect(&data, "connect", opt_remote, &error)) {
+ fprintf(stderr, "Error: \"%s\"\n", error);
+ return -1;
+ }
+
+ if (optind == argc) {
+ data.interactive = true;
+
+ printf("Welcome to PipeWire version %s. Type 'help' for usage.\n",
+ pw_get_library_version());
+
+#ifdef HAVE_READLINE
+ readline_init();
+#endif
+
+ pw_loop_add_io(l, STDIN_FILENO, SPA_IO_IN|SPA_IO_HUP, false, do_input, &data);
+
+ pw_main_loop_run(data.loop);
+
+#ifdef HAVE_READLINE
+ readline_cleanup();
+#endif
+ } else {
+ char buf[4096], *p, *error;
+
+ p = buf;
+ for (i = optind; i < argc; i++) {
+ p = stpcpy(p, argv[i]);
+ p = stpcpy(p, " ");
+ }
+
+ pw_main_loop_run(data.loop);
+
+ if (!parse(&data, buf, &error)) {
+ fprintf(stderr, "Error: \"%s\"\n", error);
+ free(error);
+ }
+ data.current->prompt_pending = pw_core_sync(data.current->core, 0, 0);
+ while (!data.quit && data.current) {
+ pw_main_loop_run(data.loop);
+ if (!monitor)
+ break;
+ }
+ }
+ spa_list_consume(rd, &data.remotes, link)
+ remote_data_free(rd);
+
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_map_clear(&data.vars);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-dot.c b/src/tools/pw-dot.c
new file mode 100644
index 0000000..30eb780
--- /dev/null
+++ b/src/tools/pw-dot.c
@@ -0,0 +1,1169 @@
+/* PipeWire
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/json.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+
+#define GLOBAL_ID_NONE UINT32_MAX
+#define DEFAULT_DOT_PATH "pw.dot"
+
+struct global;
+
+typedef void (*draw_t)(struct global *g);
+typedef void *(*info_update_t) (void *info, const void *update);
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct spa_list globals;
+ char *dot_str;
+ const char *dot_rankdir;
+ bool dot_orthoedges;
+
+ bool show_all;
+ bool show_smart;
+ bool show_detail;
+};
+
+struct global {
+ struct spa_list link;
+
+ struct data *data;
+ struct pw_proxy *proxy;
+
+ uint32_t id;
+#define INTERFACE_Port 0
+#define INTERFACE_Node 1
+#define INTERFACE_Link 2
+#define INTERFACE_Client 3
+#define INTERFACE_Device 4
+#define INTERFACE_Module 5
+#define INTERFACE_Factory 6
+ uint32_t type;
+ void *info;
+
+ pw_destroy_t info_destroy;
+ info_update_t info_update;
+ draw_t draw;
+
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+};
+
+static char *dot_str_new(void)
+{
+ return strdup("");
+}
+
+static void dot_str_clear(char **str)
+{
+ if (str && *str) {
+ free(*str);
+ *str = NULL;
+ }
+}
+
+static SPA_PRINTF_FUNC(2,0) void dot_str_vadd(char **str, const char *fmt, va_list varargs)
+{
+ char *res = NULL;
+ char *fmt2 = NULL;
+
+ spa_return_if_fail(str != NULL);
+ spa_return_if_fail(fmt != NULL);
+
+ if (asprintf(&fmt2, "%s%s", *str, fmt) < 0) {
+ spa_assert_not_reached();
+ return;
+ }
+
+ if (vasprintf(&res, fmt2, varargs) < 0) {
+ free (fmt2);
+ spa_assert_not_reached();
+ return;
+ }
+ free (fmt2);
+
+ free(*str);
+ *str = res;
+}
+
+static SPA_PRINTF_FUNC(2,3) void dot_str_add(char **str, const char *fmt, ...)
+{
+ va_list varargs;
+ va_start(varargs, fmt);
+ dot_str_vadd(str, fmt, varargs);
+ va_end(varargs);
+}
+
+static void draw_dict(char **str, const char *title,
+ const struct spa_dict *props)
+{
+ const struct spa_dict_item *item;
+
+ dot_str_add(str, "%s:\\l", title);
+ if (props == NULL || props->n_items == 0) {
+ dot_str_add(str, "- none\\l");
+ return;
+ }
+
+ spa_dict_for_each(item, props) {
+ if (item->value)
+ dot_str_add(str, "- %s: %s\\l", item->key, item->value);
+ else
+ dot_str_add(str, "- %s: (null)\\l", item->key);
+ }
+}
+
+static SPA_PRINTF_FUNC(6,0) void draw_vlabel(char **str, const char *name, uint32_t id, bool detail,
+ const struct spa_dict *props, const char *fmt, va_list varargs)
+{
+ /* draw the label header */
+ dot_str_add(str, "%s_%u [label=\"", name, id);
+
+ /* draw the label body */
+ dot_str_vadd(str, fmt, varargs);
+
+ if (detail)
+ draw_dict(str, "properties", props);
+
+ /*draw the label footer */
+ dot_str_add(str, "%s", "\"];\n");
+}
+
+static SPA_PRINTF_FUNC(6,7) void draw_label(char **str, const char *name, uint32_t id, bool detail,
+ const struct spa_dict *props, const char *fmt, ...)
+{
+ va_list varargs;
+ va_start(varargs, fmt);
+ draw_vlabel(str, name, id, detail, props, fmt, varargs);
+ va_end(varargs);
+}
+
+static void draw_port(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Port);
+
+ struct pw_port_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ /* draw the box */
+ dot_str_add(dot_str,
+ "port_%u [shape=box style=filled fillcolor=%s];\n",
+ g->id,
+ info->direction == PW_DIRECTION_INPUT ? "lightslateblue" : "lightcoral"
+ );
+
+ /* draw the label */
+ draw_label(dot_str,
+ "port", g->id, g->data->show_detail, info->props,
+ "port_id: %u\\lname: %s\\ldirection: %s\\l",
+ g->id,
+ spa_dict_lookup(info->props, PW_KEY_PORT_NAME),
+ pw_direction_as_string(info->direction)
+ );
+}
+
+
+static void draw_node(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Node);
+
+ struct pw_node_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ const char *client_id_str, *factory_id_str;
+ uint32_t client_id, factory_id;
+
+ client_id_str = spa_dict_lookup(info->props, PW_KEY_CLIENT_ID);
+ factory_id_str = spa_dict_lookup(info->props, PW_KEY_FACTORY_ID);
+ client_id = client_id_str ? (uint32_t)atoi(client_id_str) : GLOBAL_ID_NONE;
+ factory_id = factory_id_str ? (uint32_t)atoi(factory_id_str) : GLOBAL_ID_NONE;
+
+ /* draw the node header */
+ dot_str_add(dot_str, "subgraph cluster_node_%u {\n", g->id);
+ dot_str_add(dot_str, "bgcolor=palegreen;\n");
+
+ /* draw the label header */
+ dot_str_add(dot_str, "label=\"");
+
+ /* draw the label body */
+ dot_str_add(dot_str, "node_id: %u\\lname: %s\\lmedia_class: %s\\l",
+ g->id,
+ spa_dict_lookup(info->props, PW_KEY_NODE_NAME),
+ spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS));
+
+ if (g->data->show_detail)
+ draw_dict(dot_str, "properties", info->props);
+
+ /*draw the label footer */
+ dot_str_add(dot_str, "%s", "\"\n");
+
+ /* draw all node ports */
+ struct global *p;
+ const char *prop_node_id;
+ spa_list_for_each(p, &g->data->globals, link) {
+ struct pw_port_info *pinfo;
+ if (p->info == NULL)
+ continue;
+ if (p->type != INTERFACE_Port)
+ continue;
+ pinfo = p->info;
+ prop_node_id = spa_dict_lookup(pinfo->props, PW_KEY_NODE_ID);
+ if (!prop_node_id || (uint32_t)atoi(prop_node_id) != g->id)
+ continue;
+ if (p->draw)
+ p->draw(p);
+ }
+
+ /* draw the client/factory box if all option is enabled */
+ if (g->data->show_all) {
+ dot_str_add(dot_str, "node_%u [shape=box style=filled fillcolor=white];\n", g->id);
+ dot_str_add(dot_str, "node_%u [label=\"client_id: %u\\lfactory_id: %u\\l\"];\n", g->id, client_id, factory_id);
+ }
+
+ /* draw the node footer */
+ dot_str_add(dot_str, "}\n");
+
+ /* draw the client/factory arrows if all option is enabled */
+ if (g->data->show_all) {
+ dot_str_add(dot_str, "node_%u -> client_%u [style=dashed];\n", g->id, client_id);
+ dot_str_add(dot_str, "node_%u -> factory_%u [style=dashed];\n", g->id, factory_id);
+ }
+}
+
+static void draw_link(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Link);
+
+ struct pw_link_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ /* draw the box */
+ dot_str_add(dot_str, "link_%u [shape=box style=filled fillcolor=lightblue];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "link", g->id, g->data->show_detail, info->props,
+ "link_id: %u\\loutput_node_id: %u\\linput_node_id: %u\\loutput_port_id: %u\\linput_port_id: %u\\lstate: %s\\l",
+ g->id,
+ info->output_node_id,
+ info->input_node_id,
+ info->output_port_id,
+ info->input_port_id,
+ pw_link_state_as_string(info->state)
+ );
+
+ /* draw the arrows */
+ dot_str_add(dot_str, "port_%u -> link_%u -> port_%u;\n", info->output_port_id, g->id, info->input_port_id);
+}
+
+static void draw_client(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Client);
+
+ struct pw_client_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ /* draw the box */
+ dot_str_add(dot_str, "client_%u [shape=box style=filled fillcolor=tan1];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "client", g->id, g->data->show_detail, info->props,
+ "client_id: %u\\lname: %s\\lpid: %s\\l",
+ g->id,
+ spa_dict_lookup(info->props, PW_KEY_APP_NAME),
+ spa_dict_lookup(info->props, PW_KEY_APP_PROCESS_ID)
+ );
+}
+
+static void draw_device(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Device);
+
+ struct pw_device_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ const char *client_id_str = spa_dict_lookup(info->props, PW_KEY_CLIENT_ID);
+ const char *factory_id_str = spa_dict_lookup(info->props, PW_KEY_FACTORY_ID);
+ uint32_t client_id = client_id_str ? (uint32_t)atoi(client_id_str) : GLOBAL_ID_NONE;
+ uint32_t factory_id = factory_id_str ? (uint32_t)atoi(factory_id_str) : GLOBAL_ID_NONE;
+
+ /* draw the box */
+ dot_str_add(dot_str, "device_%u [shape=box style=filled fillcolor=lightpink];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "device", g->id, g->data->show_detail, info->props,
+ "device_id: %u\\lname: %s\\lmedia_class: %s\\lapi: %s\\lpath: %s\\l",
+ g->id,
+ spa_dict_lookup(info->props, PW_KEY_DEVICE_NAME),
+ spa_dict_lookup(info->props, PW_KEY_MEDIA_CLASS),
+ spa_dict_lookup(info->props, PW_KEY_DEVICE_API),
+ spa_dict_lookup(info->props, PW_KEY_OBJECT_PATH)
+ );
+
+ /* draw the arrows */
+ dot_str_add(dot_str, "device_%u -> client_%u [style=dashed];\n", g->id, client_id);
+ dot_str_add(dot_str, "device_%u -> factory_%u [style=dashed];\n", g->id, factory_id);
+}
+
+static void draw_factory(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Factory);
+
+ struct pw_factory_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ const char *module_id_str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID);
+ uint32_t module_id = module_id_str ? (uint32_t)atoi(module_id_str) : GLOBAL_ID_NONE;
+
+ /* draw the box */
+ dot_str_add(dot_str, "factory_%u [shape=box style=filled fillcolor=lightyellow];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "factory", g->id, g->data->show_detail, info->props,
+ "factory_id: %u\\lname: %s\\lmodule_id: %u\\l",
+ g->id, info->name, module_id
+ );
+
+ /* draw the arrow */
+ dot_str_add(dot_str, "factory_%u -> module_%u [style=dashed];\n", g->id, module_id);
+}
+
+static void draw_module(struct global *g)
+{
+ spa_assert(g != NULL);
+ spa_assert(g->info != NULL);
+ spa_assert(g->type == INTERFACE_Module);
+
+ struct pw_module_info *info = g->info;
+ char **dot_str = &g->data->dot_str;
+
+ /* draw the box */
+ dot_str_add(dot_str, "module_%u [shape=box style=filled fillcolor=lightgrey];\n", g->id);
+
+ /* draw the label */
+ draw_label(dot_str,
+ "module", g->id, g->data->show_detail, info->props,
+ "module_id: %u\\lname: %s\\l",
+ g->id, info->name
+ );
+}
+
+static bool is_node_id_link_referenced(uint32_t id, struct spa_list *globals)
+{
+ struct global *g;
+ struct pw_link_info *info;
+ spa_list_for_each(g, globals, link) {
+ if (g->info == NULL)
+ continue;
+ if (g->type != INTERFACE_Link)
+ continue;
+ info = g->info;
+ if (info->input_node_id == id || info->output_node_id == id)
+ return true;
+ }
+ return false;
+}
+
+static bool is_module_id_factory_referenced(uint32_t id, struct spa_list *globals)
+{
+ struct global *g;
+ struct pw_factory_info *info;
+ const char *module_id_str;
+ spa_list_for_each(g, globals, link) {
+ if (g->info == NULL)
+ continue;
+ if (g->type != INTERFACE_Factory)
+ continue;
+ info = g->info;
+ module_id_str = spa_dict_lookup(info->props, PW_KEY_MODULE_ID);
+ if (module_id_str && (uint32_t)atoi(module_id_str) == id)
+ return true;
+ }
+ return false;
+}
+
+static bool is_global_referenced(struct global *g)
+{
+ switch (g->type) {
+ case INTERFACE_Node:
+ return is_node_id_link_referenced(g->id, &g->data->globals);
+ case INTERFACE_Module:
+ return is_module_id_factory_referenced(g->id, &g->data->globals);
+ default:
+ break;
+ }
+
+ return true;
+}
+
+static int draw_graph(struct data *d, const char *path)
+{
+ FILE *fp;
+ struct global *g;
+
+ /* draw the header */
+ dot_str_add(&d->dot_str, "digraph pipewire {\n");
+
+ if (d->dot_rankdir) {
+ /* set rank direction, if provided */
+ dot_str_add(&d->dot_str, "rankdir = \"%s\";\n", d->dot_rankdir);
+ }
+
+ if (d->dot_orthoedges) {
+ /* enable orthogonal edges */
+ dot_str_add(&d->dot_str, "splines = ortho;\n");
+ }
+
+ /* iterate the globals */
+ spa_list_for_each(g, &d->globals, link) {
+ /* skip null and non-info globals */
+ if (g->info == NULL)
+ continue;
+
+ /* always skip ports since they are drawn by the nodes */
+ if (g->type == INTERFACE_Port)
+ continue;
+
+ /* skip clients, devices, factories and modules if all option is disabled */
+ if (!d->show_all) {
+ switch (g->type) {
+ case INTERFACE_Client:
+ case INTERFACE_Device:
+ case INTERFACE_Factory:
+ case INTERFACE_Module:
+ continue;
+ default:
+ break;
+ }
+ }
+
+ /* skip not referenced globals if smart option is enabled */
+ if (d->show_smart && !is_global_referenced(g))
+ continue;
+
+ /* draw the global */
+ if (g->draw)
+ g->draw(g);
+ }
+
+ /* draw the footer */
+ dot_str_add(&d->dot_str, "}\n");
+
+ if (spa_streq(path, "-")) {
+ /* wire the dot graph into to stdout */
+ fputs(d->dot_str, stdout);
+ } else {
+ /* open the file */
+ fp = fopen(path, "we");
+ if (fp == NULL) {
+ printf("open error: could not open %s for writing\n", path);
+ return -1;
+ }
+
+ /* wire the dot graph into the file */
+ fputs(d->dot_str, fp);
+ fclose(fp);
+ }
+ return 0;
+}
+
+static void global_event_info(struct global *g, const void *info)
+{
+ if (g->info_update)
+ g->info = g->info_update(g->info, info);
+}
+
+static void port_event_info(void *data, const struct pw_port_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = port_event_info,
+};
+
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+};
+
+static void link_event_info(void *data, const struct pw_link_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_link_events link_events = {
+ PW_VERSION_LINK_EVENTS,
+ .info = link_event_info
+};
+
+static void client_event_info(void *data, const struct pw_client_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info
+};
+
+static void device_event_info(void *data, const struct pw_device_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info
+};
+
+static void factory_event_info(void *data, const struct pw_factory_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_factory_events factory_events = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = factory_event_info
+};
+
+static void module_event_info(void *data, const struct pw_module_info *info)
+{
+ global_event_info(data, info);
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info
+};
+
+static void removed_proxy(void *data)
+{
+ struct global *g = data;
+ pw_proxy_destroy(g->proxy);
+}
+
+static void destroy_proxy(void *data)
+{
+ struct global *g = data;
+ spa_hook_remove(&g->object_listener);
+ spa_hook_remove(&g->proxy_listener);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = removed_proxy,
+ .destroy = destroy_proxy,
+};
+
+static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct pw_proxy *proxy;
+ uint32_t client_version;
+ uint32_t object_type;
+ const void *events;
+ pw_destroy_t info_destroy;
+ info_update_t info_update;
+ draw_t draw;
+ struct global *g;
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ events = &port_events;
+ info_destroy = (pw_destroy_t)pw_port_info_free;
+ info_update = (info_update_t)pw_port_info_update;
+ draw = draw_port;
+ client_version = PW_VERSION_PORT;
+ object_type = INTERFACE_Port;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ events = &node_events;
+ info_destroy = (pw_destroy_t)pw_node_info_free;
+ info_update = (info_update_t)pw_node_info_update;
+ draw = draw_node;
+ client_version = PW_VERSION_NODE;
+ object_type = INTERFACE_Node;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) {
+ events = &link_events;
+ info_destroy = (pw_destroy_t)pw_link_info_free;
+ info_update = (info_update_t)pw_link_info_update;
+ draw = draw_link;
+ client_version = PW_VERSION_LINK;
+ object_type = INTERFACE_Link;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Client)) {
+ events = &client_events;
+ info_destroy = (pw_destroy_t)pw_client_info_free;
+ info_update = (info_update_t)pw_client_info_update;
+ draw = draw_client;
+ client_version = PW_VERSION_CLIENT;
+ object_type = INTERFACE_Client;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Device)) {
+ events = &device_events;
+ info_destroy = (pw_destroy_t)pw_device_info_free;
+ info_update = (info_update_t)pw_device_info_update;
+ draw = draw_device;
+ client_version = PW_VERSION_DEVICE;
+ object_type = INTERFACE_Device;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Factory)) {
+ events = &factory_events;
+ info_destroy = (pw_destroy_t)pw_factory_info_free;
+ info_update = (info_update_t)pw_factory_info_update;
+ draw = draw_factory;
+ client_version = PW_VERSION_FACTORY;
+ object_type = INTERFACE_Factory;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Module)) {
+ events = &module_events;
+ info_destroy = (pw_destroy_t)pw_module_info_free;
+ info_update = (info_update_t)pw_module_info_update;
+ draw = draw_module;
+ client_version = PW_VERSION_MODULE;
+ object_type = INTERFACE_Module;
+ }
+ else if (spa_streq(type, PW_TYPE_INTERFACE_Core)) {
+ /* sync to notify we are done with globals */
+ pw_core_sync(d->core, 0, 0);
+ return;
+ }
+ else {
+ return;
+ }
+
+ proxy = pw_registry_bind(d->registry, id, type, client_version, 0);
+ if (proxy == NULL)
+ return;
+
+ /* set the global data */
+ g = calloc(1, sizeof(struct global));
+ g->data = d;
+ g->proxy = proxy;
+
+ g->id = id;
+ g->type = object_type;
+ g->info = NULL;
+
+ g->info_destroy = info_destroy;
+ g->info_update = info_update;
+ g->draw = draw;
+
+ pw_proxy_add_object_listener(proxy, &g->object_listener, events, g);
+ pw_proxy_add_listener(proxy, &g->proxy_listener, &proxy_events, g);
+
+ /* add the global to the list */
+ spa_list_insert(&d->globals, &g->link);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ 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,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static int get_data_from_pipewire(struct data *data, const char *opt_remote)
+{
+ struct pw_loop *l;
+ struct global *g;
+
+ data->loop = pw_main_loop_new(NULL);
+ if (data->loop == NULL) {
+ fprintf(stderr, "can't create main loop: %m\n");
+ return -1;
+ }
+
+ 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");
+ pw_main_loop_destroy(data->loop);
+ return -1;
+ }
+
+ data->core = pw_context_connect(data->context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data->core == NULL) {
+ fprintf(stderr, "can't connect: %m\n");
+ pw_context_destroy(data->context);
+ pw_main_loop_destroy(data->loop);
+ return -1;
+ }
+
+ pw_core_add_listener(data->core,
+ &data->core_listener,
+ &core_events, data);
+
+ data->registry = pw_core_get_registry(data->core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data->registry,
+ &data->registry_listener,
+ &registry_events, data);
+
+ pw_main_loop_run(data->loop);
+
+ spa_hook_remove(&data->registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data->registry);
+ spa_list_for_each(g, &data->globals, link)
+ pw_proxy_destroy(g->proxy);
+ spa_hook_remove(&data->core_listener);
+ pw_context_destroy(data->context);
+ pw_main_loop_destroy(data->loop);
+
+ return 0;
+}
+
+static void handle_json_obj(struct data *data, struct pw_properties *obj)
+{
+ struct global *g;
+ struct pw_properties *info, *props;
+ const char *str;
+
+ str = pw_properties_get(obj, "type");
+ if (!str) {
+ fprintf(stderr, "invalid object without type\n");
+ return;
+ }
+
+ g = calloc(1, sizeof (struct global));
+ g->data = data;
+
+ if (spa_streq(str, PW_TYPE_INTERFACE_Port)) {
+ g->info_destroy = (pw_destroy_t)pw_port_info_free;
+ g->draw = draw_port;
+ g->type = INTERFACE_Port;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Node)) {
+ g->info_destroy = (pw_destroy_t)pw_node_info_free;
+ g->draw = draw_node;
+ g->type = INTERFACE_Node;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Link)) {
+ g->info_destroy = (pw_destroy_t)pw_link_info_free;
+ g->draw = draw_link;
+ g->type = INTERFACE_Link;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Client)) {
+ g->info_destroy = (pw_destroy_t)pw_client_info_free;
+ g->draw = draw_client;
+ g->type = INTERFACE_Client;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Device)) {
+ g->info_destroy = (pw_destroy_t)pw_device_info_free;
+ g->draw = draw_device;
+ g->type = INTERFACE_Device;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Factory)) {
+ g->info_destroy = (pw_destroy_t)pw_factory_info_free;
+ g->draw = draw_factory;
+ g->type = INTERFACE_Factory;
+ }
+ else if (spa_streq(str, PW_TYPE_INTERFACE_Module)) {
+ g->info_destroy = (pw_destroy_t)pw_module_info_free;
+ g->draw = draw_module;
+ g->type = INTERFACE_Module;
+ }
+ else {
+ free(g);
+ return;
+ }
+
+ g->id = pw_properties_get_uint32(obj, "id", 0);
+
+ str = pw_properties_get(obj, "info");
+ info = pw_properties_new_string(str);
+
+ str = pw_properties_get(info, "props");
+ props = str ? pw_properties_new_string(str) : NULL;
+
+ switch (g->type) {
+ case INTERFACE_Port: {
+ struct pw_port_info pinfo = {0};
+ pinfo.id = g->id;
+ str = pw_properties_get(info, "direction");
+ pinfo.direction = spa_streq(str, "output") ?
+ PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
+ pinfo.props = props ? &props->dict : NULL;
+ pinfo.change_mask = PW_PORT_CHANGE_MASK_PROPS;
+ g->info = pw_port_info_update(NULL, &pinfo);
+ break;
+ }
+ case INTERFACE_Node: {
+ struct pw_node_info ninfo = {0};
+ ninfo.id = g->id;
+ ninfo.max_input_ports =
+ pw_properties_get_uint32(info, "max-input-ports", 0);
+ ninfo.max_output_ports =
+ pw_properties_get_uint32(info, "max-output-ports", 0);
+ ninfo.n_input_ports =
+ pw_properties_get_uint32(info, "n-input-ports", 0);
+ ninfo.n_output_ports =
+ pw_properties_get_uint32(info, "n-output-ports", 0);
+
+ str = pw_properties_get(info, "state");
+ if (spa_streq(str, "running"))
+ ninfo.state = PW_NODE_STATE_RUNNING;
+ else if (spa_streq(str, "idle"))
+ ninfo.state = PW_NODE_STATE_IDLE;
+ else if (spa_streq(str, "suspended"))
+ ninfo.state = PW_NODE_STATE_SUSPENDED;
+ else if (spa_streq(str, "creating"))
+ ninfo.state = PW_NODE_STATE_CREATING;
+ else
+ ninfo.state = PW_NODE_STATE_ERROR;
+ ninfo.error = pw_properties_get(info, "error");
+
+ ninfo.props = props ? &props->dict : NULL;
+ ninfo.change_mask = PW_NODE_CHANGE_MASK_INPUT_PORTS |
+ PW_NODE_CHANGE_MASK_OUTPUT_PORTS |
+ PW_NODE_CHANGE_MASK_STATE |
+ PW_NODE_CHANGE_MASK_PROPS;
+ g->info = pw_node_info_update(NULL, &ninfo);
+ break;
+ }
+ case INTERFACE_Link: {
+ struct pw_link_info linfo = {0};
+ linfo.id = g->id;
+ linfo.output_node_id =
+ pw_properties_get_uint32(info, "output-node-id", 0);
+ linfo.output_port_id =
+ pw_properties_get_uint32(info, "output-port-id", 0);
+ linfo.input_node_id =
+ pw_properties_get_uint32(info, "input-node-id", 0);
+ linfo.input_port_id =
+ pw_properties_get_uint32(info, "input-port-id", 0);
+
+ str = pw_properties_get(info, "state");
+ if (spa_streq(str, "active"))
+ linfo.state = PW_LINK_STATE_ACTIVE;
+ else if (spa_streq(str, "paused"))
+ linfo.state = PW_LINK_STATE_PAUSED;
+ else if (spa_streq(str, "allocating"))
+ linfo.state = PW_LINK_STATE_ALLOCATING;
+ else if (spa_streq(str, "negotiating"))
+ linfo.state = PW_LINK_STATE_NEGOTIATING;
+ else if (spa_streq(str, "init"))
+ linfo.state = PW_LINK_STATE_INIT;
+ else if (spa_streq(str, "unlinked"))
+ linfo.state = PW_LINK_STATE_UNLINKED;
+ else
+ linfo.state = PW_LINK_STATE_ERROR;
+ linfo.error = pw_properties_get(info, "error");
+
+ linfo.props = props ? &props->dict : NULL;
+ linfo.change_mask = PW_LINK_CHANGE_MASK_STATE |
+ PW_LINK_CHANGE_MASK_PROPS;
+ g->info = pw_link_info_update(NULL, &linfo);
+ break;
+ }
+ case INTERFACE_Client: {
+ struct pw_client_info cinfo = {0};
+ cinfo.id = g->id;
+ cinfo.props = props ? &props->dict : NULL;
+ cinfo.change_mask = PW_CLIENT_CHANGE_MASK_PROPS;
+ g->info = pw_client_info_update(NULL, &cinfo);
+ break;
+ }
+ case INTERFACE_Device: {
+ struct pw_device_info dinfo = {0};
+ dinfo.id = g->id;
+ dinfo.props = props ? &props->dict : NULL;
+ dinfo.change_mask = PW_DEVICE_CHANGE_MASK_PROPS;
+ g->info = pw_device_info_update(NULL, &dinfo);
+ break;
+ }
+ case INTERFACE_Factory: {
+ struct pw_factory_info finfo = {0};
+ finfo.id = g->id;
+ finfo.name = pw_properties_get(info, "name");
+ finfo.type = pw_properties_get(info, "type");
+ finfo.version = pw_properties_get_uint32(info, "version", 0);
+ finfo.props = props ? &props->dict : NULL;
+ finfo.change_mask = PW_FACTORY_CHANGE_MASK_PROPS;
+ g->info = pw_factory_info_update(NULL, &finfo);
+ break;
+ }
+ case INTERFACE_Module: {
+ struct pw_module_info minfo = {0};
+ minfo.id = g->id;
+ minfo.name = pw_properties_get(info, "name");
+ minfo.filename = pw_properties_get(info, "filename");
+ minfo.args = pw_properties_get(info, "args");
+ minfo.props = props ? &props->dict : NULL;
+ minfo.change_mask = PW_MODULE_CHANGE_MASK_PROPS;
+ g->info = pw_module_info_update(NULL, &minfo);
+ break;
+ }
+ default:
+ break;
+ }
+
+ pw_properties_free(info);
+ pw_properties_free(props);
+
+ /* add the global to the list */
+ spa_list_insert(&data->globals, &g->link);
+}
+
+static int get_data_from_json(struct data *data, const char *json_path)
+{
+ int fd, len;
+ void *json;
+ struct stat sbuf;
+ struct spa_json it[2];
+ const char *value;
+
+ if ((fd = open(json_path, O_CLOEXEC | O_RDONLY)) < 0) {
+ fprintf(stderr, "error opening file '%s': %m\n", json_path);
+ return -1;
+ }
+ if (fstat(fd, &sbuf) < 0) {
+ fprintf(stderr, "error statting file '%s': %m\n", json_path);
+ close(fd);
+ return -1;
+ }
+ if ((json = mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED) {
+ fprintf(stderr, "error mmapping file '%s': %m\n", json_path);
+ close(fd);
+ return -1;
+ }
+
+ close(fd);
+ spa_json_init(&it[0], json, sbuf.st_size);
+
+ if (spa_json_enter_array(&it[0], &it[1]) <= 0) {
+ fprintf(stderr, "expected top-level array in JSON file '%s'\n", json_path);
+ munmap(json, sbuf.st_size);
+ return -1;
+ }
+
+ while ((len = spa_json_next(&it[1], &value)) > 0 && spa_json_is_object(value, len)) {
+ struct pw_properties *obj;
+ obj = pw_properties_new(NULL, NULL);
+ len = spa_json_container_len(&it[1], value, len);
+ pw_properties_update_string(obj, value, len);
+ handle_json_obj(data, obj);
+ pw_properties_free(obj);
+ }
+
+ munmap(json, sbuf.st_size);
+ return 0;
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -a, --all Show all object types\n"
+ " -s, --smart Show linked objects only\n"
+ " -d, --detail Show all object properties\n"
+ " -r, --remote Remote daemon name\n"
+ " -o, --output Output file (Default %s)\n"
+ " -L, --lr Use left-right rank direction\n"
+ " -9, --90 Use orthogonal edges\n"
+ " -j, --json Read objects from pw-dump JSON file\n",
+ name,
+ DEFAULT_DOT_PATH);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct global *g;
+ const char *opt_remote = NULL;
+ const char *dot_path = DEFAULT_DOT_PATH;
+ const char *json_path = NULL;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "all", no_argument, NULL, 'a' },
+ { "smart", no_argument, NULL, 's' },
+ { "detail", no_argument, NULL, 'd' },
+ { "remote", required_argument, NULL, 'r' },
+ { "output", required_argument, NULL, 'o' },
+ { "lr", no_argument, NULL, 'L' },
+ { "90", no_argument, NULL, '9' },
+ { "json", required_argument, NULL, 'j' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ while ((c = getopt_long(argc, argv, "hVasdr:o:L9j:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h' :
+ show_help(argv[0], false);
+ return 0;
+ case 'V' :
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'a' :
+ data.show_all = true;
+ fprintf(stderr, "all option enabled\n");
+ break;
+ case 's' :
+ data.show_smart = true;
+ fprintf(stderr, "smart option enabled\n");
+ break;
+ case 'd' :
+ data.show_detail = true;
+ fprintf(stderr, "detail option enabled\n");
+ break;
+ case 'r' :
+ opt_remote = optarg;
+ fprintf(stderr, "set remote to %s\n", opt_remote);
+ break;
+ case 'o' :
+ dot_path = optarg;
+ fprintf(stderr, "set output file %s\n", dot_path);
+ break;
+ case 'L' :
+ data.dot_rankdir = "LR";
+ fprintf(stderr, "set rank direction to LR\n");
+ break;
+ case '9' :
+ data.dot_orthoedges = true;
+ fprintf(stderr, "orthogonal edges enabled\n");
+ break;
+ case 'j' :
+ json_path = optarg;
+ fprintf(stderr, "Using JSON file %s as input\n", json_path);
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ if (!(data.dot_str = dot_str_new()))
+ return -1;
+
+ spa_list_init(&data.globals);
+
+ if (!json_path && get_data_from_pipewire(&data, opt_remote) < 0)
+ return -1;
+ else if (json_path && get_data_from_json(&data, json_path) < 0)
+ return -1;
+
+ draw_graph(&data, dot_path);
+
+ dot_str_clear(&data.dot_str);
+ spa_list_consume(g, &data.globals, link) {
+ if (g->info && g->info_destroy)
+ g->info_destroy(g->info);
+ spa_list_remove(&g->link);
+ free(g);
+ }
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-dump.c b/src/tools/pw-dump.c
new file mode 100644
index 0000000..ecc2c55
--- /dev/null
+++ b/src/tools/pw-dump.c
@@ -0,0 +1,1664 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <string.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+#include <fnmatch.h>
+#include <locale.h>
+
+#if !defined(FNM_EXTMATCH)
+#define FNM_EXTMATCH 0
+#endif
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/pod/iter.h>
+#include <spa/debug/types.h>
+#include <spa/utils/json.h>
+#include <spa/utils/ansi.h>
+#include <spa/utils/string.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/extensions/metadata.h>
+
+#define INDENT 2
+
+static bool colors = 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;
+
+ struct pw_core_info *info;
+ struct pw_core *core;
+ struct spa_hook core_listener;
+ int sync_seq;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct spa_list object_list;
+
+ const char *pattern;
+
+ FILE *out;
+ int level;
+#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;
+
+ unsigned int monitor:1;
+};
+
+struct param {
+ uint32_t id;
+ int32_t seq;
+ struct spa_list link;
+ struct spa_pod *param;
+};
+
+struct object;
+
+struct class {
+ const char *type;
+ uint32_t version;
+ const void *events;
+ void (*destroy) (struct object *object);
+ void (*dump) (struct object *object);
+ const char *name_key;
+};
+
+struct object {
+ struct spa_list link;
+
+ struct data *data;
+
+ uint32_t id;
+ uint32_t permissions;
+ char *type;
+ uint32_t version;
+ struct pw_properties *props;
+
+ const struct class *class;
+ void *info;
+ struct spa_param_info *params;
+ uint32_t n_params;
+
+ int changed;
+ struct spa_list param_list;
+ struct spa_list pending_list;
+ struct spa_list data_list;
+
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+};
+
+static void core_sync(struct data *d)
+{
+ d->sync_seq = pw_core_sync(d->core, PW_ID_CORE, d->sync_seq);
+ pw_log_debug("sync start %u", d->sync_seq);
+}
+
+static uint32_t clear_params(struct spa_list *param_list, uint32_t id)
+{
+ struct param *p, *t;
+ uint32_t count = 0;
+
+ spa_list_for_each_safe(p, t, param_list, link) {
+ if (id == SPA_ID_INVALID || p->id == id) {
+ spa_list_remove(&p->link);
+ free(p);
+ count++;
+ }
+ }
+ return count;
+}
+
+static struct param *add_param(struct spa_list *params, int seq,
+ uint32_t id, const struct spa_pod *param)
+{
+ struct param *p;
+
+ if (id == SPA_ID_INVALID) {
+ if (param == NULL || !spa_pod_is_object(param)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ id = SPA_POD_OBJECT_ID(param);
+ }
+
+ p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0));
+ if (p == NULL)
+ return NULL;
+
+ p->id = id;
+ p->seq = seq;
+ if (param != NULL) {
+ p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod);
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ } else {
+ clear_params(params, id);
+ p->param = NULL;
+ }
+ spa_list_append(params, &p->link);
+
+ return p;
+}
+
+static struct object *find_object(struct data *d, uint32_t id)
+{
+ struct object *o;
+ spa_list_for_each(o, &d->object_list, link) {
+ if (o->id == id)
+ return o;
+ }
+ return NULL;
+}
+
+static void object_update_params(struct spa_list *param_list, struct spa_list *pending_list,
+ uint32_t n_params, struct spa_param_info *params)
+{
+ struct param *p, *t;
+ uint32_t i;
+
+ for (i = 0; i < n_params; i++) {
+ spa_list_for_each_safe(p, t, pending_list, link) {
+ if (p->id == params[i].id &&
+ p->seq != params[i].seq &&
+ p->param != NULL) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+ }
+
+ spa_list_consume(p, pending_list, link) {
+ spa_list_remove(&p->link);
+ if (p->param == NULL) {
+ clear_params(param_list, p->id);
+ free(p);
+ } else {
+ spa_list_append(param_list, &p->link);
+ }
+ }
+}
+
+static void object_destroy(struct object *o)
+{
+ spa_list_remove(&o->link);
+ if (o->proxy)
+ pw_proxy_destroy(o->proxy);
+ pw_properties_free(o->props);
+ clear_params(&o->param_list, SPA_ID_INVALID);
+ clear_params(&o->pending_list, SPA_ID_INVALID);
+ free(o->type);
+ free(o);
+}
+
+static void put_key(struct data *d, const char *key);
+
+static SPA_PRINTF_FUNC(3,4) void put_fmt(struct data *d, const char *key, const char *fmt, ...)
+{
+ va_list va;
+ if (key)
+ put_key(d, key);
+ fprintf(d->out, "%s%s%*s",
+ d->state & STATE_COMMA ? "," : "",
+ d->state & (STATE_MASK | STATE_KEY) ? " " : d->state & STATE_FIRST ? "" : "\n",
+ d->state & (STATE_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;
+ char *str = alloca(size);
+ spa_json_encode_string(str, size, key);
+ put_fmt(d, NULL, "%s%s%s:", KEY, str, NORMAL);
+ d->state = (d->state & STATE_MASK) + STATE_KEY;
+}
+
+static void put_begin(struct data *d, const char *key, const char *type, uint32_t flags)
+{
+ put_fmt(d, key, "%s", type);
+ d->level += INDENT;
+ d->state = (d->state & STATE_MASK) + (flags & STATE_SIMPLE);
+}
+
+static void put_end(struct data *d, const char *type, uint32_t flags)
+{
+ d->level -= INDENT;
+ d->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;
+ 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), 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_dict_for_each(it, dict)
+ put_value(d, it->key, it->value);
+ put_end(d, "}", 0);
+}
+
+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:
+ put_value(d, NULL, *(int32_t*)body ? "true" : "false");
+ break;
+ case SPA_TYPE_Id:
+ {
+ const char *str;
+ char fallback[32];
+ uint32_t id = *(uint32_t*)body;
+ str = spa_debug_type_find_short_name(info, *(uint32_t*)body);
+ if (str == NULL) {
+ snprintf(fallback, sizeof(fallback), "id-%08x", id);
+ str = fallback;
+ }
+ put_value(d, NULL, str);
+ break;
+ }
+ case SPA_TYPE_Int:
+ put_int(d, NULL, *(int32_t*)body);
+ break;
+ case SPA_TYPE_Fd:
+ case SPA_TYPE_Long:
+ put_int(d, NULL, *(int64_t*)body);
+ break;
+ case SPA_TYPE_Float:
+ put_double(d, NULL, *(float*)body);
+ break;
+ case SPA_TYPE_Double:
+ put_double(d, NULL, *(double*)body);
+ break;
+ case SPA_TYPE_String:
+ put_string(d, NULL, (const char*)body);
+ break;
+ case SPA_TYPE_Rectangle:
+ {
+ struct spa_rectangle *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);
+ break;
+ }
+ case SPA_TYPE_Fraction:
+ {
+ struct spa_fraction *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);
+ break;
+ }
+ case SPA_TYPE_Array:
+ {
+ struct spa_pod_array_body *b = (struct spa_pod_array_body *)body;
+ void *p;
+ info = info && info->values ? info->values: info;
+ put_begin(d, NULL, "[", STATE_SIMPLE);
+ 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);
+ break;
+ }
+ case SPA_TYPE_Choice:
+ {
+ struct spa_pod_choice_body *b = (struct spa_pod_choice_body *)body;
+ int index = 0;
+
+ if (b->type == SPA_CHOICE_None) {
+ put_pod_value(d, NULL, info, b->child.type,
+ SPA_POD_CONTENTS(struct spa_pod, &b->child),
+ b->child.size);
+ } else {
+ static const char * const range_labels[] = { "default", "min", "max", NULL };
+ static const char * const step_labels[] = { "default", "min", "max", "step", NULL };
+ static const char * const enum_labels[] = { "default", "alt%u" };
+ static const char * const flags_labels[] = { "default", "flag%u" };
+
+ const char * const *labels;
+ const char *label;
+ char buffer[64];
+ int max_labels, flags = 0;
+ void *p;
+
+ switch (b->type) {
+ case SPA_CHOICE_Range:
+ labels = range_labels;
+ max_labels = 3;
+ flags |= STATE_SIMPLE;
+ break;
+ case SPA_CHOICE_Step:
+ labels = step_labels;
+ max_labels = 4;
+ flags |= STATE_SIMPLE;
+ break;
+ case SPA_CHOICE_Enum:
+ labels = enum_labels;
+ max_labels = 1;
+ break;
+ case SPA_CHOICE_Flags:
+ labels = flags_labels;
+ max_labels = 1;
+ break;
+ default:
+ labels = NULL;
+ break;
+ }
+ if (labels == NULL)
+ break;
+
+ put_begin(d, NULL, "{", flags);
+ SPA_POD_CHOICE_BODY_FOREACH(b, size, p) {
+ if ((label = labels[SPA_CLAMP(index, 0, max_labels)]) == NULL)
+ break;
+ snprintf(buffer, sizeof(buffer), label, index);
+ put_pod_value(d, buffer, info, b->child.type, p, b->child.size);
+ index++;
+ }
+ put_end(d, "}", flags);
+ }
+ break;
+ }
+ case SPA_TYPE_Object:
+ {
+ put_begin(d, NULL, "{", 0);
+ struct spa_pod_object_body *b = (struct spa_pod_object_body *)body;
+ struct spa_pod_prop *p;
+ const struct spa_type_info *ti, *ii;
+
+ ti = spa_debug_type_find(info, b->type);
+ ii = ti ? spa_debug_type_find(ti->values, 0) : NULL;
+ ii = ii ? spa_debug_type_find(ii->values, b->id) : NULL;
+
+ info = ti ? ti->values : info;
+
+ SPA_POD_OBJECT_BODY_FOREACH(b, size, p) {
+ char fallback[32];
+ const char *name;
+
+ ii = spa_debug_type_find(info, p->key);
+ name = ii ? spa_debug_type_short_name(ii->name) : NULL;
+ if (name == NULL) {
+ snprintf(fallback, sizeof(fallback), "id-%08x", p->key);
+ name = fallback;
+ }
+ put_pod_value(d, name,
+ ii ? ii->values : NULL,
+ p->value.type,
+ SPA_POD_CONTENTS(struct spa_pod_prop, p),
+ p->value.size);
+ }
+ put_end(d, "}", 0);
+ break;
+ }
+ case SPA_TYPE_Struct:
+ {
+ struct spa_pod *b = (struct spa_pod *)body, *p;
+ put_begin(d, NULL, "[", 0);
+ SPA_POD_FOREACH(b, size, p)
+ put_pod_value(d, NULL, info, p->type, SPA_POD_BODY(p), p->size);
+ put_end(d, "]", 0);
+ break;
+ }
+ case SPA_TYPE_None:
+ put_value(d, NULL, NULL);
+ break;
+ }
+}
+static void put_pod(struct data *d, const char *key, const struct spa_pod *pod)
+{
+ if (pod == NULL) {
+ put_value(d, key, NULL);
+ } else {
+ put_pod_value(d, key, SPA_TYPE_ROOT,
+ SPA_POD_TYPE(pod),
+ SPA_POD_BODY(pod),
+ SPA_POD_BODY_SIZE(pod));
+ }
+}
+
+static void put_params(struct data *d, const char *key,
+ struct spa_param_info *params, uint32_t n_params,
+ struct spa_list *list)
+{
+ uint32_t i;
+
+ put_begin(d, key, "{", 0);
+ for (i = 0; i < n_params; i++) {
+ struct spa_param_info *pi = &params[i];
+ struct param *p;
+ uint32_t flags;
+
+ flags = pi->flags & SPA_PARAM_INFO_READ ? 0 : STATE_SIMPLE;
+
+ put_begin(d, 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);
+ }
+ put_end(d, "}", 0);
+}
+
+struct flags_info {
+ const char *name;
+ uint64_t mask;
+};
+
+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);
+ for (i = 0; info[i].name != NULL; i++) {
+ if (info[i].mask & flags)
+ put_string(d, NULL, info[i].name);
+ }
+ put_end(d, "]", STATE_SIMPLE);
+}
+
+/* core */
+static void core_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_CORE_CHANGE_MASK_PROPS },
+ { NULL, 0 },
+ };
+
+ 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);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static const struct class core_class = {
+ .type = PW_TYPE_INTERFACE_Core,
+ .version = PW_VERSION_CORE,
+ .dump = core_dump,
+ .name_key = PW_KEY_CORE_NAME,
+};
+
+/* client */
+static void client_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_CLIENT_CHANGE_MASK_PROPS },
+ { NULL, 0 },
+ };
+
+ struct data *d = o->data;
+ struct pw_client_info *i = o->info;
+
+ put_begin(d, "info", "{", 0);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static void client_event_info(void *data, const struct pw_client_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_client_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info,
+};
+
+static void client_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_client_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class client_class = {
+ .type = PW_TYPE_INTERFACE_Client,
+ .version = PW_VERSION_CLIENT,
+ .events = &client_events,
+ .destroy = client_destroy,
+ .dump = client_dump,
+ .name_key = PW_KEY_APP_NAME,
+};
+
+/* module */
+static void module_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_MODULE_CHANGE_MASK_PROPS },
+ { NULL, 0 },
+ };
+
+ 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);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static void module_event_info(void *data, const struct pw_module_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_module_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_MODULE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info,
+};
+
+static void module_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_module_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class module_class = {
+ .type = PW_TYPE_INTERFACE_Module,
+ .version = PW_VERSION_MODULE,
+ .events = &module_events,
+ .destroy = module_destroy,
+ .dump = module_dump,
+ .name_key = PW_KEY_MODULE_NAME,
+};
+
+/* factory */
+static void factory_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_FACTORY_CHANGE_MASK_PROPS },
+ { NULL, 0 },
+ };
+
+ 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);
+ put_flags(d, "change-mask", i->change_mask, fl);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static void factory_event_info(void *data, const struct pw_factory_info *info)
+{
+ struct object *o = data;
+ int changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_factory_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_FACTORY_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static const struct pw_factory_events factory_events = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = factory_event_info,
+};
+
+static void factory_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_factory_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class factory_class = {
+ .type = PW_TYPE_INTERFACE_Factory,
+ .version = PW_VERSION_FACTORY,
+ .events = &factory_events,
+ .destroy = factory_destroy,
+ .dump = factory_dump,
+ .name_key = PW_KEY_FACTORY_NAME,
+};
+
+/* device */
+static void device_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_DEVICE_CHANGE_MASK_PROPS },
+ { "params", PW_DEVICE_CHANGE_MASK_PARAMS },
+ { NULL, 0 },
+ };
+
+ struct data *d = o->data;
+ struct pw_device_info *i = o->info;
+
+ put_begin(d, "info", "{", 0);
+ 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);
+}
+
+static void device_event_info(void *data, const struct pw_device_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+ int res;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_device_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ o->params = info->params;
+ o->n_params = info->n_params;
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ changed++;
+ add_param(&o->pending_list, 0, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_device_enum_params((struct pw_device*)o->proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static void device_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data;
+ add_param(&o->pending_list, seq, id, param);
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info,
+ .param = device_event_param,
+};
+
+static void device_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_device_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class device_class = {
+ .type = PW_TYPE_INTERFACE_Device,
+ .version = PW_VERSION_DEVICE,
+ .events = &device_events,
+ .destroy = device_destroy,
+ .dump = device_dump,
+ .name_key = PW_KEY_DEVICE_NAME,
+};
+
+/* node */
+static void node_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "input-ports", PW_NODE_CHANGE_MASK_INPUT_PORTS },
+ { "output-ports", PW_NODE_CHANGE_MASK_OUTPUT_PORTS },
+ { "state", PW_NODE_CHANGE_MASK_STATE },
+ { "props", PW_NODE_CHANGE_MASK_PROPS },
+ { "params", PW_NODE_CHANGE_MASK_PARAMS },
+ { NULL, 0 },
+ };
+
+ 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);
+ 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);
+ put_dict(d, "props", i->props);
+ put_params(d, "params", i->params, i->n_params, &o->param_list);
+ put_end(d, "}", 0);
+}
+
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+ int res;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_node_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ o->params = info->params;
+ o->n_params = info->n_params;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_STATE)
+ changed++;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ changed++;
+ add_param(&o->pending_list, 0, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_node_enum_params((struct pw_node*)o->proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static void node_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data;
+ add_param(&o->pending_list, seq, id, param);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = node_event_param,
+};
+
+static void node_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_node_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class node_class = {
+ .type = PW_TYPE_INTERFACE_Node,
+ .version = PW_VERSION_NODE,
+ .events = &node_events,
+ .destroy = node_destroy,
+ .dump = node_dump,
+ .name_key = PW_KEY_NODE_NAME,
+};
+
+/* port */
+static void port_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "props", PW_PORT_CHANGE_MASK_PROPS },
+ { "params", PW_PORT_CHANGE_MASK_PARAMS },
+ { NULL, },
+ };
+
+ 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));
+ 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);
+}
+
+static void port_event_info(void *data, const struct pw_port_info *info)
+{
+ struct object *o = data;
+ uint32_t i, changed = 0;
+ int res;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_port_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ o->params = info->params;
+ o->n_params = info->n_params;
+
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ uint32_t id = info->params[i].id;
+
+ if (info->params[i].user == 0)
+ continue;
+ info->params[i].user = 0;
+
+ changed++;
+ add_param(&o->pending_list, 0, id, NULL);
+ if (!(info->params[i].flags & SPA_PARAM_INFO_READ))
+ continue;
+
+ res = pw_port_enum_params((struct pw_port*)o->proxy,
+ ++info->params[i].seq, id, 0, -1, NULL);
+ if (SPA_RESULT_IS_ASYNC(res))
+ info->params[i].seq = res;
+ }
+ }
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static void port_event_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct object *o = data;
+ add_param(&o->pending_list, seq, id, param);
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = port_event_info,
+ .param = port_event_param,
+};
+
+static void port_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_port_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class port_class = {
+ .type = PW_TYPE_INTERFACE_Port,
+ .version = PW_VERSION_PORT,
+ .events = &port_events,
+ .destroy = port_destroy,
+ .dump = port_dump,
+ .name_key = PW_KEY_PORT_NAME,
+};
+
+/* link */
+static void link_dump(struct object *o)
+{
+ static const struct flags_info fl[] = {
+ { "state", PW_LINK_CHANGE_MASK_STATE },
+ { "format", PW_LINK_CHANGE_MASK_FORMAT },
+ { "props", PW_LINK_CHANGE_MASK_PROPS },
+ { NULL, },
+ };
+
+ 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);
+ 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);
+ put_pod(d, "format", i->format);
+ put_dict(d, "props", i->props);
+ put_end(d, "}", 0);
+}
+
+static void link_event_info(void *data, const struct pw_link_info *info)
+{
+ struct object *o = data;
+ uint32_t changed = 0;
+
+ pw_log_debug("object %p: id:%d change-mask:%08"PRIx64, o, o->id, info->change_mask);
+
+ info = o->info = pw_link_info_update(o->info, info);
+ if (info == NULL)
+ return;
+
+ if (info->change_mask & PW_LINK_CHANGE_MASK_STATE)
+ changed++;
+
+ if (info->change_mask & PW_LINK_CHANGE_MASK_FORMAT)
+ changed++;
+
+ if (info->change_mask & PW_LINK_CHANGE_MASK_PROPS)
+ changed++;
+
+ if (changed) {
+ o->changed += changed;
+ core_sync(o->data);
+ }
+}
+
+static const struct pw_link_events link_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = link_event_info,
+};
+
+static void link_destroy(struct object *o)
+{
+ if (o->info) {
+ pw_link_info_free(o->info);
+ o->info = NULL;
+ }
+}
+
+static const struct class link_class = {
+ .type = PW_TYPE_INTERFACE_Link,
+ .version = PW_VERSION_LINK,
+ .events = &link_events,
+ .destroy = link_destroy,
+ .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 (spa_json_get_string(&sub, val, sizeof(val)) > 0) {
+ if ((len = spa_json_next(&sub, &value)) <= 0)
+ break;
+ 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;
+ spa_json_init(&it[0], value, strlen(value));
+ if ((len = spa_json_next(&it[0], &val)) >= 0)
+ json_dump_val(d, key, &it[0], val, len);
+}
+
+/* metadata */
+
+struct metadata_entry {
+ struct spa_list link;
+ uint32_t changed;
+ uint32_t subject;
+ char *key;
+ char *value;
+ char *type;
+};
+
+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_list_for_each(e, &o->data_list, link) {
+ 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);
+ e->changed = 0;
+ }
+ put_end(d, "]", 0);
+}
+
+static struct metadata_entry *metadata_find(struct object *o, uint32_t subject, const char *key)
+{
+ struct metadata_entry *e;
+ spa_list_for_each(e, &o->data_list, link) {
+ if ((e->subject == subject) &&
+ (key == NULL || spa_streq(e->key, key)))
+ return e;
+ }
+ return NULL;
+}
+
+static int metadata_property(void *data,
+ uint32_t subject,
+ const char *key,
+ const char *type,
+ const char *value)
+{
+ struct object *o = data;
+ struct metadata_entry *e;
+
+ while ((e = metadata_find(o, subject, key)) != NULL) {
+ spa_list_remove(&e->link);
+ free(e);
+ }
+ if (key != NULL && value != NULL) {
+ size_t size = strlen(key) + 1;
+ size += strlen(value) + 1;
+ size += type ? strlen(type) + 1 : 0;
+
+ e = calloc(1, sizeof(*e) + size);
+ if (e == NULL)
+ return -errno;
+
+ e->subject = subject;
+ e->key = SPA_PTROFF(e, sizeof(*e), void);
+ strcpy(e->key, key);
+ e->value = SPA_PTROFF(e->key, strlen(e->key) + 1, void);
+ strcpy(e->value, value);
+ if (type) {
+ e->type = SPA_PTROFF(e->value, strlen(e->value) + 1, void);
+ strcpy(e->type, type);
+ } else {
+ e->type = NULL;
+ }
+ spa_list_append(&o->data_list, &e->link);
+ e->changed++;
+ }
+ o->changed++;
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property,
+};
+
+static void metadata_destroy(struct object *o)
+{
+ struct metadata_entry *e;
+ spa_list_consume(e, &o->data_list, link) {
+ spa_list_remove(&e->link);
+ free(e);
+ }
+}
+
+static const struct class metadata_class = {
+ .type = PW_TYPE_INTERFACE_Metadata,
+ .version = PW_VERSION_METADATA,
+ .events = &metadata_events,
+ .destroy = metadata_destroy,
+ .dump = metadata_dump,
+ .name_key = PW_KEY_METADATA_NAME,
+};
+
+static const struct class *classes[] =
+{
+ &core_class,
+ &module_class,
+ &factory_class,
+ &client_class,
+ &device_class,
+ &node_class,
+ &port_class,
+ &link_class,
+ &metadata_class,
+};
+
+static const struct class *find_class(const char *type, uint32_t version)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(classes, c) {
+ if (spa_streq((*c)->type, type) &&
+ (*c)->version <= version)
+ return *c;
+ }
+ return NULL;
+}
+
+static void
+destroy_removed(void *data)
+{
+ struct object *o = data;
+ pw_proxy_destroy(o->proxy);
+}
+
+static void
+destroy_proxy(void *data)
+{
+ struct object *o = data;
+
+ spa_hook_remove(&o->proxy_listener);
+ if (o->class != NULL) {
+ if (o->class->events)
+ spa_hook_remove(&o->object_listener);
+ if (o->class->destroy)
+ o->class->destroy(o);
+ }
+ o->proxy = NULL;
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = destroy_removed,
+ .destroy = destroy_proxy,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct object *o;
+
+ o = calloc(1, sizeof(*o));
+ if (o == NULL) {
+ pw_log_error("can't alloc object for %u %s/%d: %m", id, type, version);
+ return;
+ }
+ o->data = d;
+ o->id = id;
+ o->permissions = permissions;
+ o->type = strdup(type);
+ o->version = version;
+ o->props = props ? pw_properties_new_dict(props) : NULL;
+ spa_list_init(&o->param_list);
+ spa_list_init(&o->pending_list);
+ spa_list_init(&o->data_list);
+
+ o->class = find_class(type, version);
+ if (o->class != NULL) {
+ o->proxy = pw_registry_bind(d->registry,
+ id, type, o->class->version, 0);
+ if (o->proxy == NULL)
+ goto bind_failed;
+
+ pw_proxy_add_listener(o->proxy,
+ &o->proxy_listener,
+ &proxy_events, o);
+
+ if (o->class->events)
+ pw_proxy_add_object_listener(o->proxy,
+ &o->object_listener,
+ o->class->events, o);
+ else
+ o->changed++;
+ } else {
+ o->changed++;
+ }
+ spa_list_append(&d->object_list, &o->link);
+
+ core_sync(d);
+ return;
+
+bind_failed:
+ pw_log_error("can't bind object for %u %s/%d: %m", id, type, version);
+ pw_properties_free(o->props);
+ free(o);
+ return;
+}
+
+static bool object_matches(struct object *o, const char *pattern)
+{
+ uint32_t id;
+ const char *str;
+
+ if (spa_atou32(pattern, &id, 0) && o->id == id)
+ return true;
+
+ if (o->props == NULL)
+ return false;
+
+ if (strstr(o->type, pattern) != NULL)
+ return true;
+ if ((str = pw_properties_get(o->props, PW_KEY_OBJECT_PATH)) != NULL &&
+ fnmatch(pattern, str, FNM_EXTMATCH) == 0)
+ return true;
+ if ((str = pw_properties_get(o->props, PW_KEY_OBJECT_SERIAL)) != NULL &&
+ spa_streq(pattern, str))
+ return true;
+ if (o->class != NULL && o->class->name_key != NULL &&
+ (str = pw_properties_get(o->props, o->class->name_key)) != NULL &&
+ fnmatch(pattern, str, FNM_EXTMATCH) == 0)
+ return true;
+ return false;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct data *d = data;
+ struct object *o;
+
+ if ((o = find_object(d, id)) == NULL)
+ return;
+
+ d->state = STATE_FIRST;
+ if (d->pattern != NULL && !object_matches(o, d->pattern))
+ return;
+ if (d->state == STATE_FIRST)
+ put_begin(d, NULL, "[", 0);
+ put_begin(d, NULL, "{", 0);
+ put_int(d, "id", o->id);
+ if (o->class && o->class->dump)
+ put_value(d, "info", NULL);
+ else if (o->props)
+ put_value(d, "props", NULL);
+ put_end(d, "}", 0);
+ if (d->state != STATE_FIRST)
+ put_end(d, "]\n", 0);
+
+ object_destroy(o);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void dump_objects(struct data *d)
+{
+ static const struct flags_info fl[] = {
+ { "r", PW_PERM_R },
+ { "w", PW_PERM_W },
+ { "x", PW_PERM_X },
+ { "m", PW_PERM_M },
+ { NULL, },
+ };
+
+ struct object *o;
+
+ d->state = STATE_FIRST;
+ 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);
+ 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);
+ o->changed = 0;
+ }
+ if (d->state != STATE_FIRST)
+ put_end(d, "]\n", 0);
+}
+
+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 void on_core_info(void *data, const struct pw_core_info *info)
+{
+ struct data *d = data;
+ d->info = pw_core_info_update(d->info, info);
+}
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct data *d = data;
+ struct object *o;
+
+ if (id == PW_ID_CORE) {
+ if (d->sync_seq != seq)
+ return;
+
+ pw_log_debug("sync end %u/%u", d->sync_seq, seq);
+
+ spa_list_for_each(o, &d->object_list, link)
+ object_update_params(&o->param_list, &o->pending_list,
+ o->n_params, o->params);
+
+ dump_objects(d);
+ if (!d->monitor)
+ pw_main_loop_quit(d->loop);
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .done = on_core_done,
+ .info = on_core_info,
+ .error = on_core_error,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options] [<id>]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -m, --monitor monitor changes\n"
+ " -N, --no-colors disable color output\n"
+ " -C, --color[=WHEN] whether to enable color support. WHEN is `never`, `always`, or `auto`\n",
+ name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct object *o;
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "no-colors", no_argument, NULL, 'N' },
+ { "color", optional_argument, NULL, 'C' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ data.out = stdout;
+ if (isatty(fileno(data.out)) && getenv("NO_COLOR") == NULL)
+ colors = true;
+ setlinebuf(data.out);
+
+ while ((c = getopt_long(argc, argv, "hVr:mNC", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h' :
+ show_help(&data, argv[0], false);
+ return 0;
+ case 'V' :
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r' :
+ opt_remote = optarg;
+ break;
+ case 'm' :
+ data.monitor = true;
+ break;
+ 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_help(&data, argv[0], true);
+ return -1;
+ }
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ }
+
+ if (optind < argc)
+ data.pattern = argv[optind++];
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "can't create main loop: %m\n");
+ return -1;
+ }
+
+ 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");
+ return -1;
+ }
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "can't connect: %m\n");
+ return -1;
+ }
+
+ spa_list_init(&data.object_list);
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ pw_main_loop_run(data.loop);
+
+ spa_list_consume(o, &data.object_list, link)
+ object_destroy(o);
+ if (data.info)
+ pw_core_info_free(data.info);
+
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-link.c b/src/tools/pw-link.c
new file mode 100644
index 0000000..8696eb2
--- /dev/null
+++ b/src/tools/pw-link.c
@@ -0,0 +1,912 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <math.h>
+#include <getopt.h>
+#include <regex.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/defs.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/filter.h>
+
+struct object {
+ struct spa_list link;
+
+ uint32_t id;
+#define OBJECT_ANY 0
+#define OBJECT_NODE 1
+#define OBJECT_PORT 2
+#define OBJECT_LINK 3
+ uint32_t type;
+ struct pw_properties *props;
+ uint32_t extra[2];
+};
+
+struct data {
+ struct pw_main_loop *loop;
+
+ const char *opt_remote;
+#define MODE_LIST_OUTPUT (1<<0)
+#define MODE_LIST_INPUT (1<<1)
+#define MODE_LIST_PORTS (MODE_LIST_OUTPUT|MODE_LIST_INPUT)
+#define MODE_LIST_LINKS (1<<2)
+#define MODE_LIST (MODE_LIST_PORTS|MODE_LIST_LINKS)
+#define MODE_MONITOR (1<<3)
+#define MODE_DISCONNECT (1<<4)
+ uint32_t opt_mode;
+ bool opt_id;
+ bool opt_verbose;
+ const char *opt_output;
+ const char *opt_input;
+ struct pw_properties *props;
+
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct spa_list objects;
+
+ int sync;
+ int link_res;
+ bool monitoring;
+ bool list_inputs;
+ bool list_outputs;
+ const char *prefix;
+
+ regex_t out_port_regex, *out_regex;
+ regex_t in_port_regex, *in_regex;
+};
+
+static void link_proxy_error(void *data, int seq, int res, const char *message)
+{
+ struct data *d = data;
+ d->link_res = res;
+}
+
+static const struct pw_proxy_events link_proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .error = link_proxy_error,
+};
+
+static void core_sync(struct data *data)
+{
+ data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync);
+}
+
+static int create_link(struct data *data)
+{
+ struct pw_proxy *proxy;
+ struct spa_hook listener;
+
+ data->link_res = 0;
+
+ proxy = pw_core_create_object(data->core,
+ "link-factory",
+ PW_TYPE_INTERFACE_Link,
+ PW_VERSION_LINK,
+ &data->props->dict, 0);
+ if (proxy == NULL)
+ return -errno;
+
+ spa_zero(listener);
+ pw_proxy_add_listener(proxy, &listener, &link_proxy_events, data);
+
+ core_sync(data);
+ pw_main_loop_run(data->loop);
+
+ spa_hook_remove(&listener);
+
+ pw_proxy_destroy(proxy);
+
+ return data->link_res;
+}
+
+static struct object *find_object(struct data *data, uint32_t type, uint32_t id)
+{
+ struct object *o;
+ spa_list_for_each(o, &data->objects, link)
+ if ((type == OBJECT_ANY || o->type == type) && o->id == id)
+ return o;
+ return NULL;
+}
+
+static struct object *find_node_port(struct data *data, struct object *node, enum pw_direction direction, const char *port_id)
+{
+ struct object *o;
+
+ spa_list_for_each(o, &data->objects, link) {
+ const char *o_port_id;
+ if (o->type != OBJECT_PORT)
+ continue;
+ if (o->extra[1] != node->id)
+ continue;
+ if (o->extra[0] != direction)
+ continue;
+ if ((o_port_id = pw_properties_get(o->props, PW_KEY_PORT_ID)) == NULL)
+ continue;
+ if (spa_streq(o_port_id, port_id))
+ return o;
+ }
+
+ return NULL;
+}
+
+static char *node_name(char *buffer, int size, struct object *n)
+{
+ const char *name;
+ buffer[0] = '\0';
+ if ((name = pw_properties_get(n->props, PW_KEY_NODE_NAME)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s", name);
+ return buffer;
+}
+
+static char *node_path(char *buffer, int size, struct object *n)
+{
+ const char *name;
+ buffer[0] = '\0';
+ if ((name = pw_properties_get(n->props, PW_KEY_OBJECT_PATH)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s", name);
+ return buffer;
+}
+
+static char *port_name(char *buffer, int size, struct object *n, struct object *p)
+{
+ const char *name1, *name2;
+ buffer[0] = '\0';
+ if ((name1 = pw_properties_get(n->props, PW_KEY_NODE_NAME)) == NULL)
+ return buffer;
+ if ((name2 = pw_properties_get(p->props, PW_KEY_PORT_NAME)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s:%s", name1, name2);
+ return buffer;
+}
+
+static char *port_path(char *buffer, int size, struct object *n, struct object *p)
+{
+ const char *name;
+ buffer[0] = '\0';
+ if ((name = pw_properties_get(p->props, PW_KEY_OBJECT_PATH)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s", name);
+ return buffer;
+}
+
+static char *port_alias(char *buffer, int size, struct object *n, struct object *p)
+{
+ const char *name;
+ buffer[0] = '\0';
+ if ((name = pw_properties_get(p->props, PW_KEY_PORT_ALIAS)) == NULL)
+ return buffer;
+ snprintf(buffer, size, "%s", name);
+ return buffer;
+}
+
+static void print_port(struct data *data, const char *prefix, struct object *n,
+ struct object *p, bool verbose)
+{
+ char buffer[1024], id[64] = "", *prefix2 = "";
+ if (data->opt_id) {
+ snprintf(id, sizeof(id), "%4d ", p->id);
+ prefix2 = " ";
+ }
+
+ printf("%s%s%s%s\n", data->prefix, prefix,
+ id, port_name(buffer, sizeof(buffer), n, p));
+ if (verbose) {
+ port_path(buffer, sizeof(buffer), n, p);
+ if (buffer[0] != '\0')
+ printf("%s %s%s%s\n", data->prefix, prefix2, prefix, buffer);
+ port_alias(buffer, sizeof(buffer), n, p);
+ if (buffer[0] != '\0')
+ printf("%s %s%s%s\n", data->prefix, prefix2, prefix, buffer);
+ }
+}
+
+static void print_port_id(struct data *data, const char *prefix, uint32_t peer)
+{
+ struct object *n, *p;
+ if ((p = find_object(data, OBJECT_PORT, peer)) == NULL)
+ return;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ return;
+ print_port(data, prefix, n, p, false);
+}
+
+static void do_list_port_links(struct data *data, struct object *node, struct object *port)
+{
+ struct object *o;
+ bool first = false;
+
+ if ((data->opt_mode & MODE_LIST_PORTS) == 0)
+ first = true;
+
+ spa_list_for_each(o, &data->objects, link) {
+ uint32_t peer;
+ char prefix[64], id[16] = "";
+
+ if (data->opt_id)
+ snprintf(id, sizeof(id), "%4d ", o->id);
+
+ if (o->type != OBJECT_LINK)
+ continue;
+
+ if (port->extra[0] == PW_DIRECTION_OUTPUT &&
+ o->extra[0] == port->id) {
+ peer = o->extra[1];
+ snprintf(prefix, sizeof(prefix), "%s |-> ", id);
+ }
+ else if (port->extra[0] == PW_DIRECTION_INPUT &&
+ o->extra[1] == port->id) {
+ peer = o->extra[0];
+ snprintf(prefix, sizeof(prefix), "%s |<- ", id);
+ }
+ else
+ continue;
+
+ if (first) {
+ print_port(data, "", node, port, data->opt_verbose);
+ first = false;
+ }
+ print_port_id(data, prefix, peer);
+ }
+}
+
+static int node_matches(struct data *data, struct object *n, const char *name)
+{
+ char buffer[1024];
+ uint32_t id = atoi(name);
+ if (n->id == id)
+ return 1;
+ if (spa_streq(node_name(buffer, sizeof(buffer), n), name))
+ return 1;
+ if (spa_streq(node_path(buffer, sizeof(buffer), n), name))
+ return 1;
+ return 0;
+}
+
+static int port_matches(struct data *data, struct object *n, struct object *p, const char *name)
+{
+ char buffer[1024];
+ uint32_t id = atoi(name);
+ if (p->id == id)
+ return 1;
+ if (spa_streq(port_name(buffer, sizeof(buffer), n, p), name))
+ return 1;
+ if (spa_streq(port_path(buffer, sizeof(buffer), n, p), name))
+ return 1;
+ if (spa_streq(port_alias(buffer, sizeof(buffer), n, p), name))
+ return 1;
+ return 0;
+}
+
+static int port_regex(struct data *data, struct object *n, struct object *p, regex_t *regex)
+{
+ char buffer[1024];
+ if (regexec(regex, port_name(buffer, sizeof(buffer), n, p), 0, NULL, 0) == 0)
+ return 1;
+ return 0;
+}
+
+static void do_list_ports(struct data *data, struct object *node,
+ enum pw_direction direction, regex_t *regex)
+{
+ struct object *o;
+ spa_list_for_each(o, &data->objects, link) {
+ if (o->type != OBJECT_PORT)
+ continue;
+ if (o->extra[1] != node->id)
+ continue;
+ if (o->extra[0] != direction)
+ continue;
+
+ if (regex && !port_regex(data, node, o, regex))
+ continue;
+
+ if (data->opt_mode & MODE_LIST_PORTS)
+ print_port(data, "", node, o, data->opt_verbose);
+ if (data->opt_mode & MODE_LIST_LINKS)
+ do_list_port_links(data, node, o);
+ }
+}
+
+static void do_list(struct data *data)
+{
+ struct object *n;
+
+ spa_list_for_each(n, &data->objects, link) {
+ if (n->type != OBJECT_NODE)
+ continue;
+ if (data->list_outputs)
+ do_list_ports(data, n, PW_DIRECTION_OUTPUT, data->out_regex);
+ if (data->list_inputs)
+ do_list_ports(data, n, PW_DIRECTION_INPUT, data->in_regex);
+ }
+}
+
+static int do_link_ports(struct data *data)
+{
+ uint32_t in_port = 0, out_port = 0;
+ struct object *n, *p;
+ struct object *in_node = NULL, *out_node = NULL;
+
+ spa_list_for_each(n, &data->objects, link) {
+ if (n->type != OBJECT_NODE)
+ continue;
+
+ if (out_node == NULL && node_matches(data, n, data->opt_output)) {
+ out_node = n;
+ continue;
+ } else if (in_node == NULL && node_matches(data, n, data->opt_input)) {
+ in_node = n;
+ continue;
+ }
+
+ spa_list_for_each(p, &data->objects, link) {
+ if (p->type != OBJECT_PORT)
+ continue;
+ if (p->extra[1] != n->id)
+ continue;
+
+ if (out_port == 0 && p->extra[0] == PW_DIRECTION_OUTPUT &&
+ port_matches(data, n, p, data->opt_output))
+ out_port = p->id;
+ else if (in_port == 0 && p->extra[0] == PW_DIRECTION_INPUT &&
+ port_matches(data, n, p, data->opt_input))
+ in_port = p->id;
+ }
+ }
+
+ if (in_node && out_node) {
+ int i, ret;
+ char port_id[32];
+ bool all_links_exist = true;
+
+ for (i=0;; i++) {
+ snprintf(port_id, sizeof(port_id), "%d", i);
+
+ struct object *port_out = find_node_port(data, out_node, PW_DIRECTION_OUTPUT, port_id);
+ struct object *port_in = find_node_port(data, in_node, PW_DIRECTION_INPUT, port_id);
+
+ if (!port_out && !port_in) {
+ fprintf(stderr, "Input & output port do not exist\n");
+ goto no_port;
+ } else if (!port_in) {
+ fprintf(stderr, "Input port does not exist\n");
+ goto no_port;
+ } else if (!port_out) {
+ fprintf(stderr, "Output port does not exist\n");
+ goto no_port;
+ }
+
+ pw_properties_setf(data->props, PW_KEY_LINK_OUTPUT_PORT, "%u", port_out->id);
+ pw_properties_setf(data->props, PW_KEY_LINK_INPUT_PORT, "%u", port_in->id);
+
+ if ((ret = create_link(data)) < 0 && ret != -EEXIST)
+ return ret;
+
+ if (ret >= 0)
+ all_links_exist = false;
+ }
+ return (all_links_exist ? -EEXIST : 0);
+ }
+
+ if (in_port == 0 || out_port == 0)
+ return -ENOENT;
+
+ pw_properties_setf(data->props, PW_KEY_LINK_OUTPUT_PORT, "%u", out_port);
+ pw_properties_setf(data->props, PW_KEY_LINK_INPUT_PORT, "%u", in_port);
+
+ return create_link(data);
+
+no_port:
+ return -ENOENT;
+}
+
+static int do_unlink_ports(struct data *data)
+{
+ struct object *l, *n, *p;
+ bool found_any = false;
+ struct object *in_node = NULL, *out_node = NULL;
+
+ if (data->opt_input != NULL) {
+ /* 2 args, check if they are node names */
+ spa_list_for_each(n, &data->objects, link) {
+ if (n->type != OBJECT_NODE)
+ continue;
+
+ if (out_node == NULL && node_matches(data, n, data->opt_output)) {
+ out_node = n;
+ continue;
+ } else if (in_node == NULL && node_matches(data, n, data->opt_input)) {
+ in_node = n;
+ continue;
+ }
+ }
+ }
+
+ spa_list_for_each(l, &data->objects, link) {
+ if (l->type != OBJECT_LINK)
+ continue;
+
+ if (data->opt_input == NULL) {
+ /* 1 arg, check link id */
+ if (l->id != (uint32_t)atoi(data->opt_output))
+ continue;
+ } else if (out_node && in_node) {
+ /* 2 args, check nodes */
+ if ((p = find_object(data, OBJECT_PORT, l->extra[0])) == NULL)
+ continue;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ continue;
+ if (n->id != out_node->id)
+ continue;
+
+ if ((p = find_object(data, OBJECT_PORT, l->extra[1])) == NULL)
+ continue;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ continue;
+ if (n->id != in_node->id)
+ continue;
+ } else {
+ /* 2 args, check port names */
+ if ((p = find_object(data, OBJECT_PORT, l->extra[0])) == NULL)
+ continue;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ continue;
+ if (!port_matches(data, n, p, data->opt_output))
+ continue;
+
+ if ((p = find_object(data, OBJECT_PORT, l->extra[1])) == NULL)
+ continue;
+ if ((n = find_object(data, OBJECT_NODE, p->extra[1])) == NULL)
+ continue;
+ if (!port_matches(data, n, p, data->opt_input))
+ continue;
+ }
+ pw_registry_destroy(data->registry, l->id);
+ found_any = true;
+ }
+ if (!found_any)
+ return -ENOENT;
+
+ core_sync(data);
+ pw_main_loop_run(data->loop);
+
+ return 0;
+}
+
+static int do_monitor_port(struct data *data, struct object *port)
+{
+ regex_t *regex = NULL;
+ bool do_print = false;
+ struct object *node;
+
+ if (port->extra[0] == PW_DIRECTION_OUTPUT && data->list_outputs) {
+ regex = data->out_regex;
+ do_print = true;
+ }
+ if (port->extra[0] == PW_DIRECTION_INPUT && data->list_inputs) {
+ regex = data->in_regex;
+ do_print = true;
+ }
+ if (!do_print)
+ return 0;
+
+ if ((node = find_object(data, OBJECT_NODE, port->extra[1])) == NULL)
+ return -ENOENT;
+
+ if (regex && !port_regex(data, node, port, regex))
+ return 0;
+
+ print_port(data, "", node, port, data->opt_verbose);
+ return 0;
+}
+
+static int do_monitor_link(struct data *data, struct object *link)
+{
+ char buffer1[1024], buffer2[1024], id[64] = "";
+ struct object *n1, *n2, *p1, *p2;
+
+ if (!(data->opt_mode & MODE_LIST_LINKS))
+ return 0;
+
+ if ((p1 = find_object(data, OBJECT_PORT, link->extra[0])) == NULL)
+ return -ENOENT;
+ if ((n1 = find_object(data, OBJECT_NODE, p1->extra[1])) == NULL)
+ return -ENOENT;
+ if (data->out_regex && !port_regex(data, n1, p1, data->out_regex))
+ return 0;
+
+ if ((p2 = find_object(data, OBJECT_PORT, link->extra[1])) == NULL)
+ return -ENOENT;
+ if ((n2 = find_object(data, OBJECT_NODE, p2->extra[1])) == NULL)
+ return -ENOENT;
+ if (data->in_regex && !port_regex(data, n2, p2, data->in_regex))
+ return 0;
+
+ if (data->opt_id)
+ snprintf(id, sizeof(id), "%4d ", link->id);
+
+ printf("%s%s%s -> %s\n", data->prefix, id,
+ port_name(buffer1, sizeof(buffer1), n1, p1),
+ port_name(buffer2, sizeof(buffer2), n2, p2));
+ return 0;
+}
+
+static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ uint32_t t, extra[2];
+ struct object *obj;
+ const char *str;
+
+ if (props == NULL)
+ return;
+
+ spa_zero(extra);
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ t = OBJECT_NODE;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ t = OBJECT_PORT;
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION)) == NULL)
+ return;
+ if (spa_streq(str, "in"))
+ extra[0] = PW_DIRECTION_INPUT;
+ else if (spa_streq(str, "out"))
+ extra[0] = PW_DIRECTION_OUTPUT;
+ else
+ return;
+ if ((str = spa_dict_lookup(props, PW_KEY_NODE_ID)) == NULL)
+ return;
+ extra[1] = atoi(str);
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) {
+ t = OBJECT_LINK;
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_PORT)) == NULL)
+ return;
+ extra[0] = atoi(str);
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT)) == NULL)
+ return;
+ extra[1] = atoi(str);
+ } else
+ return;
+
+ obj = calloc(1, sizeof(*obj));
+ obj->type = t;
+ obj->id = id;
+ obj->props = pw_properties_new_dict(props);
+ memcpy(obj->extra, extra, sizeof(extra));
+ spa_list_append(&d->objects, &obj->link);
+
+ if (d->monitoring) {
+ d->prefix = "+ ";
+ switch (obj->type) {
+ case OBJECT_PORT:
+ do_monitor_port(d, obj);
+ break;
+ case OBJECT_LINK:
+ do_monitor_link(d, obj);
+ break;
+ }
+ }
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct data *d = data;
+ struct object *obj;
+
+ if ((obj = find_object(d, OBJECT_ANY, id)) == NULL)
+ return;
+
+ if (d->monitoring) {
+ d->prefix = "- ";
+ switch (obj->type) {
+ case OBJECT_PORT:
+ do_monitor_port(d, obj);
+ break;
+ case OBJECT_LINK:
+ do_monitor_link(d, obj);
+ break;
+ }
+ }
+
+ spa_list_remove(&obj->link);
+ pw_properties_free(obj->props);
+ free(obj);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct data *d = data;
+ if (d->sync == seq)
+ 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,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%1$s : PipeWire port and link manager.\n"
+ "Generic: %1$s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote=NAME Remote daemon name\n"
+ "List: %1$s [options] [out-pattern] [in-pattern]\n"
+ " -o, --output List output ports\n"
+ " -i, --input List input ports\n"
+ " -l, --links List links\n"
+ " -m, --monitor Monitor links and ports\n"
+ " -I, --id List IDs\n"
+ " -v, --verbose Verbose port properties\n"
+ "Connect: %1$s [options] output input\n"
+ " -L, --linger Linger (default, unless -m is used)\n"
+ " -P, --passive Passive link\n"
+ " -p, --props=PROPS Properties as JSON object\n"
+ "Disconnect: %1$s -d [options] output input\n"
+ " %1$s -d [options] link-id\n"
+ " -d, --disconnect Disconnect ports\n",
+ name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ int res = 0, c;
+ regex_t out_port_regex;
+ regex_t in_port_regex;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "output", no_argument, NULL, 'o' },
+ { "input", no_argument, NULL, 'i' },
+ { "links", no_argument, NULL, 'l' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "id", no_argument, NULL, 'I' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "linger", no_argument, NULL, 'L' },
+ { "passive", no_argument, NULL, 'P' },
+ { "props", required_argument, NULL, 'p' },
+ { "disconnect", no_argument, NULL, 'd' },
+ { NULL, 0, NULL, 0}
+ };
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+ spa_list_init(&data.objects);
+
+ setlinebuf(stdout);
+
+ data.props = pw_properties_new(NULL, NULL);
+ if (data.props == NULL) {
+ fprintf(stderr, "can't create properties: %m\n");
+ return -1;
+ }
+
+ while ((c = getopt_long(argc, argv, "hVr:oilmIvLPp:d", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(&data, argv[0], NULL);
+ 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 'o':
+ data.opt_mode |= MODE_LIST_OUTPUT;
+ break;
+ case 'i':
+ data.opt_mode |= MODE_LIST_INPUT;
+ break;
+ case 'l':
+ data.opt_mode |= MODE_LIST_LINKS;
+ break;
+ case 'm':
+ data.opt_mode |= MODE_MONITOR;
+ break;
+ case 'I':
+ data.opt_id = true;
+ break;
+ case 'v':
+ data.opt_verbose = true;
+ break;
+ case 'L':
+ pw_properties_set(data.props, PW_KEY_OBJECT_LINGER, "true");
+ break;
+ case 'P':
+ pw_properties_set(data.props, PW_KEY_LINK_PASSIVE, "true");
+ break;
+ case 'p':
+ pw_properties_update_string(data.props, optarg, strlen(optarg));
+ break;
+ case 'd':
+ data.opt_mode |= MODE_DISCONNECT;
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ }
+ if (argc == 1)
+ show_help(&data, argv[0], true);
+
+ if (data.opt_id && (data.opt_mode & MODE_LIST) == 0) {
+ fprintf(stderr, "-I option needs one or more of -l, -i or -o\n");
+ return -1;
+ }
+
+ if ((data.opt_mode & MODE_MONITOR) == 0)
+ pw_properties_set(data.props, PW_KEY_OBJECT_LINGER, "true");
+
+ if (optind < argc)
+ data.opt_output = argv[optind++];
+ if (optind < argc)
+ data.opt_input = argv[optind++];
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "can't create mainloop: %m\n");
+ return -1;
+ }
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ return -1;
+ }
+
+ 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: %m\n");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ data.prefix = (data.opt_mode & MODE_MONITOR) ? "= " : "";
+
+ core_sync(&data);
+ pw_main_loop_run(data.loop);
+
+ if ((data.opt_mode & (MODE_LIST_PORTS|MODE_LIST_LINKS)) == MODE_LIST_LINKS)
+ data.list_inputs = data.list_outputs = true;
+ if ((data.opt_mode & MODE_LIST_INPUT) == MODE_LIST_INPUT)
+ data.list_inputs = true;
+ if ((data.opt_mode & MODE_LIST_OUTPUT) == MODE_LIST_OUTPUT)
+ data.list_outputs = true;
+
+ if (data.opt_output) {
+ if (regcomp(&out_port_regex, data.opt_output, REG_EXTENDED | REG_NOSUB) == 0)
+ data.out_regex = &out_port_regex;
+ }
+ if (data.opt_input) {
+ if (regcomp(&in_port_regex, data.opt_input, REG_EXTENDED | REG_NOSUB) == 0)
+ data.in_regex = &in_port_regex;
+ }
+
+ if (data.opt_mode & (MODE_LIST)) {
+ do_list(&data);
+ } else if (data.opt_mode & MODE_DISCONNECT) {
+ if (data.opt_output == NULL) {
+ fprintf(stderr, "missing link-id or output and input port names to disconnect\n");
+ return -1;
+ }
+ if ((res = do_unlink_ports(&data)) < 0) {
+ fprintf(stderr, "failed to unlink ports: %s\n", spa_strerror(res));
+ return -1;
+ }
+ } else {
+ if (data.opt_output == NULL ||
+ data.opt_input == NULL) {
+ fprintf(stderr, "missing output and input port names to connect\n");
+ return -1;
+ }
+ if ((res = do_link_ports(&data)) < 0) {
+ fprintf(stderr, "failed to link ports: %s\n", spa_strerror(res));
+ return -1;
+ }
+ }
+
+ if (data.opt_mode & MODE_MONITOR) {
+ data.monitoring = true;
+ pw_main_loop_run(data.loop);
+ data.monitoring = false;
+ }
+
+ if (data.out_regex)
+ regfree(data.out_regex);
+ if (data.in_regex)
+ regfree(data.in_regex);
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_core_disconnect(data.core);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return res;
+}
diff --git a/src/tools/pw-loopback.c b/src/tools/pw-loopback.c
new file mode 100644
index 0000000..5f39be6
--- /dev/null
+++ b/src/tools/pw-loopback.c
@@ -0,0 +1,284 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Wim Taymans <wim.taymans@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <getopt.h>
+#include <limits.h>
+#include <math.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/pod/builder.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/impl.h>
+
+#define DEFAULT_RATE 48000
+#define DEFAULT_CHANNELS 2
+#define DEFAULT_CHANNEL_MAP "[ FL, FR ]"
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_impl_module *module;
+ struct spa_hook module_listener;
+
+ const char *opt_node_name;
+ const char *opt_group_name;
+ const char *opt_channel_map;
+
+ uint32_t channels;
+ uint32_t latency;
+ float delay;
+
+ struct pw_properties *capture_props;
+ struct pw_properties *playback_props;
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void module_destroy(void *data)
+{
+ struct data *d = data;
+ spa_hook_remove(&d->module_listener);
+ d->module = NULL;
+ pw_main_loop_quit(d->loop);
+}
+
+static const struct pw_impl_module_events module_events = {
+ PW_VERSION_IMPL_MODULE_EVENTS,
+ .destroy = module_destroy
+};
+
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -n, --name Node name (default '%s')\n"
+ " -g, --group Node group (default '%s')\n"
+ " -c, --channels Number of channels (default %d)\n"
+ " -m, --channel-map Channel map (default '%s')\n"
+ " -l, --latency Desired latency in ms\n"
+ " -d, --delay Desired delay in float s\n"
+ " -C --capture Capture source to connect to (name or serial)\n"
+ " --capture-props Capture stream properties\n"
+ " -P --playback Playback sink to connect to (name or serial)\n"
+ " --playback-props Playback stream properties\n",
+ name,
+ data->opt_node_name,
+ data->opt_group_name,
+ data->channels,
+ data->opt_channel_map);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ char cname[256], value[256];
+ char *args;
+ size_t size;
+ FILE *f;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "group", required_argument, NULL, 'g' },
+ { "name", required_argument, NULL, 'n' },
+ { "channels", required_argument, NULL, 'c' },
+ { "latency", required_argument, NULL, 'l' },
+ { "delay", required_argument, NULL, 'd' },
+ { "capture", required_argument, NULL, 'C' },
+ { "playback", required_argument, NULL, 'P' },
+ { "capture-props", required_argument, NULL, 'i' },
+ { "playback-props", required_argument, NULL, 'o' },
+ { NULL, 0, NULL, 0}
+ };
+ int c, res = -1;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ data.channels = DEFAULT_CHANNELS;
+ data.opt_channel_map = DEFAULT_CHANNEL_MAP;
+ data.opt_group_name = pw_get_client_name();
+ if (snprintf(cname, sizeof(cname), "%s-%zd", argv[0], (size_t) getpid()) > 0)
+ data.opt_group_name = cname;
+ data.opt_node_name = data.opt_group_name;
+
+ data.capture_props = pw_properties_new(NULL, NULL);
+ data.playback_props = pw_properties_new(NULL, NULL);
+ if (data.capture_props == NULL || data.playback_props == NULL) {
+ fprintf(stderr, "can't create properties: %m\n");
+ goto exit;
+ }
+
+ while ((c = getopt_long(argc, argv, "hVr:n:g:c:m:l:d:C:P:i:o:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(&data, argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ case 'n':
+ data.opt_node_name = optarg;
+ break;
+ case 'g':
+ data.opt_group_name = optarg;
+ break;
+ case 'c':
+ data.channels = atoi(optarg);
+ break;
+ case 'm':
+ data.opt_channel_map = optarg;
+ break;
+ case 'l':
+ data.latency = atoi(optarg) * DEFAULT_RATE / SPA_MSEC_PER_SEC;
+ break;
+ case 'd':
+ data.delay = atof(optarg);
+ break;
+ case 'C':
+ pw_properties_set(data.capture_props, PW_KEY_TARGET_OBJECT, optarg);
+ break;
+ case 'P':
+ pw_properties_set(data.playback_props, PW_KEY_TARGET_OBJECT, optarg);
+ break;
+ case 'i':
+ pw_properties_update_string(data.capture_props, optarg, strlen(optarg));
+ break;
+ case 'o':
+ pw_properties_update_string(data.playback_props, optarg, strlen(optarg));
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ 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;
+ }
+
+
+ if ((f = open_memstream(&args, &size)) == NULL) {
+ fprintf(stderr, "can't open memstream: %m\n");
+ goto exit;
+ }
+
+ fprintf(f, "{");
+
+ if (opt_remote != NULL)
+ fprintf(f, " remote.name = \"%s\"", opt_remote);
+ if (data.latency != 0)
+ fprintf(f, " node.latency = %u/%u", data.latency, DEFAULT_RATE);
+ if (data.delay != 0.0f)
+ fprintf(f, " target.delay.sec = %s",
+ spa_json_format_float(value, sizeof(value), data.delay));
+ if (data.channels != 0)
+ fprintf(f, " audio.channels = %u", data.channels);
+ if (data.opt_channel_map != NULL)
+ fprintf(f, " audio.position = %s", data.opt_channel_map);
+ if (data.opt_node_name != NULL)
+ fprintf(f, " node.name = %s", data.opt_node_name);
+
+ if (data.opt_group_name != NULL) {
+ pw_properties_set(data.capture_props, PW_KEY_NODE_GROUP, data.opt_group_name);
+ pw_properties_set(data.playback_props, PW_KEY_NODE_GROUP, data.opt_group_name);
+ }
+
+ fprintf(f, " capture.props = {");
+ pw_properties_serialize_dict(f, &data.capture_props->dict, 0);
+ fprintf(f, " } playback.props = {");
+ pw_properties_serialize_dict(f, &data.playback_props->dict, 0);
+ fprintf(f, " } }");
+ fclose(f);
+
+ pw_log_info("loading module with %s", args);
+
+ data.module = pw_context_load_module(data.context,
+ "libpipewire-module-loopback", args,
+ NULL);
+ free(args);
+
+ if (data.module == NULL) {
+ fprintf(stderr, "can't load module: %m\n");
+ goto exit;
+ }
+
+ pw_impl_module_add_listener(data.module,
+ &data.module_listener, &module_events, &data);
+
+ pw_main_loop_run(data.loop);
+
+ res = 0;
+exit:
+ if (data.module)
+ pw_impl_module_destroy(data.module);
+ if (data.context)
+ pw_context_destroy(data.context);
+ if (data.loop)
+ pw_main_loop_destroy(data.loop);
+ pw_properties_free(data.capture_props);
+ pw_properties_free(data.playback_props);
+ pw_deinit();
+
+ return res;
+}
diff --git a/src/tools/pw-metadata.c b/src/tools/pw-metadata.c
new file mode 100644
index 0000000..768bd91
--- /dev/null
+++ b/src/tools/pw-metadata.c
@@ -0,0 +1,297 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <math.h>
+#include <getopt.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/defs.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/filter.h>
+#include <pipewire/extensions/metadata.h>
+
+struct data {
+ struct pw_main_loop *loop;
+
+ const char *opt_remote;
+ const char *opt_name;
+ bool opt_monitor;
+ bool opt_delete;
+ uint32_t opt_id;
+ const char *opt_key;
+ const char *opt_value;
+ const char *opt_type;
+
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_metadata *metadata;
+ struct spa_hook metadata_listener;
+
+ int sync;
+};
+
+
+static int metadata_property(void *data, uint32_t id,
+ const char *key, const char *type, const char *value)
+{
+ struct data *d = data;
+
+ if ((d->opt_id == SPA_ID_INVALID || d->opt_id == id) &&
+ (d->opt_key == NULL || spa_streq(d->opt_key, key))) {
+ if (key == NULL) {
+ printf("remove: id:%u all keys\n", id);
+ } else if (value == NULL) {
+ printf("remove: id:%u key:'%s'\n", id, key);
+ } else {
+ printf("update: id:%u key:'%s' value:'%s' type:'%s'\n", id, key, value, type);
+ }
+ }
+
+ return 0;
+}
+
+static const struct pw_metadata_events metadata_events = {
+ PW_VERSION_METADATA_EVENTS,
+ .property = metadata_property
+};
+
+static void registry_event_global(void *data, uint32_t id, uint32_t permissions,
+ const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ const char *str;
+
+ if (!spa_streq(type, PW_TYPE_INTERFACE_Metadata))
+ return;
+
+ if (props != NULL &&
+ (str = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) != NULL &&
+ !spa_streq(str, d->opt_name))
+ return;
+
+ if (d->metadata != NULL) {
+ pw_log_warn("Multiple metadata: ignoring metadata %d", id);
+ return;
+ }
+
+ printf("Found \"%s\" metadata %d\n", d->opt_name, id);
+ d->metadata = pw_registry_bind(d->registry,
+ id, type, PW_VERSION_METADATA, 0);
+
+ if (d->opt_delete) {
+ if (d->opt_id != SPA_ID_INVALID) {
+ if (d->opt_key != NULL)
+ printf("delete property: id:%u key:%s\n", d->opt_id, d->opt_key);
+ else
+ printf("delete properties: id:%u\n", d->opt_id);
+ pw_metadata_set_property(d->metadata, d->opt_id, d->opt_key, NULL, NULL);
+ } else {
+ printf("delete all properties\n");
+ pw_metadata_clear(d->metadata);
+ }
+ } else if (d->opt_id != SPA_ID_INVALID && d->opt_key != NULL && d->opt_value != NULL) {
+ printf("set property: id:%u key:%s value:%s type:%s\n",
+ d->opt_id, d->opt_key, d->opt_value, d->opt_type);
+ pw_metadata_set_property(d->metadata, d->opt_id, d->opt_key, d->opt_type, d->opt_value);
+ } else {
+ pw_metadata_add_listener(d->metadata,
+ &d->metadata_listener,
+ &metadata_events, d);
+ }
+
+ d->sync = pw_core_sync(d->core, PW_ID_CORE, d->sync);
+}
+
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+};
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct data *d = data;
+ if (d->sync == seq && !d->opt_monitor)
+ 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,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+static void show_help(struct data *data, const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options] [ id [ key [ value [ type ] ] ] ]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -m, --monitor Monitor metadata\n"
+ " -d, --delete Delete metadata\n"
+ " -n, --name Metadata name (default: \"%s\")\n",
+ name, data->opt_name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ int res = 0, c;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "delete", no_argument, NULL, 'd' },
+ { "name", required_argument, NULL, 'n' },
+ { NULL, 0, NULL, 0}
+ };
+
+ setlinebuf(stdout);
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ data.opt_name = "default";
+
+ while ((c = getopt_long(argc, argv, "hVr:mdn:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(&data, argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ data.opt_remote = optarg;
+ break;
+ case 'm':
+ data.opt_monitor = true;
+ break;
+ case 'd':
+ data.opt_delete = true;
+ break;
+ case 'n':
+ data.opt_name = optarg;
+ break;
+ default:
+ show_help(&data, argv[0], true);
+ return -1;
+ }
+ }
+
+ data.opt_id = SPA_ID_INVALID;
+ if (optind < argc)
+ data.opt_id = atoi(argv[optind++]);
+ if (optind < argc)
+ data.opt_key = argv[optind++];
+ if (optind < argc)
+ data.opt_value = argv[optind++];
+ if (optind < argc)
+ data.opt_type = argv[optind++];
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "can't create mainloop: %m\n");
+ return -1;
+ }
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data);
+
+ data.context = pw_context_new(pw_main_loop_get_loop(data.loop), NULL, 0);
+ if (data.context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ return -1;
+ }
+
+ 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: %m\n");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ data.sync = pw_core_sync(data.core, PW_ID_CORE, data.sync);
+
+ pw_main_loop_run(data.loop);
+
+ if (data.metadata)
+ pw_proxy_destroy((struct pw_proxy*)data.metadata);
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_core_disconnect(data.core);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return res;
+}
diff --git a/src/tools/pw-mididump.c b/src/tools/pw-mididump.c
new file mode 100644
index 0000000..d6b803f
--- /dev/null
+++ b/src/tools/pw-mididump.c
@@ -0,0 +1,233 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <math.h>
+#include <getopt.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/defs.h>
+#include <spa/control/control.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/props.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/filter.h>
+
+#include "midifile.h"
+
+struct data;
+
+struct port {
+ struct data *data;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ const char *opt_remote;
+ struct pw_filter *filter;
+ struct port *in_port;
+ int64_t clock_time;
+};
+
+static int dump_file(const char *filename)
+{
+ struct midi_file *file;
+ struct midi_file_info info;
+ struct midi_event ev;
+
+ file = midi_file_open(filename, "r", &info);
+ if (file == NULL) {
+ fprintf(stderr, "error opening %s: %m\n", filename);
+ return -1;
+ }
+
+ printf("opened %s\n", filename);
+
+ while (midi_file_read_event(file, &ev) == 1) {
+ midi_file_dump_event(stdout, &ev);
+ }
+ midi_file_close(file);
+
+ return 0;
+}
+
+static void on_process(void *_data, struct spa_io_position *position)
+{
+ struct data *data = _data;
+ struct pw_buffer *b;
+ struct spa_buffer *buf;
+ struct spa_data *d;
+ struct spa_pod *pod;
+ struct spa_pod_control *c;
+ uint64_t frame;
+
+ frame = data->clock_time;
+ data->clock_time += position->clock.duration;
+
+ b = pw_filter_dequeue_buffer(data->in_port);
+ if (b == NULL)
+ return;
+
+ buf = b->buffer;
+ d = &buf->datas[0];
+
+ if (d->data == NULL)
+ return;
+
+ if ((pod = spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size)) == NULL)
+ return;
+ if (!spa_pod_is_sequence(pod))
+ return;
+
+ SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c) {
+ struct midi_event ev;
+
+ if (c->type != SPA_CONTROL_Midi)
+ continue;
+
+ ev.track = 0;
+ ev.sec = (frame + c->offset) / (float) position->clock.rate.denom;
+ ev.data = SPA_POD_BODY(&c->value),
+ ev.size = SPA_POD_BODY_SIZE(&c->value);
+
+ printf("%4d: ", c->offset);
+ midi_file_dump_event(stdout, &ev);
+ }
+
+ pw_filter_queue_buffer(data->in_port, b);
+}
+
+static const struct pw_filter_events filter_events = {
+ PW_VERSION_FILTER_EVENTS,
+ .process = on_process,
+};
+
+static void do_quit(void *userdata, int signal_number)
+{
+ struct data *data = userdata;
+ pw_main_loop_quit(data->loop);
+}
+
+static int dump_filter(struct data *data)
+{
+ data->loop = pw_main_loop_new(NULL);
+ if (data->loop == NULL)
+ return -errno;
+
+ pw_loop_add_signal(pw_main_loop_get_loop(data->loop), SIGINT, do_quit, data);
+ pw_loop_add_signal(pw_main_loop_get_loop(data->loop), SIGTERM, do_quit, data);
+
+ data->filter = pw_filter_new_simple(
+ pw_main_loop_get_loop(data->loop),
+ "midi-dump",
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, data->opt_remote,
+ PW_KEY_MEDIA_TYPE, "Midi",
+ PW_KEY_MEDIA_CATEGORY, "Filter",
+ PW_KEY_MEDIA_ROLE, "DSP",
+ NULL),
+ &filter_events,
+ data);
+
+ data->in_port = pw_filter_add_port(data->filter,
+ PW_DIRECTION_INPUT,
+ PW_FILTER_PORT_FLAG_MAP_BUFFERS,
+ sizeof(struct port),
+ pw_properties_new(
+ PW_KEY_FORMAT_DSP, "8 bit raw midi",
+ PW_KEY_PORT_NAME, "input",
+ NULL),
+ NULL, 0);
+
+ if (pw_filter_connect(data->filter, PW_FILTER_FLAG_RT_PROCESS, NULL, 0) < 0) {
+ fprintf(stderr, "can't connect\n");
+ return -1;
+ }
+
+ pw_main_loop_run(data->loop);
+
+ pw_filter_destroy(data->filter);
+ pw_main_loop_destroy(data->loop);
+
+ return 0;
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options] [FILE]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n",
+ name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0, };
+ int res = 0, c;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { NULL, 0, NULL, 0}
+ };
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ setlinebuf(stdout);
+
+ while ((c = getopt_long(argc, argv, "hVr:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ data.opt_remote = optarg;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ if (optind < argc) {
+ res = dump_file(argv[optind]);
+ } else {
+ res = dump_filter(&data);
+ }
+ pw_deinit();
+ return res;
+}
diff --git a/src/tools/pw-mon.c b/src/tools/pw-mon.c
new file mode 100644
index 0000000..2eeb4dd
--- /dev/null
+++ b/src/tools/pw-mon.c
@@ -0,0 +1,875 @@
+/* PipeWire
+ *
+ * Copyright © 2018 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <getopt.h>
+#include <unistd.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/utils/ansi.h>
+#include <spa/debug/pod.h>
+#include <spa/debug/format.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/pipewire.h>
+
+struct proxy_data;
+
+typedef void (*print_func_t) (struct proxy_data *data);
+
+static struct pprefix {
+ const char *prefix;
+ const char *suffix;
+} pprefix[2] = {
+ { .prefix = " ", .suffix = "" },
+ { .prefix = "*", .suffix = "" },
+};
+
+#define with_prefix(use_prefix_) \
+ for (bool once_ = !!printf("%s", (pprefix[!!(use_prefix_)]).prefix); \
+ once_; \
+ once_ = false, printf("%s", (pprefix[!!(use_prefix_)]).suffix))
+
+
+struct param {
+ struct spa_list link;
+ uint32_t id;
+ int seq;
+ struct spa_pod *param;
+ unsigned int changed:1;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct spa_list pending_list;
+ struct spa_list global_list;
+};
+
+struct proxy_data {
+ struct data *data;
+ bool first;
+ struct pw_proxy *proxy;
+ uint32_t id;
+ uint32_t permissions;
+ uint32_t version;
+ char *type;
+ void *info;
+ pw_destroy_t destroy;
+ struct spa_hook proxy_listener;
+ struct spa_hook object_listener;
+ int pending_seq;
+ struct spa_list global_link;
+ struct spa_list pending_link;
+ print_func_t print_func;
+ struct spa_list param_list;
+};
+
+static void add_pending(struct proxy_data *pd)
+{
+ struct data *d = pd->data;
+
+ if (pd->pending_seq == 0) {
+ spa_list_append(&d->pending_list, &pd->pending_link);
+ }
+ pd->pending_seq = pw_core_sync(d->core, 0, pd->pending_seq);
+}
+
+static void remove_pending(struct proxy_data *pd)
+{
+ if (pd->pending_seq != 0) {
+ spa_list_remove(&pd->pending_link);
+ pd->pending_seq = 0;
+ }
+}
+
+static void on_core_done(void *data, uint32_t id, int seq)
+{
+ struct data *d = data;
+ struct proxy_data *pd, *t;
+
+ spa_list_for_each_safe(pd, t, &d->pending_list, pending_link) {
+ if (pd->pending_seq == seq) {
+ remove_pending(pd);
+ pd->print_func(pd);
+ }
+ }
+}
+
+static void clear_params(struct proxy_data *data)
+{
+ struct param *p;
+ spa_list_consume(p, &data->param_list, link) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+}
+
+static void remove_params(struct proxy_data *data, uint32_t id, int seq)
+{
+ struct param *p, *t;
+
+ spa_list_for_each_safe(p, t, &data->param_list, link) {
+ if (p->id == id && seq != p->seq) {
+ spa_list_remove(&p->link);
+ free(p);
+ }
+ }
+}
+
+static void event_param(void *_data, int seq, uint32_t id,
+ uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+ struct proxy_data *data = _data;
+ struct param *p;
+
+ /* remove all params with the same id and older seq */
+ remove_params(data, id, seq);
+
+ /* add new param */
+ p = malloc(sizeof(struct param) + SPA_POD_SIZE(param));
+ if (p == NULL) {
+ pw_log_error("can't add param: %m");
+ return;
+ }
+
+ p->id = id;
+ p->seq = seq;
+ p->param = SPA_PTROFF(p, sizeof(struct param), struct spa_pod);
+ p->changed = true;
+ memcpy(p->param, param, SPA_POD_SIZE(param));
+ spa_list_append(&data->param_list, &p->link);
+}
+
+static void print_params(struct proxy_data *data, bool use_prefix)
+{
+ struct param *p;
+
+ with_prefix(use_prefix) {
+ printf("\tparams:\n");
+ }
+
+ spa_list_for_each(p, &data->param_list, link) {
+ with_prefix(p->changed) {
+ printf("\t id:%u (%s)\n",
+ p->id,
+ spa_debug_type_find_name(spa_type_param, p->id));
+ if (spa_pod_is_object_type(p->param, SPA_TYPE_OBJECT_Format))
+ spa_debug_format(10, NULL, p->param);
+ else
+ spa_debug_pod(10, NULL, p->param);
+ }
+ p->changed = false;
+ }
+}
+
+static void print_properties(const struct spa_dict *props, bool use_prefix)
+{
+ const struct spa_dict_item *item;
+
+ with_prefix(use_prefix) {
+ printf("\tproperties:\n");
+ if (props == NULL || props->n_items == 0) {
+ printf("\t\tnone\n");
+ return;
+ }
+ }
+
+ spa_dict_for_each(item, props) {
+ with_prefix(use_prefix) {
+ if (item->value)
+ printf("\t\t%s = \"%s\"\n", item->key, item->value);
+ else
+ printf("\t\t%s = (null)\n", item->key);
+ }
+ }
+}
+
+#define MARK_CHANGE(f) (!!(print_mark && ((info)->change_mask & (f))))
+
+static void on_core_info(void *data, const struct pw_core_info *info)
+{
+ bool print_all = true, print_mark = true;
+
+ printf("\ttype: %s\n", PW_TYPE_INTERFACE_Core);
+ printf("\tcookie: %u\n", info->cookie);
+ printf("\tuser-name: \"%s\"\n", info->user_name);
+ printf("\thost-name: \"%s\"\n", info->host_name);
+ printf("\tversion: \"%s\"\n", info->version);
+ printf("\tname: \"%s\"\n", info->name);
+ if (print_all) {
+ print_properties(info->props, MARK_CHANGE(PW_CORE_CHANGE_MASK_PROPS));
+ }
+}
+
+static void module_event_info(void *_data, const struct pw_module_info *info)
+{
+ struct proxy_data *data = _data;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->info == NULL) {
+ printf("added:\n");
+ print_mark = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ info = data->info = pw_module_info_update(data->info, info);
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+ printf("\tname: \"%s\"\n", info->name);
+ printf("\tfilename: \"%s\"\n", info->filename);
+ printf("\targs: \"%s\"\n", info->args);
+ if (print_all) {
+ print_properties(info->props, MARK_CHANGE(PW_MODULE_CHANGE_MASK_PROPS));
+ }
+}
+
+static const struct pw_module_events module_events = {
+ PW_VERSION_MODULE_EVENTS,
+ .info = module_event_info,
+};
+
+static void print_node(struct proxy_data *data)
+{
+ struct pw_node_info *info = data->info;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->first) {
+ printf("added:\n");
+ print_mark = false;
+ data->first = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+ if (print_all) {
+ print_params(data, MARK_CHANGE(PW_NODE_CHANGE_MASK_PARAMS));
+ with_prefix(MARK_CHANGE(PW_NODE_CHANGE_MASK_INPUT_PORTS)) {
+ printf("\tinput ports: %u/%u\n",
+ info->n_input_ports, info->max_input_ports);
+ }
+ with_prefix(MARK_CHANGE(PW_NODE_CHANGE_MASK_OUTPUT_PORTS)) {
+ printf("\toutput ports: %u/%u\n",
+ info->n_output_ports, info->max_output_ports);
+ }
+ with_prefix(MARK_CHANGE(PW_NODE_CHANGE_MASK_STATE)) {
+ printf("\tstate: \"%s\"",
+ pw_node_state_as_string(info->state));
+ }
+ if (info->state == PW_NODE_STATE_ERROR && info->error)
+ printf(" \"%s\"\n", info->error);
+ else
+ printf("\n");
+ print_properties(info->props, MARK_CHANGE(PW_NODE_CHANGE_MASK_PROPS));
+ }
+}
+
+static void node_event_info(void *_data, const struct pw_node_info *info)
+{
+ struct proxy_data *data = _data;
+ uint32_t i;
+
+ info = data->info = pw_node_info_update(data->info, info);
+
+ if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if (info->params[i].user == 0)
+ continue;
+ remove_params(data, info->params[i].id, 0);
+ if (!SPA_FLAG_IS_SET(info->params[i].flags, SPA_PARAM_INFO_READ))
+ continue;
+ pw_node_enum_params((struct pw_node*)data->proxy,
+ 0, info->params[i].id, 0, 0, NULL);
+ info->params[i].user = 0;
+ }
+ add_pending(data);
+ }
+
+ if (data->pending_seq == 0)
+ data->print_func(data);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE_EVENTS,
+ .info = node_event_info,
+ .param = event_param
+};
+
+static void print_port(struct proxy_data *data)
+{
+ struct pw_port_info *info = data->info;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->first) {
+ printf("added:\n");
+ print_mark = false;
+ data->first = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ printf("\tdirection: \"%s\"\n", pw_direction_as_string(info->direction));
+ if (print_all) {
+ print_params(data, MARK_CHANGE(PW_PORT_CHANGE_MASK_PARAMS));
+ print_properties(info->props, MARK_CHANGE(PW_PORT_CHANGE_MASK_PROPS));
+ }
+}
+
+static void port_event_info(void *_data, const struct pw_port_info *info)
+{
+ struct proxy_data *data = _data;
+ uint32_t i;
+
+ info = data->info = pw_port_info_update(data->info, info);
+
+ if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if (info->params[i].user == 0)
+ continue;
+ remove_params(data, info->params[i].id, 0);
+ if (!SPA_FLAG_IS_SET(info->params[i].flags, SPA_PARAM_INFO_READ))
+ continue;
+ pw_port_enum_params((struct pw_port*)data->proxy,
+ 0, info->params[i].id, 0, 0, NULL);
+ info->params[i].user = 0;
+ }
+ add_pending(data);
+ }
+
+ if (data->pending_seq == 0)
+ data->print_func(data);
+}
+
+static const struct pw_port_events port_events = {
+ PW_VERSION_PORT_EVENTS,
+ .info = port_event_info,
+ .param = event_param
+};
+
+static void factory_event_info(void *_data, const struct pw_factory_info *info)
+{
+ struct proxy_data *data = _data;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->info == NULL) {
+ printf("added:\n");
+ print_mark = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ info = data->info = pw_factory_info_update(data->info, info);
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ printf("\tname: \"%s\"\n", info->name);
+ printf("\tobject-type: %s/%d\n", info->type, info->version);
+ if (print_all) {
+ print_properties(info->props, MARK_CHANGE(PW_FACTORY_CHANGE_MASK_PROPS));
+ }
+}
+
+static const struct pw_factory_events factory_events = {
+ PW_VERSION_FACTORY_EVENTS,
+ .info = factory_event_info
+};
+
+static void client_event_info(void *_data, const struct pw_client_info *info)
+{
+ struct proxy_data *data = _data;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->info == NULL) {
+ printf("added:\n");
+ print_mark = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ info = data->info = pw_client_info_update(data->info, info);
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ if (print_all) {
+ print_properties(info->props, MARK_CHANGE(PW_CLIENT_CHANGE_MASK_PROPS));
+ }
+}
+
+static const struct pw_client_events client_events = {
+ PW_VERSION_CLIENT_EVENTS,
+ .info = client_event_info
+};
+
+static void link_event_info(void *_data, const struct pw_link_info *info)
+{
+ struct proxy_data *data = _data;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->info == NULL) {
+ printf("added:\n");
+ print_mark = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ info = data->info = pw_link_info_update(data->info, info);
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ printf("\toutput-node-id: %u\n", info->output_node_id);
+ printf("\toutput-port-id: %u\n", info->output_port_id);
+ printf("\tinput-node-id: %u\n", info->input_node_id);
+ printf("\tinput-port-id: %u\n", info->input_port_id);
+ if (print_all) {
+ with_prefix(MARK_CHANGE(PW_LINK_CHANGE_MASK_STATE)) {
+ printf("\tstate: \"%s\"",
+ pw_link_state_as_string(info->state));
+ }
+ if (info->state == PW_LINK_STATE_ERROR && info->error)
+ printf(" \"%s\"\n", info->error);
+ else
+ printf("\n");
+ with_prefix(MARK_CHANGE(PW_LINK_CHANGE_MASK_FORMAT)) {
+ printf("\tformat:\n");
+ if (info->format)
+ spa_debug_format(2, NULL, info->format);
+ else
+ printf("\t\tnone\n");
+ }
+ print_properties(info->props, MARK_CHANGE(PW_LINK_CHANGE_MASK_PROPS));
+ }
+}
+
+static const struct pw_link_events link_events = {
+ PW_VERSION_LINK_EVENTS,
+ .info = link_event_info
+};
+
+static void print_device(struct proxy_data *data)
+{
+ struct pw_device_info *info = data->info;
+ bool print_all, print_mark;
+
+ print_all = true;
+ if (data->first) {
+ printf("added:\n");
+ print_mark = false;
+ data->first = false;
+ }
+ else {
+ printf("changed:\n");
+ print_mark = true;
+ }
+
+ printf("\tid: %d\n", data->id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(data->permissions));
+ printf("\ttype: %s (version %d)\n", data->type, data->version);
+
+ if (print_all) {
+ print_params(data, MARK_CHANGE(PW_DEVICE_CHANGE_MASK_PARAMS));
+ print_properties(info->props, MARK_CHANGE(PW_DEVICE_CHANGE_MASK_PROPS));
+ }
+}
+
+
+static void device_event_info(void *_data, const struct pw_device_info *info)
+{
+ struct proxy_data *data = _data;
+ uint32_t i;
+
+ info = data->info = pw_device_info_update(data->info, info);
+
+ if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) {
+ for (i = 0; i < info->n_params; i++) {
+ if (info->params[i].user == 0)
+ continue;
+ remove_params(data, info->params[i].id, 0);
+ if (!SPA_FLAG_IS_SET(info->params[i].flags, SPA_PARAM_INFO_READ))
+ continue;
+ pw_device_enum_params((struct pw_device*)data->proxy,
+ 0, info->params[i].id, 0, 0, NULL);
+ info->params[i].user = 0;
+ }
+ add_pending(data);
+ }
+ if (data->pending_seq == 0)
+ data->print_func(data);
+}
+
+static const struct pw_device_events device_events = {
+ PW_VERSION_DEVICE_EVENTS,
+ .info = device_event_info,
+ .param = event_param
+};
+
+static void
+removed_proxy (void *data)
+{
+ struct proxy_data *pd = data;
+ pw_proxy_destroy(pd->proxy);
+}
+
+static void
+destroy_proxy (void *data)
+{
+ struct proxy_data *pd = data;
+
+ spa_list_remove(&pd->global_link);
+
+ spa_hook_remove(&pd->object_listener);
+ spa_hook_remove(&pd->proxy_listener);
+
+ clear_params(pd);
+ remove_pending(pd);
+ free(pd->type);
+
+ if (pd->info == NULL)
+ return;
+ if (pd->destroy)
+ pd->destroy(pd->info);
+ pd->info = NULL;
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = removed_proxy,
+ .destroy = destroy_proxy,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct pw_proxy *proxy;
+ uint32_t client_version;
+ const void *events;
+ struct proxy_data *pd;
+ pw_destroy_t destroy;
+ print_func_t print_func = NULL;
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ events = &node_events;
+ client_version = PW_VERSION_NODE;
+ destroy = (pw_destroy_t) pw_node_info_free;
+ print_func = print_node;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ events = &port_events;
+ client_version = PW_VERSION_PORT;
+ destroy = (pw_destroy_t) pw_port_info_free;
+ print_func = print_port;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Module)) {
+ events = &module_events;
+ client_version = PW_VERSION_MODULE;
+ destroy = (pw_destroy_t) pw_module_info_free;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Device)) {
+ events = &device_events;
+ client_version = PW_VERSION_DEVICE;
+ destroy = (pw_destroy_t) pw_device_info_free;
+ print_func = print_device;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Factory)) {
+ events = &factory_events;
+ client_version = PW_VERSION_FACTORY;
+ destroy = (pw_destroy_t) pw_factory_info_free;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Client)) {
+ events = &client_events;
+ client_version = PW_VERSION_CLIENT;
+ destroy = (pw_destroy_t) pw_client_info_free;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) {
+ events = &link_events;
+ client_version = PW_VERSION_LINK;
+ destroy = (pw_destroy_t) pw_link_info_free;
+ } else {
+ printf("added:\n");
+ printf("\tid: %u\n", id);
+ printf("\tpermissions: "PW_PERMISSION_FORMAT"\n",
+ PW_PERMISSION_ARGS(permissions));
+ printf("\ttype: %s (version %d)\n", type, version);
+ print_properties(props, false);
+ return;
+ }
+
+ proxy = pw_registry_bind(d->registry, id, type,
+ client_version,
+ sizeof(struct proxy_data));
+ if (proxy == NULL)
+ goto no_mem;
+
+ pd = pw_proxy_get_user_data(proxy);
+ pd->data = d;
+ pd->first = true;
+ pd->proxy = proxy;
+ pd->id = id;
+ pd->permissions = permissions;
+ pd->version = version;
+ pd->type = strdup(type);
+ pd->destroy = destroy;
+ pd->pending_seq = 0;
+ pd->print_func = print_func;
+ spa_list_init(&pd->param_list);
+ pw_proxy_add_object_listener(proxy, &pd->object_listener, events, pd);
+ pw_proxy_add_listener(proxy, &pd->proxy_listener, &proxy_events, pd);
+ spa_list_append(&d->global_list, &pd->global_link);
+
+ return;
+
+no_mem:
+ fprintf(stderr, "failed to create proxy");
+ return;
+}
+
+static struct proxy_data *find_proxy(struct data *d, uint32_t id)
+{
+ struct proxy_data *pd;
+ spa_list_for_each(pd, &d->global_list, global_link) {
+ if (pd->id == id)
+ return pd;
+ }
+ return NULL;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct data *d = data;
+ struct proxy_data *pd;
+
+ printf("removed:\n");
+ printf("\tid: %u\n", id);
+
+ pd = find_proxy(d, id);
+ if (pd == NULL)
+ return;
+ if (pd->proxy)
+ pw_proxy_destroy(pd->proxy);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *data = _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(data->loop);
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .info = on_core_info,
+ .done = on_core_done,
+ .error = on_core_error,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -N, --no-colors disable color output\n"
+ " -C, --color[=WHEN] whether to enable color support. WHEN is `never`, `always`, or `auto`\n",
+ name);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "no-colors", no_argument, NULL, 'N' },
+ { "color", optional_argument, NULL, 'C' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+ bool colors = false;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ setlinebuf(stdout);
+
+ if (isatty(STDERR_FILENO) && getenv("NO_COLOR") == NULL)
+ colors = true;
+
+ while ((c = getopt_long(argc, argv, "hVr:NC", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ case '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, "Invalid color: %s\n", optarg);
+ show_help(argv[0], true);
+ return -1;
+ }
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ if (colors) {
+ pprefix[1].prefix = SPA_ANSI_RED "*";
+ pprefix[1].suffix = SPA_ANSI_RESET;
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "can't create main loop: %m\n");
+ return -1;
+ }
+
+ 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");
+ return -1;
+ }
+
+ spa_list_init(&data.pending_list);
+ spa_list_init(&data.global_list);
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "can't connect: %m\n");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ pw_main_loop_run(data.loop);
+
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-profiler.c b/src/tools/pw-profiler.c
new file mode 100644
index 0000000..7ee85bb
--- /dev/null
+++ b/src/tools/pw-profiler.c
@@ -0,0 +1,665 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <getopt.h>
+#include <locale.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/profiler.h>
+
+#define MAX_NAME 128
+#define MAX_FOLLOWERS 64
+#define DEFAULT_FILENAME "profiler.log"
+
+struct follower {
+ uint32_t id;
+ char name[MAX_NAME];
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ const char *filename;
+ FILE *output;
+
+ int64_t count;
+ int64_t start_status;
+ int64_t last_status;
+
+ struct pw_proxy *profiler;
+ struct spa_hook profiler_listener;
+ int check_profiler;
+
+ uint32_t driver_id;
+
+ int n_followers;
+ struct follower followers[MAX_FOLLOWERS];
+};
+
+struct measurement {
+ int64_t period;
+ int64_t prev_signal;
+ int64_t signal;
+ int64_t awake;
+ int64_t finish;
+ int32_t status;
+};
+
+struct point {
+ int64_t count;
+ float cpu_load[3];
+ struct spa_io_clock clock;
+ struct measurement driver;
+ struct measurement follower[MAX_FOLLOWERS];
+};
+
+static int process_info(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ return spa_pod_parse_struct(pod,
+ SPA_POD_Long(&point->count),
+ SPA_POD_Float(&point->cpu_load[0]),
+ SPA_POD_Float(&point->cpu_load[1]),
+ SPA_POD_Float(&point->cpu_load[2]));
+}
+
+static int process_clock(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ return spa_pod_parse_struct(pod,
+ SPA_POD_Int(&point->clock.flags),
+ SPA_POD_Int(&point->clock.id),
+ SPA_POD_Stringn(point->clock.name, sizeof(point->clock.name)),
+ SPA_POD_Long(&point->clock.nsec),
+ SPA_POD_Fraction(&point->clock.rate),
+ SPA_POD_Long(&point->clock.position),
+ SPA_POD_Long(&point->clock.duration),
+ SPA_POD_Long(&point->clock.delay),
+ SPA_POD_Double(&point->clock.rate_diff),
+ SPA_POD_Long(&point->clock.next_nsec));
+}
+
+static int process_driver_block(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ char *name = NULL;
+ uint32_t driver_id = 0;
+ struct measurement driver;
+ int res;
+
+ spa_zero(driver);
+ if ((res = spa_pod_parse_struct(pod,
+ SPA_POD_Int(&driver_id),
+ SPA_POD_String(&name),
+ SPA_POD_Long(&driver.prev_signal),
+ SPA_POD_Long(&driver.signal),
+ SPA_POD_Long(&driver.awake),
+ SPA_POD_Long(&driver.finish),
+ SPA_POD_Int(&driver.status))) < 0)
+ return res;
+
+ if (d->driver_id == 0) {
+ d->driver_id = driver_id;
+ printf("logging driver %u\n", driver_id);
+ }
+ else if (d->driver_id != driver_id)
+ return -1;
+
+ point->driver = driver;
+ return 0;
+}
+
+static int find_follower(struct data *d, uint32_t id, const char *name)
+{
+ int i;
+ for (i = 0; i < d->n_followers; i++) {
+ if (d->followers[i].id == id && spa_streq(d->followers[i].name, name))
+ return i;
+ }
+ return -1;
+}
+
+static int add_follower(struct data *d, uint32_t id, const char *name)
+{
+ int idx = d->n_followers;
+
+ if (idx == MAX_FOLLOWERS)
+ return -1;
+
+ d->n_followers++;
+
+ strncpy(d->followers[idx].name, name, MAX_NAME);
+ d->followers[idx].name[MAX_NAME-1] = '\0';
+ d->followers[idx].id = id;
+ printf("logging follower %u (\"%s\")\n", id, name);
+
+ return idx;
+}
+
+static int process_follower_block(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ uint32_t id = 0;
+ const char *name = NULL;
+ struct measurement m;
+ int res, idx;
+
+ spa_zero(m);
+ if ((res = spa_pod_parse_struct(pod,
+ SPA_POD_Int(&id),
+ SPA_POD_String(&name),
+ SPA_POD_Long(&m.prev_signal),
+ SPA_POD_Long(&m.signal),
+ SPA_POD_Long(&m.awake),
+ SPA_POD_Long(&m.finish),
+ SPA_POD_Int(&m.status))) < 0)
+ return res;
+
+ if ((idx = find_follower(d, id, name)) < 0) {
+ if ((idx = add_follower(d, id, name)) < 0) {
+ pw_log_warn("too many followers");
+ return -ENOSPC;
+ }
+ }
+ point->follower[idx] = m;
+ return 0;
+}
+
+static void dump_point(struct data *d, struct point *point)
+{
+ int i;
+ int64_t d1, d2;
+ int64_t delay, period_usecs;
+
+#define CLOCK_AS_USEC(cl,val) (val * (float)SPA_USEC_PER_SEC / (cl)->rate.denom)
+#define CLOCK_AS_SUSEC(cl,val) (val * (float)SPA_USEC_PER_SEC / ((cl)->rate.denom * (cl)->rate_diff))
+
+ delay = CLOCK_AS_USEC(&point->clock, point->clock.delay);
+ period_usecs = CLOCK_AS_SUSEC(&point->clock, point->clock.duration);
+
+ d1 = (point->driver.signal - point->driver.prev_signal) / 1000;
+ d2 = (point->driver.finish - point->driver.signal) / 1000;
+
+ if (d1 > period_usecs * 1.3 ||
+ d2 > period_usecs * 1.3)
+ d1 = d2 = period_usecs * 1.4;
+
+ /* 4 columns for the driver */
+ fprintf(d->output, "%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t",
+ d1 > 0 ? d1 : 0, d2 > 0 ? d2 : 0, delay, period_usecs);
+
+ for (i = 0; i < MAX_FOLLOWERS; i++) {
+ /* 8 columns for each follower */
+ if (point->follower[i].status == 0) {
+ fprintf(d->output, " \t \t \t \t \t \t \t \t");
+ } else {
+ int64_t d4 = (point->follower[i].signal - point->driver.signal) / 1000;
+ int64_t d5 = (point->follower[i].awake - point->driver.signal) / 1000;
+ int64_t d6 = (point->follower[i].finish - point->driver.signal) / 1000;
+
+ fprintf(d->output, "%u\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%"PRIi64"\t%d\t0\t",
+ d->followers[i].id,
+ d4 > 0 ? d4 : 0,
+ d5 > 0 ? d5 : 0,
+ d6 > 0 ? d6 : 0,
+ (d5 > 0 && d4 > 0 && d5 > d4) ? d5 - d4 : 0,
+ (d6 > 0 && d5 > 0 && d6 > d5) ? d6 - d5 : 0,
+ point->follower[i].status);
+ }
+ }
+ fprintf(d->output, "\n");
+ if (d->count == 0) {
+ d->start_status = point->clock.nsec;
+ d->last_status = point->clock.nsec;
+ }
+ else if (point->clock.nsec - d->last_status > SPA_NSEC_PER_SEC) {
+ printf("logging %"PRIi64" samples %"PRIi64" seconds [CPU %f %f %f]\r",
+ d->count, (int64_t) ((d->last_status - d->start_status) / SPA_NSEC_PER_SEC),
+ point->cpu_load[0], point->cpu_load[1], point->cpu_load[2]);
+ d->last_status = point->clock.nsec;
+ }
+ d->count++;
+}
+
+static void dump_scripts(struct data *d)
+{
+ FILE *out;
+ int i;
+
+ if (d->driver_id == 0)
+ return;
+
+ printf("\ndumping scripts for %d followers\n", d->n_followers);
+
+ out = fopen("Timing1.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing1.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing1.svg\n"
+ "set terminal svg\n"
+ "set multiplot\n"
+ "set grid\n"
+ "set title \"Audio driver timing\"\n"
+ "set xlabel \"audio cycles\"\n"
+ "set ylabel \"usec\"\n"
+ "plot \"%1$s\" using 3 title \"Audio driver delay\" with lines, "
+ "\"%1$s\" using 1 title \"Audio period\" with lines,"
+ "\"%1$s\" using 4 title \"Audio estimated\" with lines\n"
+ "unset multiplot\n"
+ "unset output\n", d->filename);
+ fclose(out);
+ }
+
+ out = fopen("Timing2.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing2.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing2.svg\n"
+ "set terminal svg\n"
+ "set grid\n"
+ "set title \"Driver end date\"\n"
+ "set xlabel \"audio cycles\"\n"
+ "set ylabel \"usec\"\n"
+ "plot \"%s\" using 2 title \"Driver end date\" with lines \n"
+ "unset output\n", d->filename);
+ fclose(out);
+ }
+
+ out = fopen("Timing3.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing3.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing3.svg\n"
+ "set terminal svg\n"
+ "set multiplot\n"
+ "set grid\n"
+ "set title \"Clients end date\"\n"
+ "set xlabel \"audio cycles\"\n"
+ "set ylabel \"usec\"\n"
+ "plot "
+ "\"%s\" using 1 title \"Audio period\" with lines%s",
+ d->filename,
+ d->n_followers > 0 ? ", " : "");
+
+ for (i = 0; i < d->n_followers; i++) {
+ fprintf(out,
+ "\"%s\" using %d title \"%s/%u\" with lines%s",
+ d->filename, 4 + (i * 8) + 4,
+ d->followers[i].name, d->followers[i].id,
+ i+1 < d->n_followers ? ", " : "");
+ }
+ fprintf(out,
+ "\nunset multiplot\n"
+ "unset output\n");
+ fclose(out);
+ }
+
+ out = fopen("Timing4.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing4.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing4.svg\n"
+ "set terminal svg\n"
+ "set multiplot\n"
+ "set grid\n"
+ "set title \"Clients scheduling latency\"\n"
+ "set xlabel \"audio cycles\"\n"
+ "set ylabel \"usec\"\n"
+ "plot ");
+
+ for (i = 0; i < d->n_followers; i++) {
+ fprintf(out,
+ "\"%s\" using %d title \"%s/%u\" with lines%s",
+ d->filename, 4 + (i * 8) + 5,
+ d->followers[i].name, d->followers[i].id,
+ i+1 < d->n_followers ? ", " : "");
+ }
+ fprintf(out,
+ "\nunset multiplot\n"
+ "unset output\n");
+ fclose(out);
+ }
+
+ out = fopen("Timing5.plot", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timing5.plot: %m");
+ } else {
+ fprintf(out,
+ "set output 'Timing5.svg\n"
+ "set terminal svg\n"
+ "set multiplot\n"
+ "set grid\n"
+ "set title \"Clients duration\"\n"
+ "set xlabel \"audio cycles\"\n"
+ "set ylabel \"usec\"\n"
+ "plot ");
+
+ for (i = 0; i < d->n_followers; i++) {
+ fprintf(out,
+ "\"%s\" using %d title \"%s/%u\" with lines%s",
+ d->filename, 4 + (i * 8) + 6,
+ d->followers[i].name, d->followers[i].id,
+ i+1 < d->n_followers ? ", " : "");
+ }
+ fprintf(out,
+ "\nunset multiplot\n"
+ "unset output\n");
+ fclose(out);
+ }
+ out = fopen("Timings.html", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open Timings.html: %m");
+ } else {
+ fprintf(out,
+ "<?xml version='1.0' encoding='utf-8'?>\n"
+ "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n"
+ "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"
+ "<html xmlns='http://www.w3.org/1999/xhtml' lang='en'>\n"
+ " <head>\n"
+ " <title>PipeWire profiling</title>\n"
+ " <!-- assuming that images are 600px wide -->\n"
+ " <style media='all' type='text/css'>\n"
+ " .center { margin-left:auto ; margin-right: auto; width: 650px; height: 550px }\n"
+ " </style>\n"
+ " </head>\n"
+ " <body>\n"
+ " <h2 style='text-align:center'>PipeWire profiling</h2>\n"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing1.svg'>Timing1</object></div>"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing2.svg'>Timing2</object></div>"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing3.svg'>Timing3</object></div>"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing4.svg'>Timing4</object></div>"
+ " <div class='center'><object class='center' type='image/svg+xml' data='Timing5.svg'>Timing5</object></div>"
+ " </body>\n"
+ "</html>\n");
+ fclose(out);
+ }
+
+ out = fopen("generate_timings.sh", "we");
+ if (out == NULL) {
+ pw_log_error("Can't open generate_timings.sh: %m");
+ } else {
+ fprintf(out,
+ "gnuplot Timing1.plot\n"
+ "gnuplot Timing2.plot\n"
+ "gnuplot Timing3.plot\n"
+ "gnuplot Timing4.plot\n"
+ "gnuplot Timing5.plot\n");
+ fclose(out);
+ }
+ printf("run 'sh generate_timings.sh' and load Timings.html in a browser\n");
+}
+
+static void profiler_profile(void *data, const struct spa_pod *pod)
+{
+ struct data *d = data;
+ struct spa_pod *o;
+ struct spa_pod_prop *p;
+ struct point point;
+
+ SPA_POD_STRUCT_FOREACH(pod, o) {
+ int res = 0;
+ if (!spa_pod_is_object_type(o, SPA_TYPE_OBJECT_Profiler))
+ continue;
+
+ spa_zero(point);
+ SPA_POD_OBJECT_FOREACH((struct spa_pod_object*)o, p) {
+ switch(p->key) {
+ case SPA_PROFILER_info:
+ res = process_info(d, &p->value, &point);
+ break;
+ case SPA_PROFILER_clock:
+ res = process_clock(d, &p->value, &point);
+ break;
+ case SPA_PROFILER_driverBlock:
+ res = process_driver_block(d, &p->value, &point);
+ break;
+ case SPA_PROFILER_followerBlock:
+ process_follower_block(d, &p->value, &point);
+ break;
+ default:
+ break;
+ }
+ if (res < 0)
+ break;
+ }
+ if (res < 0)
+ continue;
+
+ dump_point(d, &point);
+ }
+}
+
+static const struct pw_profiler_events profiler_events = {
+ PW_VERSION_PROFILER_EVENTS,
+ .profile = profiler_profile,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct pw_proxy *proxy;
+
+ if (!spa_streq(type, PW_TYPE_INTERFACE_Profiler))
+ return;
+
+ if (d->profiler != NULL) {
+ fprintf(stderr, "Ignoring profiler %d: already attached\n", id);
+ return;
+ }
+
+ proxy = pw_registry_bind(d->registry, id, type, PW_VERSION_PROFILER, 0);
+ if (proxy == NULL)
+ goto error_proxy;
+
+ printf("Attaching to Profiler id:%d\n", id);
+ d->profiler = proxy;
+ pw_proxy_add_object_listener(proxy, &d->profiler_listener, &profiler_events, d);
+
+ return;
+
+error_proxy:
+ pw_log_error("failed to create proxy: %m");
+ return;
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+};
+
+static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *data = _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(data->loop);
+}
+
+static void on_core_done(void *_data, uint32_t id, int seq)
+{
+ struct data *d = _data;
+
+ if (seq == d->check_profiler) {
+ if (d->profiler == NULL) {
+ pw_log_error("no Profiler Interface found, please load one in the server");
+ pw_main_loop_quit(d->loop);
+ }
+ }
+}
+
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+ .done = on_core_done,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n"
+ " -o, --output Profiler output name (default \"%s\")\n",
+ name,
+ DEFAULT_FILENAME);
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ const char *opt_output = DEFAULT_FILENAME;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { "output", required_argument, NULL, 'o' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ while ((c = getopt_long(argc, argv, "hVr:o:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'o':
+ opt_output = optarg;
+ break;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "Can't create data loop: %m\n");
+ return -1;
+ }
+
+ 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");
+ return -1;
+ }
+
+ pw_context_load_module(data.context, PW_EXTENSION_MODULE_PROFILER, NULL, NULL);
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "Can't connect: %m\n");
+ return -1;
+ }
+
+ data.filename = opt_output;
+
+ data.output = fopen(data.filename, "we");
+ if (data.output == NULL) {
+ fprintf(stderr, "Can't open file %s: %m\n", data.filename);
+ return -1;
+ }
+
+ printf("Logging to %s\n", data.filename);
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ data.check_profiler = pw_core_sync(data.core, 0, 0);
+
+ pw_main_loop_run(data.loop);
+
+ if (data.profiler) {
+ spa_hook_remove(&data.profiler_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.profiler);
+ }
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+
+ fclose(data.output);
+
+ dump_scripts(&data);
+
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/pw-reserve.c b/src/tools/pw-reserve.c
new file mode 100644
index 0000000..45a3c45
--- /dev/null
+++ b/src/tools/pw-reserve.c
@@ -0,0 +1,253 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <getopt.h>
+#include <signal.h>
+#include <locale.h>
+
+#include <dbus/dbus.h>
+
+#include <spa/utils/result.h>
+#include <spa/support/dbus.h>
+
+#include "pipewire/pipewire.h"
+#include "pipewire/log.h"
+
+#include "reserve.h"
+
+struct impl {
+ struct pw_main_loop *mainloop;
+ struct pw_loop *loop;
+ struct pw_context *context;
+
+ struct spa_dbus *dbus;
+ struct spa_dbus_connection *dbus_connection;
+ DBusConnection *conn;
+
+ struct rd_device *device;
+};
+
+static void reserve_acquired(void *data, struct rd_device *d)
+{
+ printf("reserve acquired\n");
+}
+
+static void reserve_release(void *data, struct rd_device *d, int forced)
+{
+ struct impl *impl = data;
+ printf("reserve release\n");
+ rd_device_complete_release(impl->device, true);
+}
+
+static void reserve_busy(void *data, struct rd_device *d, const char *name, int32_t prio)
+{
+ printf("reserve busy %s, prio %d\n", name, prio);
+}
+
+static void reserve_available(void *data, struct rd_device *d, const char *name)
+{
+ printf("reserve available %s\n", name);
+}
+
+static const struct rd_device_callbacks reserve_callbacks = {
+ .acquired = reserve_acquired,
+ .release = reserve_release,
+ .busy = reserve_busy,
+ .available = reserve_available,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct impl *impl = data;
+ pw_main_loop_quit(impl->mainloop);
+}
+
+#define DEFAULT_APPNAME "pw-reserve"
+#define DEFAULT_PRIORITY 0
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -n, --name Name to reserve (Audio0, Midi0, Video0, ..)\n"
+ " -a, --appname Application Name (default %s)\n"
+ " -p, --priority Priority (default %d)\n"
+ " -m, --monitor Monitor only, don't try to acquire\n"
+ " -r, --release Request release when busy\n",
+ name, DEFAULT_APPNAME, DEFAULT_PRIORITY);
+}
+
+int main(int argc, char *argv[])
+{
+ struct impl impl = { 0, };
+ const struct spa_support *support;
+ uint32_t n_support;
+ const char *opt_name = NULL;
+ const char *opt_appname = DEFAULT_APPNAME;
+ bool opt_monitor = false;
+ bool opt_release = false;
+ int opt_priority= DEFAULT_PRIORITY;
+
+ int res = 0, c;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "name", required_argument, NULL, 'n' },
+ { "app", required_argument, NULL, 'a' },
+ { "priority", required_argument, NULL, 'p' },
+ { "monitor", no_argument, NULL, 'm' },
+ { "release", no_argument, NULL, 'r' },
+ { NULL, 0, NULL, 0}
+ };
+
+ setlinebuf(stdout);
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ while ((c = getopt_long(argc, argv, "hVn:a:p:mr", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'n':
+ opt_name = optarg;
+ break;
+ case 'a':
+ opt_appname = optarg;
+ break;
+ case 'p':
+ opt_priority = atoi(optarg);
+ break;
+ case 'm':
+ opt_monitor = true;
+ break;
+ case 'r':
+ opt_release = true;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+ if (opt_name == NULL) {
+ fprintf(stderr, "name must be given\n");
+ return -1;
+ }
+
+ impl.mainloop = pw_main_loop_new(NULL);
+ if (impl.mainloop == NULL) {
+ fprintf(stderr, "can't create mainloop: %m\n");
+ res = -errno;
+ goto exit;
+ }
+ impl.loop = pw_main_loop_get_loop(impl.mainloop);
+
+ pw_loop_add_signal(impl.loop, SIGINT, do_quit, &impl);
+ pw_loop_add_signal(impl.loop, SIGTERM, do_quit, &impl);
+
+ impl.context = pw_context_new(impl.loop, NULL, 0);
+ if (impl.context == NULL) {
+ fprintf(stderr, "can't create context: %m\n");
+ res = -errno;
+ goto exit;
+ }
+
+ support = pw_context_get_support(impl.context, &n_support);
+
+ impl.dbus = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DBus);
+ if (impl.dbus)
+ impl.dbus_connection = spa_dbus_get_connection(impl.dbus, SPA_DBUS_TYPE_SESSION);
+ if (impl.dbus_connection == NULL) {
+ fprintf(stderr, "no dbus connection: %m\n");
+ res = -errno;
+ goto exit;
+ }
+ impl.conn = spa_dbus_connection_get(impl.dbus_connection);
+ if (impl.conn == NULL) {
+ fprintf(stderr, "no dbus connection: %m\n");
+ res = -errno;
+ goto exit;
+ }
+
+ /* XXX: we don't handle dbus reconnection yet, so ref the handle instead */
+ dbus_connection_ref(impl.conn);
+
+ impl.device = rd_device_new(impl.conn,
+ opt_name,
+ opt_appname,
+ opt_priority,
+ &reserve_callbacks, &impl);
+
+ if (!opt_monitor) {
+ res = rd_device_acquire(impl.device);
+ if (res == -EBUSY) {
+ printf("device %s is busy\n", opt_name);
+ if (opt_release) {
+ printf("doing RequestRelease on %s\n", opt_name);
+ res = rd_device_request_release(impl.device);
+ } else {
+ printf("use -r to attempt to release\n");
+ }
+ } else if (res < 0) {
+ printf("Device %s can not be acquired: %s\n", opt_name,
+ spa_strerror(res));
+ }
+ }
+
+ if (res >= 0)
+ pw_main_loop_run(impl.mainloop);
+
+ if (!opt_monitor) {
+ if (opt_release) {
+ printf("doing Release on %s\n", opt_name);
+ rd_device_release(impl.device);
+ }
+ }
+
+exit:
+ if (impl.conn)
+ dbus_connection_unref(impl.conn);
+ if (impl.dbus)
+ spa_dbus_connection_destroy(impl.dbus_connection);
+ if (impl.context)
+ pw_context_destroy(impl.context);
+ if (impl.mainloop)
+ pw_main_loop_destroy(impl.mainloop);
+
+ pw_deinit();
+
+ return res;
+}
diff --git a/src/tools/pw-top.c b/src/tools/pw-top.c
new file mode 100644
index 0000000..a20bdf9
--- /dev/null
+++ b/src/tools/pw-top.c
@@ -0,0 +1,862 @@
+/* PipeWire
+ *
+ * Copyright © 2020 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <signal.h>
+#include <getopt.h>
+#include <locale.h>
+#include <ncurses.h>
+
+#include <spa/utils/result.h>
+#include <spa/utils/string.h>
+#include <spa/pod/parser.h>
+#include <spa/debug/types.h>
+#include <spa/param/format-utils.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/video/format-utils.h>
+
+#include <pipewire/impl.h>
+#include <pipewire/extensions/profiler.h>
+
+#define MAX_FORMAT 16
+#define MAX_NAME 128
+
+struct driver {
+ int64_t count;
+ float cpu_load[3];
+ struct spa_io_clock clock;
+ uint32_t xrun_count;
+};
+
+struct measurement {
+ int32_t index;
+ int32_t status;
+ int64_t quantum;
+ int64_t prev_signal;
+ int64_t signal;
+ int64_t awake;
+ int64_t finish;
+ struct spa_fraction latency;
+};
+
+struct node {
+ struct spa_list link;
+ struct data *data;
+ uint32_t id;
+ char name[MAX_NAME+1];
+ enum pw_node_state state;
+ struct measurement measurement;
+ struct driver info;
+ struct node *driver;
+ uint32_t errors;
+ int32_t last_error_status;
+ uint32_t generation;
+ char format[MAX_FORMAT+1];
+ struct pw_proxy *proxy;
+ struct spa_hook proxy_listener;
+ unsigned int inactive:1;
+ struct spa_hook object_listener;
+};
+
+struct data {
+ struct pw_main_loop *loop;
+ struct pw_context *context;
+
+ struct pw_core *core;
+ struct spa_hook core_listener;
+
+ struct pw_registry *registry;
+ struct spa_hook registry_listener;
+
+ struct pw_proxy *profiler;
+ struct spa_hook profiler_listener;
+ int check_profiler;
+
+ struct spa_source *timer;
+
+ int n_nodes;
+ struct spa_list node_list;
+ uint32_t generation;
+ unsigned pending_refresh:1;
+
+ WINDOW *win;
+};
+
+struct point {
+ struct node *driver;
+ struct driver info;
+};
+
+static int process_info(struct data *d, const struct spa_pod *pod, struct driver *info)
+{
+ return spa_pod_parse_struct(pod,
+ SPA_POD_Long(&info->count),
+ SPA_POD_Float(&info->cpu_load[0]),
+ SPA_POD_Float(&info->cpu_load[1]),
+ SPA_POD_Float(&info->cpu_load[2]),
+ SPA_POD_Int(&info->xrun_count));
+}
+
+static int process_clock(struct data *d, const struct spa_pod *pod, struct driver *info)
+{
+ return spa_pod_parse_struct(pod,
+ SPA_POD_Int(&info->clock.flags),
+ SPA_POD_Int(&info->clock.id),
+ SPA_POD_Stringn(info->clock.name, sizeof(info->clock.name)),
+ SPA_POD_Long(&info->clock.nsec),
+ SPA_POD_Fraction(&info->clock.rate),
+ SPA_POD_Long(&info->clock.position),
+ SPA_POD_Long(&info->clock.duration),
+ SPA_POD_Long(&info->clock.delay),
+ SPA_POD_Double(&info->clock.rate_diff),
+ SPA_POD_Long(&info->clock.next_nsec));
+}
+
+static struct node *find_node(struct data *d, uint32_t id)
+{
+ struct node *n;
+ spa_list_for_each(n, &d->node_list, link) {
+ if (n->id == id)
+ return n;
+ }
+ return NULL;
+}
+
+static void on_node_removed(void *data)
+{
+ struct node *n = data;
+ pw_proxy_destroy(n->proxy);
+}
+
+static void on_node_destroy(void *data)
+{
+ struct node *n = data;
+ n->proxy = NULL;
+ spa_hook_remove(&n->proxy_listener);
+ spa_hook_remove(&n->object_listener);
+}
+
+static const struct pw_proxy_events proxy_events = {
+ PW_VERSION_PROXY_EVENTS,
+ .removed = on_node_removed,
+ .destroy = on_node_destroy,
+};
+
+static void do_refresh(struct data *d);
+
+static void node_info(void *data, const struct pw_node_info *info)
+{
+ struct node *n = data;
+
+ if (n->state != info->state) {
+ n->state = info->state;
+ do_refresh(n->data);
+ }
+}
+
+static void node_param(void *data, int seq,
+ uint32_t id, uint32_t index, uint32_t next,
+ const struct spa_pod *param)
+{
+ struct node *n = data;
+
+ if (param == NULL) {
+ spa_zero(n->format);
+ goto done;
+ }
+
+ switch (id) {
+ case SPA_PARAM_Format:
+ {
+ uint32_t media_type, media_subtype;
+
+ spa_format_parse(param, &media_type, &media_subtype);
+
+ switch(media_type) {
+ case SPA_MEDIA_TYPE_audio:
+ switch(media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ {
+ struct spa_audio_info_raw info = { 0 };
+ if (spa_format_audio_raw_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "%6.6s %d %d",
+ spa_debug_type_find_short_name(
+ spa_type_audio_format, info.format),
+ info.channels, info.rate);
+ }
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_dsd:
+ {
+ struct spa_audio_info_dsd info = { 0 };
+ if (spa_format_audio_dsd_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "DSD%d %d ",
+ 8 * info.rate / 44100, info.channels);
+
+ }
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_iec958:
+ {
+ struct spa_audio_info_iec958 info = { 0 };
+ if (spa_format_audio_iec958_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "IEC958 %s %d",
+ spa_debug_type_find_short_name(
+ spa_type_audio_iec958_codec, info.codec),
+ info.rate);
+
+ }
+ break;
+ }
+ }
+ break;
+ case SPA_MEDIA_TYPE_video:
+ switch(media_subtype) {
+ case SPA_MEDIA_SUBTYPE_raw:
+ {
+ struct spa_video_info_raw info = { 0 };
+ if (spa_format_video_raw_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "%6.6s %dx%d",
+ spa_debug_type_find_short_name(spa_type_video_format, info.format),
+ info.size.width, info.size.height);
+ }
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_mjpg:
+ {
+ struct spa_video_info_mjpg info = { 0 };
+ if (spa_format_video_mjpg_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "MJPG %dx%d",
+ info.size.width, info.size.height);
+ }
+ break;
+ }
+ case SPA_MEDIA_SUBTYPE_h264:
+ {
+ struct spa_video_info_h264 info = { 0 };
+ if (spa_format_video_h264_parse(param, &info) >= 0) {
+ snprintf(n->format, sizeof(n->format), "H264 %dx%d",
+ info.size.width, info.size.height);
+ }
+ break;
+ }
+ }
+ break;
+ case SPA_MEDIA_TYPE_application:
+ switch(media_subtype) {
+ case SPA_MEDIA_SUBTYPE_control:
+ snprintf(n->format, sizeof(n->format), "%s", "CONTROL");
+ break;
+ }
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+done:
+ do_refresh(n->data);
+}
+
+static const struct pw_node_events node_events = {
+ PW_VERSION_NODE,
+ .info = node_info,
+ .param = node_param,
+};
+
+static struct node *add_node(struct data *d, uint32_t id, const char *name)
+{
+ struct node *n;
+
+ if ((n = calloc(1, sizeof(*n))) == NULL)
+ return NULL;
+
+ if (name)
+ strncpy(n->name, name, MAX_NAME);
+ else
+ snprintf(n->name, sizeof(n->name), "%u", id);
+ n->data = d;
+ n->id = id;
+ n->driver = n;
+ n->proxy = pw_registry_bind(d->registry, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
+ if (n->proxy) {
+ uint32_t ids[1] = { SPA_PARAM_Format };
+
+ pw_proxy_add_listener(n->proxy,
+ &n->proxy_listener, &proxy_events, n);
+ pw_proxy_add_object_listener(n->proxy,
+ &n->object_listener, &node_events, n);
+
+ pw_node_subscribe_params((struct pw_node*)n->proxy,
+ ids, 1);
+ }
+ spa_list_append(&d->node_list, &n->link);
+ d->n_nodes++;
+ d->pending_refresh = true;
+
+ return n;
+}
+
+static void remove_node(struct data *d, struct node *n)
+{
+ if (n->proxy)
+ pw_proxy_destroy(n->proxy);
+ spa_list_remove(&n->link);
+ d->n_nodes--;
+ d->pending_refresh = true;
+ free(n);
+}
+
+static int process_driver_block(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ char *name = NULL;
+ uint32_t id = 0;
+ struct measurement m;
+ struct node *n;
+ int res;
+
+ spa_zero(m);
+ if ((res = spa_pod_parse_struct(pod,
+ SPA_POD_Int(&id),
+ SPA_POD_String(&name),
+ SPA_POD_Long(&m.prev_signal),
+ SPA_POD_Long(&m.signal),
+ SPA_POD_Long(&m.awake),
+ SPA_POD_Long(&m.finish),
+ SPA_POD_Int(&m.status),
+ SPA_POD_Fraction(&m.latency))) < 0)
+ return res;
+
+ if ((n = find_node(d, id)) == NULL)
+ return -ENOENT;
+
+ n->driver = n;
+ n->measurement = m;
+ n->info = point->info;
+ point->driver = n;
+ n->generation = d->generation;
+
+ if (m.status != 3) {
+ n->errors++;
+ if (n->last_error_status == -1)
+ n->last_error_status = m.status;
+ }
+ return 0;
+}
+
+static int process_follower_block(struct data *d, const struct spa_pod *pod, struct point *point)
+{
+ uint32_t id = 0;
+ const char *name = NULL;
+ struct measurement m;
+ struct node *n;
+ int res;
+
+ spa_zero(m);
+ if ((res = spa_pod_parse_struct(pod,
+ SPA_POD_Int(&id),
+ SPA_POD_String(&name),
+ SPA_POD_Long(&m.prev_signal),
+ SPA_POD_Long(&m.signal),
+ SPA_POD_Long(&m.awake),
+ SPA_POD_Long(&m.finish),
+ SPA_POD_Int(&m.status),
+ SPA_POD_Fraction(&m.latency))) < 0)
+ return res;
+
+ if ((n = find_node(d, id)) == NULL)
+ return -ENOENT;
+
+ n->measurement = m;
+ if (n->driver != point->driver) {
+ n->driver = point->driver;
+ d->pending_refresh = true;
+ }
+ n->generation = d->generation;
+ if (m.status != 3) {
+ n->errors++;
+ if (n->last_error_status == -1)
+ n->last_error_status = m.status;
+ }
+ return 0;
+}
+
+static const char *print_time(char *buf, bool active, size_t len, uint64_t val)
+{
+ if (val == (uint64_t)-1 || !active)
+ snprintf(buf, len, " --- ");
+ else if (val == (uint64_t)-2)
+ snprintf(buf, len, " +++ ");
+ else if (val < 1000000llu)
+ snprintf(buf, len, "%5.1fus", val/1000.f);
+ else if (val < 1000000000llu)
+ snprintf(buf, len, "%5.1fms", val/1000000.f);
+ else
+ snprintf(buf, len, "%5.1fs", val/1000000000.f);
+ return buf;
+}
+
+static const char *print_perc(char *buf, bool active, size_t len, uint64_t val, float quantum)
+{
+ if (val == (uint64_t)-1 || !active) {
+ snprintf(buf, len, " --- ");
+ } else if (val == (uint64_t)-2) {
+ snprintf(buf, len, " +++ ");
+ } else {
+ float frac = val / 1000000000.f;
+ snprintf(buf, len, "%5.2f", quantum == 0.0f ? 0.0f : frac/quantum);
+ }
+ return buf;
+}
+
+static const char *state_as_string(enum pw_node_state state)
+{
+ switch (state) {
+ case PW_NODE_STATE_ERROR:
+ return "E";
+ case PW_NODE_STATE_CREATING:
+ return "C";
+ case PW_NODE_STATE_SUSPENDED:
+ return "S";
+ case PW_NODE_STATE_IDLE:
+ return "I";
+ case PW_NODE_STATE_RUNNING:
+ return "R";
+ }
+ return "!";
+}
+
+static void print_node(struct data *d, struct driver *i, struct node *n, int y)
+{
+ char buf1[64];
+ char buf2[64];
+ char buf3[64];
+ char buf4[64];
+ uint64_t waiting, busy;
+ float quantum;
+ struct spa_fraction frac;
+ bool active;
+
+ active = n->state == PW_NODE_STATE_RUNNING || n->state == PW_NODE_STATE_IDLE;
+
+ if (!active)
+ frac = SPA_FRACTION(0, 0);
+ else if (n->driver == n)
+ frac = SPA_FRACTION((uint32_t)(i->clock.duration * i->clock.rate.num), i->clock.rate.denom);
+ else
+ frac = SPA_FRACTION(n->measurement.latency.num, n->measurement.latency.denom);
+
+ if (i->clock.rate.denom)
+ quantum = (float)i->clock.duration * i->clock.rate.num / (float)i->clock.rate.denom;
+ else
+ quantum = 0.0;
+
+ if (n->measurement.awake >= n->measurement.signal)
+ waiting = n->measurement.awake - n->measurement.signal;
+ else if (n->measurement.signal > n->measurement.prev_signal)
+ waiting = -2;
+ else
+ waiting = -1;
+
+ if (n->measurement.finish >= n->measurement.awake)
+ busy = n->measurement.finish - n->measurement.awake;
+ else if (n->measurement.awake > n->measurement.prev_signal)
+ busy = -2;
+ else
+ busy = -1;
+
+ mvwprintw(d->win, y, 0, "%s %4.1u %6.1u %6.1u %s %s %s %s %3.1u %16.16s %s%s",
+ state_as_string(n->state),
+ n->id,
+ frac.num, frac.denom,
+ print_time(buf1, active, 64, waiting),
+ print_time(buf2, active, 64, busy),
+ print_perc(buf3, active, 64, waiting, quantum),
+ print_perc(buf4, active, 64, busy, quantum),
+ i->xrun_count + n->errors,
+ active ? n->format : "",
+ n->driver == n ? "" : " + ",
+ n->name);
+}
+
+static void clear_node(struct node *n)
+{
+ n->driver = n;
+ spa_zero(n->measurement);
+ spa_zero(n->info);
+ n->errors = 0;
+ n->last_error_status = 0;
+}
+
+static void do_refresh(struct data *d)
+{
+ struct node *n, *t, *f;
+ int y = 1;
+
+ wclear(d->win);
+ wattron(d->win, A_REVERSE);
+ wprintw(d->win, "%-*.*s", COLS, COLS, "S ID QUANT RATE WAIT BUSY W/Q B/Q ERR FORMAT NAME ");
+ wattroff(d->win, A_REVERSE);
+ wprintw(d->win, "\n");
+
+ spa_list_for_each_safe(n, t, &d->node_list, link) {
+ if (n->driver != n)
+ continue;
+
+ print_node(d, &n->info, n, y++);
+ if(y > LINES)
+ break;
+
+ spa_list_for_each(f, &d->node_list, link) {
+ if (d->generation > f->generation + 22)
+ clear_node(f);
+
+ if (f->driver != n || f == n)
+ continue;
+
+ print_node(d, &n->info, f, y++);
+ if(y > LINES)
+ break;
+
+ }
+ }
+
+ // Clear from last line to the end of the window to hide text wrapping from the last node
+ wmove(d->win, y, 0);
+ wclrtobot(d->win);
+
+ wrefresh(d->win);
+ d->pending_refresh = false;
+}
+
+static void do_timeout(void *data, uint64_t expirations)
+{
+ struct data *d = data;
+ d->generation++;
+ do_refresh(d);
+}
+
+static void profiler_profile(void *data, const struct spa_pod *pod)
+{
+ struct data *d = data;
+ struct spa_pod *o;
+ struct spa_pod_prop *p;
+ struct point point;
+
+ SPA_POD_STRUCT_FOREACH(pod, o) {
+ int res = 0;
+ if (!spa_pod_is_object_type(o, SPA_TYPE_OBJECT_Profiler))
+ continue;
+
+ spa_zero(point);
+ SPA_POD_OBJECT_FOREACH((struct spa_pod_object*)o, p) {
+ switch(p->key) {
+ case SPA_PROFILER_info:
+ res = process_info(d, &p->value, &point.info);
+ break;
+ case SPA_PROFILER_clock:
+ res = process_clock(d, &p->value, &point.info);
+ break;
+ case SPA_PROFILER_driverBlock:
+ res = process_driver_block(d, &p->value, &point);
+ break;
+ case SPA_PROFILER_followerBlock:
+ process_follower_block(d, &p->value, &point);
+ break;
+ default:
+ break;
+ }
+ if (res < 0)
+ break;
+ }
+ if (res < 0)
+ continue;
+ }
+ if (d->pending_refresh)
+ do_refresh(d);
+}
+
+static const struct pw_profiler_events profiler_events = {
+ PW_VERSION_PROFILER_EVENTS,
+ .profile = profiler_profile,
+};
+
+static void registry_event_global(void *data, uint32_t id,
+ uint32_t permissions, const char *type, uint32_t version,
+ const struct spa_dict *props)
+{
+ struct data *d = data;
+ struct pw_proxy *proxy;
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ const char *str;
+
+ if ((str = spa_dict_lookup(props, PW_KEY_NODE_NAME)) == NULL &&
+ (str = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION)) == NULL) {
+ str = spa_dict_lookup(props, PW_KEY_APP_NAME);
+ }
+
+ if (add_node(d, id, str) == NULL) {
+ pw_log_warn("can add node %u: %m", id);
+ }
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Profiler)) {
+ if (d->profiler != NULL) {
+ printf("Ignoring profiler %d: already attached\n", id);
+ return;
+ }
+
+ proxy = pw_registry_bind(d->registry, id, type, PW_VERSION_PROFILER, 0);
+ if (proxy == NULL)
+ goto error_proxy;
+
+ d->profiler = proxy;
+ pw_proxy_add_object_listener(proxy, &d->profiler_listener, &profiler_events, d);
+ }
+ if (d->pending_refresh)
+ do_refresh(d);
+ return;
+
+error_proxy:
+ pw_log_error("failed to create proxy: %m");
+ return;
+}
+
+static void registry_event_global_remove(void *data, uint32_t id)
+{
+ struct data *d = data;
+ struct node *n;
+ if ((n = find_node(d, id)) != NULL)
+ remove_node(d, n);
+ if (d->pending_refresh)
+ do_refresh(d);
+}
+
+static const struct pw_registry_events registry_events = {
+ PW_VERSION_REGISTRY_EVENTS,
+ .global = registry_event_global,
+ .global_remove = registry_event_global_remove,
+};
+
+static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message)
+{
+ struct data *data = _data;
+
+ if (id == PW_ID_CORE) {
+ switch (res) {
+ case -EPIPE:
+ pw_main_loop_quit(data->loop);
+ break;
+ default:
+ pw_log_error("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ break;
+ }
+ } else {
+ pw_log_info("error id:%u seq:%d res:%d (%s): %s",
+ id, seq, res, spa_strerror(res), message);
+ }
+}
+
+static void on_core_done(void *_data, uint32_t id, int seq)
+{
+ struct data *d = _data;
+
+ if (seq == d->check_profiler) {
+ if (d->profiler == NULL) {
+ pw_log_error("no Profiler Interface found, please load one in the server");
+ pw_main_loop_quit(d->loop);
+ } else {
+ do_refresh(d);
+ }
+ }
+}
+
+static const struct pw_core_events core_events = {
+ PW_VERSION_CORE_EVENTS,
+ .error = on_core_error,
+ .done = on_core_done,
+};
+
+static void do_quit(void *data, int signal_number)
+{
+ struct data *d = data;
+ pw_main_loop_quit(d->loop);
+}
+
+static void show_help(const char *name, bool error)
+{
+ fprintf(error ? stderr : stdout, "%s [options]\n"
+ " -h, --help Show this help\n"
+ " --version Show version\n"
+ " -r, --remote Remote daemon name\n",
+ name);
+}
+
+static void terminal_start(void)
+{
+ initscr();
+ cbreak();
+ noecho();
+ refresh();
+}
+
+static void terminal_stop(void)
+{
+ endwin();
+}
+
+static void do_handle_io(void *data, int fd, uint32_t mask)
+{
+ struct data *d = data;
+
+ if (mask & SPA_IO_IN) {
+ int ch = getch();
+
+ switch(ch) {
+ case 'q':
+ pw_main_loop_quit(d->loop);
+ break;
+ default:
+ do_refresh(d);
+ break;
+ }
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ struct data data = { 0 };
+ struct pw_loop *l;
+ const char *opt_remote = NULL;
+ static const struct option long_options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "remote", required_argument, NULL, 'r' },
+ { NULL, 0, NULL, 0}
+ };
+ int c;
+ struct timespec value, interval;
+ struct node *n;
+
+ setlocale(LC_ALL, "");
+ pw_init(&argc, &argv);
+
+ spa_list_init(&data.node_list);
+
+ while ((c = getopt_long(argc, argv, "hVr:o:", long_options, NULL)) != -1) {
+ switch (c) {
+ case 'h':
+ show_help(argv[0], false);
+ return 0;
+ case 'V':
+ printf("%s\n"
+ "Compiled with libpipewire %s\n"
+ "Linked with libpipewire %s\n",
+ argv[0],
+ pw_get_headers_version(),
+ pw_get_library_version());
+ return 0;
+ case 'r':
+ opt_remote = optarg;
+ break;
+ default:
+ show_help(argv[0], true);
+ return -1;
+ }
+ }
+
+ data.loop = pw_main_loop_new(NULL);
+ if (data.loop == NULL) {
+ fprintf(stderr, "Can't create data loop: %m\n");
+ return -1;
+ }
+
+ 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");
+ return -1;
+ }
+
+ pw_context_load_module(data.context, PW_EXTENSION_MODULE_PROFILER, NULL, NULL);
+
+ data.core = pw_context_connect(data.context,
+ pw_properties_new(
+ PW_KEY_REMOTE_NAME, opt_remote,
+ NULL),
+ 0);
+ if (data.core == NULL) {
+ fprintf(stderr, "Can't connect: %m\n");
+ return -1;
+ }
+
+ pw_core_add_listener(data.core,
+ &data.core_listener,
+ &core_events, &data);
+ data.registry = pw_core_get_registry(data.core,
+ PW_VERSION_REGISTRY, 0);
+ pw_registry_add_listener(data.registry,
+ &data.registry_listener,
+ &registry_events, &data);
+
+ data.check_profiler = pw_core_sync(data.core, 0, 0);
+
+ terminal_start();
+
+ data.win = newwin(LINES, COLS, 0, 0);
+
+ data.timer = pw_loop_add_timer(l, do_timeout, &data);
+ value.tv_sec = 1;
+ value.tv_nsec = 0;
+ interval.tv_sec = 1;
+ interval.tv_nsec = 0;
+ pw_loop_update_timer(l, data.timer, &value, &interval, false);
+
+ pw_loop_add_io(l, fileno(stdin), SPA_IO_IN, false, do_handle_io, &data);
+
+ pw_main_loop_run(data.loop);
+
+ terminal_stop();
+
+ spa_list_consume(n, &data.node_list, link)
+ remove_node(&data, n);
+
+ if (data.profiler) {
+ spa_hook_remove(&data.profiler_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.profiler);
+ }
+ spa_hook_remove(&data.registry_listener);
+ pw_proxy_destroy((struct pw_proxy*)data.registry);
+ spa_hook_remove(&data.core_listener);
+ pw_context_destroy(data.context);
+ pw_main_loop_destroy(data.loop);
+
+ pw_deinit();
+
+ return 0;
+}
diff --git a/src/tools/reserve.c b/src/tools/reserve.c
new file mode 100644
index 0000000..2329a14
--- /dev/null
+++ b/src/tools/reserve.c
@@ -0,0 +1,527 @@
+/* DBus device reservation API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef NAME
+#define NAME "reserve"
+#endif
+
+#include "reserve.h"
+
+#include <spa/utils/string.h>
+#include <pipewire/log.h>
+
+#define SERVICE_PREFIX "org.freedesktop.ReserveDevice1."
+#define OBJECT_PREFIX "/org/freedesktop/ReserveDevice1/"
+
+static const char introspection[] =
+ DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE
+ "<node>"
+ " <!-- If you are looking for documentation make sure to check out\n"
+ " http://git.0pointer.de/?p=reserve.git;a=blob;f=reserve.txt -->\n"
+ " <interface name=\"org.freedesktop.ReserveDevice1\">"
+ " <method name=\"RequestRelease\">"
+ " <arg name=\"priority\" type=\"i\" direction=\"in\"/>"
+ " <arg name=\"result\" type=\"b\" direction=\"out\"/>"
+ " </method>"
+ " <property name=\"Priority\" type=\"i\" access=\"read\"/>"
+ " <property name=\"ApplicationName\" type=\"s\" access=\"read\"/>"
+ " <property name=\"ApplicationDeviceName\" type=\"s\" access=\"read\"/>"
+ " </interface>"
+ " <interface name=\"org.freedesktop.DBus.Properties\">"
+ " <method name=\"Get\">"
+ " <arg name=\"interface\" direction=\"in\" type=\"s\"/>"
+ " <arg name=\"property\" direction=\"in\" type=\"s\"/>"
+ " <arg name=\"value\" direction=\"out\" type=\"v\"/>"
+ " </method>"
+ " </interface>"
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">"
+ " <method name=\"Introspect\">"
+ " <arg name=\"data\" type=\"s\" direction=\"out\"/>"
+ " </method>"
+ " </interface>"
+ "</node>";
+
+struct rd_device {
+ DBusConnection *connection;
+
+ int32_t priority;
+ char *service_name;
+ char *object_path;
+ char *application_name;
+ char *application_device_name;
+
+ const struct rd_device_callbacks *callbacks;
+ void *data;
+
+ DBusMessage *reply;
+
+ unsigned int filtering:1;
+ unsigned int registered:1;
+ unsigned int acquiring:1;
+ unsigned int owning:1;
+};
+
+static dbus_bool_t add_variant(DBusMessage *m, int type, const void *data)
+{
+ DBusMessageIter iter, sub;
+ char t[2];
+
+ t[0] = (char) type;
+ t[1] = 0;
+
+ dbus_message_iter_init_append(m, &iter);
+
+ if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_VARIANT, t, &sub))
+ return false;
+
+ if (!dbus_message_iter_append_basic(&sub, type, data))
+ return false;
+
+ if (!dbus_message_iter_close_container(&iter, &sub))
+ return false;
+
+ return true;
+}
+
+static DBusHandlerResult object_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct rd_device *d = userdata;
+ DBusError error;
+ DBusMessage *reply = NULL;
+
+ dbus_error_init(&error);
+
+ if (dbus_message_is_method_call(m, "org.freedesktop.ReserveDevice1",
+ "RequestRelease")) {
+ int32_t priority;
+
+ if (!dbus_message_get_args(m, &error,
+ DBUS_TYPE_INT32, &priority,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ pw_log_debug("%p: request release priority:%d", d, priority);
+
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (d->reply)
+ rd_device_complete_release(d, false);
+ d->reply = reply;
+
+ if (priority > d->priority && d->callbacks->release)
+ d->callbacks->release(d->data, d, 0);
+ else
+ rd_device_complete_release(d, false);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+ } else if (dbus_message_is_method_call(
+ m,
+ "org.freedesktop.DBus.Properties",
+ "Get")) {
+
+ const char *interface, *property;
+
+ if (!dbus_message_get_args( m, &error,
+ DBUS_TYPE_STRING, &interface,
+ DBUS_TYPE_STRING, &property,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ if (spa_streq(interface, "org.freedesktop.ReserveDevice1")) {
+ const char *empty = "";
+
+ if (spa_streq(property, "ApplicationName") && d->application_name) {
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (!add_variant(reply,
+ DBUS_TYPE_STRING,
+ d->application_name ? (const char**) &d->application_name : &empty))
+ goto oom;
+
+ } else if (spa_streq(property, "ApplicationDeviceName")) {
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (!add_variant(reply,
+ DBUS_TYPE_STRING,
+ d->application_device_name ? (const char**) &d->application_device_name : &empty))
+ goto oom;
+
+ } else if (spa_streq(property, "Priority")) {
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (!add_variant(reply,
+ DBUS_TYPE_INT32, &d->priority))
+ goto oom;
+ } else {
+ if (!(reply = dbus_message_new_error_printf(m,
+ DBUS_ERROR_UNKNOWN_METHOD,
+ "Unknown property %s", property)))
+ goto oom;
+ }
+
+ if (!dbus_connection_send(c, reply, NULL))
+ goto oom;
+
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+ } else if (dbus_message_is_method_call(
+ m,
+ "org.freedesktop.DBus.Introspectable",
+ "Introspect")) {
+ const char *i = introspection;
+
+ if (!(reply = dbus_message_new_method_return(m)))
+ goto oom;
+
+ if (!dbus_message_append_args(reply,
+ DBUS_TYPE_STRING, &i,
+ DBUS_TYPE_INVALID))
+ goto oom;
+
+ if (!dbus_connection_send(c, reply, NULL))
+ goto oom;
+
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+ }
+
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+invalid:
+ if (!(reply = dbus_message_new_error(m,
+ DBUS_ERROR_INVALID_ARGS,
+ "Invalid arguments")))
+ goto oom;
+
+ if (!dbus_connection_send(c, reply, NULL))
+ goto oom;
+
+ dbus_message_unref(reply);
+
+ dbus_error_free(&error);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+
+oom:
+ if (reply)
+ dbus_message_unref(reply);
+
+ dbus_error_free(&error);
+
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+}
+
+static const struct DBusObjectPathVTable vtable ={
+ .message_function = object_handler
+};
+
+static DBusHandlerResult filter_handler(DBusConnection *c, DBusMessage *m, void *userdata)
+{
+ struct rd_device *d = userdata;
+ DBusError error;
+ const char *name;
+
+ dbus_error_init(&error);
+
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameAcquired")) {
+ if (!dbus_message_get_args( m, &error,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ if (!spa_streq(name, d->service_name))
+ goto invalid;
+
+ pw_log_debug("%p: acquired %s, %s", d, name, d->service_name);
+
+ d->owning = true;
+
+ if (!d->registered) {
+ if (!(dbus_connection_register_object_path(d->connection,
+ d->object_path,
+ &vtable,
+ d)))
+ goto invalid;
+
+ if (!spa_streq(name, d->service_name))
+ goto invalid;
+
+ d->registered = true;
+
+ if (d->callbacks->acquired)
+ d->callbacks->acquired(d->data, d);
+ }
+ } else if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameLost")) {
+ if (!dbus_message_get_args( m, &error,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ if (!spa_streq(name, d->service_name))
+ goto invalid;
+
+ pw_log_debug("%p: lost %s", d, name);
+
+ d->owning = false;
+
+ if (d->registered) {
+ dbus_connection_unregister_object_path(d->connection,
+ d->object_path);
+ d->registered = false;
+ }
+ }
+ if (dbus_message_is_signal(m, "org.freedesktop.DBus", "NameOwnerChanged")) {
+ const char *old, *new;
+ if (!dbus_message_get_args( m, &error,
+ DBUS_TYPE_STRING, &name,
+ DBUS_TYPE_STRING, &old,
+ DBUS_TYPE_STRING, &new,
+ DBUS_TYPE_INVALID))
+ goto invalid;
+
+ if (!spa_streq(name, d->service_name) || d->owning)
+ goto invalid;
+
+ pw_log_debug("%p: changed %s: %s -> %s", d, name, old, new);
+
+ if (old == NULL || *old == 0) {
+ if (d->callbacks->busy && !d->acquiring)
+ d->callbacks->busy(d->data, d, name, 0);
+ } else {
+ if (d->callbacks->available)
+ d->callbacks->available(d->data, d, name);
+ }
+ }
+
+invalid:
+ dbus_error_free(&error);
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+struct rd_device *
+rd_device_new(DBusConnection *connection, const char *device_name, const char *application_name,
+ int32_t priority, const struct rd_device_callbacks *callbacks, void *data)
+{
+ struct rd_device *d;
+ int res;
+
+ d = calloc(1, sizeof(struct rd_device));
+ if (d == NULL)
+ return NULL;
+
+ d->connection = connection;
+ d->priority = priority;
+ d->callbacks = callbacks;
+ d->data = data;
+
+ d->application_name = strdup(application_name);
+
+ d->object_path = spa_aprintf(OBJECT_PREFIX "%s", device_name);
+ if (d->object_path == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+ d->service_name = spa_aprintf(SERVICE_PREFIX "%s", device_name);
+ if (d->service_name == NULL) {
+ res = -errno;
+ goto error_free;
+ }
+
+ if (!dbus_connection_add_filter(d->connection,
+ filter_handler,
+ d,
+ NULL)) {
+ res = -ENOMEM;
+ goto error_free;
+ }
+ dbus_bus_add_match(d->connection,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameLost'", NULL);
+ dbus_bus_add_match(d->connection,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameAcquired'", NULL);
+ dbus_bus_add_match(d->connection,
+ "type='signal',sender='org.freedesktop.DBus',"
+ "interface='org.freedesktop.DBus',member='NameOwnerChanged'", NULL);
+
+ dbus_connection_ref(d->connection);
+
+ pw_log_debug("%p: new device %s", d, device_name);
+
+ return d;
+
+error_free:
+ free(d->service_name);
+ free(d->object_path);
+ free(d);
+ errno = -res;
+ return NULL;
+}
+
+int rd_device_acquire(struct rd_device *d)
+{
+ int res;
+ DBusError error;
+
+ dbus_error_init(&error);
+
+ pw_log_debug("%p: reserve %s", d, d->service_name);
+
+ d->acquiring = true;
+
+ if ((res = dbus_bus_request_name(d->connection,
+ d->service_name,
+ (d->priority < INT32_MAX ? DBUS_NAME_FLAG_ALLOW_REPLACEMENT : 0),
+ &error)) < 0) {
+ pw_log_warn("%p: reserve failed: %s", d, error.message);
+ dbus_error_free(&error);
+ return -EIO;
+ }
+
+ pw_log_debug("%p: reserve result: %d", d, res);
+
+ if (res == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER ||
+ res == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER)
+ return 0;
+
+ if (res == DBUS_REQUEST_NAME_REPLY_EXISTS ||
+ res == DBUS_REQUEST_NAME_REPLY_IN_QUEUE)
+ return -EBUSY;
+
+ return -EIO;
+}
+
+int rd_device_request_release(struct rd_device *d)
+{
+ DBusMessage *m = NULL;
+
+ if (d->priority <= INT32_MIN)
+ return -EBUSY;
+
+ if ((m = dbus_message_new_method_call(d->service_name,
+ d->object_path,
+ "org.freedesktop.ReserveDevice1",
+ "RequestRelease")) == NULL) {
+ return -ENOMEM;
+ }
+ if (!dbus_message_append_args(m,
+ DBUS_TYPE_INT32, &d->priority,
+ DBUS_TYPE_INVALID)) {
+ dbus_message_unref(m);
+ return -ENOMEM;
+ }
+ if (!dbus_connection_send(d->connection, m, NULL)) {
+ return -EIO;
+ }
+ return 0;
+}
+
+int rd_device_complete_release(struct rd_device *d, int res)
+{
+ dbus_bool_t ret = res != 0;
+
+ if (d->reply == NULL)
+ return -EINVAL;
+
+ pw_log_debug("%p: complete release %d", d, res);
+
+ if (!dbus_message_append_args(d->reply,
+ DBUS_TYPE_BOOLEAN, &ret,
+ DBUS_TYPE_INVALID)) {
+ res = -ENOMEM;
+ goto exit;
+ }
+
+ if (!dbus_connection_send(d->connection, d->reply, NULL)) {
+ res = -EIO;
+ goto exit;
+ }
+ res = 0;
+exit:
+ dbus_message_unref(d->reply);
+ d->reply = NULL;
+ return res;
+}
+
+void rd_device_release(struct rd_device *d)
+{
+ pw_log_debug("%p: release %d", d, d->owning);
+
+ if (d->owning) {
+ DBusError error;
+ dbus_error_init(&error);
+
+ dbus_bus_release_name(d->connection,
+ d->service_name, &error);
+ dbus_error_free(&error);
+ }
+ d->acquiring = false;
+}
+
+void rd_device_destroy(struct rd_device *d)
+{
+ dbus_connection_remove_filter(d->connection,
+ filter_handler, d);
+
+ if (d->registered)
+ dbus_connection_unregister_object_path(d->connection,
+ d->object_path);
+
+ rd_device_release(d);
+
+ free(d->service_name);
+ free(d->object_path);
+ free(d->application_name);
+ free(d->application_device_name);
+ if (d->reply)
+ dbus_message_unref(d->reply);
+
+ dbus_connection_unref(d->connection);
+
+ free(d);
+}
+
+int rd_device_set_application_device_name(struct rd_device *d, const char *name)
+{
+ char *t;
+
+ if (!d)
+ return -EINVAL;
+
+ if (!(t = strdup(name)))
+ return -ENOMEM;
+
+ free(d->application_device_name);
+ d->application_device_name = t;
+
+ return 0;
+}
diff --git a/src/tools/reserve.h b/src/tools/reserve.h
new file mode 100644
index 0000000..c31e2d0
--- /dev/null
+++ b/src/tools/reserve.h
@@ -0,0 +1,82 @@
+/* DBus device reservation API
+ *
+ * Copyright © 2019 Wim Taymans
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef DEVICE_RESERVE_H
+#define DEVICE_RESERVE_H
+
+#include <dbus/dbus.h>
+#include <inttypes.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rd_device;
+
+struct rd_device_callbacks {
+ /** the device is acquired by us */
+ void (*acquired) (void *data, struct rd_device *d);
+ /** request a release of the device */
+ void (*release) (void *data, struct rd_device *d, int forced);
+ /** the device is busy by someone else */
+ void (*busy) (void *data, struct rd_device *d, const char *name, int32_t priority);
+ /** the device is made available by someone else */
+ void (*available) (void *data, struct rd_device *d, const char *name);
+};
+
+/* create a new device and start watching */
+struct rd_device *
+rd_device_new(DBusConnection *connection, /**< Bus to watch */
+ const char *device_name, /**< The device to lock, e.g. "Audio0" */
+ const char *application_name, /**< A human readable name of the application,
+ * e.g. "PipeWire Server" */
+ int32_t priority, /**< The priority for this application.
+ * If unsure use 0 */
+ const struct rd_device_callbacks *callbacks, /**< Called when device name is acquired/released */
+ void *data);
+
+/** try to acquire the device */
+int rd_device_acquire(struct rd_device *d);
+
+/** request the owner to release the device */
+int rd_device_request_release(struct rd_device *d);
+
+/** complete the release of the device */
+int rd_device_complete_release(struct rd_device *d, int res);
+
+/** release a device */
+void rd_device_release(struct rd_device *d);
+
+/** destroy a device */
+void rd_device_destroy(struct rd_device *d);
+
+/* Set the application device name for an rd_device object. Returns 0
+ * on success, a negative errno style return value on error. */
+int rd_device_set_application_device_name(struct rd_device *d, const char *name);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif