/* 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 #include #include #include #include #include #include #include #include #include #include #include #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_core *core; struct spa_hook core_listener; const char *opt_group_name; const char *opt_channel_map; uint32_t channels; uint32_t latency; struct pw_properties *capture_props; struct pw_stream *capture; struct spa_hook capture_listener; struct pw_properties *playback_props; struct pw_stream *playback; struct spa_hook playback_listener; }; static void capture_process(void *d) { struct data *data = d; struct pw_buffer *in, *out; if ((in = pw_stream_dequeue_buffer(data->capture)) == NULL) pw_log_warn("out of capture buffers: %m"); if ((out = pw_stream_dequeue_buffer(data->playback)) == NULL) pw_log_warn("out of playback buffers: %m"); if (in != NULL && out != NULL) *out->buffer = *in->buffer; if (in != NULL) pw_stream_queue_buffer(data->capture, in); if (out != NULL) pw_stream_queue_buffer(data->playback, out); } static const struct pw_stream_events in_stream_events = { PW_VERSION_STREAM_EVENTS, .process = capture_process }; static const struct pw_stream_events out_stream_events = { PW_VERSION_STREAM_EVENTS, }; static int setup_streams(struct data *data) { int res; uint32_t n_params; const struct spa_pod *params[1]; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); data->capture = pw_stream_new(data->core, "loopback capture", data->capture_props); data->capture_props = NULL; if (data->capture == NULL) return -errno; pw_stream_add_listener(data->capture, &data->capture_listener, &in_stream_events, data); data->playback = pw_stream_new(data->core, "loopback playback", data->playback_props); data->playback_props = NULL; if (data->playback == NULL) return -errno; pw_stream_add_listener(data->playback, &data->playback_listener, &out_stream_events, data); n_params = 0; params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &SPA_AUDIO_INFO_RAW_INIT( .flags = SPA_AUDIO_FLAG_UNPOSITIONED, .format = SPA_AUDIO_FORMAT_F32P, .channels = data->channels)); if ((res = pw_stream_connect(data->capture, PW_DIRECTION_INPUT, PW_ID_ANY, PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS, params, n_params)) < 0) return res; if ((res = pw_stream_connect(data->playback, PW_DIRECTION_OUTPUT, PW_ID_ANY, PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS, params, n_params)) < 0) return res; return 0; } static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message) { struct data *d = data; pw_log_error("error id:%u seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), message); if (id == PW_ID_CORE && res == -EPIPE) pw_main_loop_quit(d->loop); } static const struct pw_core_events core_events = { PW_VERSION_CORE_EVENTS, .error = on_core_error, }; static 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) { fprintf(stdout, "%s [options] []\n" " -h, --help Show this help\n" " --version Show version\n" " -r, --remote Remote daemon name\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" " -C --capture Capture source to connect to\n" " --capture-props Capture stream properties\n" " -P --playback Playback sink to connect to\n" " --playback-props Playback stream properties\n", 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]; 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' }, { "channels", required_argument, NULL, 'c' }, { "latency", required_argument, NULL, 'l' }, { "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; 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.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:g:c:m:l:C:P:i:o:", long_options, NULL)) != -1) { switch (c) { case 'h': show_help(&data, argv[0]); return 0; case 'V': fprintf(stdout, "%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 '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 'C': pw_properties_set(data.capture_props, PW_KEY_NODE_TARGET, optarg); break; case 'P': pw_properties_set(data.playback_props, PW_KEY_NODE_TARGET, 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]); 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 (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); } if (data.latency != 0) { pw_properties_setf(data.capture_props, PW_KEY_NODE_LATENCY, "%u/%u", data.latency, DEFAULT_RATE); pw_properties_setf(data.playback_props, PW_KEY_NODE_LATENCY, "%u/%u", data.latency, DEFAULT_RATE); } 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"); goto exit; } pw_core_add_listener(data.core, &data.core_listener, &core_events, &data); setup_streams(&data); pw_main_loop_run(data.loop); res = 0; exit: if (data.core) pw_core_disconnect(data.core); if (data.context) pw_context_destroy(data.context); if (data.loop) pw_main_loop_destroy(data.loop); if (data.capture_props) pw_properties_free(data.capture_props); if (data.playback_props) pw_properties_free(data.playback_props); pw_deinit(); return res; }