snd_pcm_start_at() - kernel-side
The headlines:
- Provide snd_pcm_start_at ioctl - Add SND_PCM_TSTAMP_TYPE_AUDIO_WALLCLOCK - Use the stream tstamp_type to interpret the start_at timespec - snd_pcm_start_at_ops defines handler for a TSTAMP_TYPE - handler requirements clearly defined - If posix clock, use a high-res timer implementation (thx Nick Stoughton!) - If audio wallclock, delegate to the pcm driver if possible, otherwise error - Further implementations easily added - Cancel pending timer on: - Subsequent call to snd_pcm_start_at() - Any /attempt/ to change the stream state
Major points for discussion:
- snd_pcm_start_at_ops requirements aren't clear enough - callback must not do cleanup - cancel is guaranteed to be called - do cleanup here - snd_pcm_gettime() can't currently return a value when tstamp_type is AUDIO_WALLCLOCK - start_at timer callbacks *should* probably lock the stream, contrary to Nick's note - The user story for start_at cancellation from userspace is a bit weird... - Cancellation on state-change seems necessary, but not sufficient - Maybe add snd_pcm_start_at_cancel()?
Future thoughts:
- Perhaps snd_pcm_start_at should be a blocking call (EAGAIN when stream is non-blocking) - Maybe tstamp_types and startat_types should be different things
I've become convinced that exposing the nature of the pcm clock is troublesome, and unnecessary, and fell back to Takashi's suggestion of a DEVICE_SPECIFIC tstamp_type. I note that AUDIO_WALLCLOCK seems to capture the concept of 'pcm time', so I've called it that.
Because I'm using SND_PCM_TSTAMP_TYPE_AUDIO_WALLCLOCK instead of SNDRV_PCM_TSTAMP_TYPE_PTP, there's no additional payload required for snd_pcm_sw_set_tstamp_type().
Comments and suggestions welcome :) Tim
diff --git a/include/sound/pcm.h b/include/sound/pcm.h index 1e7f74a..b8bfa9b 100644 --- a/include/sound/pcm.h +++ b/include/sound/pcm.h @@ -82,6 +82,8 @@ struct snd_pcm_ops { unsigned long offset); int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma); int (*ack)(struct snd_pcm_substream *substream); + int (*wall_clock_start_at)(struct snd_pcm_substream *substream, const struct timespec *ts); + int (*wall_clock_start_at_cancel)(struct snd_pcm_substream *substream); };
/* @@ -210,6 +212,8 @@ struct snd_pcm_ops { #define SNDRV_PCM_FMTBIT_IEC958_SUBFRAME SNDRV_PCM_FMTBIT_IEC958_SUBFRAME_BE #endif
+#define SNDRV_PCM_START_AT_TSTAMP_TYPE_NULL (SNDRV_PCM_TSTAMP_TYPE_LAST+1) + struct snd_pcm_file { struct snd_pcm_substream *substream; int no_compat_mmap; @@ -364,6 +368,10 @@ struct snd_pcm_runtime { #ifdef CONFIG_SND_PCM_XRUN_DEBUG struct snd_pcm_hwptr_log *hwptr_log; #endif + + int start_at_tstamp_type; /* start_at timer tstamp_type Set to + SNDRV_PCM_START_AT_TSTAMP_TYPE_NULL if not active */ + void *start_at_timer_data; /* start_at timer data */ };
struct snd_pcm_group { /* keep linked substreams */ @@ -1069,6 +1077,7 @@ static inline void snd_pcm_gettime(struct snd_pcm_runtime *runtime, case SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW: getrawmonotonic(tv); break; + case SNDRV_PCM_TSTAMP_TYPE_AUDIO_WALLCLOCK: default: getnstimeofday(tv); break; diff --git a/include/uapi/sound/asound.h b/include/uapi/sound/asound.h index 941d32f..dfe2bac 100644 --- a/include/uapi/sound/asound.h +++ b/include/uapi/sound/asound.h @@ -467,8 +467,9 @@ struct snd_xfern { enum { SNDRV_PCM_TSTAMP_TYPE_GETTIMEOFDAY = 0, /* gettimeofday equivalent */ SNDRV_PCM_TSTAMP_TYPE_MONOTONIC, /* posix_clock_monotonic equivalent */ - SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW, /* monotonic_raw (no NTP) */ - SNDRV_PCM_TSTAMP_TYPE_LAST = SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW, + SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW, /* monotonic_raw (no NTP) */ + SNDRV_PCM_TSTAMP_TYPE_AUDIO_WALLCLOCK, /* audio wallclock timestamp */ + SNDRV_PCM_TSTAMP_TYPE_LAST = SNDRV_PCM_TSTAMP_TYPE_AUDIO_WALLCLOCK, };
/* channel positions */ @@ -549,6 +550,7 @@ enum { #define SNDRV_PCM_IOCTL_READN_FRAMES _IOR('A', 0x53, struct snd_xfern) #define SNDRV_PCM_IOCTL_LINK _IOW('A', 0x60, int) #define SNDRV_PCM_IOCTL_UNLINK _IO('A', 0x61) +#define SNDRV_PCM_IOCTL_START_AT _IOW('A', 0x62, struct timespec)
/***************************************************************************** * * diff --git a/sound/core/pcm.c b/sound/core/pcm.c index cfc56c8..483f85d 100644 --- a/sound/core/pcm.c +++ b/sound/core/pcm.c @@ -1003,6 +1003,7 @@ int snd_pcm_attach_substream(struct snd_pcm *pcm, int stream, init_waitqueue_head(&runtime->tsleep);
runtime->status->state = SNDRV_PCM_STATE_OPEN; + runtime->start_at_tstamp_type = SNDRV_PCM_START_AT_TSTAMP_TYPE_NULL;
substream->runtime = runtime; substream->private_data = pcm->private_data; diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c index 095d957..352921d 100644 --- a/sound/core/pcm_native.c +++ b/sound/core/pcm_native.c @@ -35,6 +35,9 @@ #include <sound/timer.h> #include <sound/minors.h> #include <asm/io.h> +#if defined(CONFIG_HIGH_RES_TIMERS) +#include <linux/hrtimer.h> +#endif
/* * Compatibility @@ -67,6 +70,8 @@ static int snd_pcm_hw_params_old_user(struct snd_pcm_substream *substream, #endif static int snd_pcm_open(struct file *file, struct snd_pcm *pcm, int stream);
+static int snd_pcm_start_at_cancel(struct snd_pcm_substream *substream); + /* * */ @@ -265,6 +270,7 @@ static const char * const snd_pcm_hw_param_names[] = { }; #endif
+ int snd_pcm_hw_refine(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) { @@ -834,6 +840,11 @@ static int snd_pcm_action_group(struct action_ops *ops, struct snd_pcm_substream *s1; int res = 0, depth = 1;
+ /* Any attempt to change state cancels a pending start_at timer */ + res = snd_pcm_start_at_cancel(substream); + if (res < 0) + return res; + snd_pcm_group_for_each_entry(s, substream) { if (do_lock && s != substream) { if (s->pcm->nonatomic) @@ -888,6 +899,11 @@ static int snd_pcm_action_single(struct action_ops *ops, int state) { int res; + + /* Any attempt to change state cancels a pending start_at timer */ + res = snd_pcm_start_at_cancel(substream); + if (res < 0) + return res; res = ops->pre_action(substream, state); if (res < 0) @@ -1015,6 +1031,234 @@ static struct action_ops snd_pcm_action_start = { .post_action = snd_pcm_post_start };
+static inline clockid_t snd_pcm_get_clockid(struct snd_pcm_substream* substream) +{ + switch(substream->runtime->tstamp_type) + { + case SNDRV_PCM_TSTAMP_TYPE_MONOTONIC: + return CLOCK_MONOTONIC; + case SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW: + return CLOCK_MONOTONIC_RAW; + default: + return CLOCK_REALTIME; + } +} + +/* snd_pcm_start_at_ops + * There are various mechanisms for supporting snd_pcm_start_at. Posix clocks may + * use hires timers if available. + * + * schedule() may use the runtime member 'start_at_timer_data' to store enough + * information to cancel the timer. When the timer fires, it must be single-shot, + * and must not cancel/delete the timer: cancel() is guaranteed to be called + * before the timer is reused, so resources should be freed within cancel(), not + * by the timer callback. + * + * cancel() returns when the start_at timer callback is guaranteed to be both + * cancelled and not currently running. +*/ + +struct snd_pcm_start_at_ops { + int (*schedule)(struct snd_pcm_substream *substream, const struct timespec *start_time); + int (*cancel)(struct snd_pcm_substream *substream); +}; + +/* To support snd_pcm_start_at for posix tstamp_types, we use high-res timers, if + * kernel is configured appropriately. + */ + +#ifdef CONFIG_HIGH_RES_TIMERS +/* + * hrtimer interface + */ + +struct hrtimer_pcm { + struct hrtimer timer; + struct snd_pcm_substream *substream; +}; + +/* + * called from a hard irq context - no need for locks. + * only problem is that the caller might have gone away and closed the substream + * before the timer expires. + */ +enum hrtimer_restart snd_pcm_do_start_time(struct hrtimer *timer) +{ + struct hrtimer_pcm *pcm_timer; + struct snd_pcm_substream *substream; + int ret; + + pcm_timer = container_of(timer, struct hrtimer_pcm, timer); + substream = pcm_timer->substream; + + ret = snd_pcm_do_start(substream, SNDRV_PCM_STATE_RUNNING); + if (ret == 0) { + snd_pcm_post_start(substream, SNDRV_PCM_STATE_RUNNING); + } + return HRTIMER_NORESTART; +} +#endif + + + +static int start_at_posix_schedule(struct snd_pcm_substream *substream, const struct timespec *start_time) +{ +#ifdef CONFIG_HIGH_RES_TIMERS + struct hrtimer_pcm *pcm_timer; + struct timespec now; + int ret; + + /* Get time now and check if start_time is in the past */ + snd_pcm_gettime(substream->runtime, &now); + if (timespec_compare(&now, start_time) >= 0) { + return -ETIME; + } + + /* Allocate a hrtimer to handle the start_at */ + pcm_timer = kmalloc(sizeof(*pcm_timer), GFP_KERNEL); + if (!pcm_timer) + return -ENOMEM; + + hrtimer_init(&pcm_timer->timer, snd_pcm_get_clockid(substream), HRTIMER_MODE_ABS); + + /* Setup timer */ + pcm_timer->timer.function = snd_pcm_do_start_time; + pcm_timer->substream = substream; + + /* Store timer in start_at info */ + substream->runtime->start_at_timer_data = pcm_timer; + + /* Pre start */ + ret = snd_pcm_pre_start(substream, SNDRV_PCM_STATE_PREPARED); + if (ret < 0) + goto error; + + ret = hrtimer_start(&pcm_timer->timer, timespec_to_ktime(*start_time), HRTIMER_MODE_ABS); + if (ret < 0 ) + goto error; + + return 0; +error: + kfree(pcm_timer); + return ret; +#else + return -ENOSYS; +#endif +} + +static int start_at_posix_cancel(struct snd_pcm_substream *substream) +{ +#ifdef CONFIG_HIGH_RES_TIMERS + struct hrtimer_pcm *pcm_timer = substream->runtime->start_at_timer_data; + hrtimer_cancel(&pcm_timer->timer); /* Cancel existing timer. (NOP if it's not running) */ + kfree(pcm_timer); + return 0; +#else + return -ENOSYS; +#endif +} + +static int start_at_wallclock_schedule(struct snd_pcm_substream *substream, const struct timespec *start_time) +{ + if (substream->ops->wall_clock_start_at) + return substream->ops->wall_clock_start_at(substream, start_time); + else + return -ENOSYS; +} + +static int start_at_wallclock_cancel(struct snd_pcm_substream *substream) +{ + if (substream->ops->wall_clock_start_at_cancel) + return substream->ops->wall_clock_start_at_cancel(substream); + else + return -ENOSYS; +} + +static struct snd_pcm_start_at_ops start_at_ops[SNDRV_PCM_START_AT_TSTAMP_TYPE_NULL+1] = { + [SNDRV_PCM_TSTAMP_TYPE_GETTIMEOFDAY] = { + .schedule = start_at_posix_schedule, + .cancel = start_at_posix_cancel + }, + [SNDRV_PCM_TSTAMP_TYPE_MONOTONIC] = { + .schedule = start_at_posix_schedule, + .cancel = start_at_posix_cancel + }, + [SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW] = { /* hrtimers can't handle CLOCK_MONOTONIC_RAW */ + .schedule = NULL, + .cancel = NULL, + }, + [SNDRV_PCM_TSTAMP_TYPE_AUDIO_WALLCLOCK] = { /* delegate start_at to pcm driver */ + .schedule = start_at_wallclock_schedule, + .cancel = start_at_wallclock_cancel, + }, + [SNDRV_PCM_START_AT_TSTAMP_TYPE_NULL] = { /* null handler - required for handling first cancellation */ + .schedule = NULL, + .cancel = NULL, + }, +}; + +/* snd_pcm_start_at_cancel() allows state-transition code to conveniently cancel the pending timer */ +static int snd_pcm_start_at_cancel(struct snd_pcm_substream *substream) +{ + struct snd_pcm_start_at_ops *ops = &start_at_ops[substream->runtime->start_at_tstamp_type]; + int ret = 0; + + /* If ops->cancel is NULL, it's not an error. */ + if (ops->cancel) { + ret = ops->cancel(substream); + if (ret == 0) + substream->runtime->start_at_tstamp_type = SNDRV_PCM_START_AT_TSTAMP_TYPE_NULL; + } + + return ret; +} + +int snd_pcm_start_at(struct snd_pcm_substream *substream, + struct timespec __user *_start_time) +{ + struct timespec start_time; + int new_tstamp_type; + struct snd_pcm_start_at_ops *ops; + int ret; + + if (copy_from_user(&start_time, _start_time, sizeof(start_time))) + return -EFAULT; + + if (!timespec_valid(&start_time)) + return -EINVAL; + + /* If not a playback substream, give up */ + if (substream->stream != SNDRV_PCM_STREAM_PLAYBACK) + return -EINVAL; + + /* Cancel any existing timer */ + ret = snd_pcm_start_at_cancel(substream); + if (ret < 0) + return ret; + + /* Get new tstamp_type */ + new_tstamp_type = substream->runtime->tstamp_type; + + /* Save current start_at tstamp_type. This way, it's valid before + schedule() is called */ + substream->runtime->start_at_tstamp_type = new_tstamp_type; + + /* Get apprpriate start_at ops */ + ops = &start_at_ops[new_tstamp_type]; + + /* Schedule start_at. If it doesn't exist, that's an error. */ + if (ops->schedule) { + ret = ops->schedule(substream, &start_time); + /* If successful, mark timer as cancelled */ + if (ret < 0) + /* If schedule() failed, reset tstamp type */ + substream->runtime->start_at_tstamp_type = SNDRV_PCM_START_AT_TSTAMP_TYPE_NULL; + return ret; + } + else + return -ENOSYS; +} + /** * snd_pcm_start - start all linked streams * @substream: the PCM substream instance @@ -2721,6 +2965,8 @@ static int snd_pcm_common_ioctl1(struct file *file, return snd_pcm_action_lock_irq(&snd_pcm_action_start, substream, SNDRV_PCM_STATE_RUNNING); case SNDRV_PCM_IOCTL_LINK: return snd_pcm_link(substream, (int)(unsigned long) arg); + case SNDRV_PCM_IOCTL_START_AT: + return snd_pcm_start_at(substream, arg); case SNDRV_PCM_IOCTL_UNLINK: return snd_pcm_unlink(substream); case SNDRV_PCM_IOCTL_RESUME: