diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
commit | 7a46c07230b8d8108c0e8e80df4522d0ac116538 (patch) | |
tree | d483300dab478b994fe199a5d19d18d74153718a /src/tools | |
parent | Initial commit. (diff) | |
download | pipewire-7a46c07230b8d8108c0e8e80df4522d0ac116538.tar.xz pipewire-7a46c07230b8d8108c0e8e80df4522d0ac116538.zip |
Adding upstream version 0.3.65.upstream/0.3.65upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/tools/dsffile.c | 266 | ||||
-rw-r--r-- | src/tools/dsffile.h | 52 | ||||
-rw-r--r-- | src/tools/meson.build | 88 | ||||
-rw-r--r-- | src/tools/midifile.c | 740 | ||||
-rw-r--r-- | src/tools/midifile.h | 64 | ||||
-rw-r--r-- | src/tools/pw-cat.c | 1971 | ||||
-rw-r--r-- | src/tools/pw-cli.c | 2375 | ||||
-rw-r--r-- | src/tools/pw-dot.c | 1169 | ||||
-rw-r--r-- | src/tools/pw-dump.c | 1664 | ||||
-rw-r--r-- | src/tools/pw-link.c | 912 | ||||
-rw-r--r-- | src/tools/pw-loopback.c | 284 | ||||
-rw-r--r-- | src/tools/pw-metadata.c | 297 | ||||
-rw-r--r-- | src/tools/pw-mididump.c | 233 | ||||
-rw-r--r-- | src/tools/pw-mon.c | 875 | ||||
-rw-r--r-- | src/tools/pw-profiler.c | 665 | ||||
-rw-r--r-- | src/tools/pw-reserve.c | 253 | ||||
-rw-r--r-- | src/tools/pw-top.c | 862 | ||||
-rw-r--r-- | src/tools/reserve.c | 527 | ||||
-rw-r--r-- | src/tools/reserve.h | 82 |
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, + ®istry_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, + ®istry_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 = ¶ms[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, + ®istry_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, + ®istry_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, + ®istry_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, + ®istry_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, + ®istry_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, + ®istry_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 |