mirror of
				https://gitlab.freedesktop.org/pipewire/pipewire.git
				synced 2025-10-29 05:40:27 -04:00 
			
		
		
		
	alsa: rework timing
Use a DLL to track the hardware pointer and use this to set the timer. Handle XRUN and recover.
This commit is contained in:
		
							parent
							
								
									5444b850d2
								
							
						
					
					
						commit
						0343297257
					
				
					 4 changed files with 270 additions and 232 deletions
				
			
		|  | @ -229,11 +229,11 @@ static int impl_node_send_command(struct spa_node *node, const struct spa_comman | ||||||
| 		if (this->n_buffers == 0) | 		if (this->n_buffers == 0) | ||||||
| 			return -EIO; | 			return -EIO; | ||||||
| 
 | 
 | ||||||
| 		if ((res = spa_alsa_start(this, false)) < 0) | 		if ((res = spa_alsa_start(this)) < 0) | ||||||
| 			return res; | 			return res; | ||||||
| 		break; | 		break; | ||||||
| 	case SPA_NODE_COMMAND_Pause: | 	case SPA_NODE_COMMAND_Pause: | ||||||
| 		if ((res = spa_alsa_pause(this, false)) < 0) | 		if ((res = spa_alsa_pause(this)) < 0) | ||||||
| 			return res; | 			return res; | ||||||
| 		break; | 		break; | ||||||
| 	default: | 	default: | ||||||
|  | @ -431,6 +431,13 @@ impl_node_port_enum_params(struct spa_node *node, | ||||||
| 				SPA_PARAM_IO_size, &SPA_POD_Int(sizeof(struct spa_io_clock)), | 				SPA_PARAM_IO_size, &SPA_POD_Int(sizeof(struct spa_io_clock)), | ||||||
| 				0); | 				0); | ||||||
| 			break; | 			break; | ||||||
|  | 		case 2: | ||||||
|  | 			param = spa_pod_builder_object(&b, | ||||||
|  | 				SPA_TYPE_OBJECT_ParamIO, id, | ||||||
|  | 				SPA_PARAM_IO_id,   &SPA_POD_Id(SPA_IO_Notify), | ||||||
|  | 				SPA_PARAM_IO_size, &SPA_POD_Int(sizeof(struct spa_io_sequence) + 1024), | ||||||
|  | 				0); | ||||||
|  | 			break; | ||||||
| 		default: | 		default: | ||||||
| 			return 0; | 			return 0; | ||||||
| 		} | 		} | ||||||
|  | @ -467,7 +474,7 @@ static int port_set_format(struct spa_node *node, | ||||||
| 
 | 
 | ||||||
| 	if (format == NULL) { | 	if (format == NULL) { | ||||||
| 		spa_log_debug(this->log, "clear format"); | 		spa_log_debug(this->log, "clear format"); | ||||||
| 		spa_alsa_pause(this, false); | 		spa_alsa_pause(this); | ||||||
| 		clear_buffers(this); | 		clear_buffers(this); | ||||||
| 		spa_alsa_close(this); | 		spa_alsa_close(this); | ||||||
| 		this->have_format = false; | 		this->have_format = false; | ||||||
|  | @ -535,7 +542,7 @@ impl_node_port_use_buffers(struct spa_node *node, | ||||||
| 		return -EIO; | 		return -EIO; | ||||||
| 
 | 
 | ||||||
| 	if (n_buffers == 0) { | 	if (n_buffers == 0) { | ||||||
| 		spa_alsa_pause(this, false); | 		spa_alsa_pause(this); | ||||||
| 		clear_buffers(this); | 		clear_buffers(this); | ||||||
| 		return 0; | 		return 0; | ||||||
| 	} | 	} | ||||||
|  | @ -609,6 +616,9 @@ impl_node_port_set_io(struct spa_node *node, | ||||||
| 	case SPA_IO_Clock: | 	case SPA_IO_Clock: | ||||||
| 		this->clock = data; | 		this->clock = data; | ||||||
| 		break; | 		break; | ||||||
|  | 	case SPA_IO_Notify: | ||||||
|  | 		this->notify = data; | ||||||
|  | 		break; | ||||||
| 	default: | 	default: | ||||||
| 		return -ENOENT; | 		return -ENOENT; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -231,11 +231,11 @@ static int impl_node_send_command(struct spa_node *node, const struct spa_comman | ||||||
| 		if (this->n_buffers == 0) | 		if (this->n_buffers == 0) | ||||||
| 			return -EIO; | 			return -EIO; | ||||||
| 
 | 
 | ||||||
| 		if ((res = spa_alsa_start(this, false)) < 0) | 		if ((res = spa_alsa_start(this)) < 0) | ||||||
| 			return res; | 			return res; | ||||||
| 		break; | 		break; | ||||||
| 	case SPA_NODE_COMMAND_Pause: | 	case SPA_NODE_COMMAND_Pause: | ||||||
| 		if ((res = spa_alsa_pause(this, false)) < 0) | 		if ((res = spa_alsa_pause(this)) < 0) | ||||||
| 			return res; | 			return res; | ||||||
| 		break; | 		break; | ||||||
| 	default: | 	default: | ||||||
|  | @ -477,7 +477,7 @@ static int port_set_format(struct spa_node *node, | ||||||
| 	int err; | 	int err; | ||||||
| 
 | 
 | ||||||
| 	if (format == NULL) { | 	if (format == NULL) { | ||||||
| 		spa_alsa_pause(this, false); | 		spa_alsa_pause(this); | ||||||
| 		clear_buffers(this); | 		clear_buffers(this); | ||||||
| 		spa_alsa_close(this); | 		spa_alsa_close(this); | ||||||
| 		this->have_format = false; | 		this->have_format = false; | ||||||
|  | @ -544,7 +544,7 @@ impl_node_port_use_buffers(struct spa_node *node, | ||||||
| 		return -EIO; | 		return -EIO; | ||||||
| 
 | 
 | ||||||
| 	if (this->n_buffers > 0) { | 	if (this->n_buffers > 0) { | ||||||
| 		spa_alsa_pause(this, false); | 		spa_alsa_pause(this); | ||||||
| 		if ((res = clear_buffers(this)) < 0) | 		if ((res = clear_buffers(this)) < 0) | ||||||
| 			return res; | 			return res; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ | ||||||
| #include <sys/timerfd.h> | #include <sys/timerfd.h> | ||||||
| 
 | 
 | ||||||
| #include <spa/pod/filter.h> | #include <spa/pod/filter.h> | ||||||
|  | #include <spa/control/control.h> | ||||||
| 
 | 
 | ||||||
| #include "alsa-utils.h" | #include "alsa-utils.h" | ||||||
| 
 | 
 | ||||||
|  | @ -36,6 +37,7 @@ static int spa_alsa_open(struct state *state) | ||||||
| 	state->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); | 	state->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); | ||||||
| 	state->opened = true; | 	state->opened = true; | ||||||
| 	state->sample_count = 0; | 	state->sample_count = 0; | ||||||
|  | 	state->sample_time = 0; | ||||||
| 
 | 
 | ||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
|  | @ -450,7 +452,6 @@ static int set_swparams(struct state *state) | ||||||
| 	snd_pcm_t *hndl = state->hndl; | 	snd_pcm_t *hndl = state->hndl; | ||||||
| 	int err = 0; | 	int err = 0; | ||||||
| 	snd_pcm_sw_params_t *params; | 	snd_pcm_sw_params_t *params; | ||||||
| 	snd_pcm_uframes_t boundary; |  | ||||||
| 
 | 
 | ||||||
| 	snd_pcm_sw_params_alloca(¶ms); | 	snd_pcm_sw_params_alloca(¶ms); | ||||||
| 
 | 
 | ||||||
|  | @ -461,9 +462,6 @@ static int set_swparams(struct state *state) | ||||||
| 
 | 
 | ||||||
| 	/* start the transfer */ | 	/* start the transfer */ | ||||||
| 	CHECK(snd_pcm_sw_params_set_start_threshold(hndl, params, LONG_MAX), "set_start_threshold"); | 	CHECK(snd_pcm_sw_params_set_start_threshold(hndl, params, LONG_MAX), "set_start_threshold"); | ||||||
| 	CHECK(snd_pcm_sw_params_get_boundary(params, &boundary), "get_boundary"); |  | ||||||
| 
 |  | ||||||
| 	CHECK(snd_pcm_sw_params_set_stop_threshold(hndl, params, boundary), "set_stop_threshold"); |  | ||||||
| 
 | 
 | ||||||
| 	CHECK(snd_pcm_sw_params_set_period_event(hndl, params, 0), "set_period_event"); | 	CHECK(snd_pcm_sw_params_set_period_event(hndl, params, 0), "set_period_event"); | ||||||
| 
 | 
 | ||||||
|  | @ -473,65 +471,144 @@ static int set_swparams(struct state *state) | ||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static inline void calc_timeout(size_t target, size_t current, | static int set_timeout(struct state *state, uint64_t time) | ||||||
| 				size_t rate, snd_htimestamp_t *now, |  | ||||||
| 				struct timespec *ts) |  | ||||||
| { |  | ||||||
| 	ts->tv_sec = now->tv_sec; |  | ||||||
| 	ts->tv_nsec = now->tv_nsec; |  | ||||||
| 	if (target > current) |  | ||||||
| 		ts->tv_nsec += ((target - current) * SPA_NSEC_PER_SEC) / rate; |  | ||||||
| 
 |  | ||||||
| 	while (ts->tv_nsec >= SPA_NSEC_PER_SEC) { |  | ||||||
| 		ts->tv_sec++; |  | ||||||
| 		ts->tv_nsec -= SPA_NSEC_PER_SEC; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static int set_timeout(struct state *state, size_t extra) |  | ||||||
| { | { | ||||||
| 	struct itimerspec ts; | 	struct itimerspec ts; | ||||||
| 
 | 
 | ||||||
| 	if (!state->slaved) { | 	if (!state->slaved) { | ||||||
| 		calc_timeout(state->filled + extra, state->threshold, state->rate, &state->now, &ts.it_value); | 		ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC; | ||||||
| 
 | 		ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC; | ||||||
| 		ts.it_interval.tv_sec = 0; | 		ts.it_interval.tv_sec = 0; | ||||||
| 		ts.it_interval.tv_nsec = ((size_t)state->threshold * SPA_NSEC_PER_SEC) / state->rate; | 		ts.it_interval.tv_nsec = 0; | ||||||
| 		timerfd_settime(state->timerfd, TFD_TIMER_ABSTIME, &ts, NULL); | 		timerfd_settime(state->timerfd, TFD_TIMER_ABSTIME, &ts, NULL); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static int get_status(struct state *state, snd_pcm_sframes_t *avail, snd_htimestamp_t *now) | static int get_status(struct state *state, snd_pcm_sframes_t *delay) | ||||||
| { | { | ||||||
| 	snd_pcm_status_t *status; | 	snd_pcm_status_t *status; | ||||||
| 	int res; | 	snd_pcm_sframes_t av; | ||||||
|  | 	int res, st; | ||||||
| 
 | 
 | ||||||
| 	snd_pcm_status_alloca(&status); | 	snd_pcm_status_alloca(&status); | ||||||
| 
 | again: | ||||||
| 	if ((res = snd_pcm_status(state->hndl, status)) < 0) { | 	if ((res = snd_pcm_status(state->hndl, status)) < 0) { | ||||||
| 		spa_log_error(state->log, "snd_pcm_status error: %s", snd_strerror(res)); | 		spa_log_error(state->log, "snd_pcm_status error: %s", snd_strerror(res)); | ||||||
| 		return res; | 		return res; | ||||||
| 	} | 	} | ||||||
|  | 	st = snd_pcm_status_get_state(status); | ||||||
|  | 	if (st == SND_PCM_STATE_XRUN) { | ||||||
|  | 		struct timeval now, trigger, diff; | ||||||
|  | 		uint64_t xrun, missing; | ||||||
| 
 | 
 | ||||||
| 	if (avail) { | 	        snd_pcm_status_get_tstamp (status, &now); | ||||||
| 		*avail = snd_pcm_status_get_avail(status); | 		snd_pcm_status_get_trigger_tstamp (status, &trigger); | ||||||
| 		if (*avail > state->buffer_frames) |                 timersub(&now, &trigger, &diff); | ||||||
| 			*avail = state->buffer_frames; | 
 | ||||||
| 	} | 		xrun = SPA_TIMEVAL_TO_USEC(&diff); | ||||||
| 	if (now) { | 		missing = xrun * state->rate / SPA_USEC_PER_SEC; | ||||||
|  | 
 | ||||||
|  | 		state->sample_time = state->sample_count; | ||||||
|  | 		state->sample_count += missing; | ||||||
| #if 0 | #if 0 | ||||||
| 		clock_gettime(CLOCK_MONOTONIC, now); | 		state->safety = SPA_MIN(state->safety + 0.000333, 0.0013333); | ||||||
| #else | 		dll_bandwidth(&state->dll, DLL_BW_MAX); | ||||||
| 		snd_pcm_status_get_htstamp(status, now); |  | ||||||
| 		if (now->tv_sec == 0 && now->tv_nsec == 0) { |  | ||||||
| 			spa_log_warn(state->log, "0 from snd_pcm_status_get_htstamp %ld", *avail); |  | ||||||
| 			clock_gettime(CLOCK_MONOTONIC, now); |  | ||||||
| 		} |  | ||||||
| #endif | #endif | ||||||
|  | 		spa_log_error(state->log, "xrun of %"PRIu64" usec %"PRIu64" %f", | ||||||
|  | 				xrun, missing, state->safety); | ||||||
|  | 
 | ||||||
|  | 		if ((res = snd_pcm_prepare(state->hndl)) < 0) { | ||||||
|  | 			spa_log_error(state->log, "snd_pcm_prepare error: %s", snd_strerror(res)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (state->stream == SND_PCM_STREAM_CAPTURE) { | ||||||
|  | 			if ((res = snd_pcm_start(state->hndl)) < 0) { | ||||||
|  | 				spa_log_error(state->log, "snd_pcm_start: %s", snd_strerror(res)); | ||||||
|  | 				return res; | ||||||
|  | 			} | ||||||
|  | 			state->alsa_started = true; | ||||||
|  | 		} else { | ||||||
|  | 			state->alsa_started = false; | ||||||
|  | 		} | ||||||
|  | 		spa_alsa_write(state, state->threshold * 2); | ||||||
|  | 		goto again; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	av = snd_pcm_status_get_avail(status); | ||||||
|  | 	if (av > state->buffer_frames) | ||||||
|  | 		av = state->buffer_frames; | ||||||
|  | 
 | ||||||
|  | 	if (delay) { | ||||||
|  | 		if (state->stream == SND_PCM_STREAM_PLAYBACK) | ||||||
|  | 			*delay = state->buffer_frames - av; | ||||||
|  | 		else | ||||||
|  | 			*delay = av; | ||||||
|  | 	} | ||||||
|  | 	return 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static int update_time(struct state *state, uint64_t nsec, snd_pcm_sframes_t delay, | ||||||
|  | 		uint64_t *period, bool slaved) | ||||||
|  | { | ||||||
|  | 	uint64_t sample_time, elapsed; | ||||||
|  | 	double tw; | ||||||
|  | 
 | ||||||
|  | 	if (!slaved) { | ||||||
|  | 		sample_time = state->sample_count; | ||||||
|  | 		elapsed = sample_time - state->sample_time; | ||||||
|  | 		state->sample_time = sample_time; | ||||||
|  | 	} else { | ||||||
|  | 		elapsed = state->threshold; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/* if our buffers are too full, pause the dll */ | ||||||
|  | 	if (delay >= state->threshold * 2 || elapsed == 0) { | ||||||
|  | 		elapsed = state->threshold; | ||||||
|  | 		delay = state->threshold; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/* we try to match the delay with the number of played samples */ | ||||||
|  | 	tw = nsec * 1e-9 + (double)delay / state->rate - state->safety; | ||||||
|  | 	tw = dll_update(&state->dll, tw, (double)elapsed / state->rate); | ||||||
|  | 	state->next_time = (tw - state->safety) * 1e9; | ||||||
|  | 
 | ||||||
|  | 	if (state->dll.bw > DLL_BW_MIN && tw > state->dll.base + DLL_BW_PERIOD) | ||||||
|  | 		dll_bandwidth(&state->dll, DLL_BW_MIN); | ||||||
|  | 
 | ||||||
|  | 	if (state->clock) { | ||||||
|  | 		state->clock->nsec = state->last_time; | ||||||
|  | 		state->clock->rate = SPA_FRACTION(1, state->rate); | ||||||
|  | 		state->clock->position = state->sample_count; | ||||||
|  | 		state->clock->delay = state->stream == SND_PCM_STREAM_CAPTURE ? delay : -delay; | ||||||
|  | 		state->clock->rate_diff = state->dll.dt; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	state->old_dt = SPA_CLAMP(state->dll.dt, 0.95, 1.05); | ||||||
|  | 
 | ||||||
|  | #if 0 | ||||||
|  | 	if (slaved && state->notify) { | ||||||
|  | 		struct spa_pod_builder b = { 0 }; | ||||||
|  | 	        spa_pod_builder_init(&b, state->notify, 1024); | ||||||
|  | 		spa_pod_builder_push_sequence(&b, 0); | ||||||
|  | 		spa_pod_builder_control_header(&b, 0, SPA_CONTROL_Properties); | ||||||
|  | 		spa_pod_builder_push_object(&b, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props); | ||||||
|  | 		spa_pod_builder_prop(&b, SPA_PROP_rate, 0); | ||||||
|  | 		spa_pod_builder_double(&b, state->old_dt); | ||||||
|  | 		spa_pod_builder_pop(&b); | ||||||
|  | 		spa_pod_builder_pop(&b); | ||||||
|  | 	} | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | 	spa_log_trace(state->log, "%"PRIu64" %f %"PRIi64" %"PRIi64" %"PRIi64" %d %"PRIu64, nsec, | ||||||
|  | 			state->old_dt, delay, elapsed, (int64_t)(nsec - state->last_time), | ||||||
|  | 			state->threshold, state->next_time); | ||||||
|  | 
 | ||||||
|  | 	state->last_time = nsec; | ||||||
|  | 	if (period) | ||||||
|  | 		*period = elapsed; | ||||||
|  | 
 | ||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -539,34 +616,31 @@ int spa_alsa_write(struct state *state, snd_pcm_uframes_t silence) | ||||||
| { | { | ||||||
| 	snd_pcm_t *hndl = state->hndl; | 	snd_pcm_t *hndl = state->hndl; | ||||||
| 	const snd_pcm_channel_area_t *my_areas; | 	const snd_pcm_channel_area_t *my_areas; | ||||||
| 	snd_pcm_uframes_t written, frames, offset, off, to_write; | 	snd_pcm_uframes_t written, frames, offset, off, to_write, total_written; | ||||||
| 	int res; | 	int res; | ||||||
| 
 | 
 | ||||||
| 	if (state->position) | 	if (state->position && state->threshold != state->position->size) | ||||||
| 		state->threshold = state->position->size; | 		state->threshold = state->position->size; | ||||||
| 
 | 
 | ||||||
| 	if (state->slaved) { | 	if (state->slaved) { | ||||||
| 		double dts, pts, rate_diff = 1.0; | 		uint64_t nsec, master; | ||||||
| 		struct timespec now; | 		snd_pcm_sframes_t delay; | ||||||
| 		snd_pcm_sframes_t avail; |  | ||||||
| 
 | 
 | ||||||
| 		if ((res = get_status(state, &avail, &now)) < 0) | 		master = state->position->clock.position + state->position->clock.delay; | ||||||
|  | 		nsec = master * SPA_NSEC_PER_SEC / state->rate; | ||||||
|  | 
 | ||||||
|  | 		if ((res = get_status(state, &delay)) < 0) | ||||||
| 			return res; | 			return res; | ||||||
| 
 | 
 | ||||||
| 		state->now = now; | 		if ((res = update_time(state, nsec, delay, NULL, true)) < 0) | ||||||
| 		state->filled = state->buffer_frames - avail; | 			return res; | ||||||
| 
 | 
 | ||||||
| 		dts = ((state->position->clock.position - state->filled) * 1000000ll / state->rate); | 		spa_log_trace(state->log, "slave %f %"PRIi64" %"PRIu64" %d", | ||||||
| 		pts = dll_update(&state->dll, dts, state->threshold); | 				state->dll.dt, nsec, delay, state->rate); | ||||||
| 		rate_diff = state->dll.T * state->rate / 1000000.f; |  | ||||||
| 
 |  | ||||||
| 		if (state->bw != 0.05 && state->sample_count / state->rate > 4) { |  | ||||||
| 			state->bw = 0.05; |  | ||||||
| 			dll_bandwidth(&state->dll, state->threshold, state->rate, state->bw); |  | ||||||
| 		} |  | ||||||
| 		spa_log_trace(state->log, "slave %f %f %f", dts, pts, rate_diff); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	total_written = 0; | ||||||
|  | again: | ||||||
| 	frames = state->buffer_frames; | 	frames = state->buffer_frames; | ||||||
| 	if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0) { | 	if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0) { | ||||||
| 		spa_log_error(state->log, "snd_pcm_mmap_begin error: %s", snd_strerror(res)); | 		spa_log_error(state->log, "snd_pcm_mmap_begin error: %s", snd_strerror(res)); | ||||||
|  | @ -628,8 +702,6 @@ int spa_alsa_write(struct state *state, snd_pcm_uframes_t silence) | ||||||
| 		else | 		else | ||||||
| 			silence = 0; | 			silence = 0; | ||||||
| 	} | 	} | ||||||
| 	if (written == 0) |  | ||||||
| 		silence = SPA_MIN(to_write, state->threshold); |  | ||||||
| 
 | 
 | ||||||
| 	if (silence > 0) { | 	if (silence > 0) { | ||||||
| 		spa_log_trace(state->log, "silence %ld", silence); | 		spa_log_trace(state->log, "silence %ld", silence); | ||||||
|  | @ -637,24 +709,29 @@ int spa_alsa_write(struct state *state, snd_pcm_uframes_t silence) | ||||||
| 		written += silence; | 		written += silence; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	spa_log_trace(state->log, "commit %ld %ld", offset, written); | 	spa_log_trace(state->log, "commit %ld %ld %"PRIi64, offset, written, state->sample_count); | ||||||
|  | 	total_written += written; | ||||||
|  | 
 | ||||||
| 	if ((res = snd_pcm_mmap_commit(hndl, offset, written)) < 0) { | 	if ((res = snd_pcm_mmap_commit(hndl, offset, written)) < 0) { | ||||||
| 		spa_log_error(state->log, "snd_pcm_mmap_commit error: %s", snd_strerror(res)); | 		spa_log_error(state->log, "snd_pcm_mmap_commit error: %s", snd_strerror(res)); | ||||||
| 		if (res != -EPIPE && res != -ESTRPIPE) | 		if (res != -EPIPE && res != -ESTRPIPE) | ||||||
| 			return res; | 			return res; | ||||||
| 	} | 	} | ||||||
| 	state->sample_count += written; | 
 | ||||||
| 	state->filled += written; | 	if (!spa_list_is_empty(&state->ready) && total_written < state->threshold) | ||||||
|  | 		goto again; | ||||||
|  | 
 | ||||||
|  | 	state->sample_count += total_written; | ||||||
| 
 | 
 | ||||||
| 	if (!state->alsa_started && written > 0) { | 	if (!state->alsa_started && written > 0) { | ||||||
| 		spa_log_trace(state->log, "snd_pcm_start"); | 		spa_log_trace(state->log, "snd_pcm_start %lu", written); | ||||||
| 		if ((res = snd_pcm_start(hndl)) < 0) { | 		if ((res = snd_pcm_start(hndl)) < 0) { | ||||||
| 			spa_log_error(state->log, "snd_pcm_start: %s", snd_strerror(res)); | 			spa_log_error(state->log, "snd_pcm_start: %s", snd_strerror(res)); | ||||||
| 			return res; | 			return res; | ||||||
| 		} | 		} | ||||||
| 		state->alsa_started = true; | 		state->alsa_started = true; | ||||||
| 	} | 	} | ||||||
| 	set_timeout(state, 0); | 	set_timeout(state, state->next_time); | ||||||
| 
 | 
 | ||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
|  | @ -714,176 +791,114 @@ push_frames(struct state *state, | ||||||
| 	return total_frames; | 	return total_frames; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static int alsa_try_resume(struct state *state) |  | ||||||
| { |  | ||||||
| 	int res; |  | ||||||
| 
 |  | ||||||
| 	while ((res = snd_pcm_resume(state->hndl)) == -EAGAIN) |  | ||||||
| 		usleep(250000); |  | ||||||
| 	if (res < 0) { |  | ||||||
| 		spa_log_error(state->log, "suspended, failed to resume %s", snd_strerror(res)); |  | ||||||
| 		res = snd_pcm_prepare(state->hndl); |  | ||||||
| 		if (res < 0) |  | ||||||
| 			spa_log_error(state->log, "suspended, failed to prepare %s", snd_strerror(res)); |  | ||||||
| 	} |  | ||||||
| 	return res; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static void alsa_on_playback_timeout_event(struct spa_source *source) | static void alsa_on_playback_timeout_event(struct spa_source *source) | ||||||
| { | { | ||||||
| 	uint64_t exp, nsec_now; |  | ||||||
| 	int res; | 	int res; | ||||||
| 	struct state *state = source->data; | 	struct state *state = source->data; | ||||||
| 	snd_pcm_t *hndl = state->hndl; | 	snd_pcm_sframes_t delay; | ||||||
| 	snd_pcm_sframes_t avail; | 	uint64_t nsec, expire, elapsed; | ||||||
| 	struct timespec now; |  | ||||||
| 	double pts, dts, rate_diff; |  | ||||||
| 
 | 
 | ||||||
| 	if (state->started && read(state->timerfd, &exp, sizeof(uint64_t)) != sizeof(uint64_t)) | 	if (state->started && read(state->timerfd, &expire, sizeof(uint64_t)) != sizeof(uint64_t)) | ||||||
| 		spa_log_warn(state->log, "error reading timerfd: %s", strerror(errno)); | 		spa_log_warn(state->log, "error reading timerfd: %s", strerror(errno)); | ||||||
| 
 | 
 | ||||||
| 	if ((res = get_status(state, &avail, &now)) < 0) | 	if (state->position && state->threshold != state->position->size) | ||||||
| 		return; |  | ||||||
| 
 |  | ||||||
| 	state->now = now; |  | ||||||
| 
 |  | ||||||
| 	if (state->position) |  | ||||||
| 		state->threshold = state->position->size; | 		state->threshold = state->position->size; | ||||||
| 
 | 
 | ||||||
| 	if (avail > state->buffer_frames) | 	clock_gettime(CLOCK_MONOTONIC, &state->now); | ||||||
| 		avail = state->buffer_frames; | 	if ((res = get_status(state, &delay)) < 0) | ||||||
|  | 		return; | ||||||
| 
 | 
 | ||||||
| 	state->filled = state->buffer_frames - avail; | 	spa_log_trace(state->log, "timeout %ld %d %ld", delay, | ||||||
| 	nsec_now = SPA_TIMESPEC_TO_NSEC(&state->now); | 		state->threshold, state->sample_count); | ||||||
| 
 | 
 | ||||||
| 	dts = nsec_now / 1000ll - (state->filled * 1000000ll / state->rate); | 	nsec = SPA_TIMESPEC_TO_NSEC(&state->now); | ||||||
| 	pts = dll_update(&state->dll, dts, state->threshold); | 	if ((res = update_time(state, nsec, delay, &elapsed, false)) < 0) | ||||||
| 	rate_diff = state->dll.T * state->rate / 1000000.f; | 		return; | ||||||
| 
 | 
 | ||||||
| 	if (state->clock) { | 	if (delay >= state->threshold * 2) | ||||||
| 		state->clock->nsec = nsec_now; | 		goto next; | ||||||
| 		state->clock->rate = SPA_FRACTION(1, state->rate); | 
 | ||||||
| 		state->clock->position = state->sample_count; | 	if (spa_list_is_empty(&state->ready)) { | ||||||
| 		state->clock->delay = -state->filled; | 		struct spa_io_buffers *io = state->io; | ||||||
| 		state->clock->rate_diff = rate_diff; | 
 | ||||||
|  | 		spa_log_trace(state->log, "alsa-util %p: %d", state, io->status); | ||||||
|  | 
 | ||||||
|  | 		io->status = SPA_STATUS_NEED_BUFFER; | ||||||
|  | 		if (state->range) { | ||||||
|  | 			state->range->offset = state->sample_count * state->frame_size; | ||||||
|  | 			state->range->min_size = state->threshold * state->frame_size; | ||||||
|  | 			state->range->max_size = state->threshold * state->frame_size; | ||||||
|  | 		} | ||||||
|  | 		state->callbacks->process(state->callbacks_data, SPA_STATUS_NEED_BUFFER); | ||||||
|  | 
 | ||||||
|  | next: | ||||||
|  | 		set_timeout(state, state->next_time); | ||||||
| 	} | 	} | ||||||
| 
 | 	else { | ||||||
| 	if (state->bw != 0.05 && state->sample_count / state->rate > 4) { | 		spa_alsa_write(state, 0); | ||||||
| 		state->bw = 0.05; |  | ||||||
| 		dll_bandwidth(&state->dll, state->threshold, state->rate, state->bw); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	spa_log_trace(state->log, "timeout %ld %d %ld %ld %f %f %f", state->filled, state->threshold, |  | ||||||
| 		      state->sample_count, nsec_now, pts, dts, rate_diff); |  | ||||||
| 
 |  | ||||||
| 	if (state->filled > state->threshold * 2) { |  | ||||||
| 		if (snd_pcm_state(hndl) == SND_PCM_STATE_SUSPENDED) { |  | ||||||
| 			spa_log_error(state->log, "suspended: try resume"); |  | ||||||
| 			if ((res = alsa_try_resume(state)) < 0) |  | ||||||
| 				return; |  | ||||||
| 		} |  | ||||||
| 		set_timeout(state, 0); |  | ||||||
| 	} else { |  | ||||||
| 		if (spa_list_is_empty(&state->ready)) { |  | ||||||
| 			struct spa_io_buffers *io = state->io; |  | ||||||
| 
 |  | ||||||
| 			if (state->filled == 0) { |  | ||||||
| 				if (state->alsa_started) |  | ||||||
| 					spa_log_warn(state->log, |  | ||||||
| 							"alsa-util %p: underrun", state); |  | ||||||
| 				spa_alsa_write(state, state->threshold); |  | ||||||
| 			} |  | ||||||
| 			spa_log_trace(state->log, "alsa-util %p: %d %lu", state, io->status, |  | ||||||
| 					state->filled); |  | ||||||
| 
 |  | ||||||
| 			io->status = SPA_STATUS_NEED_BUFFER; |  | ||||||
| 			if (state->range) { |  | ||||||
| 				state->range->offset = state->sample_count * state->frame_size; |  | ||||||
| 				state->range->min_size = state->threshold * state->frame_size; |  | ||||||
| 				state->range->max_size = avail * state->frame_size; |  | ||||||
| 			} |  | ||||||
| 			state->callbacks->process(state->callbacks_data, SPA_STATUS_NEED_BUFFER); |  | ||||||
| 		} |  | ||||||
| 		else { |  | ||||||
| 			spa_alsa_write(state, 0); |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| static void alsa_on_capture_timeout_event(struct spa_source *source) | static void alsa_on_capture_timeout_event(struct spa_source *source) | ||||||
| { | { | ||||||
| 	uint64_t exp; | 	uint64_t expire, nsec; | ||||||
| 	int res; | 	int res; | ||||||
| 	struct state *state = source->data; | 	struct state *state = source->data; | ||||||
| 	snd_pcm_t *hndl = state->hndl; | 	snd_pcm_t *hndl = state->hndl; | ||||||
| 	snd_pcm_sframes_t avail; | 	snd_pcm_sframes_t delay; | ||||||
| 	snd_pcm_uframes_t total_read = 0; | 	snd_pcm_uframes_t total_read = 0, to_read; | ||||||
| 	struct itimerspec ts; |  | ||||||
| 	const snd_pcm_channel_area_t *my_areas; | 	const snd_pcm_channel_area_t *my_areas; | ||||||
| 	struct timespec now; |  | ||||||
| 
 | 
 | ||||||
| 	if (state->started && read(state->timerfd, &exp, sizeof(uint64_t)) != sizeof(uint64_t)) | 	if (state->started && read(state->timerfd, &expire, sizeof(uint64_t)) != sizeof(uint64_t)) | ||||||
| 		spa_log_warn(state->log, "error reading timerfd: %s", strerror(errno)); | 		spa_log_warn(state->log, "error reading timerfd: %s", strerror(errno)); | ||||||
| 
 | 
 | ||||||
| 	if ((res = get_status(state, &avail, &now)) < 0) |  | ||||||
| 		return; |  | ||||||
| 
 |  | ||||||
| 	state->now = now; |  | ||||||
| 
 |  | ||||||
| 	if (state->position) | 	if (state->position) | ||||||
| 		state->threshold = state->position->size; | 		state->threshold = state->position->size; | ||||||
| 
 | 
 | ||||||
| 	if (state->clock) { | 	clock_gettime(CLOCK_MONOTONIC, &state->now); | ||||||
| 		state->clock->nsec = SPA_TIMESPEC_TO_NSEC(&state->now); | 	if ((res = get_status(state, &delay)) < 0) | ||||||
| 		state->clock->rate = SPA_FRACTION(1, state->rate); | 		return; | ||||||
| 		state->clock->position = state->sample_count; |  | ||||||
| 		state->clock->delay = avail; |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	spa_log_trace(state->log, "timeout %ld %d %ld %ld %ld %ld %ld", avail, state->threshold, | 	spa_log_trace(state->log, "timeout %ld %d %ld", | ||||||
| 		      state->sample_count, state->now.tv_sec, state->now.tv_nsec, | 			delay, state->threshold, state->sample_count); | ||||||
| 		      now.tv_sec, now.tv_nsec); |  | ||||||
| 
 | 
 | ||||||
| 	if (avail < state->threshold) { | 	if (delay < state->threshold) | ||||||
| 		if (snd_pcm_state(hndl) == SND_PCM_STATE_SUSPENDED) { | 		goto next; | ||||||
| 			spa_log_error(state->log, "suspended: try resume"); | 
 | ||||||
| 			if ((res = alsa_try_resume(state)) < 0) | 	to_read = SPA_MIN(delay, state->threshold); | ||||||
|  | 
 | ||||||
|  | 	nsec = SPA_TIMESPEC_TO_NSEC(&state->now); | ||||||
|  | 	if ((res = update_time(state, nsec, delay, NULL, false)) < 0) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	while (total_read < to_read) { | ||||||
|  | 		snd_pcm_uframes_t read, frames, offset; | ||||||
|  | 
 | ||||||
|  | 		frames = to_read - total_read; | ||||||
|  | 		if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0) { | ||||||
|  | 			spa_log_error(state->log, "snd_pcm_mmap_begin error: %s", snd_strerror(res)); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		read = push_frames(state, my_areas, offset, frames); | ||||||
|  | 		if (read < frames) | ||||||
|  | 			to_read = 0; | ||||||
|  | 
 | ||||||
|  | 		if ((res = snd_pcm_mmap_commit(hndl, offset, read)) < 0) { | ||||||
|  | 			spa_log_error(state->log, "snd_pcm_mmap_commit error: %s", snd_strerror(res)); | ||||||
|  | 			if (res != -EPIPE && res != -ESTRPIPE) | ||||||
| 				return; | 				return; | ||||||
| 		} | 		} | ||||||
| 	} else { | 		total_read += read; | ||||||
| 		snd_pcm_uframes_t to_read = SPA_MIN(avail, state->threshold); |  | ||||||
| 
 |  | ||||||
| 		while (total_read < to_read) { |  | ||||||
| 			snd_pcm_uframes_t read, frames, offset; |  | ||||||
| 
 |  | ||||||
| 			frames = to_read - total_read; |  | ||||||
| 			if ((res = snd_pcm_mmap_begin(hndl, &my_areas, &offset, &frames)) < 0) { |  | ||||||
| 				spa_log_error(state->log, "snd_pcm_mmap_begin error: %s", snd_strerror(res)); |  | ||||||
| 				return; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			read = push_frames(state, my_areas, offset, frames); |  | ||||||
| 			if (read < frames) |  | ||||||
| 				to_read = 0; |  | ||||||
| 
 |  | ||||||
| 			if ((res = snd_pcm_mmap_commit(hndl, offset, read)) < 0) { |  | ||||||
| 				spa_log_error(state->log, "snd_pcm_mmap_commit error: %s", snd_strerror(res)); |  | ||||||
| 				if (res != -EPIPE && res != -ESTRPIPE) |  | ||||||
| 					return; |  | ||||||
| 			} |  | ||||||
| 			total_read += read; |  | ||||||
| 		} |  | ||||||
| 		state->sample_count += total_read; |  | ||||||
| 	} | 	} | ||||||
| 	calc_timeout(state->threshold, avail - total_read, state->rate, &state->now, &ts.it_value); | 	state->sample_count += total_read; | ||||||
| 
 | 
 | ||||||
| 	ts.it_interval.tv_sec = 0; | next: | ||||||
| 	ts.it_interval.tv_nsec = 0; | 	set_timeout(state, state->next_time); | ||||||
| 	timerfd_settime(state->timerfd, TFD_TIMER_ABSTIME, &ts, NULL); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| int spa_alsa_start(struct state *state, bool xrun_recover) | int spa_alsa_start(struct state *state) | ||||||
| { | { | ||||||
| 	int err; | 	int err; | ||||||
| 	struct itimerspec ts; | 	struct itimerspec ts; | ||||||
|  | @ -900,14 +915,13 @@ int spa_alsa_start(struct state *state, bool xrun_recover) | ||||||
| 			state->slaved = true; | 			state->slaved = true; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	state->bw = 0.128; | 	dll_init(&state->dll, DLL_BW_MAX); | ||||||
| 	dll_init(&state->dll, state->threshold, state->rate, state->bw); | 	state->safety = 0.0; | ||||||
| 
 | 
 | ||||||
| 	spa_log_debug(state->log, "alsa %p: start %d", state, state->threshold); | 	spa_log_debug(state->log, "alsa %p: start %d %d", state, state->threshold, state->slaved); | ||||||
| 
 | 
 | ||||||
| 	CHECK(set_swparams(state), "swparams"); | 	CHECK(set_swparams(state), "swparams"); | ||||||
| 	if (!xrun_recover) | 	snd_pcm_dump(state->hndl, state->output); | ||||||
| 		snd_pcm_dump(state->hndl, state->output); |  | ||||||
| 
 | 
 | ||||||
| 	if ((err = snd_pcm_prepare(state->hndl)) < 0) { | 	if ((err = snd_pcm_prepare(state->hndl)) < 0) { | ||||||
| 		spa_log_error(state->log, "snd_pcm_prepare error: %s", snd_strerror(err)); | 		spa_log_error(state->log, "snd_pcm_prepare error: %s", snd_strerror(err)); | ||||||
|  | @ -945,6 +959,9 @@ int spa_alsa_start(struct state *state, bool xrun_recover) | ||||||
| 		ts.it_interval.tv_nsec = 0; | 		ts.it_interval.tv_nsec = 0; | ||||||
| 		timerfd_settime(state->timerfd, 0, &ts, NULL); | 		timerfd_settime(state->timerfd, 0, &ts, NULL); | ||||||
| 	} | 	} | ||||||
|  | 	else { | ||||||
|  | 		spa_alsa_write(state, state->threshold * 2); | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	state->io->status = SPA_STATUS_OK; | 	state->io->status = SPA_STATUS_OK; | ||||||
| 	state->io->buffer_id = SPA_ID_INVALID; | 	state->io->buffer_id = SPA_ID_INVALID; | ||||||
|  | @ -976,7 +993,7 @@ static int do_remove_source(struct spa_loop *loop, | ||||||
| 	return 0; | 	return 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| int spa_alsa_pause(struct state *state, bool xrun_recover) | int spa_alsa_pause(struct state *state) | ||||||
| { | { | ||||||
| 	int err; | 	int err; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -64,11 +64,15 @@ struct buffer { | ||||||
| 	struct spa_list link; | 	struct spa_list link; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | #define DLL_BW_MAX	0.256 | ||||||
|  | #define DLL_BW_MIN	0.05 | ||||||
|  | #define DLL_BW_PERIOD	4.0 | ||||||
|  | 
 | ||||||
| struct dll { | struct dll { | ||||||
|     double T; | 	double w1, w2; | ||||||
|     double b, c; | 	double base, t0, dt; | ||||||
|     double n0; | 	double bw; | ||||||
|     int count; | 	int count; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| struct state { | struct state { | ||||||
|  | @ -95,7 +99,6 @@ struct state { | ||||||
| 	bool have_format; | 	bool have_format; | ||||||
| 	struct spa_audio_info current_format; | 	struct spa_audio_info current_format; | ||||||
| 	struct dll dll; | 	struct dll dll; | ||||||
| 	double bw; |  | ||||||
| 
 | 
 | ||||||
| 	snd_pcm_uframes_t buffer_frames; | 	snd_pcm_uframes_t buffer_frames; | ||||||
| 	snd_pcm_uframes_t period_frames; | 	snd_pcm_uframes_t period_frames; | ||||||
|  | @ -109,6 +112,7 @@ struct state { | ||||||
| 	struct spa_io_range *range; | 	struct spa_io_range *range; | ||||||
| 	struct spa_io_clock *clock; | 	struct spa_io_clock *clock; | ||||||
| 	struct spa_io_position *position; | 	struct spa_io_position *position; | ||||||
|  | 	struct spa_io_sequence *notify; | ||||||
| 
 | 
 | ||||||
| 	struct buffer buffers[MAX_BUFFERS]; | 	struct buffer buffers[MAX_BUFFERS]; | ||||||
| 	unsigned int n_buffers; | 	unsigned int n_buffers; | ||||||
|  | @ -127,9 +131,14 @@ struct state { | ||||||
| 
 | 
 | ||||||
| 	snd_htimestamp_t now; | 	snd_htimestamp_t now; | ||||||
| 	int64_t sample_count; | 	int64_t sample_count; | ||||||
| 	int64_t filled; | 
 | ||||||
|  | 	int64_t sample_time; | ||||||
|  | 	uint64_t last_time; | ||||||
|  | 	uint64_t next_time; | ||||||
| 
 | 
 | ||||||
| 	uint64_t underrun; | 	uint64_t underrun; | ||||||
|  | 	double old_dt; | ||||||
|  | 	double safety; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| int | int | ||||||
|  | @ -141,40 +150,42 @@ spa_alsa_enum_format(struct state *state, | ||||||
| 
 | 
 | ||||||
| int spa_alsa_set_format(struct state *state, struct spa_audio_info *info, uint32_t flags); | int spa_alsa_set_format(struct state *state, struct spa_audio_info *info, uint32_t flags); | ||||||
| 
 | 
 | ||||||
| int spa_alsa_start(struct state *state, bool xrun_recover); | int spa_alsa_start(struct state *state); | ||||||
| int spa_alsa_pause(struct state *state, bool xrun_recover); | int spa_alsa_pause(struct state *state); | ||||||
| int spa_alsa_close(struct state *state); | int spa_alsa_close(struct state *state); | ||||||
| 
 | 
 | ||||||
| int spa_alsa_write(struct state *state, snd_pcm_uframes_t silence); | int spa_alsa_write(struct state *state, snd_pcm_uframes_t silence); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| static inline void dll_bandwidth(struct dll *dll, double period, double rate, double bandwidth) | static inline void dll_bandwidth(struct dll *dll, double bandwidth) | ||||||
| { | { | ||||||
| 	double w = 2 * M_PI * bandwidth * period / rate; | 	double w = 2 * M_PI * bandwidth; | ||||||
| 	dll->b = 1.0 - exp(-M_SQRT2 * w); | 	dll->w1 = w * M_SQRT2; | ||||||
| 	dll->c = (1.0 - exp(-w * w)) / period; | 	dll->w2 = w * w; | ||||||
|  | 	dll->bw = bandwidth; | ||||||
|  | 	dll->base = dll->t0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static inline void dll_init(struct dll *dll, double period, double rate, double bandwidth) | static inline void dll_init(struct dll *dll, double bandwidth) | ||||||
| { | { | ||||||
| 	dll->T = 1000000.0 / rate; | 	dll->dt = 1.0; | ||||||
| 	dll->count = 0; | 	dll->count = 0; | ||||||
| 	dll_bandwidth(dll, period, rate, bandwidth); | 	dll_bandwidth(dll, bandwidth); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static inline double dll_update(struct dll *dll, double system_time, double period) | static inline double dll_update(struct dll *dll, double tw, double period) | ||||||
| { | { | ||||||
| 	double e; | 	double e; | ||||||
| 
 | 
 | ||||||
| 	if (dll->count++ == 0) { | 	if (dll->count++ == 0) { | ||||||
| 		dll->n0 = system_time; | 		dll->t0 = dll->base = tw; | ||||||
| 	} else { | 	} else { | ||||||
| 		dll->n0 += period * dll->T; | 		dll->t0 += dll->dt * period; | ||||||
| 		e = system_time - dll->n0; | 		e = (tw - dll->t0) * period; | ||||||
| 		dll->n0 += SPA_MAX(dll->b, 1.0 / dll->count) * e; | 		dll->t0 += dll->w1 * e; | ||||||
| 		dll->T += dll->c * e; | 		dll->dt += dll->w2 * e; | ||||||
| 	} | 	} | ||||||
| 	return dll->n0; | 	return dll->t0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #ifdef __cplusplus | #ifdef __cplusplus | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Wim Taymans
						Wim Taymans