mirror of
				https://gitlab.freedesktop.org/pipewire/pipewire.git
				synced 2025-11-03 09:01:54 -05:00 
			
		
		
		
	jack: Add UMP to MIDI conversions
Add 2 new port format extensions, one for OSC and another for UMP. Make sure we convert the events from UMP/OSC/MIDI to jack events depending on the port type. Try to produce UMP by default.
This commit is contained in:
		
							parent
							
								
									7b03a41e3d
								
							
						
					
					
						commit
						648ba982d4
					
				
					 2 changed files with 180 additions and 46 deletions
				
			
		| 
						 | 
					@ -25,6 +25,13 @@ int jack_get_video_image_size(jack_client_t *client, jack_image_size_t *size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
int jack_set_sample_rate (jack_client_t *client, jack_nframes_t nframes);
 | 
					int jack_set_sample_rate (jack_client_t *client, jack_nframes_t nframes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* raw OSC message */
 | 
				
			||||||
 | 
					#define JACK_DEFAULT_OSC_TYPE "8 bit raw OSC"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* MIDI 2.0 UMP type. This contains raw UMP data, which can have MIDI 1.0 or
 | 
				
			||||||
 | 
					 * MIDI 2.0 packets. The data is an array of 32 bit ints. */
 | 
				
			||||||
 | 
					#define JACK_DEFAULT_UMP_TYPE "32 bit raw UMP"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef __cplusplus
 | 
					#ifdef __cplusplus
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@
 | 
				
			||||||
#include <spa/utils/result.h>
 | 
					#include <spa/utils/result.h>
 | 
				
			||||||
#include <spa/utils/string.h>
 | 
					#include <spa/utils/string.h>
 | 
				
			||||||
#include <spa/utils/ringbuffer.h>
 | 
					#include <spa/utils/ringbuffer.h>
 | 
				
			||||||
 | 
					#include <spa/control/ump-utils.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <pipewire/pipewire.h>
 | 
					#include <pipewire/pipewire.h>
 | 
				
			||||||
#include <pipewire/private.h>
 | 
					#include <pipewire/private.h>
 | 
				
			||||||
| 
						 | 
					@ -39,8 +40,6 @@
 | 
				
			||||||
#include "pipewire/extensions/metadata.h"
 | 
					#include "pipewire/extensions/metadata.h"
 | 
				
			||||||
#include "pipewire-jack-extensions.h"
 | 
					#include "pipewire-jack-extensions.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define JACK_DEFAULT_VIDEO_TYPE	"32 bit float RGBA video"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* use 512KB stack per thread - the default is way too high to be feasible
 | 
					/* use 512KB stack per thread - the default is way too high to be feasible
 | 
				
			||||||
 * with mlockall() on many systems */
 | 
					 * with mlockall() on many systems */
 | 
				
			||||||
#define THREAD_STACK 524288
 | 
					#define THREAD_STACK 524288
 | 
				
			||||||
| 
						 | 
					@ -65,9 +64,16 @@ PW_LOG_TOPIC_STATIC(jack_log_topic, "jack");
 | 
				
			||||||
#define PW_LOG_TOPIC_DEFAULT jack_log_topic
 | 
					#define PW_LOG_TOPIC_DEFAULT jack_log_topic
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define TYPE_ID_AUDIO	0
 | 
					#define TYPE_ID_AUDIO	0
 | 
				
			||||||
#define TYPE_ID_MIDI	1
 | 
					#define TYPE_ID_VIDEO	1
 | 
				
			||||||
#define TYPE_ID_VIDEO	2
 | 
					#define TYPE_ID_MIDI	2
 | 
				
			||||||
#define TYPE_ID_OTHER	3
 | 
					#define TYPE_ID_OSC	3
 | 
				
			||||||
 | 
					#define TYPE_ID_UMP	4
 | 
				
			||||||
 | 
					#define TYPE_ID_OTHER	5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define TYPE_ID_IS_EVENT(t)	((t) >= TYPE_ID_MIDI && (t) <= TYPE_ID_UMP)
 | 
				
			||||||
 | 
					#define TYPE_ID_CAN_OSC(t)	((t) == TYPE_ID_MIDI || (t) == TYPE_ID_OSC)
 | 
				
			||||||
 | 
					#define TYPE_ID_IS_HIDDEN(t)	((t) >= TYPE_ID_OTHER)
 | 
				
			||||||
 | 
					#define TYPE_ID_IS_COMPATIBLE(a,b)(((a) == (b)) || (TYPE_ID_IS_EVENT(a) && TYPE_ID_IS_EVENT(b)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define SELF_CONNECT_ALLOW	0
 | 
					#define SELF_CONNECT_ALLOW	0
 | 
				
			||||||
#define SELF_CONNECT_FAIL_EXT	-1
 | 
					#define SELF_CONNECT_FAIL_EXT	-1
 | 
				
			||||||
| 
						 | 
					@ -638,7 +644,7 @@ static struct mix *find_port_peer(struct port *port, uint32_t peer_id)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	struct mix *mix;
 | 
						struct mix *mix;
 | 
				
			||||||
	spa_list_for_each(mix, &port->mix, port_link) {
 | 
						spa_list_for_each(mix, &port->mix, port_link) {
 | 
				
			||||||
		pw_log_info("%p %d %d", port, mix->peer_id, peer_id);
 | 
							pw_log_trace("%p %d %d", port, mix->peer_id, peer_id);
 | 
				
			||||||
		if (mix->peer_id == peer_id)
 | 
							if (mix->peer_id == peer_id)
 | 
				
			||||||
			return mix;
 | 
								return mix;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -1381,12 +1387,25 @@ static inline bool is_osc(jack_midi_event_t *ev)
 | 
				
			||||||
	return ev->size >= 1 && (ev->buffer[0] == '#' || ev->buffer[0] == '/');
 | 
						return ev->size >= 1 && (ev->buffer[0] == '#' || ev->buffer[0] == '/');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static size_t convert_from_midi(void *midi, void *buffer, size_t size)
 | 
					static size_t convert_from_event(void *midi, void *buffer, size_t size, uint32_t type)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	struct spa_pod_builder b = { 0, };
 | 
						struct spa_pod_builder b = { 0, };
 | 
				
			||||||
	uint32_t i, count;
 | 
						uint32_t i, count;
 | 
				
			||||||
	struct spa_pod_frame f;
 | 
						struct spa_pod_frame f;
 | 
				
			||||||
 | 
						uint32_t event_type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch (type) {
 | 
				
			||||||
 | 
						case TYPE_ID_MIDI:
 | 
				
			||||||
 | 
						case TYPE_ID_OSC:
 | 
				
			||||||
 | 
							/* we handle MIDI as OSC, check below */
 | 
				
			||||||
 | 
							event_type = SPA_CONTROL_OSC;
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case TYPE_ID_UMP:
 | 
				
			||||||
 | 
							event_type = SPA_CONTROL_UMP;
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	count = jack_midi_get_event_count(midi);
 | 
						count = jack_midi_get_event_count(midi);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	spa_pod_builder_init(&b, buffer, size);
 | 
						spa_pod_builder_init(&b, buffer, size);
 | 
				
			||||||
| 
						 | 
					@ -1395,14 +1414,43 @@ static size_t convert_from_midi(void *midi, void *buffer, size_t size)
 | 
				
			||||||
	for (i = 0; i < count; i++) {
 | 
						for (i = 0; i < count; i++) {
 | 
				
			||||||
		jack_midi_event_t ev;
 | 
							jack_midi_event_t ev;
 | 
				
			||||||
		jack_midi_event_get(&ev, midi, i);
 | 
							jack_midi_event_get(&ev, midi, i);
 | 
				
			||||||
		spa_pod_builder_control(&b, ev.time,
 | 
					
 | 
				
			||||||
				is_osc(&ev) ? SPA_CONTROL_OSC : SPA_CONTROL_Midi);
 | 
							if (type != TYPE_ID_MIDI || is_osc(&ev)) {
 | 
				
			||||||
 | 
								/* no midi port or it's OSC */
 | 
				
			||||||
 | 
								spa_pod_builder_control(&b, ev.time, event_type);
 | 
				
			||||||
			spa_pod_builder_bytes(&b, ev.buffer, ev.size);
 | 
								spa_pod_builder_bytes(&b, ev.buffer, ev.size);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								/* midi port and it's not OSC, convert to UMP */
 | 
				
			||||||
 | 
								uint8_t *data = ev.buffer;
 | 
				
			||||||
 | 
								size_t size = ev.size;
 | 
				
			||||||
 | 
								uint64_t state = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								while (size > 0) {
 | 
				
			||||||
 | 
									uint32_t ump[4];
 | 
				
			||||||
 | 
									int ump_size = spa_ump_from_midi(&data, &size,
 | 
				
			||||||
 | 
											ump, sizeof(ump), 0, &state);
 | 
				
			||||||
 | 
									if (ump_size <= 0)
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
									spa_pod_builder_control(&b, ev.time, SPA_CONTROL_UMP);
 | 
				
			||||||
 | 
									spa_pod_builder_bytes(&b, ump, ump_size);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	spa_pod_builder_pop(&b, &f);
 | 
						spa_pod_builder_pop(&b, &f);
 | 
				
			||||||
	return b.state.offset;
 | 
						return b.state.offset;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static inline int event_compare(uint8_t s1, uint8_t s2)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						/* 11 (controller) > 12 (program change) >
 | 
				
			||||||
 | 
						 * 8 (note off) > 9 (note on) > 10 (aftertouch) >
 | 
				
			||||||
 | 
						 * 13 (channel pressure) > 14 (pitch bend) */
 | 
				
			||||||
 | 
						static int priotab[] = { 5,4,3,7,6,2,1,0 };
 | 
				
			||||||
 | 
						if ((s1 & 0xf) != (s2 & 0xf))
 | 
				
			||||||
 | 
							return 0;
 | 
				
			||||||
 | 
						return priotab[(s2>>4) & 7] - priotab[(s1>>4) & 7];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b)
 | 
					static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *b)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	if (a->offset < b->offset)
 | 
						if (a->offset < b->offset)
 | 
				
			||||||
| 
						 | 
					@ -1414,21 +1462,20 @@ static inline int event_sort(struct spa_pod_control *a, struct spa_pod_control *
 | 
				
			||||||
	switch(a->type) {
 | 
						switch(a->type) {
 | 
				
			||||||
	case SPA_CONTROL_Midi:
 | 
						case SPA_CONTROL_Midi:
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		/* 11 (controller) > 12 (program change) >
 | 
							uint8_t *sa = SPA_POD_BODY(&a->value), *sb = SPA_POD_BODY(&b->value);
 | 
				
			||||||
		 * 8 (note off) > 9 (note on) > 10 (aftertouch) >
 | 
							if (SPA_POD_BODY_SIZE(&a->value) < 1 || SPA_POD_BODY_SIZE(&b->value) < 1)
 | 
				
			||||||
		 * 13 (channel pressure) > 14 (pitch bend) */
 | 
					 | 
				
			||||||
		static int priotab[] = { 5,4,3,7,6,2,1,0 };
 | 
					 | 
				
			||||||
		uint8_t *da, *db;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (SPA_POD_BODY_SIZE(&a->value) < 1 ||
 | 
					 | 
				
			||||||
		    SPA_POD_BODY_SIZE(&b->value) < 1)
 | 
					 | 
				
			||||||
			return 0;
 | 
								return 0;
 | 
				
			||||||
 | 
							return event_compare(sa[0], sb[0]);
 | 
				
			||||||
		da = SPA_POD_BODY(&a->value);
 | 
						}
 | 
				
			||||||
		db = SPA_POD_BODY(&b->value);
 | 
						case SPA_CONTROL_UMP:
 | 
				
			||||||
		if ((da[0] & 0xf) != (db[0] & 0xf))
 | 
						{
 | 
				
			||||||
 | 
							uint32_t *sa = SPA_POD_BODY(&a->value), *sb = SPA_POD_BODY(&b->value);
 | 
				
			||||||
 | 
							if (SPA_POD_BODY_SIZE(&a->value) < 4 || SPA_POD_BODY_SIZE(&b->value) < 4)
 | 
				
			||||||
			return 0;
 | 
								return 0;
 | 
				
			||||||
		return priotab[(db[0]>>4) & 7] - priotab[(da[0]>>4) & 7];
 | 
							if ((sa[0] >> 28) != 2 || (sa[0] >> 28) != 4 ||
 | 
				
			||||||
 | 
							    (sb[0] >> 28) != 2 || (sb[0] >> 28) != 4)
 | 
				
			||||||
 | 
								return 0;
 | 
				
			||||||
 | 
							return event_compare(sa[0] >> 16, sb[0] >> 16);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		return 0;
 | 
							return 0;
 | 
				
			||||||
| 
						 | 
					@ -1487,11 +1534,12 @@ static inline int midi_event_write(void *port_buffer,
 | 
				
			||||||
	return 0;
 | 
						return 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi, bool fix)
 | 
					static void convert_to_event(struct spa_pod_sequence **seq, uint32_t n_seq, void *midi, bool fix, uint32_t type)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	struct spa_pod_control *c[n_seq];
 | 
						struct spa_pod_control *c[n_seq];
 | 
				
			||||||
 | 
						uint64_t state = 0;
 | 
				
			||||||
	uint32_t i;
 | 
						uint32_t i;
 | 
				
			||||||
	int res;
 | 
						int res = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for (i = 0; i < n_seq; i++)
 | 
						for (i = 0; i < n_seq; i++)
 | 
				
			||||||
		c[i] = spa_pod_control_first(&seq[i]->body);
 | 
							c[i] = spa_pod_control_first(&seq[i]->body);
 | 
				
			||||||
| 
						 | 
					@ -1515,16 +1563,51 @@ static void convert_to_midi(struct spa_pod_sequence **seq, uint32_t n_seq, void
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		switch(next->type) {
 | 
							switch(next->type) {
 | 
				
			||||||
		case SPA_CONTROL_OSC:
 | 
							case SPA_CONTROL_OSC:
 | 
				
			||||||
 | 
								if (!TYPE_ID_CAN_OSC(type))
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								SPA_FALLTHROUGH;
 | 
				
			||||||
		case SPA_CONTROL_Midi:
 | 
							case SPA_CONTROL_Midi:
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			uint8_t *data = SPA_POD_BODY(&next->value);
 | 
								uint8_t *data = SPA_POD_BODY(&next->value);
 | 
				
			||||||
			size_t size = SPA_POD_BODY_SIZE(&next->value);
 | 
								size_t size = SPA_POD_BODY_SIZE(&next->value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if ((res = midi_event_write(midi, next->offset, data, size, fix)) < 0)
 | 
								if (type == TYPE_ID_UMP) {
 | 
				
			||||||
 | 
									while (size > 0) {
 | 
				
			||||||
 | 
										uint32_t ump[4];
 | 
				
			||||||
 | 
										int ump_size = spa_ump_from_midi(&data, &size, ump, sizeof(ump), 0, &state);
 | 
				
			||||||
 | 
										if (ump_size <= 0)
 | 
				
			||||||
 | 
											break;
 | 
				
			||||||
 | 
										if ((res = midi_event_write(midi, next->offset,
 | 
				
			||||||
 | 
													(uint8_t*)ump, ump_size, false)) < 0)
 | 
				
			||||||
 | 
											break;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									res = midi_event_write(midi, next->offset, data, size, fix);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (res < 0)
 | 
				
			||||||
				pw_log_warn("midi %p: can't write event: %s", midi,
 | 
									pw_log_warn("midi %p: can't write event: %s", midi,
 | 
				
			||||||
						spa_strerror(res));
 | 
											spa_strerror(res));
 | 
				
			||||||
			break;
 | 
								break;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							case SPA_CONTROL_UMP:
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								void *data = SPA_POD_BODY(&next->value);
 | 
				
			||||||
 | 
								size_t size = SPA_POD_BODY_SIZE(&next->value);
 | 
				
			||||||
 | 
								uint8_t ev[32];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (type == TYPE_ID_MIDI) {
 | 
				
			||||||
 | 
									int ev_size = spa_ump_to_midi(data, size, ev, sizeof(ev));
 | 
				
			||||||
 | 
									if (ev_size <= 0)
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
									size = ev_size;
 | 
				
			||||||
 | 
									data = ev;
 | 
				
			||||||
 | 
								} else if (type != TYPE_ID_UMP)
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if ((res = midi_event_write(midi, next->offset, data, size, fix)) < 0)
 | 
				
			||||||
 | 
									pw_log_warn("midi %p: can't write event: %s", midi,
 | 
				
			||||||
 | 
											spa_strerror(res));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		c[next_index] = spa_pod_control_next(c[next_index]);
 | 
							c[next_index] = spa_pod_control_next(c[next_index]);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -1591,19 +1674,22 @@ static inline void process_empty(struct port *p, uint32_t frames)
 | 
				
			||||||
	struct client *c = p->client;
 | 
						struct client *c = p->client;
 | 
				
			||||||
	void *ptr, *src = p->emptyptr;
 | 
						void *ptr, *src = p->emptyptr;
 | 
				
			||||||
	struct port *tied = p->tied;
 | 
						struct port *tied = p->tied;
 | 
				
			||||||
 | 
						uint32_t type = p->object->port.type_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (SPA_UNLIKELY(tied != NULL)) {
 | 
						if (SPA_UNLIKELY(tied != NULL)) {
 | 
				
			||||||
		if ((src = tied->get_buffer(tied, frames)) == NULL)
 | 
							if ((src = tied->get_buffer(tied, frames)) == NULL)
 | 
				
			||||||
			src = p->emptyptr;
 | 
								src = p->emptyptr;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch (p->object->port.type_id) {
 | 
						switch (type) {
 | 
				
			||||||
	case TYPE_ID_AUDIO:
 | 
						case TYPE_ID_AUDIO:
 | 
				
			||||||
		ptr = get_buffer_output(p, frames, sizeof(float), NULL);
 | 
							ptr = get_buffer_output(p, frames, sizeof(float), NULL);
 | 
				
			||||||
		if (SPA_LIKELY(ptr != NULL))
 | 
							if (SPA_LIKELY(ptr != NULL))
 | 
				
			||||||
			memcpy(ptr, src, frames * sizeof(float));
 | 
								memcpy(ptr, src, frames * sizeof(float));
 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
	case TYPE_ID_MIDI:
 | 
						case TYPE_ID_MIDI:
 | 
				
			||||||
 | 
						case TYPE_ID_OSC:
 | 
				
			||||||
 | 
						case TYPE_ID_UMP:
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		struct buffer *b;
 | 
							struct buffer *b;
 | 
				
			||||||
		ptr = get_buffer_output(p, c->max_frames, 1, &b);
 | 
							ptr = get_buffer_output(p, c->max_frames, 1, &b);
 | 
				
			||||||
| 
						 | 
					@ -1611,8 +1697,8 @@ static inline void process_empty(struct port *p, uint32_t frames)
 | 
				
			||||||
			/* first build the complete pod in scratch memory, then copy it
 | 
								/* first build the complete pod in scratch memory, then copy it
 | 
				
			||||||
			 * to the target buffer. This makes it possible for multiple threads
 | 
								 * to the target buffer. This makes it possible for multiple threads
 | 
				
			||||||
			 * to do this concurrently */
 | 
								 * to do this concurrently */
 | 
				
			||||||
			b->datas[0].chunk->size = convert_from_midi(src,
 | 
								b->datas[0].chunk->size = convert_from_event(src, midi_scratch,
 | 
				
			||||||
					midi_scratch, MIDI_SCRATCH_FRAMES * sizeof(float));
 | 
										MIDI_SCRATCH_FRAMES * sizeof(float), type);
 | 
				
			||||||
			memcpy(ptr, midi_scratch, b->datas[0].chunk->size);
 | 
								memcpy(ptr, midi_scratch, b->datas[0].chunk->size);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
| 
						 | 
					@ -2393,6 +2479,8 @@ static int param_enum_format(struct client *c, struct port *p,
 | 
				
			||||||
			SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
 | 
								SPA_FORMAT_mediaSubtype,   SPA_POD_Id(SPA_MEDIA_SUBTYPE_dsp),
 | 
				
			||||||
	                SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
 | 
						                SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
 | 
						case TYPE_ID_UMP:
 | 
				
			||||||
 | 
						case TYPE_ID_OSC:
 | 
				
			||||||
	case TYPE_ID_MIDI:
 | 
						case TYPE_ID_MIDI:
 | 
				
			||||||
		*param = spa_pod_builder_add_object(b,
 | 
							*param = spa_pod_builder_add_object(b,
 | 
				
			||||||
			SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
 | 
								SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
 | 
				
			||||||
| 
						 | 
					@ -2424,6 +2512,8 @@ static int param_format(struct client *c, struct port *p,
 | 
				
			||||||
		                SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
 | 
							                SPA_FORMAT_AUDIO_format,   SPA_POD_Id(SPA_AUDIO_FORMAT_DSP_F32));
 | 
				
			||||||
		break;
 | 
							break;
 | 
				
			||||||
	case TYPE_ID_MIDI:
 | 
						case TYPE_ID_MIDI:
 | 
				
			||||||
 | 
						case TYPE_ID_OSC:
 | 
				
			||||||
 | 
						case TYPE_ID_UMP:
 | 
				
			||||||
		*param = spa_pod_builder_add_object(b,
 | 
							*param = spa_pod_builder_add_object(b,
 | 
				
			||||||
				SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
 | 
									SPA_TYPE_OBJECT_Format, SPA_PARAM_Format,
 | 
				
			||||||
				SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
 | 
									SPA_FORMAT_mediaType,      SPA_POD_Id(SPA_MEDIA_TYPE_application),
 | 
				
			||||||
| 
						 | 
					@ -2448,6 +2538,8 @@ static int param_buffers(struct client *c, struct port *p,
 | 
				
			||||||
	switch (p->object->port.type_id) {
 | 
						switch (p->object->port.type_id) {
 | 
				
			||||||
	case TYPE_ID_AUDIO:
 | 
						case TYPE_ID_AUDIO:
 | 
				
			||||||
	case TYPE_ID_MIDI:
 | 
						case TYPE_ID_MIDI:
 | 
				
			||||||
 | 
						case TYPE_ID_OSC:
 | 
				
			||||||
 | 
						case TYPE_ID_UMP:
 | 
				
			||||||
		*param = spa_pod_builder_add_object(b,
 | 
							*param = spa_pod_builder_add_object(b,
 | 
				
			||||||
			SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
 | 
								SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
 | 
				
			||||||
			SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
 | 
								SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
 | 
				
			||||||
| 
						 | 
					@ -2781,7 +2873,7 @@ static inline void *init_buffer(struct port *p, uint32_t nframes)
 | 
				
			||||||
	if (p->zeroed)
 | 
						if (p->zeroed)
 | 
				
			||||||
		return data;
 | 
							return data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (p->object->port.type_id == TYPE_ID_MIDI) {
 | 
						if (TYPE_ID_IS_EVENT(p->object->port.type_id)) {
 | 
				
			||||||
		struct midi_buffer *mb = data;
 | 
							struct midi_buffer *mb = data;
 | 
				
			||||||
		midi_init_buffer(data, c->max_frames, nframes);
 | 
							midi_init_buffer(data, c->max_frames, nframes);
 | 
				
			||||||
		pw_log_debug("port %p: init midi buffer size:%d frames:%d", p,
 | 
							pw_log_debug("port %p: init midi buffer size:%d frames:%d", p,
 | 
				
			||||||
| 
						 | 
					@ -3266,10 +3358,14 @@ static jack_port_type_id_t string_to_type(const char *port_type)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	if (spa_streq(JACK_DEFAULT_AUDIO_TYPE, port_type))
 | 
						if (spa_streq(JACK_DEFAULT_AUDIO_TYPE, port_type))
 | 
				
			||||||
		return TYPE_ID_AUDIO;
 | 
							return TYPE_ID_AUDIO;
 | 
				
			||||||
	else if (spa_streq(JACK_DEFAULT_MIDI_TYPE, port_type))
 | 
					 | 
				
			||||||
		return TYPE_ID_MIDI;
 | 
					 | 
				
			||||||
	else if (spa_streq(JACK_DEFAULT_VIDEO_TYPE, port_type))
 | 
						else if (spa_streq(JACK_DEFAULT_VIDEO_TYPE, port_type))
 | 
				
			||||||
		return TYPE_ID_VIDEO;
 | 
							return TYPE_ID_VIDEO;
 | 
				
			||||||
 | 
						else if (spa_streq(JACK_DEFAULT_MIDI_TYPE, port_type))
 | 
				
			||||||
 | 
							return TYPE_ID_MIDI;
 | 
				
			||||||
 | 
						else if (spa_streq(JACK_DEFAULT_OSC_TYPE, port_type))
 | 
				
			||||||
 | 
							return TYPE_ID_OSC;
 | 
				
			||||||
 | 
						else if (spa_streq(JACK_DEFAULT_UMP_TYPE, port_type))
 | 
				
			||||||
 | 
							return TYPE_ID_UMP;
 | 
				
			||||||
	else if (spa_streq("other", port_type))
 | 
						else if (spa_streq("other", port_type))
 | 
				
			||||||
		return TYPE_ID_OTHER;
 | 
							return TYPE_ID_OTHER;
 | 
				
			||||||
	else
 | 
						else
 | 
				
			||||||
| 
						 | 
					@ -3281,22 +3377,46 @@ static const char* type_to_string(jack_port_type_id_t type_id)
 | 
				
			||||||
	switch(type_id) {
 | 
						switch(type_id) {
 | 
				
			||||||
	case TYPE_ID_AUDIO:
 | 
						case TYPE_ID_AUDIO:
 | 
				
			||||||
		return JACK_DEFAULT_AUDIO_TYPE;
 | 
							return JACK_DEFAULT_AUDIO_TYPE;
 | 
				
			||||||
	case TYPE_ID_MIDI:
 | 
					 | 
				
			||||||
		return JACK_DEFAULT_MIDI_TYPE;
 | 
					 | 
				
			||||||
	case TYPE_ID_VIDEO:
 | 
						case TYPE_ID_VIDEO:
 | 
				
			||||||
		return JACK_DEFAULT_VIDEO_TYPE;
 | 
							return JACK_DEFAULT_VIDEO_TYPE;
 | 
				
			||||||
 | 
						case TYPE_ID_MIDI:
 | 
				
			||||||
 | 
						case TYPE_ID_OSC:
 | 
				
			||||||
 | 
						case TYPE_ID_UMP:
 | 
				
			||||||
 | 
							/* all returned as MIDI */
 | 
				
			||||||
 | 
							return JACK_DEFAULT_MIDI_TYPE;
 | 
				
			||||||
	case TYPE_ID_OTHER:
 | 
						case TYPE_ID_OTHER:
 | 
				
			||||||
		return "other";
 | 
							return "other";
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		return NULL;
 | 
							return NULL;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char* type_to_format_dsp(jack_port_type_id_t type_id)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						switch(type_id) {
 | 
				
			||||||
 | 
						case TYPE_ID_AUDIO:
 | 
				
			||||||
 | 
							return JACK_DEFAULT_AUDIO_TYPE;
 | 
				
			||||||
 | 
						case TYPE_ID_VIDEO:
 | 
				
			||||||
 | 
							return JACK_DEFAULT_VIDEO_TYPE;
 | 
				
			||||||
 | 
						case TYPE_ID_OSC:
 | 
				
			||||||
 | 
							return JACK_DEFAULT_OSC_TYPE;
 | 
				
			||||||
 | 
						case TYPE_ID_MIDI:
 | 
				
			||||||
 | 
						case TYPE_ID_UMP:
 | 
				
			||||||
 | 
							/* all exposed to PipeWire as UMP */
 | 
				
			||||||
 | 
							return JACK_DEFAULT_UMP_TYPE;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static bool type_is_dsp(jack_port_type_id_t type_id)
 | 
					static bool type_is_dsp(jack_port_type_id_t type_id)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	switch(type_id) {
 | 
						switch(type_id) {
 | 
				
			||||||
	case TYPE_ID_AUDIO:
 | 
						case TYPE_ID_AUDIO:
 | 
				
			||||||
	case TYPE_ID_MIDI:
 | 
						case TYPE_ID_MIDI:
 | 
				
			||||||
	case TYPE_ID_VIDEO:
 | 
						case TYPE_ID_VIDEO:
 | 
				
			||||||
 | 
						case TYPE_ID_OSC:
 | 
				
			||||||
 | 
						case TYPE_ID_UMP:
 | 
				
			||||||
		return true;
 | 
							return true;
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
| 
						 | 
					@ -3686,7 +3806,7 @@ static void registry_event_global(void *data, uint32_t id,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (is_monitor && !c->show_monitor)
 | 
							if (is_monitor && !c->show_monitor)
 | 
				
			||||||
			goto exit;
 | 
								goto exit;
 | 
				
			||||||
		if (type_id == TYPE_ID_MIDI && !c->show_midi)
 | 
							if (TYPE_ID_IS_EVENT(type_id) && !c->show_midi)
 | 
				
			||||||
			goto exit;
 | 
								goto exit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		o = NULL;
 | 
							o = NULL;
 | 
				
			||||||
| 
						 | 
					@ -3747,6 +3867,8 @@ static void registry_event_global(void *data, uint32_t id,
 | 
				
			||||||
						(int)(sizeof(tmp)-11), tmp, serial);
 | 
											(int)(sizeof(tmp)-11), tmp, serial);
 | 
				
			||||||
			else
 | 
								else
 | 
				
			||||||
				snprintf(o->port.name, sizeof(o->port.name), "%s", tmp);
 | 
									snprintf(o->port.name, sizeof(o->port.name), "%s", tmp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								o->port.type_id = type_id;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (c->fill_aliases) {
 | 
							if (c->fill_aliases) {
 | 
				
			||||||
| 
						 | 
					@ -3766,7 +3888,6 @@ static void registry_event_global(void *data, uint32_t id,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		o->port.flags = flags;
 | 
							o->port.flags = flags;
 | 
				
			||||||
		o->port.type_id = type_id;
 | 
					 | 
				
			||||||
		o->port.node_id = node_id;
 | 
							o->port.node_id = node_id;
 | 
				
			||||||
		o->port.is_monitor = is_monitor;
 | 
							o->port.is_monitor = is_monitor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5094,7 +5215,7 @@ jack_nframes_t jack_get_sample_rate (jack_client_t *client)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	c->sample_rate = res;
 | 
						c->sample_rate = res;
 | 
				
			||||||
	pw_log_debug("sample_rate: %u", res);
 | 
						pw_log_trace_fp("sample_rate: %u", res);
 | 
				
			||||||
	return res;
 | 
						return res;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5225,6 +5346,8 @@ jack_port_t * jack_port_register (jack_client_t *client,
 | 
				
			||||||
			p->get_buffer = get_buffer_input_float;
 | 
								p->get_buffer = get_buffer_input_float;
 | 
				
			||||||
			break;
 | 
								break;
 | 
				
			||||||
		case TYPE_ID_MIDI:
 | 
							case TYPE_ID_MIDI:
 | 
				
			||||||
 | 
							case TYPE_ID_OSC:
 | 
				
			||||||
 | 
							case TYPE_ID_UMP:
 | 
				
			||||||
			p->get_buffer = get_buffer_input_midi;
 | 
								p->get_buffer = get_buffer_input_midi;
 | 
				
			||||||
			break;
 | 
								break;
 | 
				
			||||||
		default:
 | 
							default:
 | 
				
			||||||
| 
						 | 
					@ -5238,6 +5361,8 @@ jack_port_t * jack_port_register (jack_client_t *client,
 | 
				
			||||||
			p->get_buffer = get_buffer_output_float;
 | 
								p->get_buffer = get_buffer_output_float;
 | 
				
			||||||
			break;
 | 
								break;
 | 
				
			||||||
		case TYPE_ID_MIDI:
 | 
							case TYPE_ID_MIDI:
 | 
				
			||||||
 | 
							case TYPE_ID_OSC:
 | 
				
			||||||
 | 
							case TYPE_ID_UMP:
 | 
				
			||||||
			p->get_buffer = get_buffer_output_midi;
 | 
								p->get_buffer = get_buffer_output_midi;
 | 
				
			||||||
			break;
 | 
								break;
 | 
				
			||||||
		default:
 | 
							default:
 | 
				
			||||||
| 
						 | 
					@ -5250,7 +5375,7 @@ jack_port_t * jack_port_register (jack_client_t *client,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	spa_list_init(&p->mix);
 | 
						spa_list_init(&p->mix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pw_properties_set(p->props, PW_KEY_FORMAT_DSP, port_type);
 | 
						pw_properties_set(p->props, PW_KEY_FORMAT_DSP, type_to_format_dsp(type_id));
 | 
				
			||||||
	pw_properties_set(p->props, PW_KEY_PORT_NAME, port_name);
 | 
						pw_properties_set(p->props, PW_KEY_PORT_NAME, port_name);
 | 
				
			||||||
	if (flags > 0x1f) {
 | 
						if (flags > 0x1f) {
 | 
				
			||||||
		pw_properties_setf(p->props, PW_KEY_PORT_EXTRA,
 | 
							pw_properties_setf(p->props, PW_KEY_PORT_EXTRA,
 | 
				
			||||||
| 
						 | 
					@ -5493,15 +5618,13 @@ static void *get_buffer_input_midi(struct port *p, jack_nframes_t frames)
 | 
				
			||||||
	/* first convert to a thread local scratch buffer, then memcpy into
 | 
						/* first convert to a thread local scratch buffer, then memcpy into
 | 
				
			||||||
	 * the per port buffer. This makes it possible to call this function concurrently
 | 
						 * the per port buffer. This makes it possible to call this function concurrently
 | 
				
			||||||
	 * but also have different pointers per port */
 | 
						 * but also have different pointers per port */
 | 
				
			||||||
	convert_to_midi(seq, n_seq, mb, p->client->fix_midi_events);
 | 
						convert_to_event(seq, n_seq, mb, p->client->fix_midi_events, p->object->port.type_id);
 | 
				
			||||||
	memcpy(ptr, mb, sizeof(struct midi_buffer) + (mb->event_count
 | 
						memcpy(ptr, mb, sizeof(struct midi_buffer) + (mb->event_count
 | 
				
			||||||
                              * sizeof(struct midi_event)));
 | 
					                              * sizeof(struct midi_event)));
 | 
				
			||||||
	if (mb->write_pos) {
 | 
						if (mb->write_pos) {
 | 
				
			||||||
		size_t offs = mb->buffer_size - 1 - mb->write_pos;
 | 
							size_t offs = mb->buffer_size - 1 - mb->write_pos;
 | 
				
			||||||
		memcpy(SPA_PTROFF(ptr, offs, void), SPA_PTROFF(mb, offs, void), mb->write_pos);
 | 
							memcpy(SPA_PTROFF(ptr, offs, void), SPA_PTROFF(mb, offs, void), mb->write_pos);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if (mb->event_count > 0)
 | 
					 | 
				
			||||||
		spa_debug_mem(0, ptr, 64);
 | 
					 | 
				
			||||||
	return ptr;
 | 
						return ptr;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5561,7 +5684,7 @@ void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames)
 | 
				
			||||||
		if ((b = get_mix_buffer(c, mix, frames)) == NULL)
 | 
							if ((b = get_mix_buffer(c, mix, frames)) == NULL)
 | 
				
			||||||
			goto done;
 | 
								goto done;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (o->port.type_id == TYPE_ID_MIDI) {
 | 
							if (TYPE_ID_IS_EVENT(o->port.type_id)) {
 | 
				
			||||||
			struct spa_pod_sequence *seq[1];
 | 
								struct spa_pod_sequence *seq[1];
 | 
				
			||||||
			struct spa_data *d;
 | 
								struct spa_data *d;
 | 
				
			||||||
			void *pod;
 | 
								void *pod;
 | 
				
			||||||
| 
						 | 
					@ -5576,7 +5699,7 @@ void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames)
 | 
				
			||||||
			if (!spa_pod_is_sequence(pod))
 | 
								if (!spa_pod_is_sequence(pod))
 | 
				
			||||||
				goto done;
 | 
									goto done;
 | 
				
			||||||
			seq[0] = pod;
 | 
								seq[0] = pod;
 | 
				
			||||||
			convert_to_midi(seq, 1, ptr, c->fix_midi_events);
 | 
								convert_to_event(seq, 1, ptr, c->fix_midi_events, o->port.type_id);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			ptr = get_buffer_data(b, frames);
 | 
								ptr = get_buffer_data(b, frames);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -5584,7 +5707,7 @@ void * jack_port_get_buffer (jack_port_t *port, jack_nframes_t frames)
 | 
				
			||||||
		ptr = p->get_buffer(p, frames);
 | 
							ptr = p->get_buffer(p, frames);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
done:
 | 
					done:
 | 
				
			||||||
	pw_log_warn("%p: port:%p buffer:%p frames:%d", c, p, ptr, frames);
 | 
						pw_log_trace_fp("%p: port:%p buffer:%p frames:%d", c, p, ptr, frames);
 | 
				
			||||||
	return ptr;
 | 
						return ptr;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6140,7 +6263,7 @@ int jack_connect (jack_client_t *client,
 | 
				
			||||||
	if (src == NULL || dst == NULL ||
 | 
						if (src == NULL || dst == NULL ||
 | 
				
			||||||
	    !(src->port.flags & JackPortIsOutput) ||
 | 
						    !(src->port.flags & JackPortIsOutput) ||
 | 
				
			||||||
	    !(dst->port.flags & JackPortIsInput) ||
 | 
						    !(dst->port.flags & JackPortIsInput) ||
 | 
				
			||||||
	    src->port.type_id != dst->port.type_id) {
 | 
						    !TYPE_ID_IS_COMPATIBLE(src->port.type_id, dst->port.type_id)) {
 | 
				
			||||||
		res = -EINVAL;
 | 
							res = -EINVAL;
 | 
				
			||||||
		goto exit;
 | 
							goto exit;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -6297,6 +6420,10 @@ size_t jack_port_type_get_buffer_size (jack_client_t *client, const char *port_t
 | 
				
			||||||
		return jack_get_buffer_size(client) * sizeof(float);
 | 
							return jack_get_buffer_size(client) * sizeof(float);
 | 
				
			||||||
	else if (spa_streq(JACK_DEFAULT_MIDI_TYPE, port_type))
 | 
						else if (spa_streq(JACK_DEFAULT_MIDI_TYPE, port_type))
 | 
				
			||||||
		return c->max_frames * sizeof(float);
 | 
							return c->max_frames * sizeof(float);
 | 
				
			||||||
 | 
						else if (spa_streq(JACK_DEFAULT_OSC_TYPE, port_type))
 | 
				
			||||||
 | 
							return c->max_frames * sizeof(float);
 | 
				
			||||||
 | 
						else if (spa_streq(JACK_DEFAULT_UMP_TYPE, port_type))
 | 
				
			||||||
 | 
							return c->max_frames * sizeof(float);
 | 
				
			||||||
	else if (spa_streq(JACK_DEFAULT_VIDEO_TYPE, port_type))
 | 
						else if (spa_streq(JACK_DEFAULT_VIDEO_TYPE, port_type))
 | 
				
			||||||
		return 320 * 240 * 4 * sizeof(float);
 | 
							return 320 * 240 * 4 * sizeof(float);
 | 
				
			||||||
	else
 | 
						else
 | 
				
			||||||
| 
						 | 
					@ -6559,7 +6686,7 @@ const char ** jack_get_ports (jack_client_t *client,
 | 
				
			||||||
			continue;
 | 
								continue;
 | 
				
			||||||
		pw_log_debug("%p: check port type:%d flags:%08lx name:\"%s\"", c,
 | 
							pw_log_debug("%p: check port type:%d flags:%08lx name:\"%s\"", c,
 | 
				
			||||||
				o->port.type_id, o->port.flags, o->port.name);
 | 
									o->port.type_id, o->port.flags, o->port.name);
 | 
				
			||||||
		if (o->port.type_id > TYPE_ID_VIDEO)
 | 
							if (TYPE_ID_IS_HIDDEN(o->port.type_id))
 | 
				
			||||||
			continue;
 | 
								continue;
 | 
				
			||||||
		if (!SPA_FLAG_IS_SET(o->port.flags, flags))
 | 
							if (!SPA_FLAG_IS_SET(o->port.flags, flags))
 | 
				
			||||||
			continue;
 | 
								continue;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue