mirror of
				https://gitlab.freedesktop.org/pipewire/pipewire.git
				synced 2025-11-03 09:01:54 -05:00 
			
		
		
		
	tools: add pw-loopback
Sends the captured data from a source directly to a sink. It uses an input and output stream so the source and sink can be moved in pavucontrol, channel remixing is possible and the volume can be adjusted. See #959
This commit is contained in:
		
							parent
							
								
									20b8426013
								
							
						
					
					
						commit
						4bcb7e0468
					
				
					 2 changed files with 331 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -45,6 +45,13 @@ executable('pw-metadata',
 | 
			
		|||
	dependencies : [pipewire_dep],
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
executable('pw-loopback',
 | 
			
		||||
	[ 'pw-loopback.c'],
 | 
			
		||||
	c_args : [ '-D_GNU_SOURCE' ],
 | 
			
		||||
	install: true,
 | 
			
		||||
	dependencies : [pipewire_dep],
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if ncurses_dep.found()
 | 
			
		||||
  executable('pw-top',
 | 
			
		||||
	'pw-top.c',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										324
									
								
								src/tools/pw-loopback.c
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								src/tools/pw-loopback.c
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,324 @@
 | 
			
		|||
/* 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 <spa/utils/result.h>
 | 
			
		||||
#include <spa/pod/builder.h>
 | 
			
		||||
#include <spa/param/format.h>
 | 
			
		||||
#include <spa/param/audio/raw.h>
 | 
			
		||||
 | 
			
		||||
#include <pipewire/pipewire.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_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");
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	if ((out = pw_stream_dequeue_buffer(data->playback)) == NULL) {
 | 
			
		||||
		pw_log_warn("out of playback buffers: %m");
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*out->buffer = *in->buffer;
 | 
			
		||||
 | 
			
		||||
	pw_stream_queue_buffer(data->capture, in);
 | 
			
		||||
	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", pw_properties_copy(data->capture_props));
 | 
			
		||||
	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", pw_properties_copy(data->playback_props));
 | 
			
		||||
	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_pod_builder_add_object(&b,
 | 
			
		||||
			SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
 | 
			
		||||
			SPA_FORMAT_mediaType,		SPA_POD_Id(SPA_MEDIA_TYPE_audio),
 | 
			
		||||
			SPA_FORMAT_mediaSubtype,	SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
 | 
			
		||||
			SPA_FORMAT_AUDIO_format,	SPA_POD_Id(SPA_AUDIO_FORMAT_F32P),
 | 
			
		||||
			SPA_FORMAT_AUDIO_channels,	SPA_POD_Int(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] [<id>]\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;
 | 
			
		||||
 | 
			
		||||
	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");
 | 
			
		||||
		return -1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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");
 | 
			
		||||
		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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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");
 | 
			
		||||
		return -1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pw_core_add_listener(data.core,
 | 
			
		||||
			&data.core_listener,
 | 
			
		||||
			&core_events, &data);
 | 
			
		||||
 | 
			
		||||
	setup_streams(&data);
 | 
			
		||||
 | 
			
		||||
	pw_main_loop_run(data.loop);
 | 
			
		||||
 | 
			
		||||
	pw_context_destroy(data.context);
 | 
			
		||||
	pw_main_loop_destroy(data.loop);
 | 
			
		||||
	pw_deinit();
 | 
			
		||||
 | 
			
		||||
	return 0;
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue